Skip to content

Commit 9ff2432

Browse files
committed
[Translator] Add Symfony UX Translator package
1 parent b6c86be commit 9ff2432

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

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