Skip to content

Commit fde423a

Browse files
committed
[Translator] Add Symfony UX Translator package
1 parent 8e3c70c commit fde423a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1819
-0
lines changed

src/Translator/.gitattributes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/.gitattributes export-ignore
2+
/.gitignore export-ignore
3+
/.symfony.bundle.yaml export-ignore
4+
/phpunit.xml.dist export-ignore
5+
/assets/src export-ignore
6+
/assets/test export-ignore
7+
/assets/jest.config.js export-ignore
8+
/tests export-ignore

src/Translator/.gitignore

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

src/Translator/.symfony.bundle.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
branches: ["2.x"]
2+
maintained_branches: ["2.x"]
3+
doc_dir: "doc"

src/Translator/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CHANGELOG
2+
3+
## Unreleased
4+
5+
- Component added

src/Translator/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020-2022 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

src/Translator/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Symfony UX Translator
2+
3+
Symfony UX Translator integrates [Symfony Translation](https://symfony.com/doc/current/translation.html) for JavaScript.
4+
5+
**This repository is a READ-ONLY sub-tree split**. See
6+
https://github.com/symfony/ux to create issues or submit pull requests.
7+
8+
## Resources
9+
10+
- [Documentation](https://symfony.com/bundles/ux-translator/current/index.html)
11+
- [Report issues](https://github.com/symfony/ux/issues) and
12+
[send Pull Requests](https://github.com/symfony/ux/pulls)
13+
in the [main Symfony UX repository](https://github.com/symfony/ux)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { Locale, MessageId } from '../types';
2+
export declare function format(id: MessageId, parameters: Record<string, string | number> | undefined, domain: string | null | undefined, locale: Locale): string;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { Domain, Locale, MessageId } from '../types';
2+
export declare function formatIcu(id: MessageId, parameters: Record<string, string | number> | undefined, domain: Domain, locale: Locale): string;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { Domain, Locale, MessageId } from '../types';
2+
export declare function formatIntl(id: MessageId, parameters: Record<string, string | number> | undefined, domain: Domain, locale: Locale): string;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Domain, Locale, MessageId, MessageValue } from './types';
2+
declare global {
3+
interface Window {
4+
__symfony_ux_translator?: {
5+
locale: Locale;
6+
translations: Record<Locale, Record<Domain, Record<MessageId, MessageValue>>>;
7+
};
8+
setTranslatorLocale(locale: Locale): void;
9+
}
10+
}
11+
export declare function setLocale(locale: string): void;
12+
export declare function trans(id: MessageId | null, parameters?: Record<string, string | number>, domain?: Domain, locale?: Locale | null): string;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './translator';
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { IntlMessageFormat } from 'intl-messageformat';
2+
3+
function formatIntl(id, parameters = {}, domain, locale) {
4+
if (id === '') {
5+
return '';
6+
}
7+
const intlMessage = new IntlMessageFormat(id, [locale.replace('_', '-')], undefined, { ignoreTag: true });
8+
parameters = Object.assign({}, parameters);
9+
Object.entries(parameters).forEach(([key, value]) => {
10+
if (key.includes('%') || key.includes('{')) {
11+
delete parameters[key];
12+
parameters[key.replace(/[%{} ]/g, '').trim()] = value;
13+
}
14+
});
15+
return intlMessage.format(parameters);
16+
}
17+
18+
function strtr(string, replacePairs) {
19+
const regex = [];
20+
const map = {};
21+
Object.entries(replacePairs).forEach(([from, to]) => {
22+
regex.push(from.replace(/([-[\]{}()*+?.\\^$|#,])/g, '\\$1'));
23+
map[from] = to;
24+
});
25+
if (regex.length === 0) {
26+
return string;
27+
}
28+
return string.replace(new RegExp(regex.join('|'), 'g'), (matched) => map[matched].toString());
29+
}
30+
31+
function format(id, parameters = {}, domain = null, locale) {
32+
if (null === id || '' === id) {
33+
return '';
34+
}
35+
if (typeof parameters['%count%'] === 'undefined' || Number.isNaN(parameters['%count%'])) {
36+
return strtr(id, parameters);
37+
}
38+
const number = Number(parameters['%count%']);
39+
let parts = [];
40+
if (/^\|+$/.test(id)) {
41+
parts = id.split('|');
42+
}
43+
else {
44+
const matches = id.match(/(?:\|\||[^|])+/g);
45+
if (matches !== null) {
46+
parts = matches;
47+
}
48+
}
49+
const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
50+
const standardRules = [];
51+
for (let part of parts) {
52+
part = part.trim().replace(/\|\|/g, '|');
53+
let matches = part.match(intervalRegex);
54+
if (matches !== null) {
55+
if (matches[2]) {
56+
for (const n of matches[3].split(',')) {
57+
if (number === Number(n)) {
58+
return strtr(matches.groups['message'], parameters);
59+
}
60+
}
61+
}
62+
else {
63+
const leftNumber = '-Inf' === matches.groups['left'] ? Number.NEGATIVE_INFINITY : Number(matches.groups['left']);
64+
const rightNumber = ['Inf', '+Inf'].includes(matches.groups['right']) ? Number.POSITIVE_INFINITY : Number(matches.groups['right']);
65+
if (('[' === matches.groups['left_delimiter'] ? number >= leftNumber : number > leftNumber)
66+
&& (']' === matches.groups['right_delimiter'] ? number <= rightNumber : number < rightNumber)) {
67+
return strtr(matches.groups['message'], parameters);
68+
}
69+
}
70+
}
71+
else {
72+
matches = part.match(/^\w+:\s*(.*?)$/);
73+
if (matches !== null) {
74+
standardRules.push(matches[1]);
75+
}
76+
else {
77+
standardRules.push(part);
78+
}
79+
}
80+
}
81+
const position = getPluralizationRule(number, locale);
82+
if (typeof standardRules[position] === 'undefined') {
83+
if (1 === parts.length && typeof standardRules[0] !== 'undefined') {
84+
return strtr(standardRules[0], parameters);
85+
}
86+
throw new Error(`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`);
87+
}
88+
return strtr(standardRules[position], parameters);
89+
}
90+
function getPluralizationRule(number, locale) {
91+
number = Math.abs(number);
92+
let _locale = locale;
93+
if (locale === 'pt_BR' || locale === 'en_US_POSIX') {
94+
return 0;
95+
}
96+
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf('_')) : _locale;
97+
switch (_locale) {
98+
case 'af':
99+
case 'bn':
100+
case 'bg':
101+
case 'ca':
102+
case 'da':
103+
case 'de':
104+
case 'el':
105+
case 'en':
106+
case 'en_US_POSIX':
107+
case 'eo':
108+
case 'es':
109+
case 'et':
110+
case 'eu':
111+
case 'fa':
112+
case 'fi':
113+
case 'fo':
114+
case 'fur':
115+
case 'fy':
116+
case 'gl':
117+
case 'gu':
118+
case 'ha':
119+
case 'he':
120+
case 'hu':
121+
case 'is':
122+
case 'it':
123+
case 'ku':
124+
case 'lb':
125+
case 'ml':
126+
case 'mn':
127+
case 'mr':
128+
case 'nah':
129+
case 'nb':
130+
case 'ne':
131+
case 'nl':
132+
case 'nn':
133+
case 'no':
134+
case 'oc':
135+
case 'om':
136+
case 'or':
137+
case 'pa':
138+
case 'pap':
139+
case 'ps':
140+
case 'pt':
141+
case 'so':
142+
case 'sq':
143+
case 'sv':
144+
case 'sw':
145+
case 'ta':
146+
case 'te':
147+
case 'tk':
148+
case 'ur':
149+
case 'zu':
150+
return (1 == number) ? 0 : 1;
151+
case 'am':
152+
case 'bh':
153+
case 'fil':
154+
case 'fr':
155+
case 'gun':
156+
case 'hi':
157+
case 'hy':
158+
case 'ln':
159+
case 'mg':
160+
case 'nso':
161+
case 'pt_BR':
162+
case 'ti':
163+
case 'wa':
164+
return (number < 2) ? 0 : 1;
165+
case 'be':
166+
case 'bs':
167+
case 'hr':
168+
case 'ru':
169+
case 'sh':
170+
case 'sr':
171+
case 'uk':
172+
return ((1 == number % 10) && (11 != number % 100)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
173+
case 'cs':
174+
case 'sk':
175+
return (1 == number) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2);
176+
case 'ga':
177+
return (1 == number) ? 0 : ((2 == number) ? 1 : 2);
178+
case 'lt':
179+
return ((1 == number % 10) && (11 != number % 100)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2);
180+
case 'sl':
181+
return (1 == number % 100) ? 0 : ((2 == number % 100) ? 1 : (((3 == number % 100) || (4 == number % 100)) ? 2 : 3));
182+
case 'mk':
183+
return (1 == number % 10) ? 0 : 1;
184+
case 'mt':
185+
return (1 == number) ? 0 : (((0 == number) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3));
186+
case 'lv':
187+
return (0 == number) ? 0 : (((1 == number % 10) && (11 != number % 100)) ? 1 : 2);
188+
case 'pl':
189+
return (1 == number) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2);
190+
case 'cy':
191+
return (1 == number) ? 0 : ((2 == number) ? 1 : (((8 == number) || (11 == number)) ? 2 : 3));
192+
case 'ro':
193+
return (1 == number) ? 0 : (((0 == number) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2);
194+
case 'ar':
195+
return (0 == number) ? 0 : ((1 == number) ? 1 : ((2 == number) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5))));
196+
default:
197+
return 0;
198+
}
199+
}
200+
201+
function setLocale(locale) {
202+
window.__symfony_ux_translator.locale = locale;
203+
}
204+
function trans(id, parameters = {}, domain = 'messages', locale = null) {
205+
var _a, _b, _c, _d;
206+
if (null === id || '' === id) {
207+
return '';
208+
}
209+
if (typeof window.__symfony_ux_translator === 'undefined') {
210+
throw new Error('The Translator is not initialized. Did you forget to call the Twig function "initialize_js_translator()"?');
211+
}
212+
locale = locale || window.__symfony_ux_translator.locale;
213+
if (!locale) {
214+
throw new Error('No locale has been configured. Did you forget to call the Twig function "initialize_js_translator()"?');
215+
}
216+
if (typeof window.__symfony_ux_translator.translations[locale] === 'undefined') {
217+
return id;
218+
}
219+
let translatedId = (_b = (_a = window.__symfony_ux_translator.translations[locale]) === null || _a === void 0 ? void 0 : _a[domain + '+intl-icu']) === null || _b === void 0 ? void 0 : _b[id];
220+
if (typeof translatedId === 'string') {
221+
return formatIntl(translatedId, parameters, domain, locale);
222+
}
223+
translatedId = (_d = (_c = window.__symfony_ux_translator.translations[locale]) === null || _c === void 0 ? void 0 : _c[domain]) === null || _d === void 0 ? void 0 : _d[id];
224+
if (typeof translatedId === 'string') {
225+
return format(translatedId, parameters, domain, locale);
226+
}
227+
return id;
228+
}
229+
230+
export { setLocale, trans };

src/Translator/assets/dist/utils.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare function strtr(string: string, replacePairs: Record<string, string | number>): string;

src/Translator/assets/jest.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { defaults } = require('jest-config');
2+
const jestConfig = require('../../../jest.config.js');
3+
4+
jestConfig.moduleFileExtensions = [...defaults.moduleFileExtensions, 'vue'];
5+
jestConfig.transform['^.+\\.vue$'] = ['@vue/vue3-jest'];
6+
7+
module.exports = jestConfig;

src/Translator/assets/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "@symfony/ux-translator",
3+
"description": "Symfony Translator for JavaScript",
4+
"license": "MIT",
5+
"version": "1.0.0",
6+
"main": "dist/translator_controller.js",
7+
"types": "dist/translator_controller.d.ts",
8+
"peerDependencies": {
9+
"intl-messageformat": "^10.2.5"
10+
},
11+
"devDependencies": {
12+
"intl-messageformat": "^10.2.5",
13+
"ts-jest": "^27.1.5"
14+
}
15+
}

0 commit comments

Comments
 (0)