Repository: symfony/ux (php)

Issue:
[Translator] No translations if detected via HTML lang attribute
Symfony locales are specified with an underscore to split country code from language code, e.g. `de_AT`.

When using UX Translator it correctly detects the current locale if set [via the HTML attribute](https://symfony.com/bundles/ux-translator/current/index.html#configuring-the-default-locale):

```twig
<html lang="{{ app.request.locale }}">
{# <html lang="de_AT"> #}
```

Unfortunately, [according to W3](https://www.w3.org/TR/html401/struct/dirlang.html#h-8.1.1), this format is invalid, as the "subcode" has to be separated by a hyphen.

Using

```twig
<html lang="{{ app.request.locale | replace({'_': '-'}) }}">
{# <html lang="de-AT"> #}
```

doesn't work, because this format is not detected by Translator, resulting in no translations at all (only the string constants are returned).

Pull request description:
[Translator] Handle W3C locale format on document element
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Issues        | Fix #2378
| License       | MIT


According to [W3C](https://www.w3.org/TR/html401/struct/dirlang.html#h-8.1.1
) (and [RFC1766](https://www.ietf.org/rfc/rfc1766.txt)) the valid format for language codes is "primary-code"-"subcode", but Symfony expects underscores as separator, resulting in broken translations.

This changes language codes specified in the correct format for HTML to the Symfony format when reading the "lang" attribute.

Patch:
diff --git a/src/Translator/CHANGELOG.md b/src/Translator/CHANGELOG.md
index 789af7f2..34532d51 100644
--- a/src/Translator/CHANGELOG.md
+++ b/src/Translator/CHANGELOG.md
@@ -1,5 +1,9 @@
 # CHANGELOG
 
+## 2.22.0
+
+-   Support both the Symfony format (`fr_FR`) and W3C specification (`fr-FR`) for locale subcodes.
+
 ## 2.20.0
 
 -   Add `throwWhenNotFound` function to configure the behavior when a translation is not found.
diff --git a/src/Translator/assets/dist/translator_controller.js b/src/Translator/assets/dist/translator_controller.js
index 76292583..b45f3f06 100644
--- a/src/Translator/assets/dist/translator_controller.js
+++ b/src/Translator/assets/dist/translator_controller.js
@@ -227,7 +227,7 @@ function setLocale(locale) {
 function getLocale() {
     return (_locale ||
         document.documentElement.getAttribute('data-symfony-ux-translator-locale') ||
-        document.documentElement.lang ||
+        (document.documentElement.lang ? document.documentElement.lang.replace('-', '_') : null) ||
         'en');
 }
 function throwWhenNotFound(enabled) {
diff --git a/src/Translator/assets/src/translator.ts b/src/Translator/assets/src/translator.ts
index 0da20f53..2659d171 100644
--- a/src/Translator/assets/src/translator.ts
+++ b/src/Translator/assets/src/translator.ts
@@ -47,8 +47,8 @@ export function setLocale(locale: LocaleType | null) {
 export function getLocale(): LocaleType {
     return (
         _locale ||
-        document.documentElement.getAttribute('data-symfony-ux-translator-locale') || // <html data-symfony-ux-translator-locale="en">
-        document.documentElement.lang || // <html lang="en">
+        document.documentElement.getAttribute('data-symfony-ux-translator-locale') || // <html data-symfony-ux-translator-locale="en_US">
+        (document.documentElement.lang ? document.documentElement.lang.replace('-', '_') : null) || // <html lang="en-US">
         'en'
     );
 }
diff --git a/src/Translator/doc/index.rst b/src/Translator/doc/index.rst
index bb965b00..49a99305 100644
--- a/src/Translator/doc/index.rst
+++ b/src/Translator/doc/index.rst
@@ -90,9 +90,9 @@ Configuring the default locale
 
 By default, the default locale is ``en`` (English) that you can configure through many ways (in order of priority):
 
-#. With ``setLocale('your-locale')`` from ``@symfony/ux-translator`` package
-#. Or with ``<html data-symfony-ux-translator-locale="your-locale">`` attribute
-#. Or with ``<html lang="your-locale">`` attribute
+#. With ``setLocale('de')`` or ``setLocale('de_AT')`` from ``@symfony/ux-translator`` package
+#. Or with ``<html data-symfony-ux-translator-locale="{{ app.request.locale }}">`` attribute (e.g., ``de_AT`` or ``de`` using Symfony locale format)
+#. Or with ``<html lang="{{ app.request.locale|replace({ '_': '-' }) }}">`` attribute (e.g., ``de-AT`` or ``de`` following the `W3C specification on language codes`_)
 
 Detecting missing translations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -191,3 +191,4 @@ https://symfony.com/doc/current/contributing/code/bc.html
 .. _`the Symfony UX initiative`: https://ux.symfony.com/
 .. _StimulusBundle configured in your app: https://symfony.com/bundles/StimulusBundle/current/index.html
 .. _`ICU Message Format`: https://symfony.com/doc/current/reference/formats/message_format.html
+.. _`W3C specification on language codes`: https://www.w3.org/TR/html401/struct/dirlang.html#h-8.1.1


Test patch:
diff --git a/src/Translator/assets/test/translator.test.ts b/src/Translator/assets/test/translator.test.ts
index 6df854a1..77cf2c78 100644
--- a/src/Translator/assets/test/translator.test.ts
+++ b/src/Translator/assets/test/translator.test.ts
@@ -35,6 +35,18 @@ describe('Translator', () => {
         });
     });
 
+    describe('getLocale', () => {
+        test('with subcode', () => {
+            // allow format according to W3C
+            document.documentElement.lang = 'de-AT';
+            expect(getLocale()).toEqual('de_AT');
+
+            // or "incorrect" Symfony locale format
+            document.documentElement.lang = 'de_AT';
+            expect(getLocale()).toEqual('de_AT');
+        });
+    });
+
     describe('setLocale', () => {
         test('custom locale', () => {
             setLocale('fr');