├── src
├── Debug
│ ├── LanguageCollection.php
│ ├── icons
│ │ └── language.svg
│ └── LanguageCollector.php
├── FallbackLevel.php
└── Language.php
├── README.md
├── LICENSE
├── composer.json
└── .phpstorm.meta.php
/src/Debug/LanguageCollection.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Framework\Language\Debug;
11 |
12 | use Framework\Debug\Collection;
13 |
14 | /**
15 | * Class LanguageCollection.
16 | *
17 | * @package language
18 | */
19 | class LanguageCollection extends Collection
20 | {
21 | protected string $iconPath = __DIR__ . '/icons/language.svg';
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aplus Framework Language Library
4 |
5 | - [Home](https://aplus-framework.com/packages/language)
6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/language/index.html)
7 | - [API Documentation](https://docs.aplus-framework.com/packages/language.html)
8 |
9 | [](https://github.com/aplus-framework/language/actions/workflows/tests.yml)
10 | [](https://coveralls.io/github/aplus-framework/language?branch=master)
11 | [](https://packagist.org/packages/aplus/language)
12 | [](https://aplus-framework.com/sponsor)
13 |
--------------------------------------------------------------------------------
/src/FallbackLevel.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Framework\Language;
11 |
12 | /**
13 | * Enum FallbackLevel.
14 | *
15 | * @package language
16 | */
17 | enum FallbackLevel : int
18 | {
19 | /**
20 | * Disable fallback.
21 | *
22 | * Use language lines only from the given language.
23 | */
24 | case none = 0;
25 | /**
26 | * Fallback to parent language.
27 | *
28 | * If the given language is pt-BR and a line is not found, try to use the line of pt.
29 | *
30 | * NOTE: The parent locale must be set in the Supported Locales to this fallback work.
31 | */
32 | case parent = 1;
33 | /**
34 | * Fallback to default language.
35 | *
36 | * If the parent language is not found, try to use the default language.
37 | */
38 | case default = 2;
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Natan Felles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Debug/icons/language.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aplus/language",
3 | "description": "Aplus Framework Language Library",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": [
7 | "language",
8 | "lang",
9 | "locale",
10 | "localization",
11 | "internationalization",
12 | "i18n",
13 | "l10n",
14 | "intl"
15 | ],
16 | "authors": [
17 | {
18 | "name": "Natan Felles",
19 | "email": "natanfelles@gmail.com",
20 | "homepage": "https://natanfelles.github.io"
21 | }
22 | ],
23 | "homepage": "https://aplus-framework.com/packages/language",
24 | "support": {
25 | "email": "support@aplus-framework.com",
26 | "issues": "https://github.com/aplus-framework/language/issues",
27 | "forum": "https://aplus-framework.com/forum",
28 | "source": "https://github.com/aplus-framework/language",
29 | "docs": "https://docs.aplus-framework.com/guides/libraries/language/"
30 | },
31 | "funding": [
32 | {
33 | "type": "Aplus Sponsor",
34 | "url": "https://aplus-framework.com/sponsor"
35 | }
36 | ],
37 | "require": {
38 | "php": ">=8.3",
39 | "ext-intl": "*",
40 | "aplus/debug": "^4.3",
41 | "aplus/helpers": "^4.0"
42 | },
43 | "require-dev": {
44 | "ext-xdebug": "*",
45 | "aplus/coding-standard": "^2.8",
46 | "ergebnis/composer-normalize": "^2.25",
47 | "jetbrains/phpstorm-attributes": "^1.0",
48 | "phpmd/phpmd": "^2.13",
49 | "phpstan/phpstan": "^1.9",
50 | "phpunit/phpunit": "^10.5"
51 | },
52 | "minimum-stability": "dev",
53 | "prefer-stable": true,
54 | "autoload": {
55 | "psr-4": {
56 | "Framework\\Language\\": "src/"
57 | }
58 | },
59 | "autoload-dev": {
60 | "psr-4": {
61 | "Tests\\Language\\": "tests/"
62 | }
63 | },
64 | "config": {
65 | "allow-plugins": {
66 | "ergebnis/composer-normalize": true
67 | },
68 | "optimize-autoloader": true,
69 | "preferred-install": "dist",
70 | "sort-packages": true
71 | },
72 | "extra": {
73 | "branch-alias": {
74 | "dev-master": "4.x-dev"
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Debug/LanguageCollector.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Framework\Language\Debug;
11 |
12 | use Framework\Debug\Collector;
13 | use Framework\Debug\Debugger;
14 | use Framework\Language\Language;
15 |
16 | /**
17 | * Class LanguageCollector.
18 | *
19 | * @package language
20 | */
21 | class LanguageCollector extends Collector
22 | {
23 | protected Language $language;
24 |
25 | public function setLanguage(Language $language) : static
26 | {
27 | $this->language = $language;
28 | return $this;
29 | }
30 |
31 | public function getActivities() : array
32 | {
33 | $activities = [];
34 | foreach ($this->getData() as $index => $data) {
35 | $activities[] = [
36 | 'collector' => $this->getName(),
37 | 'class' => static::class,
38 | 'description' => 'Render message ' . ($index + 1),
39 | 'start' => $data['start'],
40 | 'end' => $data['end'],
41 | ];
42 | }
43 | return $activities;
44 | }
45 |
46 | public function getContents() : string
47 | {
48 | if (!isset($this->language)) {
49 | return '
A Language instance has not been set on this collector.
';
50 | }
51 | \ob_start(); ?>
52 | Default Locale: =
53 | \htmlentities($this->language->getDefaultLocale())
54 | ?>
55 | Current Locale: =
56 | \htmlentities($this->language->getCurrentLocale())
57 | ?>
58 | Supported Locales: =
59 | \htmlentities(\implode(', ', $this->language->getSupportedLocales()))
60 | ?>
61 | Fallback Level: language->getFallbackLevel();
63 | echo "{$level->value} ({$level->name})"; ?>
64 | Rendered Messages
65 | = $this->renderRenderedMessages() ?>
66 | Directories
67 | = $this->renderDirectories() ?>
68 | Available Messages
69 | renderLines();
71 | return \ob_get_clean(); // @phpstan-ignore-line
72 | }
73 |
74 | protected function renderRenderedMessages() : string
75 | {
76 | if (!$this->hasData()) {
77 | return 'No message has been rendered.
';
78 | }
79 | $count = \count($this->getData());
80 | \ob_start(); ?>
81 | = $count ?> message= $count === 1 ? '' : 's' ?> has been rendered
82 | in = $this->getMessagesRenderingTime() ?> ms:
83 |
84 |
85 |
86 |
87 | | # |
88 | File |
89 | Line |
90 | Message |
91 | Locale |
92 | Time |
93 |
94 |
95 |
96 | getData() as $index => $data): ?>
97 |
98 | | = $index + 1 ?> |
99 | = \htmlentities($data['file']) ?> |
100 | = \htmlentities($data['line']) ?> |
101 |
102 | = \htmlentities($data['message']) ?>
103 | |
104 | = \htmlentities($data['locale']) ?> |
105 | = Debugger::roundSecondsToMilliseconds($data['end'] - $data['start']) ?> |
106 |
107 |
108 |
109 |
110 | getData() as $data) {
118 | $total = $data['end'] - $data['start'];
119 | $time += $total;
120 | }
121 | return Debugger::roundSecondsToMilliseconds($time);
122 | }
123 |
124 | protected function renderDirectories() : string
125 | {
126 | $directories = $this->language->getDirectories();
127 | if (empty($directories)) {
128 | return 'No directory set for this Language instance.
';
129 | }
130 | $count = \count($directories);
131 | \ob_start(); ?>
132 | There = $count === 1 ? 'is 1 directory' : "are {$count} directories" ?> set.
133 |
134 |
135 |
136 | | # |
137 | Directory |
138 |
139 |
140 |
141 | language->getDirectories() as $index => $directory): ?>
142 |
143 | | = $index + 1 ?> |
144 | = \htmlentities($directory) ?> |
145 |
146 |
147 |
148 |
149 | getLines();
156 | if (empty($lines)) {
157 | return 'No file lines available for this Language instance.
';
158 | }
159 | $count = \count($lines);
160 | \ob_start(); ?>
161 | There = $count === 1 ? 'is 1 message line' : "are {$count} message lines"
162 | ?> available to the current locale (= $this->language->getCurrentLocale() ?>).
163 |
164 |
165 |
166 |
167 | | # |
168 | File |
169 | Line |
170 | Message Pattern |
171 | Locale |
172 | Fallback |
173 |
174 |
175 |
176 | $line): ?>
177 |
178 | | = $index + 1 ?> |
179 | = \htmlentities($line['file']) ?> |
180 | = \htmlentities($line['line']) ?> |
181 |
182 | = \htmlentities($line['message']) ?>
183 | |
184 | = \htmlentities($line['locale']) ?> |
185 | = \htmlentities($line['fallback']) ?> |
186 |
187 |
188 |
189 |
190 | >
196 | */
197 | protected function getLines() : array
198 | {
199 | $allLines = $this->language->getLines();
200 | $this->language->resetLines();
201 | $files = [];
202 | foreach ($this->language->getDirectories() as $directory) {
203 | foreach ((array) \glob($directory . '*/*.php') as $file) {
204 | $file = (string) $file;
205 | $pos = \strrpos($file, \DIRECTORY_SEPARATOR);
206 | $file = \substr($file, $pos + 1, -4);
207 | $files[$file] = true;
208 | }
209 | }
210 | $files = \array_keys($files);
211 | foreach ($files as $file) {
212 | $this->language->render($file, '.·*·.');
213 | }
214 | $result = [];
215 | foreach ($this->language->getLines() as $locale => $lines) {
216 | \ksort($lines);
217 | foreach ($lines as $file => $messages) {
218 | \ksort($messages);
219 | foreach ($messages as $line => $message) {
220 | foreach ($result as $data) {
221 | if ($data['file'] === $file && $data['line'] === $line) {
222 | continue 2;
223 | }
224 | }
225 | $result[] = [
226 | 'file' => $file,
227 | 'line' => $line,
228 | 'message' => $message,
229 | 'locale' => $locale,
230 | 'fallback' => $this->getFallbackName($locale),
231 | ];
232 | }
233 | }
234 | }
235 | \usort($result, static function ($str1, $str2) {
236 | $cmp = \strcmp($str1['file'], $str2['file']);
237 | if ($cmp === 0) {
238 | $cmp = \strcmp($str1['line'], $str2['line']);
239 | }
240 | return $cmp;
241 | });
242 | foreach ($allLines as $locale => $lines) {
243 | foreach ($lines as $file => $messages) {
244 | $this->language->addLines($locale, $file, $messages);
245 | }
246 | }
247 | return $result;
248 | }
249 |
250 | protected function getFallbackName(string $locale) : string
251 | {
252 | $currentLocale = $this->language->getCurrentLocale();
253 | if ($locale === $currentLocale) {
254 | return 'none';
255 | }
256 | $parentLocale = \explode('-', $currentLocale)[0];
257 | if ($locale === $parentLocale) {
258 | return 'parent';
259 | }
260 | if ($locale === $this->language->getDefaultLocale()) {
261 | return 'default';
262 | }
263 | return '';
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace PHPSTORM_META;
11 |
12 | registerArgumentsSet(
13 | 'date_styles',
14 | 'full',
15 | 'long',
16 | 'medium',
17 | 'short',
18 | );
19 | registerArgumentsSet(
20 | 'currencies',
21 | 'AED',
22 | 'AFN',
23 | 'ALL',
24 | 'AMD',
25 | 'ANG',
26 | 'AOA',
27 | 'ARS',
28 | 'AUD',
29 | 'AWG',
30 | 'AZN',
31 | 'BAM',
32 | 'BBD',
33 | 'BDT',
34 | 'BGN',
35 | 'BHD',
36 | 'BIF',
37 | 'BMD',
38 | 'BND',
39 | 'BOB',
40 | 'BOV',
41 | 'BRL',
42 | 'BSD',
43 | 'BTN',
44 | 'BWP',
45 | 'BYN',
46 | 'BZD',
47 | 'CAD',
48 | 'CDF',
49 | 'CHE',
50 | 'CHF',
51 | 'CHW',
52 | 'CLF',
53 | 'CLP',
54 | 'CNY',
55 | 'COU',
56 | 'CRC',
57 | 'CUC',
58 | 'CUP',
59 | 'CVE',
60 | 'CZK',
61 | 'DJF',
62 | 'DKK',
63 | 'DOP',
64 | 'DZD',
65 | 'EGP',
66 | 'ERN',
67 | 'ETB',
68 | 'EUR',
69 | 'FJD',
70 | 'FKP',
71 | 'GBP',
72 | 'GEL',
73 | 'GHS',
74 | 'GIP',
75 | 'GMD',
76 | 'GNF',
77 | 'GTQ',
78 | 'GYD',
79 | 'HKD',
80 | 'HNL',
81 | 'HRK',
82 | 'HTG',
83 | 'HUF',
84 | 'IDR',
85 | 'ILS',
86 | 'INR',
87 | 'IQD',
88 | 'IRR',
89 | 'ISK',
90 | 'JMD',
91 | 'JOD',
92 | 'JPY',
93 | 'KES',
94 | 'KGS',
95 | 'KHR',
96 | 'KMF',
97 | 'KPW',
98 | 'KRW',
99 | 'KWD',
100 | 'KYD',
101 | 'KZT',
102 | 'LAK',
103 | 'LBP',
104 | 'LKR',
105 | 'LRD',
106 | 'LSL',
107 | 'LYD',
108 | 'MAD',
109 | 'MDL',
110 | 'MGA',
111 | 'MKD',
112 | 'MMK',
113 | 'MNT',
114 | 'MOP',
115 | 'MRU',
116 | 'MUR',
117 | 'MVR',
118 | 'MWK',
119 | 'MXN',
120 | 'MXV',
121 | 'MYR',
122 | 'MZN',
123 | 'NAD',
124 | 'NGN',
125 | 'NIO',
126 | 'NOK',
127 | 'NPR',
128 | 'NZD',
129 | 'OMR',
130 | 'PAB',
131 | 'PEN',
132 | 'PGK',
133 | 'PHP',
134 | 'PKR',
135 | 'PLN',
136 | 'PYG',
137 | 'QAR',
138 | 'RON',
139 | 'RSD',
140 | 'RUB',
141 | 'RWF',
142 | 'SAR',
143 | 'SBD',
144 | 'SCR',
145 | 'SDG',
146 | 'SEK',
147 | 'SGD',
148 | 'SHP',
149 | 'SLL',
150 | 'SOS',
151 | 'SRD',
152 | 'SSP',
153 | 'STN',
154 | 'SVC',
155 | 'SYP',
156 | 'SZL',
157 | 'THB',
158 | 'TJS',
159 | 'TMT',
160 | 'TND',
161 | 'TOP',
162 | 'TRY',
163 | 'TTD',
164 | 'TWD',
165 | 'TZS',
166 | 'UAH',
167 | 'UGX',
168 | 'USD',
169 | 'USN',
170 | 'UYI',
171 | 'UYU',
172 | 'UYW',
173 | 'UZS',
174 | 'VED',
175 | 'VES',
176 | 'VND',
177 | 'VUV',
178 | 'WST',
179 | 'XAF',
180 | 'XAG',
181 | 'XAU',
182 | 'XBA',
183 | 'XBB',
184 | 'XBC',
185 | 'XBD',
186 | 'XCD',
187 | 'XDR',
188 | 'XOF',
189 | 'XPD',
190 | 'XPF',
191 | 'XPT',
192 | 'XSU',
193 | 'XTS',
194 | 'XUA',
195 | 'XXX',
196 | 'YER',
197 | 'ZAR',
198 | 'ZMW',
199 | 'ZWL',
200 | );
201 | registerArgumentsSet(
202 | 'fallback_levels',
203 | \Framework\Language\Language::FALLBACK_DEFAULT,
204 | \Framework\Language\Language::FALLBACK_NONE,
205 | \Framework\Language\Language::FALLBACK_PARENT,
206 | );
207 | registerArgumentsSet(
208 | 'locales',
209 | 'aa',
210 | 'ab',
211 | 'af',
212 | 'ak',
213 | 'als',
214 | 'am',
215 | 'an',
216 | 'ang',
217 | 'ang',
218 | 'ar',
219 | 'arc',
220 | 'as',
221 | 'ast',
222 | 'av',
223 | 'awa',
224 | 'ay',
225 | 'az',
226 | 'ba',
227 | 'bar',
228 | 'bat-smg',
229 | 'bcl',
230 | 'be',
231 | 'be-x-old',
232 | 'bg',
233 | 'bh',
234 | 'bi',
235 | 'bm',
236 | 'bn',
237 | 'bo',
238 | 'bpy',
239 | 'br',
240 | 'brx',
241 | 'bs',
242 | 'bug',
243 | 'bxr',
244 | 'ca',
245 | 'cdo',
246 | 'ce',
247 | 'ceb',
248 | 'ch',
249 | 'cho',
250 | 'chr',
251 | 'chy',
252 | 'ckb',
253 | 'co',
254 | 'cr',
255 | 'cs',
256 | 'csb',
257 | 'cu',
258 | 'cv',
259 | 'cy',
260 | 'da',
261 | 'de',
262 | 'diq',
263 | 'dsb',
264 | 'dv',
265 | 'dz',
266 | 'ee',
267 | 'el',
268 | 'en',
269 | 'eo',
270 | 'es',
271 | 'et',
272 | 'eu',
273 | 'ext',
274 | 'fa',
275 | 'ff',
276 | 'fi',
277 | 'fiu-vro',
278 | 'fj',
279 | 'fo',
280 | 'fr',
281 | 'frp',
282 | 'fur',
283 | 'fy',
284 | 'ga',
285 | 'gan',
286 | 'gbm',
287 | 'gd',
288 | 'gil',
289 | 'gl',
290 | 'gn',
291 | 'got',
292 | 'gu',
293 | 'gv',
294 | 'ha',
295 | 'hak',
296 | 'haw',
297 | 'he',
298 | 'hi',
299 | 'ho',
300 | 'hr',
301 | 'ht',
302 | 'hu',
303 | 'hy',
304 | 'hz',
305 | 'ia',
306 | 'id',
307 | 'ie',
308 | 'ig',
309 | 'ii',
310 | 'ik',
311 | 'ilo',
312 | 'inh',
313 | 'io',
314 | 'is',
315 | 'it',
316 | 'iu',
317 | 'ja',
318 | 'jbo',
319 | 'jv',
320 | 'ka',
321 | 'kg',
322 | 'khw',
323 | 'ki',
324 | 'kj',
325 | 'kk',
326 | 'kl',
327 | 'km',
328 | 'kn',
329 | 'ko',
330 | 'kr',
331 | 'ks',
332 | 'ksh',
333 | 'ku',
334 | 'kv',
335 | 'kw',
336 | 'ky',
337 | 'la',
338 | 'lad',
339 | 'lan',
340 | 'lb',
341 | 'lg',
342 | 'li',
343 | 'lij',
344 | 'lmo',
345 | 'ln',
346 | 'lo',
347 | 'lt',
348 | 'lv',
349 | 'lzz',
350 | 'man',
351 | 'map-bms',
352 | 'mg',
353 | 'mh',
354 | 'mi',
355 | 'min',
356 | 'mk',
357 | 'ml',
358 | 'mn',
359 | 'mo',
360 | 'mr',
361 | 'mrh',
362 | 'ms',
363 | 'mt',
364 | 'mus',
365 | 'mwl',
366 | 'my',
367 | 'na',
368 | 'nah',
369 | 'nap',
370 | 'nd',
371 | 'nds',
372 | 'nds-nl',
373 | 'ne',
374 | 'new',
375 | 'ng',
376 | 'nl',
377 | 'nn',
378 | 'no',
379 | 'nr',
380 | 'nrm',
381 | 'nso',
382 | 'nv',
383 | 'ny',
384 | 'oc',
385 | 'oj',
386 | 'om',
387 | 'or',
388 | 'os',
389 | 'pa',
390 | 'pag',
391 | 'pam',
392 | 'pap',
393 | 'pdc',
394 | 'pi',
395 | 'pih',
396 | 'pl',
397 | 'pms',
398 | 'ps',
399 | 'pt',
400 | 'pt-br',
401 | 'qu',
402 | 'rm',
403 | 'rmy',
404 | 'rn',
405 | 'ro',
406 | 'roa-rup',
407 | 'ru',
408 | 'rw',
409 | 'sa',
410 | 'sc',
411 | 'scn',
412 | 'sco',
413 | 'sd',
414 | 'se',
415 | 'sg',
416 | 'sh',
417 | 'si',
418 | 'simple',
419 | 'sk',
420 | 'sl',
421 | 'sm',
422 | 'sn',
423 | 'so',
424 | 'sq',
425 | 'sr',
426 | 'ss',
427 | 'st',
428 | 'su',
429 | 'sv',
430 | 'sw',
431 | 'ta',
432 | 'te',
433 | 'tet',
434 | 'tg',
435 | 'th',
436 | 'ti',
437 | 'tk',
438 | 'tl',
439 | 'tlh',
440 | 'tn',
441 | 'to',
442 | 'tpi',
443 | 'tr',
444 | 'ts',
445 | 'tt',
446 | 'tum',
447 | 'tw',
448 | 'ty',
449 | 'udm',
450 | 'ug',
451 | 'uk',
452 | 'ur',
453 | 'uz',
454 | 'uz-af',
455 | 've',
456 | 'vec',
457 | 'vi',
458 | 'vls',
459 | 'vo',
460 | 'wa',
461 | 'war',
462 | 'wo',
463 | 'xal',
464 | 'xh',
465 | 'xmf',
466 | 'yi',
467 | 'yo',
468 | 'za',
469 | 'zh',
470 | 'zh-classical',
471 | 'zh-min-nan',
472 | 'zh-yue',
473 | 'zu',
474 | );
475 | registerArgumentsSet(
476 | 'locale_directions',
477 | 'ltr',
478 | 'rtl',
479 | );
480 | expectedArguments(
481 | \Framework\Language\Language::currency(),
482 | 1,
483 | argumentsSet('currencies')
484 | );
485 | expectedArguments(
486 | \Framework\Language\Language::date(),
487 | 1,
488 | argumentsSet('date_styles')
489 | );
490 | expectedArguments(
491 | \Framework\Language\Language::setFallbackLevel(),
492 | 0,
493 | argumentsSet('fallback_levels')
494 | );
495 | expectedArguments(
496 | \Framework\Language\Language::__construct(),
497 | 0,
498 | argumentsSet('locales')
499 | );
500 | expectedArguments(
501 | \Framework\Language\Language::addLines(),
502 | 0,
503 | argumentsSet('locales')
504 | );
505 | expectedArguments(
506 | \Framework\Language\Language::currency(),
507 | 2,
508 | argumentsSet('locales')
509 | );
510 | expectedArguments(
511 | \Framework\Language\Language::date(),
512 | 2,
513 | argumentsSet('locales')
514 | );
515 | expectedArguments(
516 | \Framework\Language\Language::formatMessage(),
517 | 2,
518 | argumentsSet('locales')
519 | );
520 | expectedArguments(
521 | \Framework\Language\Language::getLocaleDirection(),
522 | 0,
523 | argumentsSet('locales')
524 | );
525 | expectedArguments(
526 | \Framework\Language\Language::lang(),
527 | 2,
528 | argumentsSet('locales')
529 | );
530 | expectedArguments(
531 | \Framework\Language\Language::ordinal(),
532 | 1,
533 | argumentsSet('locales')
534 | );
535 | expectedArguments(
536 | \Framework\Language\Language::render(),
537 | 3,
538 | argumentsSet('locales')
539 | );
540 | expectedArguments(
541 | \Framework\Language\Language::setCurrentLocale(),
542 | 0,
543 | argumentsSet('locales')
544 | );
545 | expectedArguments(
546 | \Framework\Language\Language::setDefaultLocale(),
547 | 0,
548 | argumentsSet('locales')
549 | );
550 | expectedReturnValues(
551 | \Framework\Language\Language::getCurrentLocale(),
552 | argumentsSet('locales')
553 | );
554 | expectedReturnValues(
555 | \Framework\Language\Language::getCurrentLocaleDirection(),
556 | argumentsSet('locale_directions')
557 | );
558 | expectedReturnValues(
559 | \Framework\Language\Language::getDefaultLocale(),
560 | argumentsSet('locales')
561 | );
562 | expectedReturnValues(
563 | \Framework\Language\Language::getLocaleDirection(),
564 | argumentsSet('locale_directions')
565 | );
566 | expectedReturnValues(
567 | \Framework\Language\Language::getFallbackLevel(),
568 | argumentsSet('fallback_levels')
569 | );
570 |
--------------------------------------------------------------------------------
/src/Language.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Framework\Language;
11 |
12 | use Framework\Helpers\Isolation;
13 | use Framework\Language\Debug\LanguageCollector;
14 | use InvalidArgumentException;
15 | use JetBrains\PhpStorm\ArrayShape;
16 | use JetBrains\PhpStorm\Pure;
17 |
18 | /**
19 | * Class Language.
20 | *
21 | * @see https://www.sitepoint.com/localization-demystified-understanding-php-intl/
22 | * @see https://unicode-org.github.io/icu-docs/#/icu4c/classMessageFormat.html
23 | *
24 | * @package language
25 | */
26 | class Language
27 | {
28 | /**
29 | * The current locale.
30 | */
31 | protected string $currentLocale;
32 | /**
33 | * The default locale.
34 | */
35 | protected string $defaultLocale;
36 | /**
37 | * List of directories to find for files.
38 | *
39 | * @var array
40 | */
41 | protected array $directories = [];
42 | /**
43 | * The locale fallback level.
44 | */
45 | protected FallbackLevel $fallbackLevel = FallbackLevel::default;
46 | /**
47 | * List with locales of already scanned directories.
48 | *
49 | * @var array
50 | */
51 | protected array $findedLocales = [];
52 | /**
53 | * Language lines.
54 | *
55 | * List of "locale" => "file" => "line" => "text"
56 | *
57 | * @var array>>
58 | */
59 | protected array $languages = [];
60 | /**
61 | * Supported locales. Any other will be ignored.
62 | *
63 | * The default locale is always supported.
64 | *
65 | * @var array
66 | */
67 | protected array $supportedLocales = [];
68 | protected LanguageCollector $debugCollector;
69 |
70 | /**
71 | * Language constructor.
72 | *
73 | * @param string $locale The default (and current) locale code
74 | * @param array $directories List of directory paths to find for language files
75 | */
76 | public function __construct(string $locale = 'en', array $directories = [])
77 | {
78 | $this->setDefaultLocale($locale);
79 | $this->setCurrentLocale($locale);
80 | if ($directories) {
81 | $this->setDirectories($directories);
82 | }
83 | }
84 |
85 | /**
86 | * Adds a locale to the list of already scanned directories.
87 | *
88 | * @param string $locale
89 | *
90 | * @return static
91 | */
92 | protected function addFindedLocale(string $locale) : static
93 | {
94 | $this->findedLocales[] = $locale;
95 | return $this;
96 | }
97 |
98 | /**
99 | * Adds custom lines for a specific locale.
100 | *
101 | * Useful to set lines from a database or any parsed file.
102 | *
103 | * NOTE: This function will always replace the old lines, as given from files.
104 | *
105 | * @param string $locale The locale code
106 | * @param string $file The file name
107 | * @param array $lines An array of "line" => "text"
108 | *
109 | * @return static
110 | */
111 | public function addLines(string $locale, string $file, array $lines) : static
112 | {
113 | if (!$this->isFindedLocale($locale)) {
114 | // Certify that all directories are scanned first
115 | // So, this method always have priority on replacements
116 | $this->getLine($locale, $file, '');
117 | }
118 | $this->languages[$locale][$file] = isset($this->languages[$locale][$file])
119 | ? \array_replace($this->languages[$locale][$file], $lines)
120 | : $lines;
121 | return $this;
122 | }
123 |
124 | /**
125 | * Gets a currency value formatted in a given locale.
126 | *
127 | * @param float $value The money value
128 | * @param string $currency The Currency code. i.e. USD, BRL, JPY
129 | * @param string|null $locale A custom locale or null to use the current
130 | *
131 | * @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes
132 | *
133 | * @return string
134 | */
135 | public function currency(float $value, string $currency, ?string $locale = null) : string
136 | {
137 | // @phpstan-ignore-next-line
138 | return \NumberFormatter::create(
139 | $locale ?? $this->getCurrentLocale(),
140 | \NumberFormatter::CURRENCY
141 | )->formatCurrency($value, $currency);
142 | }
143 |
144 | /**
145 | * Gets a formatted date in a given locale.
146 | *
147 | * @param int $time An Unix timestamp
148 | * @param string|null $style One of: short, medium, long or full. Leave null to use short
149 | * @param string|null $locale A custom locale or null to use the current
150 | *
151 | * @throws InvalidArgumentException for invalid style format
152 | *
153 | * @return string
154 | */
155 | public function date(int $time, ?string $style = null, ?string $locale = null) : string
156 | {
157 | if ($style && !\in_array($style, ['short', 'medium', 'long', 'full'], true)) {
158 | throw new InvalidArgumentException('Invalid date style format: ' . $style);
159 | }
160 | $style = $style ?: 'short';
161 | // @phpstan-ignore-next-line
162 | return \MessageFormatter::formatMessage(
163 | $locale ?? $this->getCurrentLocale(),
164 | "{time, date, {$style}}",
165 | ['time' => $time]
166 | );
167 | }
168 |
169 | /**
170 | * Find for absolute file paths from where language lines can be loaded.
171 | *
172 | * @param string $locale The required locale
173 | * @param string $file The required file
174 | *
175 | * @return array a list of valid filenames
176 | */
177 | #[Pure]
178 | protected function findFilenames(string $locale, string $file) : array
179 | {
180 | $filenames = [];
181 | foreach ($this->getDirectories() as $directory) {
182 | $path = $directory . $locale . \DIRECTORY_SEPARATOR . $file . '.php';
183 | if (\is_file($path)) {
184 | $filenames[] = $path;
185 | }
186 | }
187 | return $filenames;
188 | }
189 |
190 | /**
191 | * Gets the current locale.
192 | *
193 | * @return string
194 | */
195 | #[Pure]
196 | public function getCurrentLocale() : string
197 | {
198 | return $this->currentLocale;
199 | }
200 |
201 | /**
202 | * Gets the current locale directionality.
203 | *
204 | * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left
205 | */
206 | #[Pure]
207 | public function getCurrentLocaleDirection() : string
208 | {
209 | return static::getLocaleDirection($this->getCurrentLocale());
210 | }
211 |
212 | /**
213 | * Gets the default locale.
214 | *
215 | * @return string
216 | */
217 | #[Pure]
218 | public function getDefaultLocale() : string
219 | {
220 | return $this->defaultLocale;
221 | }
222 |
223 | /**
224 | * Gets the list of directories where language files can be finded.
225 | *
226 | * @return array
227 | */
228 | #[Pure]
229 | public function getDirectories() : array
230 | {
231 | return $this->directories;
232 | }
233 |
234 | /**
235 | * Gets the Fallback Level.
236 | *
237 | * @return FallbackLevel
238 | */
239 | #[Pure]
240 | public function getFallbackLevel() : FallbackLevel
241 | {
242 | return $this->fallbackLevel;
243 | }
244 |
245 | /**
246 | * Gets a text line and locale according the Fallback Level.
247 | *
248 | * @param string $locale The locale to get his fallback line
249 | * @param string $file The file
250 | * @param string $line The line
251 | *
252 | * @return array Two numeric keys containg the used locale and text
253 | */
254 | #[ArrayShape(['string', 'string|null'])]
255 | protected function getFallbackLine(string $locale, string $file, string $line) : array
256 | {
257 | $text = null;
258 | $level = $this->getFallbackLevel()->value;
259 | // Fallback to parent
260 | if ($level > FallbackLevel::none->value && \strpos($locale, '-') > 1) {
261 | [$locale] = \explode('-', $locale, 2);
262 | $text = $this->getLine($locale, $file, $line);
263 | }
264 | // Fallback to default
265 | if ($text === null
266 | && $level > FallbackLevel::parent->value
267 | && $locale !== $this->getDefaultLocale()
268 | ) {
269 | $locale = $this->getDefaultLocale();
270 | $text = $this->getLine($locale, $file, $line);
271 | }
272 | return [
273 | $locale,
274 | $text,
275 | ];
276 | }
277 |
278 | /**
279 | * @param string $filename
280 | *
281 | * @return array
282 | */
283 | protected function getFileLines(string $filename) : array
284 | {
285 | return Isolation::require($filename);
286 | }
287 |
288 | /**
289 | * Gets a language line text.
290 | *
291 | * @param string $locale The required locale
292 | * @param string $file The required file
293 | * @param string $line The required line
294 | *
295 | * @return string|null The text of the line or null if the line is not found
296 | */
297 | protected function getLine(string $locale, string $file, string $line) : ?string
298 | {
299 | if (isset($this->languages[$locale][$file][$line])) {
300 | return $this->languages[$locale][$file][$line];
301 | }
302 | if (!\in_array($locale, $this->getSupportedLocales(), true)) {
303 | return null;
304 | }
305 | $this->addFindedLocale($locale);
306 | $this->findLines($locale, $file);
307 | return $this->languages[$locale][$file][$line] ?? null;
308 | }
309 |
310 | /**
311 | * Find and add lines.
312 | *
313 | * This method can be overridden to find lines in custom storage, such as
314 | * in a database table.
315 | *
316 | * @param string $locale
317 | * @param string $file
318 | *
319 | * @return static
320 | */
321 | protected function findLines(string $locale, string $file) : static
322 | {
323 | foreach ($this->findFilenames($locale, $file) as $filename) {
324 | $this->addLines($locale, $file, $this->getFileLines($filename));
325 | }
326 | return $this;
327 | }
328 |
329 | /**
330 | * Gets the list of available locales, lines and texts.
331 | *
332 | * @return array>>
333 | */
334 | #[Pure]
335 | public function getLines() : array
336 | {
337 | return $this->languages;
338 | }
339 |
340 | public function resetLines() : static
341 | {
342 | $this->languages = [];
343 | return $this;
344 | }
345 |
346 | /**
347 | * Gets the list of Supported Locales.
348 | *
349 | * @return array
350 | */
351 | #[Pure]
352 | public function getSupportedLocales() : array
353 | {
354 | return $this->supportedLocales;
355 | }
356 |
357 | /**
358 | * Tells if a locale already was found in the directories.
359 | *
360 | * @param string $locale The locale
361 | *
362 | * @see \Framework\Language\Language::getLine()
363 | *
364 | * @return bool
365 | */
366 | #[Pure]
367 | protected function isFindedLocale(string $locale) : bool
368 | {
369 | return \in_array($locale, $this->findedLocales, true);
370 | }
371 |
372 | /**
373 | * Renders a language file line with dot notation format.
374 | *
375 | * E.g. home.hello matches home for file and hello for line.
376 | *
377 | * @param string $line The dot notation file line
378 | * @param array $args The arguments to be used in the formatted text
379 | * @param string|null $locale A custom locale or null to use the current
380 | *
381 | * @return string|null The rendered text or null if not found
382 | */
383 | public function lang(string $line, array $args = [], ?string $locale = null) : ?string
384 | {
385 | [$file, $line] = \explode('.', $line, 2);
386 | return $this->render($file, $line, $args, $locale);
387 | }
388 |
389 | /**
390 | * Gets an ordinal number in a given locale.
391 | *
392 | * @param int $number The number to be converted to ordinal
393 | * @param string|null $locale A custom locale or null to use the current
394 | *
395 | * @return string
396 | */
397 | public function ordinal(int $number, ?string $locale = null) : string
398 | {
399 | // @phpstan-ignore-next-line
400 | return \MessageFormatter::formatMessage(
401 | $locale ?? $this->getCurrentLocale(),
402 | '{number, ordinal}',
403 | ['number' => $number]
404 | );
405 | }
406 |
407 | /**
408 | * Renders a language file line.
409 | *
410 | * @param string $file The file
411 | * @param string $line The file line
412 | * @param array $args The arguments to be used in the formatted text
413 | * @param string|null $locale A custom locale or null to use the current
414 | *
415 | * @return string The rendered text or file.line expression
416 | */
417 | public function render(
418 | string $file,
419 | string $line,
420 | array $args = [],
421 | ?string $locale = null
422 | ) : string {
423 | if (isset($this->debugCollector)) {
424 | $start = \microtime(true);
425 | $rendered = $this->getRenderedLine($file, $line, $args, $locale);
426 | $end = \microtime(true);
427 | $this->debugCollector->adddata([
428 | 'start' => $start,
429 | 'end' => $end,
430 | 'file' => $file,
431 | 'line' => $line,
432 | 'locale' => $rendered['locale'],
433 | 'message' => $rendered['message'],
434 | ]);
435 | return $rendered['message'];
436 | }
437 | return $this->getRenderedLine($file, $line, $args, $locale)['message'];
438 | }
439 |
440 | /**
441 | * @param string $file
442 | * @param string $line
443 | * @param array $args
444 | * @param string|null $locale
445 | *
446 | * @return array
447 | */
448 | #[ArrayShape(['locale' => 'string', 'message' => 'string'])]
449 | protected function getRenderedLine(
450 | string $file,
451 | string $line,
452 | array $args = [],
453 | ?string $locale = null
454 | ) : array {
455 | $locale ??= $this->getCurrentLocale();
456 | $text = $this->getLine($locale, $file, $line);
457 | if ($text === null) {
458 | [$locale, $text] = $this->getFallbackLine($locale, $file, $line);
459 | }
460 | if ($text !== null) {
461 | $text = $this->formatMessage($text, $args, $locale);
462 | }
463 | return [
464 | 'locale' => $locale,
465 | 'message' => $text ?? ($file . '.' . $line),
466 | ];
467 | }
468 |
469 | /**
470 | * Checks if Language has a line.
471 | *
472 | * @param string $file The file
473 | * @param string $line The file line
474 | * @param string|null $locale A custom locale or null to use the current
475 | *
476 | * @return bool True if the line is found, otherwise false
477 | */
478 | public function hasLine(string $file, string $line, ?string $locale = null) : bool
479 | {
480 | $locale ??= $this->getCurrentLocale();
481 | $text = $this->getLine($locale, $file, $line);
482 | if ($text === null) {
483 | $text = $this->getFallbackLine($locale, $file, $line)[1];
484 | }
485 | return $text !== null;
486 | }
487 |
488 | /**
489 | * @param string $text
490 | * @param array $args
491 | * @param string|null $locale
492 | *
493 | * @return string
494 | */
495 | public function formatMessage(string $text, array $args = [], ?string $locale = null) : string
496 | {
497 | $args = \array_map(static function ($arg) : string {
498 | return (string) $arg;
499 | }, $args);
500 | $locale ??= $this->getCurrentLocale();
501 | return \MessageFormatter::formatMessage($locale, $text, $args) ?: $text;
502 | }
503 |
504 | /**
505 | * Sets the current locale.
506 | *
507 | * @param string $locale The current locale. This automatically is set as
508 | * one of Supported Locales.
509 | *
510 | * @return static
511 | */
512 | public function setCurrentLocale(string $locale) : static
513 | {
514 | $this->currentLocale = $locale;
515 | $locales = $this->getSupportedLocales();
516 | $locales[] = $locale;
517 | $this->setSupportedLocales($locales);
518 | return $this;
519 | }
520 |
521 | /**
522 | * Sets the default locale.
523 | *
524 | * @param string $locale The default locale. This automatically is set as
525 | * one of Supported Locales.
526 | *
527 | * @return static
528 | */
529 | public function setDefaultLocale(string $locale) : static
530 | {
531 | $this->defaultLocale = $locale;
532 | $locales = $this->getSupportedLocales();
533 | $locales[] = $locale;
534 | $this->setSupportedLocales($locales);
535 | return $this;
536 | }
537 |
538 | /**
539 | * Sets a list of directories where language files can be found.
540 | *
541 | * @param array $directories a list of valid directory paths
542 | *
543 | * @throws InvalidArgumentException if a directory path is inaccessible
544 | *
545 | * @return static
546 | */
547 | public function setDirectories(array $directories) : static
548 | {
549 | $dirs = [];
550 | foreach ($directories as $directory) {
551 | $path = \realpath($directory);
552 | if (!$path || !\is_dir($path)) {
553 | throw new InvalidArgumentException('Directory path inaccessible: ' . $directory);
554 | }
555 | $dirs[] = $path . \DIRECTORY_SEPARATOR;
556 | }
557 | $this->directories = $dirs ? \array_unique($dirs) : [];
558 | $this->reindex();
559 | return $this;
560 | }
561 |
562 | /**
563 | * @param string $directory
564 | *
565 | * @return static
566 | */
567 | public function addDirectory(string $directory) : static
568 | {
569 | $this->setDirectories(\array_merge([
570 | $directory,
571 | ], $this->getDirectories()));
572 | return $this;
573 | }
574 |
575 | protected function reindex() : void
576 | {
577 | $this->findedLocales = [];
578 | foreach ($this->languages as $locale => $files) {
579 | foreach (\array_keys($files) as $file) {
580 | $this->findLines($locale, $file);
581 | }
582 | $this->addFindedLocale($locale);
583 | }
584 | }
585 |
586 | /**
587 | * Sets the Fallback Level.
588 | *
589 | * @param FallbackLevel|int $level
590 | *
591 | * @return static
592 | */
593 | public function setFallbackLevel(FallbackLevel | int $level) : static
594 | {
595 | if (\is_int($level)) {
596 | $level = FallbackLevel::from($level);
597 | }
598 | $this->fallbackLevel = $level;
599 | return $this;
600 | }
601 |
602 | /**
603 | * Sets a list of Supported Locales.
604 | *
605 | * NOTE: the default locale always is supported. But the current can be exclude
606 | * if this function is called after {@see Language::setCurrentLocale()}.
607 | *
608 | * @param array $locales the supported locales
609 | *
610 | * @return static
611 | */
612 | public function setSupportedLocales(array $locales) : static
613 | {
614 | $locales[] = $this->getDefaultLocale();
615 | $locales = \array_unique($locales);
616 | \sort($locales);
617 | $this->supportedLocales = $locales;
618 | $this->reindex();
619 | return $this;
620 | }
621 |
622 | public function setDebugCollector(LanguageCollector $debugCollector) : static
623 | {
624 | $this->debugCollector = $debugCollector;
625 | $this->debugCollector->setLanguage($this);
626 | return $this;
627 | }
628 |
629 | /**
630 | * Gets text directionality based on locale.
631 | *
632 | * @param string $locale The locale code
633 | *
634 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
635 | * @see https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
636 | *
637 | * @return string 'ltr' for Left-To-Right ot 'rtl' for Right-To-Left
638 | */
639 | #[Pure]
640 | public static function getLocaleDirection(string $locale) : string
641 | {
642 | $locale = \strtolower($locale);
643 | $locale = \strtr($locale, ['_' => '-']);
644 | if (\in_array($locale, [
645 | 'ar',
646 | 'arc',
647 | 'ckb',
648 | 'dv',
649 | 'fa',
650 | 'ha',
651 | 'he',
652 | 'khw',
653 | 'ks',
654 | 'ps',
655 | 'ur',
656 | 'uz-af',
657 | 'yi',
658 | ], true)) {
659 | return 'rtl';
660 | }
661 | return 'ltr';
662 | }
663 | }
664 |
--------------------------------------------------------------------------------