Skip to content

Commit e7decc6

Browse files
authored
fix: match locale files with different casings and notations (#33)
1 parent b61ad44 commit e7decc6

8 files changed

Lines changed: 139 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## 0.3.3 - 2022-01-14
9+
10+
### Added
11+
12+
- Nothing.
13+
14+
### Changed
15+
16+
- Nothing.
17+
18+
### Deprecated
19+
20+
- Nothing.
21+
22+
### Removed
23+
24+
- Nothing.
25+
26+
### Fixed
27+
28+
- 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")
29+
830
## 0.3.2 - 2021-12-17
931

1032
### Added

src/Extractor/Parser/Descriptor/DescriptorCollectorVisitor.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@
4949
*/
5050
class DescriptorCollectorVisitor extends NodeVisitorAbstract
5151
{
52+
public ParserErrorCollection $errors;
53+
5254
private DescriptorCollection $descriptors;
5355
private string $filePath;
5456
private bool $preserveWhitespace;
5557
private IdInterpolator $idInterpolator;
5658
private string $idInterpolatorPattern;
57-
private ParserErrorCollection $errors;
5859
private bool $addGeneratedIdsToSourceCode;
5960

6061
/**

src/Extractor/Parser/Descriptor/PragmaCollectorVisitor.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@
4444
*/
4545
class PragmaCollectorVisitor extends NodeVisitorAbstract
4646
{
47+
public ParserErrorCollection $errors;
48+
4749
/**
4850
* @var array<string, string>
4951
*/
5052
private array $parsedPragma = [];
5153

5254
private string $filePath;
5355
private ?string $pragmaName;
54-
private ParserErrorCollection $errors;
5556

5657
public function __construct(string $filePath, string $pragmaName, ParserErrorCollection $errors)
5758
{
@@ -132,6 +133,11 @@ private function parseMetadata(string $metadata): void
132133

133134
preg_match_all('/(([a-z0-9_\-]+):([a-z0-9_\-]+))+/i', $metadata, $matches);
134135

136+
/**
137+
* @psalm-suppress UnnecessaryVarAnnotation
138+
* @var int $index
139+
* @var string $propertyName
140+
*/
135141
foreach ($matches[2] as $index => $propertyName) {
136142
$compareParsed .= preg_replace('/\s+/', '', strtolower("$propertyName:{$matches[3][$index]}"));
137143
$this->parsedPragma[$propertyName] = $matches[3][$index];

src/FormatPHP.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function formatMessage(array $descriptor, array $values = []): string
7979
} catch (UnableToGenerateMessageIdException $exception) {
8080
throw new InvalidArgumentException(
8181
'The message descriptor must have an ID or default message',
82-
is_int($exception->getCode()) ? $exception->getCode() : 0,
82+
is_int($exception->getCode()) ? $exception->getCode() : 0, // @phpstan-ignore-line
8383
$exception,
8484
);
8585
}

src/Icu/MessageFormat/Parser/DateTimeSkeletonParser.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option
118118
'"e..eee" (weekday) patterns are not supported',
119119
);
120120
}
121-
$options->weekday = ['short', 'long', 'narrow', 'short'][$length - 4];
121+
$options->weekday = $this->getWeekdayValue($length - 4);
122122

123123
break;
124124
case 'c':
@@ -127,7 +127,7 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option
127127
'"c..ccc" (weekday) patterns are not supported',
128128
);
129129
}
130-
$options->weekday = ['short', 'long', 'narrow', 'short'][$length - 4];
130+
$options->weekday = $this->getWeekdayValue($length - 4);
131131

132132
break;
133133
// Period
@@ -202,4 +202,28 @@ private function setOption(string $skeletonOption, DateTimeFormatOptions $option
202202
);
203203
}
204204
}
205+
206+
/**
207+
* @psalm-return "long" | "narrow" | "short"
208+
*/
209+
private function getWeekdayValue(int $index): string
210+
{
211+
switch ($index) {
212+
case 1:
213+
$value = 'long';
214+
215+
break;
216+
case 2:
217+
$value = 'narrow';
218+
219+
break;
220+
case 0:
221+
default:
222+
$value = 'short';
223+
224+
break;
225+
}
226+
227+
return $value;
228+
}
205229
}

src/MessageLoader.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@
3535
use function array_filter;
3636
use function array_unique;
3737
use function array_values;
38+
use function file_exists;
3839
use function implode;
3940
use function is_callable;
41+
use function scandir;
4042
use function sprintf;
43+
use function str_replace;
44+
use function strtolower;
4145

4246
use const DIRECTORY_SEPARATOR;
47+
use const SCANDIR_SORT_NONE;
4348

4449
/**
4550
* Loads messages for a given locale from the file system or cache
@@ -48,6 +53,8 @@
4853
*/
4954
class MessageLoader
5055
{
56+
private const MESSAGE_FILE_EXTENSION = '.json';
57+
5158
private ConfigInterface $config;
5259
private FileSystemHelper $fileSystemHelper;
5360
private FormatHelper $formatHelper;
@@ -108,8 +115,9 @@ private function getLocaleMessages(): array
108115

109116
foreach ($this->getFallbackLocales() as $locale) {
110117
try {
111-
$messagesFile = $this->messagesDirectory . DIRECTORY_SEPARATOR . $locale . '.json';
112-
$messagesContents = $this->fileSystemHelper->getJsonContents($messagesFile);
118+
$messagesContents = $this->fileSystemHelper->getJsonContents(
119+
$this->getFilePathForLocale($locale),
120+
);
113121

114122
break;
115123
} catch (UnableToProcessFileException $exception) {
@@ -119,8 +127,9 @@ private function getLocaleMessages(): array
119127

120128
if ($messagesContents === false) {
121129
throw new LocaleNotFoundException(sprintf(
122-
'Unable to find a suitable locale for "%s"; please set a default locale',
130+
'Unable to find a suitable locale for "%s" in %s; please set a default locale',
123131
$this->config->getLocale()->toString(),
132+
$this->messagesDirectory,
124133
));
125134
}
126135

@@ -171,4 +180,30 @@ private function loadFormatReader($formatReader): callable
171180

172181
return $this->formatHelper->getReader($formatReader);
173182
}
183+
184+
private function getFilePathForLocale(string $locale): string
185+
{
186+
$filePath = $this->messagesDirectory . DIRECTORY_SEPARATOR . $locale . self::MESSAGE_FILE_EXTENSION;
187+
if (file_exists($filePath)) {
188+
return $filePath;
189+
}
190+
191+
// If the file doesn't exist, check for alternate casings and notations.
192+
// e.g., en-XB, en_XB, en-xb, en_xb, EN-XB, EN_XB, eN-xB, etc.
193+
$normalize = fn (string $filename): string => str_replace('_', '-', strtolower($filename));
194+
$searchFile = $normalize($locale . self::MESSAGE_FILE_EXTENSION);
195+
$localeFiles = scandir($this->messagesDirectory, SCANDIR_SORT_NONE) ?: [];
196+
197+
foreach ($localeFiles as $localeFile) {
198+
if ($normalize($localeFile) === $searchFile) {
199+
return $this->messagesDirectory . DIRECTORY_SEPARATOR . $localeFile;
200+
}
201+
}
202+
203+
throw new UnableToProcessFileException(sprintf(
204+
'Could not find file for locale "%s" in %s',
205+
$locale,
206+
$this->messagesDirectory,
207+
));
208+
}
174209
}

tests/MessageLoaderTest.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,22 @@ public function testExceptionWhenUnableToFindSuitableLocale(): void
4444
// Esperanto, Latin script, US region.
4545
$locale = new Locale('eo-Latn-US');
4646

47+
$messagesDirectory = __DIR__ . '/fixtures/locales';
48+
4749
/** @var ReaderInterface $reader */
4850
$reader = $this->mockery(ReaderInterface::class);
4951

5052
$loader = new MessageLoader(
51-
__DIR__ . '/fixtures/locales',
53+
$messagesDirectory,
5254
new Config($locale),
5355
$reader,
5456
);
5557

5658
$this->expectException(LocaleNotFoundException::class);
57-
$this->expectExceptionMessage('Unable to find a suitable locale for "eo-Latn-US"; please set a default locale');
59+
$this->expectExceptionMessage(
60+
'Unable to find a suitable locale for "eo-Latn-US" in ' . $messagesDirectory
61+
. '; please set a default locale',
62+
);
5863

5964
$loader->loadMessages();
6065
}
@@ -144,4 +149,26 @@ public function provideCustomReader(): array
144149
['customReader' => null],
145150
];
146151
}
152+
153+
public function testLoadMessagesNormalizesFilenames(): void
154+
{
155+
$locale = new Locale('en-XB');
156+
$defaultLocale = new Locale('en');
157+
158+
$loader = new MessageLoader(
159+
__DIR__ . '/fixtures/locales',
160+
new Config($locale, $defaultLocale),
161+
new FormatPHPReader(),
162+
);
163+
164+
$collection = $loader->loadMessages();
165+
166+
$this->assertGreaterThanOrEqual(1, $collection->count());
167+
$this->assertNotNull($collection['about.inspire']);
168+
$this->assertInstanceOf(MessageInterface::class, $collection['about.inspire']);
169+
$this->assertSame(
170+
'[!! Ḁṭ Ṡǩíííĺĺśśśḫâŕŕŕè, ẘè èṁṗṗṗŏẘèèèŕ ṁṁṁèṁḃḃḃèŕśśś ṭŏŏŏ ĝèèèṭ íííńśṗṗṗíŕèèèḋ. !!]',
171+
$collection['about.inspire']->getMessage(),
172+
);
173+
}
147174
}

tests/fixtures/locales/en_xb.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"about.inspire": {
3+
"defaultMessage": "[!! Ḁṭ Ṡǩíííĺĺśśśḫâŕŕŕè, ẘè èṁṗṗṗŏẘèèèŕ ṁṁṁèṁḃḃḃèŕśśś ṭŏŏŏ ĝèèèṭ íííńśṗṗṗíŕèèèḋ. !!]"
4+
},
5+
"how.many.pets": {
6+
"defaultMessage": "[!! Ļâśśśṭ ṭṭṭíṁèèè Ḭ ćḫèèèćǩèèèḋ, !!]{gender, select, male{<italicized>[!! ḫè !!]</italicized>[!! ḫâââḋ !!]} female{<italicized>[!! śḫèèè !!]</italicized>[!! ḫâââḋ !!]} other{<italicized>[!! ṭḫèèèẏ !!]</italicized>[!! ḫâââḋ !!]}}[!! !!]{petCount, plural, =0{<bold>[!! ńŏ !!]</bold>[!! ṗèèèṭś !!]} =1{<bold>[!! â !!]</bold>[!! ṗèèèṭ !!]} other{<bold>#</bold>[!! ṗèèèṭś !!]}}[!! . !!]"
7+
},
8+
"start.with.tag": {
9+
"defaultMessage": "<foo>{argument}</foo>"
10+
},
11+
"start.with.argument": {
12+
"defaultMessage": "{argument}"
13+
}
14+
}

0 commit comments

Comments
 (0)