Skip to content

Commit a9bd712

Browse files
authored
feat: add MessageLoader for loading translation scripts from JSON files (#13)
1 parent 4200ec5 commit a9bd712

17 files changed

Lines changed: 935 additions & 0 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Exception;
24+
25+
use FormatPHP\Reader\FormatInterface;
26+
use RuntimeException as PhpRuntimeException;
27+
28+
/**
29+
* Thrown when reading a message that doesn't conform to the expected shape
30+
*
31+
* @see FormatInterface
32+
*/
33+
class InvalidMessageShapeException extends PhpRuntimeException implements FormatPHPExceptionInterface
34+
{
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Exception;
24+
25+
use RuntimeException as PhpRuntimeException;
26+
27+
/**
28+
* Thrown when unable to find a given locale messages file
29+
*/
30+
class LocaleNotFoundException extends PhpRuntimeException implements FormatPHPExceptionInterface
31+
{
32+
}

src/MessageLoader.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP;
24+
25+
use FormatPHP\Exception\InvalidArgumentException;
26+
use FormatPHP\Exception\InvalidMessageShapeException;
27+
use FormatPHP\Exception\LocaleNotFoundException;
28+
use FormatPHP\Exception\UnableToProcessFileException;
29+
use FormatPHP\Intl\Locale;
30+
use FormatPHP\Intl\LocaleInterface;
31+
use FormatPHP\Reader\FormatInterface;
32+
use FormatPHP\Util\FileSystemHelper;
33+
34+
use function array_filter;
35+
use function array_unique;
36+
use function array_values;
37+
use function implode;
38+
use function sprintf;
39+
40+
use const DIRECTORY_SEPARATOR;
41+
42+
/**
43+
* Loads messages for a given locale from the file system or cache
44+
*/
45+
final class MessageLoader
46+
{
47+
private Config $config;
48+
private FileSystemHelper $fileSystemHelper;
49+
private FormatInterface $formatReader;
50+
private string $messagesDirectory;
51+
52+
/**
53+
* @throws InvalidArgumentException
54+
*/
55+
public function __construct(
56+
string $messagesDirectory,
57+
Config $config,
58+
FormatInterface $formatReader,
59+
?FileSystemHelper $fileSystemHelper = null
60+
) {
61+
$this->config = $config;
62+
$this->formatReader = $formatReader;
63+
$this->fileSystemHelper = $fileSystemHelper ?? new FileSystemHelper();
64+
$this->messagesDirectory = $this->fileSystemHelper->getRealPath($messagesDirectory);
65+
66+
if (!$this->fileSystemHelper->isDirectory($this->messagesDirectory)) {
67+
throw new InvalidArgumentException(sprintf(
68+
'Messages directory "%s" is not a valid directory',
69+
$messagesDirectory,
70+
));
71+
}
72+
}
73+
74+
/**
75+
* Returns a MessageCollection according to the configuration provided to
76+
* this MessageLoader
77+
*
78+
* @throws InvalidArgumentException
79+
* @throws InvalidMessageShapeException
80+
* @throws LocaleNotFoundException
81+
*/
82+
public function loadMessages(): MessageCollection
83+
{
84+
[$messagesData, $resolvedLocale] = $this->getLocaleMessages();
85+
86+
return ($this->formatReader)($this->config, $messagesData, $resolvedLocale);
87+
}
88+
89+
/**
90+
* @return array{0: array<array-key, mixed>, 1: LocaleInterface}
91+
*
92+
* @throws InvalidArgumentException
93+
* @throws LocaleNotFoundException
94+
*/
95+
private function getLocaleMessages(): array
96+
{
97+
$messagesContents = false;
98+
$localeResolved = null;
99+
100+
foreach ($this->getFallbackLocales() as $locale) {
101+
try {
102+
$messagesFile = $this->messagesDirectory . DIRECTORY_SEPARATOR . $locale . '.json';
103+
$messagesContents = $this->fileSystemHelper->getJsonContents($messagesFile);
104+
$localeResolved = new Locale($locale);
105+
106+
break;
107+
} catch (UnableToProcessFileException $exception) {
108+
continue;
109+
}
110+
}
111+
112+
if ($messagesContents === false || $localeResolved === null) {
113+
throw new LocaleNotFoundException(sprintf(
114+
'Unable to find a suitable locale for "%s"; please set a default locale',
115+
$this->config->getLocale()->toString(),
116+
));
117+
}
118+
119+
return [$messagesContents, $localeResolved];
120+
}
121+
122+
/**
123+
* @return string[]
124+
*/
125+
private function getFallbackLocales(): array
126+
{
127+
$locale = $this->config->getLocale();
128+
$defaultLocale = $this->config->getDefaultLocale();
129+
130+
$fallbacks = [
131+
$locale->toString(),
132+
$locale->baseName(),
133+
implode('-', array_filter([$locale->language(), $locale->region()])),
134+
$locale->language(),
135+
$defaultLocale ? $defaultLocale->toString() : null,
136+
];
137+
138+
/** @var string[] */
139+
return array_values(array_unique(array_filter($fallbacks)));
140+
}
141+
}

src/Reader/Format/FormatPHP.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Reader\Format;
24+
25+
use FormatPHP\Config;
26+
use FormatPHP\Exception\InvalidMessageShapeException;
27+
use FormatPHP\Intl\LocaleInterface;
28+
use FormatPHP\Message;
29+
use FormatPHP\MessageCollection;
30+
use FormatPHP\Reader\FormatInterface;
31+
32+
use function assert;
33+
use function gettype;
34+
use function is_array;
35+
use function is_string;
36+
use function sprintf;
37+
38+
/**
39+
* Returns a MessageCollection parsed from JSON-decoded data that was written
40+
* using Writer\Format\FormatPHP
41+
*
42+
* @see \FormatPHP\Writer\Format\FormatPHP
43+
*/
44+
class FormatPHP implements FormatInterface
45+
{
46+
/**
47+
* @inheritdoc
48+
*/
49+
public function __invoke(Config $config, array $data, LocaleInterface $localeResolved): MessageCollection
50+
{
51+
$messages = new MessageCollection($config);
52+
53+
foreach ($data as $messageId => $message) {
54+
$this->validateShape($messageId, $message);
55+
assert(is_string($messageId));
56+
assert(isset($message['defaultMessage']));
57+
assert(is_string($message['defaultMessage']));
58+
59+
$messages[] = new Message($localeResolved, $messageId, $message['defaultMessage']);
60+
}
61+
62+
return $messages;
63+
}
64+
65+
/**
66+
* @param array-key $messageId
67+
* @param mixed $message
68+
*
69+
* @throws InvalidMessageShapeException
70+
*
71+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
72+
*/
73+
private function validateShape($messageId, $message): void
74+
{
75+
if (!is_string($messageId)) {
76+
throw new InvalidMessageShapeException(sprintf(
77+
'%s expects a string message ID; received %s',
78+
self::class,
79+
gettype($messageId),
80+
));
81+
}
82+
83+
if (!is_array($message) || !is_string($message['defaultMessage'] ?? null)) {
84+
throw new InvalidMessageShapeException(sprintf(
85+
'%s expects a string defaultMessage property; defaultMessage does not exist or is not a string',
86+
self::class,
87+
));
88+
}
89+
}
90+
}

src/Reader/Format/Simple.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Reader\Format;
24+
25+
use FormatPHP\Config;
26+
use FormatPHP\Exception\InvalidMessageShapeException;
27+
use FormatPHP\Intl\LocaleInterface;
28+
use FormatPHP\Message;
29+
use FormatPHP\MessageCollection;
30+
use FormatPHP\Reader\FormatInterface;
31+
32+
use function assert;
33+
use function gettype;
34+
use function is_string;
35+
use function sprintf;
36+
37+
/**
38+
* Returns a MessageCollection parsed from JSON-decoded data that was written
39+
* using Writer\Format\Simple
40+
*
41+
* @see \FormatPHP\Writer\Format\Simple
42+
*/
43+
class Simple implements FormatInterface
44+
{
45+
/**
46+
* @inheritdoc
47+
*/
48+
public function __invoke(Config $config, array $data, LocaleInterface $localeResolved): MessageCollection
49+
{
50+
$messages = new MessageCollection($config);
51+
52+
foreach ($data as $messageId => $message) {
53+
$this->validateShape($messageId, $message);
54+
assert(is_string($messageId));
55+
assert(is_string($message));
56+
57+
$messages[$messageId] = new Message($localeResolved, $messageId, $message);
58+
}
59+
60+
return $messages;
61+
}
62+
63+
/**
64+
* @param array-key $messageId
65+
* @param mixed $message
66+
*
67+
* @throws InvalidMessageShapeException
68+
*
69+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
70+
*/
71+
private function validateShape($messageId, $message): void
72+
{
73+
if (!is_string($messageId)) {
74+
throw new InvalidMessageShapeException(sprintf(
75+
'%s expects a string message ID; received %s',
76+
self::class,
77+
gettype($messageId),
78+
));
79+
}
80+
81+
if (!is_string($message)) {
82+
throw new InvalidMessageShapeException(sprintf(
83+
'%s expects a string message; received %s',
84+
self::class,
85+
gettype($message),
86+
));
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)