diff --git a/config/app.php b/config/app.php index ea6477f9e..bbc519872 100644 --- a/config/app.php +++ b/config/app.php @@ -74,7 +74,7 @@ return [ | */ - 'locale' => env('APP_LOCALE', 'en-US'), + 'locale' => pixelfed_normalize_locale($pixelfedLegacyLocaleMap, env('APP_LOCALE', 'en-US')), /* |-------------------------------------------------------------------------- @@ -87,7 +87,7 @@ return [ | */ - 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en-US'), + 'fallback_locale' => pixelfed_normalize_locale($pixelfedLegacyLocaleMap, env('APP_FALLBACK_LOCALE', 'en-US')), /* |-------------------------------------------------------------------------- @@ -162,3 +162,73 @@ return [ ])->toArray(), ]; + +/* +| Legacy two-letter language codes mapped to the current +| locale-coded folder names under lang/. Replace 'de' with 'de-DE'. +| +| Added Sep 2026 - DELETE AFTER COMMUNICATION WITH ADMINS +*/ +$pixelfedLegacyLocaleMap = [ + 'af' => 'af-ZA', + 'ar' => 'ar-SA', + 'bn' => 'bn-BD', + 'bs' => 'bs-BA', + 'ca' => 'ca-ES', + 'cs' => 'cs-CZ', + 'cy' => 'cy-GB', + 'da' => 'da-DK', + 'de' => 'de-DE', + 'el' => 'el-GR', + 'en' => 'en-US', + 'eo' => 'eo-UY', + 'es' => 'es-ES', + 'eu' => 'eu-ES', + 'fa' => 'fa-IR', + 'fi' => 'fi-FI', + 'fr' => 'fr-FR', + 'gd' => 'gd-GB', + 'gl' => 'gl-ES', + 'he' => 'he-IL', + 'hi' => 'hi-IN', + 'hr' => 'hr-HR', + 'hu' => 'hu-HU', + 'id' => 'id-ID', + 'it' => 'it-IT', + 'ja' => 'ja-JP', + 'ko' => 'ko-KR', + 'me' => 'me-ME', + 'mk' => 'mk-MK', + 'ms' => 'ms-MY', + 'nl' => 'nl-NL', + 'no' => 'no-NO', + 'oc' => 'oc-FR', + 'pl' => 'pl-PL', + 'pt' => 'pt-PT', + 'ro' => 'ro-RO', + 'ru' => 'ru-RU', + 'sk' => 'sk-SK', + 'sr' => 'sr-CS', + 'sv' => 'sv-SE', + 'th' => 'th-TH', + 'tr' => 'tr-TR', + 'uk' => 'uk-UA', + 'vi' => 'vi-VN', + 'zh-cn' => 'zh-CN', + 'zh-tw' => 'zh-TW', +]; + +if (! function_exists('pixelfed_normalize_locale')) { + function pixelfed_normalize_locale(array $map, ?string $locale): string + { + $locale = is_string($locale) ? trim($locale) : ''; + + if ($locale === '') { + return 'en-US'; + } + + $lower = strtolower($locale); + + return $map[$lower] ?? $locale; + } +} diff --git a/tests/Feature/LocalizationTest.php b/tests/Feature/LocalizationTest.php index 939f611e6..d3eaaf094 100644 --- a/tests/Feature/LocalizationTest.php +++ b/tests/Feature/LocalizationTest.php @@ -319,6 +319,73 @@ describe('locale-aware caching of rendered site pages', function () { })->skip(fn () => firstNonDefaultLocale() === 'en-US', 'needs a second locale'); }); +describe('legacy APP_LOCALE normalization (config/app.php)', function () { + /* + | The normalization lives in config/app.php: it maps a legacy short code + | (e.g. "es") from APP_LOCALE to the current locale folder (e.g. "es-ES") + | so instances upgrading with an old APP_LOCALE do not silently fall back + | to English. The config value is resolved once at boot, so these tests + | re-run the config file with a given APP_LOCALE and assert the result. + */ + $resolveLocale = function (?string $appLocale): string { + // Set APP_LOCALE the way the config file reads it (via env()). + if ($appLocale === null) { + putenv('APP_LOCALE'); + unset($_ENV['APP_LOCALE'], $_SERVER['APP_LOCALE']); + } else { + putenv('APP_LOCALE='.$appLocale); + $_ENV['APP_LOCALE'] = $appLocale; + $_SERVER['APP_LOCALE'] = $appLocale; + } + + try { + $config = require base_path('config/app.php'); + + return $config['locale']; + } finally { + putenv('APP_LOCALE'); + unset($_ENV['APP_LOCALE'], $_SERVER['APP_LOCALE']); + } + }; + + it('maps a legacy two-letter APP_LOCALE to its current locale code', function () use ($resolveLocale) { + expect($resolveLocale('es'))->toBe('es-ES') + ->and($resolveLocale('de'))->toBe('de-DE') + ->and($resolveLocale('fr'))->toBe('fr-FR'); + }); + + it('maps a legacy region APP_LOCALE to its current locale code', function () use ($resolveLocale) { + expect($resolveLocale('zh-cn'))->toBe('zh-CN') + ->and($resolveLocale('zh-tw'))->toBe('zh-TW'); + }); + + it('pins the ambiguous legacy codes deliberately', function () use ($resolveLocale) { + // "en" must resolve to en-US, not the en-x-pirate novelty locale. + expect($resolveLocale('en'))->toBe('en-US') + // "sr" must resolve to the prior single Serbian translation. + ->and($resolveLocale('sr'))->toBe('sr-CS'); + }); + + it('is case-insensitive on legacy short codes', function () use ($resolveLocale) { + expect($resolveLocale('ES'))->toBe('es-ES') + ->and($resolveLocale('Zh-Cn'))->toBe('zh-CN'); + }); + + it('leaves an already-current locale code unchanged', function () use ($resolveLocale) { + expect($resolveLocale('es-ES'))->toBe('es-ES') + ->and($resolveLocale('en-US'))->toBe('en-US'); + }); + + it('leaves unknown or custom locale codes unchanged', function () use ($resolveLocale) { + expect($resolveLocale('xx-YY'))->toBe('xx-YY') + ->and($resolveLocale('en-x-pirate'))->toBe('en-x-pirate'); + }); + + it('falls back to en-US when APP_LOCALE is unset', function () use ($resolveLocale) { + expect($resolveLocale(null))->toBe('en-US'); + }); +}); + describe('empty string translation fallback', function () { it('falls back to the fallback locale for empty (untranslated) strings', function () { Config::set('app.fallback_locale', 'en-US');