Skip to content

Commit 2519033

Browse files
committed
[Translator] Refactor API to use string-based translation keys instead of generated constants
1 parent 6770308 commit 2519033

File tree

12 files changed

+833
-629
lines changed

12 files changed

+833
-629
lines changed

src/Translator/CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,54 @@
11
# CHANGELOG
22

3+
## 2.32
4+
5+
- **[BC BREAK]** Refactor API to use string-based translation keys instead of generated constants.
6+
7+
Translation keys are now simple strings instead of TypeScript constants.
8+
The main advantages are:
9+
- You can now use **exactly the same translation keys** as in your Symfony PHP code
10+
- Simpler and more readable code
11+
- No need to memorize generated constant names
12+
- No need to import translation constants: smaller files
13+
- And you can still get autocompletion and type-safety :rocket:
14+
15+
**Before:**
16+
```typescript
17+
import { trans } from '@symfony/ux-translator';
18+
import { SYMFONY_GREAT } from '@app/translations';
19+
20+
trans(SYMFONY_GREAT);
21+
```
22+
23+
**After:**
24+
```typescript
25+
import { createTranslator } from '@symfony/ux-translator';
26+
import { messages } from '../var/translations/index.js';
27+
28+
const { trans } = createTranslator({ messages });
29+
trans('symfony.great');
30+
```
31+
32+
The global functions (`setLocale`, `getLocale`, `setLocaleFallbacks`, `getLocaleFallbacks`, `throwWhenNotFound`)
33+
have been replaced by a new `createTranslator()` factory function that returns an object with these methods.
34+
35+
**Tree-shaking:** While tree-shaking of individual translation keys is no longer possible, modern build tools,
36+
caching strategies, and compression techniques (Brotli, gzip) make this negligible in 2025.
37+
A future feature will allow filtering dumped translations by pattern for those who need it,
38+
further reducing bundle size.
39+
40+
**For AssetMapper users:** You can remove the following entries from your `importmap.php`:
41+
```php
42+
'@app/translations' => [
43+
'path' => './var/translations/index.js',
44+
],
45+
'@app/translations/configuration' => [
46+
'path' => './var/translations/configuration.js',
47+
],
48+
```
49+
50+
**Note:** This is a breaking change, but the UX Translator component is still experimental.
51+
352
## 2.30
453

554
- Ensure compatibility with PHP 8.5
Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
1+
type MessageId = string;
12
type DomainType = string;
23
type LocaleType = string;
3-
type TranslationsType = Record<DomainType, {
4-
parameters: ParametersType;
5-
}>;
4+
5+
type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
66
type NoParametersType = Record<string, never>;
77
type ParametersType = Record<string, string | number | Date> | NoParametersType;
8+
89
type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
910
type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
1011
type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
11-
type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends {
12-
parameters: infer Parameters;
13-
} ? Parameters : never : never;
12+
type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType>
13+
? Translations[D] extends { parameters: infer Parameters }
14+
? Parameters
15+
: never
16+
: never;
17+
1418
interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
15-
id: string;
1619
translations: {
1720
[domain in DomainType]: {
1821
[locale in Locale]: string;
1922
};
2023
};
2124
}
22-
declare function setLocale(locale: LocaleType | null): void;
23-
declare function getLocale(): LocaleType;
24-
declare function throwWhenNotFound(enabled: boolean): void;
25-
declare function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void;
26-
declare function getLocaleFallbacks(): Record<LocaleType, LocaleType>;
27-
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;
2825

29-
export { type DomainType, type DomainsOf, type LocaleOf, type LocaleType, type Message, type NoParametersType, type ParametersOf, type ParametersType, type RemoveIntlIcuSuffix, type TranslationsType, getLocale, getLocaleFallbacks, setLocale, setLocaleFallbacks, throwWhenNotFound, trans };
26+
type Messages = Record<MessageId, Message<TranslationsType, LocaleType>>;
27+
28+
declare function getDefaultLocale(): LocaleType;
29+
declare function createTranslator<TMessages extends Messages>({ messages, locale, localeFallbacks, throwWhenNotFound, }: {
30+
messages: TMessages;
31+
locale?: LocaleType;
32+
localeFallbacks?: Record<LocaleType, LocaleType>;
33+
throwWhenNotFound?: boolean;
34+
}): {
35+
setLocale(locale: LocaleType): void;
36+
getLocale(): LocaleType;
37+
setThrowWhenNotFound(throwWhenNotFound: boolean): void;
38+
trans<TMessageId extends keyof TMessages & MessageId, TMessage extends TMessages[TMessageId], TDomain extends DomainsOf<TMessage>, TParameters extends ParametersOf<TMessage, TDomain>>(id: TMessageId, parameters?: TParameters, domain?: RemoveIntlIcuSuffix<TDomain> | undefined, locale?: LocaleOf<TMessage>): string;
39+
};
40+
41+
export { type DomainType, type DomainsOf, type LocaleOf, type LocaleType, type Message, type MessageId, type Messages, type NoParametersType, type ParametersOf, type ParametersType, type RemoveIntlIcuSuffix, type TranslationsType, createTranslator, getDefaultLocale };

src/Translator/assets/dist/translator_controller.js

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ function format(id, parameters, locale) {
6262
}
6363
function getPluralizationRule(number, locale) {
6464
number = Math.abs(number);
65-
let _locale2 = locale;
65+
let _locale = locale;
6666
if (locale === "pt_BR" || locale === "en_US_POSIX") {
6767
return 0;
6868
}
69-
_locale2 = _locale2.length > 3 ? _locale2.substring(0, _locale2.indexOf("_")) : _locale2;
70-
switch (_locale2) {
69+
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf("_")) : _locale;
70+
switch (_locale) {
7171
case "af":
7272
case "bn":
7373
case "bg":
@@ -189,71 +189,78 @@ function formatIntl(id, parameters, locale) {
189189
}
190190

191191
// src/translator_controller.ts
192-
var _locale = null;
193-
var _localeFallbacks = {};
194-
var _throwWhenNotFound = false;
195-
function setLocale(locale) {
196-
_locale = locale;
197-
}
198-
function getLocale() {
199-
return _locale || document.documentElement.getAttribute("data-symfony-ux-translator-locale") || // <html data-symfony-ux-translator-locale="en_US">
192+
function getDefaultLocale() {
193+
return document.documentElement.getAttribute("data-symfony-ux-translator-locale") || // <html data-symfony-ux-translator-locale="en_US">
200194
(document.documentElement.lang ? document.documentElement.lang.replace("-", "_") : null) || // <html lang="en-US">
201195
"en";
202196
}
203-
function throwWhenNotFound(enabled) {
204-
_throwWhenNotFound = enabled;
205-
}
206-
function setLocaleFallbacks(localeFallbacks) {
207-
_localeFallbacks = localeFallbacks;
208-
}
209-
function getLocaleFallbacks() {
210-
return _localeFallbacks;
211-
}
212-
function trans(message, parameters = {}, domain = "messages", locale = null) {
213-
if (typeof domain === "undefined") {
214-
domain = "messages";
197+
function createTranslator({
198+
messages,
199+
locale = getDefaultLocale(),
200+
localeFallbacks = {},
201+
throwWhenNotFound = false
202+
}) {
203+
const _messages = messages;
204+
const _localeFallbacks = localeFallbacks;
205+
let _locale = locale;
206+
let _throwWhenNotFound = throwWhenNotFound;
207+
function setLocale(locale2) {
208+
_locale = locale2;
215209
}
216-
if (typeof locale === "undefined" || null === locale) {
217-
locale = getLocale();
210+
function getLocale() {
211+
return _locale;
218212
}
219-
if (typeof message.translations === "undefined") {
220-
return message.id;
213+
function setThrowWhenNotFound(throwWhenNotFound2) {
214+
_throwWhenNotFound = throwWhenNotFound2;
221215
}
222-
const localesFallbacks = getLocaleFallbacks();
223-
const translationsIntl = message.translations[`${domain}+intl-icu`];
224-
if (typeof translationsIntl !== "undefined") {
225-
while (typeof translationsIntl[locale] === "undefined") {
226-
locale = localesFallbacks[locale];
227-
if (!locale) {
228-
break;
229-
}
216+
function trans(id, parameters = {}, domain = "messages", locale2 = null) {
217+
if (typeof domain === "undefined") {
218+
domain = "messages";
230219
}
231-
if (locale) {
232-
return formatIntl(translationsIntl[locale], parameters, locale);
220+
if (typeof locale2 === "undefined" || null === locale2) {
221+
locale2 = _locale;
233222
}
234-
}
235-
const translations = message.translations[domain];
236-
if (typeof translations !== "undefined") {
237-
while (typeof translations[locale] === "undefined") {
238-
locale = localesFallbacks[locale];
239-
if (!locale) {
240-
break;
223+
const message = _messages[id] ?? null;
224+
if (message === null) {
225+
return id;
226+
}
227+
const translationsIntl = message.translations[`${domain}+intl-icu`] ?? void 0;
228+
if (typeof translationsIntl !== "undefined") {
229+
while (typeof translationsIntl[locale2] === "undefined") {
230+
locale2 = _localeFallbacks[locale2];
231+
if (!locale2) {
232+
break;
233+
}
234+
}
235+
if (locale2) {
236+
return formatIntl(translationsIntl[locale2], parameters, locale2);
241237
}
242238
}
243-
if (locale) {
244-
return format(translations[locale], parameters, locale);
239+
const translations = message.translations[domain] ?? void 0;
240+
if (typeof translations !== "undefined") {
241+
while (typeof translations[locale2] === "undefined") {
242+
locale2 = _localeFallbacks[locale2];
243+
if (!locale2) {
244+
break;
245+
}
246+
}
247+
if (locale2) {
248+
return format(translations[locale2], parameters, locale2);
249+
}
245250
}
251+
if (_throwWhenNotFound) {
252+
throw new Error(`No translation message found with id "${id}".`);
253+
}
254+
return id;
246255
}
247-
if (_throwWhenNotFound) {
248-
throw new Error(`No translation message found with id "${message.id}".`);
249-
}
250-
return message.id;
256+
return {
257+
setLocale,
258+
getLocale,
259+
setThrowWhenNotFound,
260+
trans
261+
};
251262
}
252263
export {
253-
getLocale,
254-
getLocaleFallbacks,
255-
setLocale,
256-
setLocaleFallbacks,
257-
throwWhenNotFound,
258-
trans
264+
createTranslator,
265+
getDefaultLocale
259266
};

src/Translator/assets/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727
"symfony": {
2828
"importmap": {
2929
"intl-messageformat": "^10.5.11",
30-
"@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js",
31-
"@app/translations": "path:var/translations/index.js",
32-
"@app/translations/configuration": "path:var/translations/configuration.js"
30+
"@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js"
3331
}
3432
},
3533
"peerDependencies": {

0 commit comments

Comments
 (0)