diff --git a/CHANGELOG.md b/CHANGELOG.md index 5721084..143b587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.3.3 - 2022-01-14 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Normalize the locale file name before searching for it in `MessageLoader`, to account for differences in case, as well as filesystem case sensitivity (e.g. "en-XB" vs. "en_xb") + ## 0.3.2 - 2021-12-17 ### Added diff --git a/src/Extractor/Parser/Descriptor/DescriptorCollectorVisitor.php b/src/Extractor/Parser/Descriptor/DescriptorCollectorVisitor.php index 8a8d931..bcdfc62 100644 --- a/src/Extractor/Parser/Descriptor/DescriptorCollectorVisitor.php +++ b/src/Extractor/Parser/Descriptor/DescriptorCollectorVisitor.php @@ -49,12 +49,13 @@ */ class DescriptorCollectorVisitor extends NodeVisitorAbstract { + public ParserErrorCollection $errors; + private DescriptorCollection $descriptors; private string $filePath; private bool $preserveWhitespace; private IdInterpolator $idInterpolator; private string $idInterpolatorPattern; - private ParserErrorCollection $errors; private bool $addGeneratedIdsToSourceCode; /** diff --git a/src/Extractor/Parser/Descriptor/PragmaCollectorVisitor.php b/src/Extractor/Parser/Descriptor/PragmaCollectorVisitor.php index 951d9e9..4ebe8f3 100644 --- a/src/Extractor/Parser/Descriptor/PragmaCollectorVisitor.php +++ b/src/Extractor/Parser/Descriptor/PragmaCollectorVisitor.php @@ -44,6 +44,8 @@ */ class PragmaCollectorVisitor extends NodeVisitorAbstract { + public ParserErrorCollection $errors; + /** * @var array */ @@ -51,7 +53,6 @@ class PragmaCollectorVisitor extends NodeVisitorAbstract private string $filePath; private ?string $pragmaName; - private ParserErrorCollection $errors; public function __construct(string $filePath, string $pragmaName, ParserErrorCollection $errors) { @@ -132,6 +133,11 @@ private function parseMetadata(string $metadata): void preg_match_all('/(([a-z0-9_\-]+):([a-z0-9_\-]+))+/i', $metadata, $matches); + /** + * @psalm-suppress UnnecessaryVarAnnotation + * @var int $index + * @var string $propertyName + */ foreach ($matches[2] as $index => $propertyName) { $compareParsed .= preg_replace('/\s+/', '', strtolower("$propertyName:{$matches[3][$index]}")); $this->parsedPragma[$propertyName] = $matches[3][$index]; diff --git a/src/FormatPHP.php b/src/FormatPHP.php index 8d45200..0ef8bdb 100644 --- a/src/FormatPHP.php +++ b/src/FormatPHP.php @@ -79,7 +79,7 @@ public function formatMessage(array $descriptor, array $values = []): string } catch (UnableToGenerateMessageIdException $exception) { throw new InvalidArgumentException( 'The message descriptor must have an ID or default message', - is_int($exception->getCode()) ? $exception->getCode() : 0, + is_int($exception->getCode()) ? $exception->getCode() : 0, // @phpstan-ignore-line $exception, ); } diff --git a/src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php b/src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php index 1bcba91..a686a4e 100644 --- a/src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php +++ b/src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php @@ -118,7 +118,7 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option '"e..eee" (weekday) patterns are not supported', ); } - $options->weekday = ['short', 'long', 'narrow', 'short'][$length - 4]; + $options->weekday = $this->getWeekdayValue($length - 4); break; case 'c': @@ -127,7 +127,7 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option '"c..ccc" (weekday) patterns are not supported', ); } - $options->weekday = ['short', 'long', 'narrow', 'short'][$length - 4]; + $options->weekday = $this->getWeekdayValue($length - 4); break; // Period @@ -202,4 +202,28 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option ); } } + + /** + * @psalm-return "long" | "narrow" | "short" + */ + private function getWeekdayValue(int $index): string + { + switch ($index) { + case 1: + $value = 'long'; + + break; + case 2: + $value = 'narrow'; + + break; + case 0: + default: + $value = 'short'; + + break; + } + + return $value; + } } diff --git a/src/MessageLoader.php b/src/MessageLoader.php index cd2d994..3050cbd 100644 --- a/src/MessageLoader.php +++ b/src/MessageLoader.php @@ -35,11 +35,16 @@ use function array_filter; use function array_unique; use function array_values; +use function file_exists; use function implode; use function is_callable; +use function scandir; use function sprintf; +use function str_replace; +use function strtolower; use const DIRECTORY_SEPARATOR; +use const SCANDIR_SORT_NONE; /** * Loads messages for a given locale from the file system or cache @@ -48,6 +53,8 @@ */ class MessageLoader { + private const MESSAGE_FILE_EXTENSION = '.json'; + private ConfigInterface $config; private FileSystemHelper $fileSystemHelper; private FormatHelper $formatHelper; @@ -108,8 +115,9 @@ private function getLocaleMessages(): array foreach ($this->getFallbackLocales() as $locale) { try { - $messagesFile = $this->messagesDirectory . DIRECTORY_SEPARATOR . $locale . '.json'; - $messagesContents = $this->fileSystemHelper->getJsonContents($messagesFile); + $messagesContents = $this->fileSystemHelper->getJsonContents( + $this->getFilePathForLocale($locale), + ); break; } catch (UnableToProcessFileException $exception) { @@ -119,8 +127,9 @@ private function getLocaleMessages(): array if ($messagesContents === false) { throw new LocaleNotFoundException(sprintf( - 'Unable to find a suitable locale for "%s"; please set a default locale', + 'Unable to find a suitable locale for "%s" in %s; please set a default locale', $this->config->getLocale()->toString(), + $this->messagesDirectory, )); } @@ -171,4 +180,30 @@ private function loadFormatReader($formatReader): callable return $this->formatHelper->getReader($formatReader); } + + private function getFilePathForLocale(string $locale): string + { + $filePath = $this->messagesDirectory . DIRECTORY_SEPARATOR . $locale . self::MESSAGE_FILE_EXTENSION; + if (file_exists($filePath)) { + return $filePath; + } + + // If the file doesn't exist, check for alternate casings and notations. + // e.g., en-XB, en_XB, en-xb, en_xb, EN-XB, EN_XB, eN-xB, etc. + $normalize = fn (string $filename): string => str_replace('_', '-', strtolower($filename)); + $searchFile = $normalize($locale . self::MESSAGE_FILE_EXTENSION); + $localeFiles = scandir($this->messagesDirectory, SCANDIR_SORT_NONE) ?: []; + + foreach ($localeFiles as $localeFile) { + if ($normalize($localeFile) === $searchFile) { + return $this->messagesDirectory . DIRECTORY_SEPARATOR . $localeFile; + } + } + + throw new UnableToProcessFileException(sprintf( + 'Could not find file for locale "%s" in %s', + $locale, + $this->messagesDirectory, + )); + } } diff --git a/tests/MessageLoaderTest.php b/tests/MessageLoaderTest.php index 495caf9..bdc9e92 100644 --- a/tests/MessageLoaderTest.php +++ b/tests/MessageLoaderTest.php @@ -44,17 +44,22 @@ public function testExceptionWhenUnableToFindSuitableLocale(): void // Esperanto, Latin script, US region. $locale = new Locale('eo-Latn-US'); + $messagesDirectory = __DIR__ . '/fixtures/locales'; + /** @var ReaderInterface $reader */ $reader = $this->mockery(ReaderInterface::class); $loader = new MessageLoader( - __DIR__ . '/fixtures/locales', + $messagesDirectory, new Config($locale), $reader, ); $this->expectException(LocaleNotFoundException::class); - $this->expectExceptionMessage('Unable to find a suitable locale for "eo-Latn-US"; please set a default locale'); + $this->expectExceptionMessage( + 'Unable to find a suitable locale for "eo-Latn-US" in ' . $messagesDirectory + . '; please set a default locale', + ); $loader->loadMessages(); } @@ -144,4 +149,26 @@ public function provideCustomReader(): array ['customReader' => null], ]; } + + public function testLoadMessagesNormalizesFilenames(): void + { + $locale = new Locale('en-XB'); + $defaultLocale = new Locale('en'); + + $loader = new MessageLoader( + __DIR__ . '/fixtures/locales', + new Config($locale, $defaultLocale), + new FormatPHPReader(), + ); + + $collection = $loader->loadMessages(); + + $this->assertGreaterThanOrEqual(1, $collection->count()); + $this->assertNotNull($collection['about.inspire']); + $this->assertInstanceOf(MessageInterface::class, $collection['about.inspire']); + $this->assertSame( + '[!! Ḁṭ Ṡǩíííĺĺśśśḫâŕŕŕè, ẘè èṁṗṗṗŏẘèèèŕ ṁṁṁèṁḃḃḃèŕśśś ṭŏŏŏ ĝèèèṭ íííńśṗṗṗíŕèèèḋ. !!]', + $collection['about.inspire']->getMessage(), + ); + } } diff --git a/tests/fixtures/locales/en_xb.json b/tests/fixtures/locales/en_xb.json new file mode 100644 index 0000000..398ddec --- /dev/null +++ b/tests/fixtures/locales/en_xb.json @@ -0,0 +1,14 @@ +{ + "about.inspire": { + "defaultMessage": "[!! Ḁṭ Ṡǩíííĺĺśśśḫâŕŕŕè, ẘè èṁṗṗṗŏẘèèèŕ ṁṁṁèṁḃḃḃèŕśśś ṭŏŏŏ ĝèèèṭ íííńśṗṗṗíŕèèèḋ. !!]" + }, + "how.many.pets": { + "defaultMessage": "[!! Ļâśśśṭ ṭṭṭíṁèèè Ḭ ćḫèèèćǩèèèḋ, !!]{gender, select, male{[!! ḫè !!][!! ḫâââḋ !!]} female{[!! śḫèèè !!][!! ḫâââḋ !!]} other{[!! ṭḫèèèẏ !!][!! ḫâââḋ !!]}}[!! !!]{petCount, plural, =0{[!! ńŏ !!][!! ṗèèèṭś !!]} =1{[!! â !!][!! ṗèèèṭ !!]} other{#[!! ṗèèèṭś !!]}}[!! . !!]" + }, + "start.with.tag": { + "defaultMessage": "{argument}" + }, + "start.with.argument": { + "defaultMessage": "{argument}" + } +}