Skip to content

Commit 3a38377

Browse files
committed
[Translator] Rewrite everthing, dump translations into JS constants + types
[Translator] Implement locale fallback system
1 parent 0d0e7f3 commit 3a38377

Some content is hidden

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

45 files changed

+3549
-462
lines changed

src/Translator/LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2020-2022 Fabien Potencier
1+
Copyright (c) 2020-2023 Fabien Potencier
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy
44
of this software and associated documentation files (the "Software"), to deal
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
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;
1+
export declare function format(id: string, parameters: Record<string, string | number> | undefined, locale: string): string;

src/Translator/assets/dist/formatters/icu-formatter.d.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
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;
1+
export declare function formatIntl(id: string, parameters: Record<string, string | number> | undefined, locale: string): string;
Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
import { Domain, Locale, MessageId, MessageValue } from './types';
1+
export declare type DomainType = string;
2+
export declare type LocaleType = string;
3+
export declare type TranslationsType = Record<DomainType, {
4+
parameters: ParametersType;
5+
}>;
6+
export declare type NoParametersType = Record<string, never>;
7+
export declare type ParametersType = Record<string, string | number> | NoParametersType;
8+
export declare type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
9+
export declare type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
10+
export declare type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
11+
export declare type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends {
12+
parameters: infer Parameters;
13+
} ? Parameters : never : never;
14+
export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
15+
id: string;
16+
translations: {
17+
[domain in DomainType]: {
18+
[locale in Locale]: string;
19+
};
20+
};
21+
}
222
declare global {
323
interface Window {
424
__symfony_ux_translator?: {
5-
locale: Locale;
6-
translations: Record<Locale, Record<Domain, Record<MessageId, MessageValue>>>;
25+
locale?: LocaleType;
26+
locales_fallbacks?: Record<LocaleType, LocaleType | null>;
727
};
8-
setTranslatorLocale(locale: Locale): void;
28+
setTranslatorLocale(locale: LocaleType): void;
929
}
1030
}
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;
31+
export declare function setLocale(locale: LocaleType): void;
32+
export declare function getLocale(): LocaleType;
33+
export declare function trans<M extends Message<TranslationsType, LocaleType>, D extends DomainsOf<M>, P extends ParametersOf<M, D>>(...args: P extends NoParametersType ? [message: M, parameters?: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>] : [message: M, parameters: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]): string;

src/Translator/assets/dist/translator_controller.js

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IntlMessageFormat } from 'intl-messageformat';
22

3-
function formatIntl(id, parameters = {}, domain, locale) {
3+
function formatIntl(id, parameters = {}, locale) {
44
if (id === '') {
55
return '';
66
}
@@ -25,7 +25,7 @@ function strtr(string, replacePairs) {
2525
return string.replace(new RegExp(regex.join('|'), 'g'), (matched) => replacePairs[matched].toString());
2626
}
2727

28-
function format(id, parameters = {}, domain = null, locale) {
28+
function format(id, parameters = {}, locale) {
2929
if (null === id || '' === id) {
3030
return '';
3131
}
@@ -196,32 +196,52 @@ function getPluralizationRule(number, locale) {
196196
}
197197

198198
function setLocale(locale) {
199-
window.__symfony_ux_translator.locale = locale;
199+
window.__symfony_ux_translator = Object.assign(Object.assign({}, (window.__symfony_ux_translator || {})), { locale });
200200
}
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);
201+
function getLocale() {
202+
var _a;
203+
return ((_a = window.__symfony_ux_translator) === null || _a === void 0 ? void 0 : _a.locale) || document.documentElement.lang || 'en';
204+
}
205+
function getLocalesFallbacks() {
206+
var _a;
207+
return ((_a = window.__symfony_ux_translator) === null || _a === void 0 ? void 0 : _a.locales_fallbacks) || {};
208+
}
209+
function trans(message, parameters = {}, domain = 'messages', locale = null) {
210+
if (typeof domain === 'undefined') {
211+
domain = 'messages';
212+
}
213+
if (typeof locale === 'undefined' || null === locale) {
214+
locale = getLocale();
215+
}
216+
if (typeof message.translations === 'undefined') {
217+
return message.id;
218+
}
219+
const localesFallbacks = getLocalesFallbacks();
220+
const translationsIntl = message.translations[`${domain}+intl-icu`];
221+
if (typeof translationsIntl !== 'undefined') {
222+
while (typeof translationsIntl[locale] === 'undefined') {
223+
locale = localesFallbacks[locale];
224+
if (!locale) {
225+
break;
226+
}
227+
}
228+
if (locale) {
229+
return formatIntl(translationsIntl[locale], parameters, locale);
230+
}
219231
}
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);
232+
const translations = message.translations[domain];
233+
if (typeof translations !== 'undefined') {
234+
while (typeof translations[locale] === 'undefined') {
235+
locale = localesFallbacks[locale];
236+
if (!locale) {
237+
break;
238+
}
239+
}
240+
if (locale) {
241+
return format(translations[locale], parameters, locale);
242+
}
223243
}
224-
return id;
244+
return message.id;
225245
}
226246

227-
export { setLocale, trans };
247+
export { getLocale, setLocale, trans };

src/Translator/assets/jest.config.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
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;
1+
module.exports = require('../../../jest.config.js');

src/Translator/assets/src/formatters/formatter.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {Domain, Locale, MessageId} from '../types';
21
import {strtr} from '../utils';
32

43
/**
@@ -44,10 +43,9 @@ import {strtr} from '../utils';
4443
*
4544
* @param id The message id
4645
* @param parameters An array of parameters for the message
47-
* @param domain The domain for the message
4846
* @param locale The locale
4947
*/
50-
export function format(id: MessageId, parameters: Record<string, string | number> = {}, domain: Domain | null = null, locale: Locale): string {
48+
export function format(id: string, parameters: Record<string, string | number> = {}, locale: string): string {
5149
if (null === id || '' === id) {
5250
return '';
5351
}
@@ -123,7 +121,7 @@ export function format(id: MessageId, parameters: Record<string, string | number
123121
* which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
124122
* Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
125123
*/
126-
function getPluralizationRule(number: number, locale: Locale): number {
124+
function getPluralizationRule(number: number, locale: string): number {
127125
number = Math.abs(number);
128126
let _locale = locale;
129127

src/Translator/assets/src/formatters/intl-formatter.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import {IntlMessageFormat} from 'intl-messageformat';
2-
import {Domain, Locale, MessageId} from '../types';
32

43
/**
54
* @param id The message id
65
* @param parameters An array of parameters for the message
7-
* @param domain The domain for the message
86
* @param locale The locale
97
*/
10-
export function formatIntl(id: MessageId, parameters: Record<string, string | number> = {}, domain: Domain, locale: Locale): string {
8+
export function formatIntl(id: string, parameters: Record<string, string | number> = {}, locale: string): string {
119
if (id === '' ) {
1210
return '';
1311
}

src/Translator/assets/src/translator.ts

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,58 @@
99

1010
'use strict';
1111

12+
export type DomainType = string;
13+
export type LocaleType = string;
14+
15+
export type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
16+
export type NoParametersType = Record<string, never>;
17+
export type ParametersType = Record<string, string | number> | NoParametersType;
18+
19+
export type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
20+
export type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
21+
export type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
22+
export type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType>
23+
? Translations[D] extends { parameters: infer Parameters }
24+
? Parameters
25+
: never
26+
: never;
27+
28+
export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
29+
id: string;
30+
translations: {
31+
[domain in DomainType]: {
32+
[locale in Locale]: string;
33+
};
34+
};
35+
}
36+
1237
import { formatIntl } from './formatters/intl-formatter';
1338
import { format } from './formatters/formatter';
14-
import { Domain, Locale, MessageId, MessageValue } from './types';
1539

1640
declare global {
1741
interface Window {
1842
__symfony_ux_translator?: {
19-
locale: Locale;
20-
translations: Record<Locale, Record<Domain, Record<MessageId, MessageValue>>>;
43+
locale?: LocaleType;
44+
locales_fallbacks?: Record<LocaleType, LocaleType | null>;
2145
};
2246

23-
setTranslatorLocale(locale: Locale): void;
47+
setTranslatorLocale(locale: LocaleType): void;
2448
}
2549
}
2650

27-
export function setLocale(locale: string) {
28-
window.__symfony_ux_translator!.locale = locale;
51+
export function setLocale(locale: LocaleType) {
52+
window.__symfony_ux_translator = {
53+
...(window.__symfony_ux_translator || {}),
54+
locale,
55+
};
56+
}
57+
58+
export function getLocale(): LocaleType {
59+
return window.__symfony_ux_translator?.locale || document.documentElement.lang || 'en';
60+
}
61+
62+
function getLocalesFallbacks(): Record<LocaleType, LocaleType | null> {
63+
return window.__symfony_ux_translator?.locales_fallbacks || {};
2964
}
3065

3166
/**
@@ -64,47 +99,71 @@ export function setLocale(locale: string) {
6499
*
65100
* @see https://en.wikipedia.org/wiki/ISO_31-11
66101
*
67-
* @param id The message id
102+
* @param message The message
68103
* @param parameters An array of parameters for the message
69104
* @param domain The domain for the message or null to use the default
70105
* @param locale The locale or null to use the default
71106
*/
72-
export function trans(
73-
id: MessageId | null,
74-
parameters: Record<string, string | number> = {},
75-
domain: Domain = 'messages',
76-
locale: Locale | null = null
107+
export function trans<
108+
M extends Message<TranslationsType, LocaleType>,
109+
D extends DomainsOf<M>,
110+
P extends ParametersOf<M, D>
111+
>(
112+
...args: P extends NoParametersType
113+
? [message: M, parameters?: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]
114+
: [message: M, parameters: P, domain?: RemoveIntlIcuSuffix<D>, locale?: LocaleOf<M>]
115+
): string;
116+
export function trans<
117+
M extends Message<TranslationsType, LocaleType>,
118+
D extends DomainsOf<M>,
119+
P extends ParametersOf<M, D>
120+
>(
121+
message: M,
122+
parameters: P = {} as P,
123+
domain: RemoveIntlIcuSuffix<DomainsOf<M>> | undefined = 'messages' as RemoveIntlIcuSuffix<DomainsOf<M>>,
124+
locale: LocaleOf<M> | null = null
77125
): string {
78-
if (null === id || '' === id) {
79-
return '';
126+
if (typeof domain === 'undefined') {
127+
domain = 'messages' as RemoveIntlIcuSuffix<DomainsOf<M>>;
80128
}
81129

82-
if (typeof window.__symfony_ux_translator === 'undefined') {
83-
throw new Error(
84-
'The Translator is not initialized. Did you forget to call the Twig function "initialize_js_translator()"?'
85-
);
130+
if (typeof locale === 'undefined' || null === locale) {
131+
locale = getLocale() as LocaleOf<M>;
86132
}
87133

88-
locale = locale || window.__symfony_ux_translator.locale;
89-
if (!locale) {
90-
throw new Error(
91-
'No locale has been configured. Did you forget to call the Twig function "initialize_js_translator()"?'
92-
);
134+
if (typeof message.translations === 'undefined') {
135+
return message.id;
93136
}
94137

95-
if (typeof window.__symfony_ux_translator.translations[locale] === 'undefined') {
96-
return id;
97-
}
138+
const localesFallbacks = getLocalesFallbacks();
139+
140+
const translationsIntl = message.translations[`${domain}+intl-icu`];
141+
if (typeof translationsIntl !== 'undefined') {
142+
while (typeof translationsIntl[locale] === 'undefined') {
143+
locale = localesFallbacks[locale] as LocaleOf<M>;
144+
if (!locale) {
145+
break;
146+
}
147+
}
98148

99-
let translatedId = window.__symfony_ux_translator.translations[locale]?.[domain + '+intl-icu']?.[id];
100-
if (typeof translatedId === 'string') {
101-
return formatIntl(translatedId, parameters, domain, locale);
149+
if (locale) {
150+
return formatIntl(translationsIntl[locale], parameters, locale);
151+
}
102152
}
103153

104-
translatedId = window.__symfony_ux_translator.translations[locale]?.[domain]?.[id];
105-
if (typeof translatedId === 'string') {
106-
return format(translatedId, parameters, domain, locale);
154+
const translations = message.translations[domain];
155+
if (typeof translations !== 'undefined') {
156+
while (typeof translations[locale] === 'undefined') {
157+
locale = localesFallbacks[locale] as LocaleOf<M>;
158+
if (!locale) {
159+
break;
160+
}
161+
}
162+
163+
if (locale) {
164+
return format(translations[locale], parameters, locale);
165+
}
107166
}
108167

109-
return id;
168+
return message.id;
110169
}

0 commit comments

Comments
 (0)