Skip to content

Commit 805dca8

Browse files
author
Ivan Vasilkov
committed
Init
0 parents  commit 805dca8

21 files changed

+742
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
.phpunit.result.cache
3+
vendor/
4+
composer.lock

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2022 Composite PHP
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.

README.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Composite Localization
2+
3+
Simple translation and pluralization php library based on files.
4+
5+
Overview:
6+
- [Localization](#localization)
7+
- [Pluralization](#pluralization)
8+
9+
## Localization
10+
11+
First, you need to create new class and extend from `Composite\Localization\AbstractLocalization`
12+
13+
```php
14+
use Composite\Localization\AbstractLocalization;
15+
use Composite\Localization\Language;
16+
17+
class Localization extends AbstractLocalization
18+
{
19+
public function __construct(Language $language)
20+
{
21+
parent::__construct(
22+
language: $language,
23+
sourcePath: '/directory/to/your/locale/files',
24+
);
25+
}
26+
27+
public function general(string $text, array $placeholders = []): string
28+
{
29+
return $this->get('general', $text, $placeholders);
30+
}
31+
32+
public function profile(string $text, array $placeholders = []): string
33+
{
34+
return $this->get('profile', $text, $placeholders);
35+
}
36+
//other categories
37+
//...
38+
}
39+
```
40+
41+
Usage:
42+
43+
```php
44+
45+
```
46+
47+
Second, create category files in your localization source path `/directory/to/your/locale/files`:
48+
49+
```
50+
- general.php
51+
- profile.php
52+
```
53+
54+
Example content of `general.php`
55+
56+
```php
57+
use Composite\Localization\Language;
58+
59+
return [
60+
'Hello World!' => [
61+
Language::FR->value => 'Bonjour le monde!'
62+
],
63+
'hello_world' => [
64+
Language::EN->value => 'Hello World!',
65+
Language::FR->value => 'Bonjour le monde!',
66+
],
67+
'Hello {{first_name}} {{last_name}}!' => [
68+
Language::FR->value => 'Bonjour {{first_name}} {{last_name}}!',
69+
],
70+
'hello_fullname' => [
71+
Language::EN->value => 'Hello {{first_name}} {{last_name}}!',
72+
Language::FR->value => 'Bonjour {{first_name}} {{last_name}}!',
73+
],
74+
'Hello {{name}}, you have {{num}} [[new_comment:num]]' => [
75+
Language::FR->value => 'Bonjour {{name}}, vous avez {{num}} [[new_comment:num]]',
76+
],
77+
];
78+
```
79+
80+
Usage:
81+
82+
```php
83+
use Composite\Localization\Language;
84+
85+
$localization = new Localization(Language::FR);
86+
87+
$localization->general('Hello World!'); //Bonjour le monde!
88+
89+
$localization->general('hello_world'); //Bonjour le monde!
90+
91+
$localization->general(
92+
'Hello {{first_name}} {{last_name}}!',
93+
['first_name' => 'John', 'last_name' => 'Smith']
94+
); //Bonjour John Smith!
95+
96+
$localization->general(
97+
'hello_fullname',
98+
['first_name' => 'John', 'last_name' => 'Smith']
99+
); //Bonjour John Smith!
100+
101+
//if translation not exists localization instance will still output it as is
102+
103+
$localization->general('Bye World!'); //Bye World!
104+
105+
$localization->general('bye_world'); //bye_world
106+
107+
$localization->general(
108+
'Bye {{first_name}} {{last_name}}!',
109+
['first_name' => 'John', 'last_name' => 'Smith']
110+
); //Bye John Smith!
111+
```
112+
113+
## Pluralization
114+
115+
Create a file `_plural.php` inside folder with translation categories `/directory/to/your/locale/files`:
116+
117+
```php
118+
use Composite\Localization\Language;
119+
use Composite\Localization\Plurals\EnglishPlural;
120+
use Composite\Localization\Plurals\FrenchPlural;
121+
122+
return [
123+
'new_comment' => [
124+
Language::EN->value => new EnglishPlural('new comment', 'new comments'),
125+
Language::FR->value => new FrenchPlural('nouveau commentaire', 'nouveaux commentaires'),
126+
],
127+
];
128+
```
129+
130+
Usage:
131+
132+
```php
133+
$localization->general(
134+
'Hello {{name}}, you have {{num}} [[new_comment:num]]',
135+
['name' => 'John', 'num' => 1]
136+
); //Bonjour John, vous avez 1 nouveau commentaire
137+
138+
$localization->general(
139+
'Hello {{name}}, you have {{num}} [[new_comment:num]]',
140+
['name' => 'John', 'num' => 2]
141+
); //Bonjour John, vous avez 2 nouveaux commentaires
142+
```
143+
144+
## License:
145+
146+
MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by Composite PHP.

composer.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "compositephp/localization",
3+
"description": "Easy to use Localization and Pluralization, based on files",
4+
"type": "library",
5+
"license": "MIT",
6+
"minimum-stability": "dev",
7+
"prefer-stable": true,
8+
"authors": [
9+
{
10+
"name": "Composite PHP",
11+
"email": "[email protected]"
12+
}
13+
],
14+
"require": {
15+
"php": "^8.1",
16+
"psr/log": "1 - 3"
17+
},
18+
"autoload": {
19+
"psr-4": {
20+
"Composite\\Localization\\": "src/"
21+
}
22+
},
23+
"autoload-dev": {
24+
"psr-4": {
25+
"Composite\\Localization\\Tests\\": "tests/"
26+
}
27+
},
28+
"require-dev": {
29+
"vimeo/psalm": "^4.29"
30+
}
31+
}

phpunit.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<phpunit
3+
bootstrap="./vendor/autoload.php"
4+
verbose="true"
5+
colors="true"
6+
></phpunit>

psalm.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<psalm
3+
errorLevel="1"
4+
resolveFromConfigFile="true"
5+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
6+
xmlns="https://getpsalm.org/schema/config"
7+
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
8+
>
9+
<projectFiles>
10+
<directory name="src" />
11+
<ignoreFiles>
12+
<directory name="vendor" />
13+
</ignoreFiles>
14+
</projectFiles>
15+
</psalm>

src/AbstractLocalization.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\Localization;
4+
5+
use Psr\Log\LoggerInterface;
6+
7+
abstract class AbstractLocalization
8+
{
9+
private const PLURAL_CATEGORY = '_plural';
10+
private const COMMON_PLACEHOLDERS = [
11+
"__EOL__" => "\n",
12+
];
13+
private static array $categories = [];
14+
15+
public function __construct(
16+
public readonly Language $language,
17+
private readonly string $sourcePath,
18+
private readonly ?LoggerInterface $logger = null,
19+
) {}
20+
21+
protected function get(string $category, string $text, array $placeholders = []): string
22+
{
23+
if (!$text) return '';
24+
25+
$this->loadCategory($category);
26+
if (isset(self::$categories[$category][$text][$this->language->value])) {
27+
$text = (string)self::$categories[$category][$text][$this->language->value];
28+
} else {
29+
$this->logger?->warning("Lexeme '$text' not found, category '$category', language '{$this->language->value}'");
30+
}
31+
return $this->replacePlaceholders($text, $placeholders);
32+
}
33+
34+
protected function getPluralWord(string $word, int $number): string
35+
{
36+
$this->loadCategory(self::PLURAL_CATEGORY);
37+
$plural = self::$categories[self::PLURAL_CATEGORY][$word][$this->language->value] ?? null;
38+
if (!$plural instanceof PluralInterface) {
39+
return "[[$word]]";
40+
}
41+
return $plural->getForm($number);
42+
}
43+
44+
private function loadCategory(string $category): void
45+
{
46+
if (isset(self::$categories[$category])) {
47+
return;
48+
}
49+
$filePath = $this->sourcePath . DIRECTORY_SEPARATOR . $category . '.php';
50+
if (file_exists($filePath)) {
51+
$content = (array)(require_once $filePath);
52+
} else {
53+
$this->logger?->warning("Category file '$filePath' not found");
54+
$content = [];
55+
}
56+
self::$categories[$category] = $content;
57+
}
58+
59+
private function replacePlaceholders(string $text, array $placeholders): string
60+
{
61+
/** @var array<string, string> $placeholders */
62+
$placeholders = array_merge($placeholders, self::COMMON_PLACEHOLDERS);
63+
$text = str_replace(
64+
array_map(
65+
fn (string $key): string => '{{' . $key . '}}',
66+
array_keys($placeholders)
67+
),
68+
array_values($placeholders),
69+
$text
70+
);
71+
preg_match_all('/\[\[(\w+):(\w+)]]/mu', $text, $matches, PREG_SET_ORDER);
72+
if (!$matches) {
73+
return $text;
74+
}
75+
$replaces = [];
76+
foreach ($matches as $match) {
77+
$number = isset($placeholders[$match[2]]) ? (int)$placeholders[$match[2]] : 0;
78+
$replaces[$match[0]] = $this->getPluralWord($match[1], $number);
79+
}
80+
return str_replace(
81+
array_keys($replaces),
82+
array_values($replaces),
83+
$text
84+
);
85+
}
86+
}

src/Language.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\Localization;
4+
5+
enum Language: string
6+
{
7+
case AR = 'ar';
8+
case AM = 'am';
9+
case BG = 'bg';
10+
case BN = 'bn';
11+
case CA = 'ca';
12+
case CS = 'cs';
13+
case DA = 'da';
14+
case DE = 'de';
15+
case EL = 'el';
16+
case EN = 'en';
17+
case EN_GB = 'en_gb';
18+
case EN_US = 'en_us';
19+
case ES = 'es';
20+
case ES_419 = 'es_419';
21+
case ET = 'et';
22+
case FA = 'fa';
23+
case FI = 'fi';
24+
case FIL = 'fil';
25+
case FR = 'fr';
26+
case GU = 'gu';
27+
case HE = 'he';
28+
case HI = 'hi';
29+
case HR = 'hr';
30+
case HU = 'hu';
31+
case ID = 'id';
32+
case IT = 'it';
33+
case JA = 'ja';
34+
case KN = 'kn';
35+
case KO = 'ko';
36+
case LT = 'lt';
37+
case LV = 'lv';
38+
case ML = 'ml';
39+
case MR = 'mr';
40+
case MS = 'ms';
41+
case NL = 'nl';
42+
case NO = 'no';
43+
case PL = 'pl';
44+
case PT_BR = 'pt_br';
45+
case PT_PT = 'pt_pt';
46+
case RO = 'ro';
47+
case RU = 'ru';
48+
case SK = 'sk';
49+
case SL = 'sl';
50+
case SR = 'sr';
51+
case SV = 'sv';
52+
case SW = 'sw';
53+
case TA = 'ta';
54+
case TE = 'te';
55+
case TH = 'th';
56+
case TR = 'tr';
57+
case UK = 'uk';
58+
case VI = 'vi';
59+
case ZH_CN = 'zh_cn';
60+
case ZH_TW = 'zh_tw';
61+
}

src/PluralInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\Localization;
4+
5+
interface PluralInterface
6+
{
7+
public function getForm(int|float $number): string;
8+
}

0 commit comments

Comments
 (0)