Commit c0952199 authored by Benni Mack's avatar Benni Mack
Browse files

[FEATURE] Automatic language key handling with regions (ISO 3166-1)

TYPO3 can now handle language keys with a given region,
such as "de-AT" without extending TYPO3's native language
handling. When a "de-AT.locallang.xlf" file exists,
it is used, but will fall back to "de.locallang.xlf" itself.

The language is based on ISO 639-1,
whereas the region should be used from ISO 3166-1.

For backwards-compatibility, locales with underscore
are also possible "de_AT".

Resolves: #86913
Releases: main
Change-Id: I1bb97f7f36053c17bf919a92b3739e59b20d65c9
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77375


Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 499ff2fb
......@@ -140,15 +140,21 @@ class Locales implements SingletonInterface
];
/**
* Dependencies for locales
* This is a reverse mapping for the built-in languages within $this->languages that contain 5-letter codes.
* Dependencies for locales.
* By default, locales with a country/region suffix such as "de_AT" will automatically have the "de"
* locale as fallback. This way TYPO3 only needs to know about the actual "base" language, however
* also allows to use country-specific languages.
* However, when a specific locale such as "lb" has a dependency to a different "de" suffix, this should
* is defined here.
* With
* $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies']
* it is possible to extend the dependency list.
*
* If "pt_BR" is chosen, but no label was found, a fallback to the label in "pt" is used.
* Example:
* If "lb" is chosen, but no label was found, a fallback to the label in "de" is used.
*/
protected array $localeDependencies = [
'pt_BR' => ['pt'],
'fr_CA' => ['fr'],
'lb' => ['de'],
'lb' => ['de'],
];
public function __construct()
......@@ -158,14 +164,9 @@ class Locales implements SingletonInterface
if (!is_string($locale) || $locale === '') {
continue;
}
if (!isset($this->languages[$locale])) {
$this->languages[$locale] = $name;
}
// Initializes the locale dependencies with TYPO3 supported locales
if (strlen($locale) === 5) {
$this->localeDependencies[$locale] = [substr($locale, 0, 2)];
}
}
// Merge user-provided locale dependencies
if (is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] ?? null)) {
......@@ -187,7 +188,24 @@ class Locales implements SingletonInterface
public function isValidLanguageKey(string $locale): bool
{
return in_array($locale, $this->getLocales(), true);
// "en" implicitly equals "default", so this is OK
if ($locale === 'en' || $locale === 'default') {
return true;
}
if (!isset($this->languages[$locale])) {
// the given locale is not found in the current locales, let us see if
// the base language (iso-639-1) is in the list of supported locales.
if (str_contains($locale, '_')) {
[$baseIsoCodeLanguageKey] = explode('_', $locale);
return $this->isValidLanguageKey($baseIsoCodeLanguageKey);
}
if (str_contains($locale, '-')) {
[$baseIsoCodeLanguageKey] = explode('-', $locale);
return $this->isValidLanguageKey($baseIsoCodeLanguageKey);
}
return false;
}
return true;
}
/**
......@@ -241,7 +259,26 @@ class Locales implements SingletonInterface
}
}
}
return $dependencies;
// Use automatic dependency resolving.
// "de_AT" automatically has a dependency on "de".
// but only do this if the actual "de_AT" does not have a custom dependency already defined in
// $this->localeDependencies
if ($dependencies === [] && str_contains($locale, '_')) {
[$languageIsoCode] = explode('_', $locale);
// "en" = "default" is always implicitly the default fallback dependency
if ($languageIsoCode !== 'en') {
$dependencies[] = $languageIsoCode;
$dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode));
}
} elseif ($dependencies === [] && str_contains($locale, '-')) {
[$languageIsoCode] = explode('-', $locale);
// "en" = "default" is always implicitly the default fallback dependency
if ($languageIsoCode !== 'en') {
$dependencies[] = $languageIsoCode;
$dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode));
}
}
return array_unique($dependencies);
}
/**
......@@ -254,7 +291,7 @@ class Locales implements SingletonInterface
public function getPreferredClientLanguage(string $languageCodesList): string
{
$allLanguageCodesFromLocales = ['en' => 'default'];
foreach ($this->getLocales() as $locale) {
foreach ($this->languages as $locale => $localeTitle) {
$locale = str_replace('_', '-', $locale);
$allLanguageCodesFromLocales[$locale] = $locale;
}
......
.. include:: /Includes.rst.txt
.. _feature-86913-1673955088:
======================================================================================
Feature: #86913 - Automatic support for language files of languages with region suffix
======================================================================================
See :issue:`86913`
Description
===========
TYPO3's native support for label files - that is: translatable text for system labels such as
from plugins, and for texts within TYPO3 Backend - supports over 50 languages. The languages are
identified by their "language key" of the ISO 639-1 standard also allow to use a region-specific
language. This happens mostly in countries/regions that have a variation of the language, such
as "en-US" for American English, or "de-CH" for the German Language in Switzerland.
In order to support these region-specific language keys, which are composed of ISO 639-1
and ISO 3166-1 and separated with `-`, TYPO3 integrators had to manually
configure the additional language to translate region-specific terms.
Common examples are "Behavior" (American English) vs. "Behaviour" (British English), or
"Offerte" (Swiss German) vs. "Angebot" (German), where all labels except a few terms should stay the same.
Impact
======
TYPO3 now allows integrators to use a custom label file with the locale prefix "de_CH.locallang.xlf"
in an extension next to "de.locallang.xlf" and "locallang.xlf" (default language english).
When integrators then use "de-CH" within their site configuration, TYPO3 first checks if
a term is available in "de-CH", and then automatically falls back to the non-region-specific "de"
label file "de.locallang.xlf" without any further configuration to TYPO3.
Previously such region-specific locales had to be configured via:
.. code-block:: php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] = [
'de-CH' => 'German (Switzerland)',
];
The same fallback functionality also works when overriding labels via TypoScript:
.. code-block:: typoscript
plugin.tx_myextension._LOCAL_LANG.de = Angebot
plugin.tx_myextension._LOCAL_LANG.de-CH = Offerte
.. index:: LocalConfiguration, TypoScript, ext:core
......@@ -42,6 +42,59 @@ class LocalesTest extends UnitTestCase
parent::tearDown();
}
/**
* @test
*/
public function isValidLanguageKeyAlsoDetectsRegionSpecificKeys(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] = [
'fr-CG' => 'French (Congo)',
];
$locales = new Locales();
// Fixed defined language keys
self::assertTrue($locales->isValidLanguageKey('fr_CA'));
self::assertTrue($locales->isValidLanguageKey('fr-CA'));
// User-defined language keys
self::assertTrue($locales->isValidLanguageKey('fr-CG'));
self::assertTrue($locales->isValidLanguageKey('de'));
// Transient language key
self::assertTrue($locales->isValidLanguageKey('de-AT'));
// Deal with "en" and "en_US"
self::assertTrue($locales->isValidLanguageKey('en-US'));
self::assertTrue($locales->isValidLanguageKey('en'));
// valid language key "en" is automatically applied with "default"
self::assertTrue($locales->isValidLanguageKey('default'));
}
/**
* @test
*/
public function getLocaleDependenciesResolvesAutomaticAndDefinedDependencies(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] = [
'fr-CG' => 'French (Congo)',
];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] = [
'de-CH' => ['fr', 'es'],
];
$locales = new Locales();
// Automatic dependency
$dependencies = $locales->getLocaleDependencies('de_AT');
self::assertEquals(['de'], $dependencies);
// Explicitly defined language with 5 keys
$dependencies = $locales->getLocaleDependencies('pt_BR');
self::assertEquals(['pt'], $dependencies);
// Explicitly defined 2-letter custom dependency
$dependencies = $locales->getLocaleDependencies('lb');
self::assertEquals(['de'], $dependencies);
// Dependency with custom dependencies
$dependencies = $locales->getLocaleDependencies('de-CH');
self::assertEquals(['fr', 'es'], $dependencies);
// Custom registered language
$dependencies = $locales->getLocaleDependencies('fr_CG');
self::assertEquals(['fr'], $dependencies);
}
public function browserLanguageDetectionWorksDataProvider(): array
{
return [
......
......@@ -199,17 +199,17 @@ class RichTextElement extends AbstractFormElement
}
$contentLanguageUid = (int)max($currentLanguageUid, 0);
if ($contentLanguageUid) {
// the language rows might not be fully inialized, so we fallback to en_US in this case
$contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'] ?? 'en_US';
// the language rows might not be fully initialized, so we fall back to en-US in this case
$contentLanguage = $this->data['systemLanguageRows'][$currentLanguageUid]['iso'] ?? 'en-US';
} else {
$contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en_US';
$contentLanguage = $this->rteConfiguration['config']['defaultContentLanguage'] ?? 'en-US';
}
$languageCodeParts = explode('_', $contentLanguage);
$contentLanguage = strtolower($languageCodeParts[0]) . (!empty($languageCodeParts[1]) ? '_' . strtoupper($languageCodeParts[1]) : '');
// Find the configured language in the list of localization locales
$locales = GeneralUtility::makeInstance(Locales::class);
// If not found, default to 'en'
if (!$locales->isValidLanguageKey($contentLanguage)) {
if ($contentLanguage === 'default' || !$locales->isValidLanguageKey($contentLanguage)) {
$contentLanguage = 'en';
}
return $contentLanguage;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment