{"record":{"id":"2dcfbc44bc2d08ec","repo":"PHPOffice/PhpSpreadsheet","slug":"unable-to-read-locale-data-for-locale","errorCode":null,"errorMessage":"Unable to read locale data for '{$locale}'","messagePattern":"Unable to read locale data for '(.+?)'","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php","lineNumber":25,"sourceCode":"\nfinal class Locale\n{\n    /**\n     * Language code: ISO-639 2 character, alpha.\n     * Optional script code: ISO-15924 4 alpha.\n     * Optional country code: ISO-3166-1, 2 character alpha.\n     * Separated by underscores or dashes.\n     */\n    public const STRUCTURE = '/^(?P<language>[a-z]{2})([-_](?P<script>[a-z]{4}))?([-_](?P<country>[a-z]{2}))?$/i';\n\n    private NumberFormatter $formatter;\n\n    public function __construct(?string $locale, int $style)\n    {\n        $formatterLocale = str_replace('-', '_', $locale ?? '');\n        $this->formatter = new NumberFormatter($formatterLocale, $style);\n        if ($this->formatter->getLocale() !== $formatterLocale) {\n            throw new Exception(\"Unable to read locale data for '{$locale}'\");\n        }\n    }\n\n    public function format(bool $stripRlm = true): string\n    {\n        $str = $this->formatter->getPattern();\n\n        return ($stripRlm && str_starts_with($str, \"\\xe2\\x80\\x8f\")) ? substr($str, 3) : $str;\n    }\n}\n","sourceCodeStart":7,"sourceCodeEnd":36,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Style/NumberFormat/Wizard/Locale.php#L7-L36","documentation":"The Locale helper used by all NumberFormat Wizards creates a NumberFormatter for the requested locale, then compares the locale ICU actually resolved. ICU silently falls back to a root/default locale when it has no data for the requested one, so a mismatch between requested and returned locale means the locale is unknown or its data is not installed on this system. The offending locale is embedded in the message.","triggerScenarios":"Calling setLocale('en-UK'), setLocale('xx-YY') or any code that passes the STRUCTURE regex but has no ICU locale data; e.g. $wizard->setLocale('en-UK') where the valid form is 'en-GB'.","commonSituations":"Typo'd locale strings; codes valid in other ecosystems (Java, CLDR aliases) but not installed in the system ICU; slim Docker images where ICU locale data was pruned.","solutions":["Correct the locale code to a standard ISO-639 language + ISO-3166 country pair ICU knows ('en-GB', 'de-DE', 'fr-FR')","Pre-verify the locale resolves: create a probe NumberFormatter and confirm ->getLocale() matches the requested locale (mirroring the library's own check)","Catch the exception and fall back to a non-localized wizard or a hardcoded format mask"],"exampleFix":"// before\n$wizard->setLocale('en-UK');\n\n// after\n$wizard->setLocale('en-GB');","handlingStrategy":"validation","validationCode":"function icuKnowsLocale(string $locale): bool\n{\n    $fmt = new \\NumberFormatter(str_replace('-', '_', $locale), \\NumberFormatter::DECIMAL);\n\n    return $fmt->getLocale() === str_replace('-', '_', $locale);\n}\n\n$locale = icuKnowsLocale('en-GB') ? 'en-GB' : 'en';\n$wizard->setLocale($locale);","typeGuard":null,"tryCatchPattern":"try {\n    $wizard->setLocale($requestedLocale);\n} catch (\\PhpOffice\\PhpSpreadsheet\\Exception $e) {\n    // unknown locale: degrade to the non-localized wizard\n    $wizard = new Number(2);\n}","preventionTips":["Keep a whitelist of locales your app actually supports","Test with a probe NumberFormatter in CI on the production image","Normalize codes early (en_UK -> en-GB) instead of trusting input"],"tags":["intl","locale","icu","number-format","phpspreadsheet"],"backgroundTag":"unsupported-locale","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}