mirror of
https://github.com/symfony/ux-translator.git
synced 2026-03-24 00:12:19 +01:00
[Translator] Refactor API to use string-based translation keys instead of generated constants
This commit is contained in:
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.32
|
||||
|
||||
- **[BC BREAK]** Refactor API to use string-based translation keys instead of generated constants.
|
||||
|
||||
Translation keys are now simple strings instead of TypeScript constants.
|
||||
The main advantages are:
|
||||
- You can now use **exactly the same translation keys** as in your Symfony PHP code
|
||||
- Simpler and more readable code
|
||||
- No need to memorize generated constant names
|
||||
- No need to import translation constants: smaller files
|
||||
- And you can still get autocompletion and type-safety :rocket:
|
||||
|
||||
**Before:**
|
||||
```typescript
|
||||
import { trans } from '@symfony/ux-translator';
|
||||
import { SYMFONY_GREAT } from '@app/translations';
|
||||
|
||||
trans(SYMFONY_GREAT);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```typescript
|
||||
import { createTranslator } from '@symfony/ux-translator';
|
||||
import { messages } from '../var/translations/index.js';
|
||||
|
||||
const { trans } = createTranslator({ messages });
|
||||
trans('symfony.great');
|
||||
```
|
||||
|
||||
The global functions (`setLocale`, `getLocale`, `setLocaleFallbacks`, `getLocaleFallbacks`, `throwWhenNotFound`)
|
||||
have been replaced by a new `createTranslator()` factory function that returns an object with these methods.
|
||||
|
||||
**Tree-shaking:** While tree-shaking of individual translation keys is no longer possible, modern build tools,
|
||||
caching strategies, and compression techniques (Brotli, gzip) make this negligible in 2025.
|
||||
A future feature will allow filtering dumped translations by pattern for those who need it,
|
||||
further reducing bundle size.
|
||||
|
||||
**For AssetMapper users:** You can remove the following entries from your `importmap.php`:
|
||||
```php
|
||||
'@app/translations' => [
|
||||
'path' => './var/translations/index.js',
|
||||
],
|
||||
'@app/translations/configuration' => [
|
||||
'path' => './var/translations/configuration.js',
|
||||
],
|
||||
```
|
||||
|
||||
**Note:** This is a breaking change, but the UX Translator component is still experimental.
|
||||
|
||||
## 2.30
|
||||
|
||||
- Ensure compatibility with PHP 8.5
|
||||
|
||||
40
assets/dist/translator_controller.d.ts
vendored
40
assets/dist/translator_controller.d.ts
vendored
@@ -1,29 +1,41 @@
|
||||
type MessageId = string;
|
||||
type DomainType = string;
|
||||
type LocaleType = string;
|
||||
type TranslationsType = Record<DomainType, {
|
||||
parameters: ParametersType;
|
||||
}>;
|
||||
|
||||
type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
|
||||
type NoParametersType = Record<string, never>;
|
||||
type ParametersType = Record<string, string | number | Date> | NoParametersType;
|
||||
|
||||
type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
|
||||
type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
|
||||
type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
|
||||
type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends {
|
||||
parameters: infer Parameters;
|
||||
} ? Parameters : never : never;
|
||||
type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType>
|
||||
? Translations[D] extends { parameters: infer Parameters }
|
||||
? Parameters
|
||||
: never
|
||||
: never;
|
||||
|
||||
interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
|
||||
id: string;
|
||||
translations: {
|
||||
[domain in DomainType]: {
|
||||
[locale in Locale]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
declare function setLocale(locale: LocaleType | null): void;
|
||||
declare function getLocale(): LocaleType;
|
||||
declare function throwWhenNotFound(enabled: boolean): void;
|
||||
declare function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void;
|
||||
declare function getLocaleFallbacks(): Record<LocaleType, LocaleType>;
|
||||
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;
|
||||
|
||||
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 };
|
||||
type Messages = Record<MessageId, Message<TranslationsType, LocaleType>>;
|
||||
|
||||
declare function getDefaultLocale(): LocaleType;
|
||||
declare function createTranslator<TMessages extends Messages>({ messages, locale, localeFallbacks, throwWhenNotFound, }: {
|
||||
messages: TMessages;
|
||||
locale?: LocaleType;
|
||||
localeFallbacks?: Record<LocaleType, LocaleType>;
|
||||
throwWhenNotFound?: boolean;
|
||||
}): {
|
||||
setLocale(locale: LocaleType): void;
|
||||
getLocale(): LocaleType;
|
||||
setThrowWhenNotFound(throwWhenNotFound: boolean): void;
|
||||
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;
|
||||
};
|
||||
|
||||
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 };
|
||||
|
||||
119
assets/dist/translator_controller.js
vendored
119
assets/dist/translator_controller.js
vendored
@@ -62,12 +62,12 @@ function format(id, parameters, locale) {
|
||||
}
|
||||
function getPluralizationRule(number, locale) {
|
||||
number = Math.abs(number);
|
||||
let _locale2 = locale;
|
||||
let _locale = locale;
|
||||
if (locale === "pt_BR" || locale === "en_US_POSIX") {
|
||||
return 0;
|
||||
}
|
||||
_locale2 = _locale2.length > 3 ? _locale2.substring(0, _locale2.indexOf("_")) : _locale2;
|
||||
switch (_locale2) {
|
||||
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf("_")) : _locale;
|
||||
switch (_locale) {
|
||||
case "af":
|
||||
case "bn":
|
||||
case "bg":
|
||||
@@ -189,71 +189,78 @@ function formatIntl(id, parameters, locale) {
|
||||
}
|
||||
|
||||
// src/translator_controller.ts
|
||||
var _locale = null;
|
||||
var _localeFallbacks = {};
|
||||
var _throwWhenNotFound = false;
|
||||
function setLocale(locale) {
|
||||
_locale = locale;
|
||||
}
|
||||
function getLocale() {
|
||||
return _locale || document.documentElement.getAttribute("data-symfony-ux-translator-locale") || // <html data-symfony-ux-translator-locale="en_US">
|
||||
function getDefaultLocale() {
|
||||
return document.documentElement.getAttribute("data-symfony-ux-translator-locale") || // <html data-symfony-ux-translator-locale="en_US">
|
||||
(document.documentElement.lang ? document.documentElement.lang.replace("-", "_") : null) || // <html lang="en-US">
|
||||
"en";
|
||||
}
|
||||
function throwWhenNotFound(enabled) {
|
||||
_throwWhenNotFound = enabled;
|
||||
}
|
||||
function setLocaleFallbacks(localeFallbacks) {
|
||||
_localeFallbacks = localeFallbacks;
|
||||
}
|
||||
function getLocaleFallbacks() {
|
||||
return _localeFallbacks;
|
||||
}
|
||||
function trans(message, parameters = {}, domain = "messages", locale = null) {
|
||||
if (typeof domain === "undefined") {
|
||||
domain = "messages";
|
||||
function createTranslator({
|
||||
messages,
|
||||
locale = getDefaultLocale(),
|
||||
localeFallbacks = {},
|
||||
throwWhenNotFound = false
|
||||
}) {
|
||||
const _messages = messages;
|
||||
const _localeFallbacks = localeFallbacks;
|
||||
let _locale = locale;
|
||||
let _throwWhenNotFound = throwWhenNotFound;
|
||||
function setLocale(locale2) {
|
||||
_locale = locale2;
|
||||
}
|
||||
if (typeof locale === "undefined" || null === locale) {
|
||||
locale = getLocale();
|
||||
function getLocale() {
|
||||
return _locale;
|
||||
}
|
||||
if (typeof message.translations === "undefined") {
|
||||
return message.id;
|
||||
function setThrowWhenNotFound(throwWhenNotFound2) {
|
||||
_throwWhenNotFound = throwWhenNotFound2;
|
||||
}
|
||||
const localesFallbacks = getLocaleFallbacks();
|
||||
const translationsIntl = message.translations[`${domain}+intl-icu`];
|
||||
if (typeof translationsIntl !== "undefined") {
|
||||
while (typeof translationsIntl[locale] === "undefined") {
|
||||
locale = localesFallbacks[locale];
|
||||
if (!locale) {
|
||||
break;
|
||||
function trans(id, parameters = {}, domain = "messages", locale2 = null) {
|
||||
if (typeof domain === "undefined") {
|
||||
domain = "messages";
|
||||
}
|
||||
if (typeof locale2 === "undefined" || null === locale2) {
|
||||
locale2 = _locale;
|
||||
}
|
||||
const message = _messages[id] ?? null;
|
||||
if (message === null) {
|
||||
return id;
|
||||
}
|
||||
const translationsIntl = message.translations[`${domain}+intl-icu`] ?? void 0;
|
||||
if (typeof translationsIntl !== "undefined") {
|
||||
while (typeof translationsIntl[locale2] === "undefined") {
|
||||
locale2 = _localeFallbacks[locale2];
|
||||
if (!locale2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (locale2) {
|
||||
return formatIntl(translationsIntl[locale2], parameters, locale2);
|
||||
}
|
||||
}
|
||||
if (locale) {
|
||||
return formatIntl(translationsIntl[locale], parameters, locale);
|
||||
}
|
||||
}
|
||||
const translations = message.translations[domain];
|
||||
if (typeof translations !== "undefined") {
|
||||
while (typeof translations[locale] === "undefined") {
|
||||
locale = localesFallbacks[locale];
|
||||
if (!locale) {
|
||||
break;
|
||||
const translations = message.translations[domain] ?? void 0;
|
||||
if (typeof translations !== "undefined") {
|
||||
while (typeof translations[locale2] === "undefined") {
|
||||
locale2 = _localeFallbacks[locale2];
|
||||
if (!locale2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (locale2) {
|
||||
return format(translations[locale2], parameters, locale2);
|
||||
}
|
||||
}
|
||||
if (locale) {
|
||||
return format(translations[locale], parameters, locale);
|
||||
if (_throwWhenNotFound) {
|
||||
throw new Error(`No translation message found with id "${id}".`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
if (_throwWhenNotFound) {
|
||||
throw new Error(`No translation message found with id "${message.id}".`);
|
||||
}
|
||||
return message.id;
|
||||
return {
|
||||
setLocale,
|
||||
getLocale,
|
||||
setThrowWhenNotFound,
|
||||
trans
|
||||
};
|
||||
}
|
||||
export {
|
||||
getLocale,
|
||||
getLocaleFallbacks,
|
||||
setLocale,
|
||||
setLocaleFallbacks,
|
||||
throwWhenNotFound,
|
||||
trans
|
||||
createTranslator,
|
||||
getDefaultLocale
|
||||
};
|
||||
|
||||
@@ -27,9 +27,7 @@
|
||||
"symfony": {
|
||||
"importmap": {
|
||||
"intl-messageformat": "^10.5.11",
|
||||
"@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js",
|
||||
"@app/translations": "path:var/translations/index.js",
|
||||
"@app/translations/configuration": "path:var/translations/configuration.js"
|
||||
"@symfony/ux-translator": "path:%PACKAGE%/dist/translator_controller.js"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -7,168 +7,176 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export type DomainType = string;
|
||||
export type LocaleType = string;
|
||||
|
||||
export type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
|
||||
export type NoParametersType = Record<string, never>;
|
||||
export type ParametersType = Record<string, string | number | Date> | NoParametersType;
|
||||
|
||||
export type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
|
||||
export type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
|
||||
export type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
|
||||
export type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType>
|
||||
? Translations[D] extends { parameters: infer Parameters }
|
||||
? Parameters
|
||||
: never
|
||||
: never;
|
||||
|
||||
export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
|
||||
id: string;
|
||||
translations: {
|
||||
[domain in DomainType]: {
|
||||
[locale in Locale]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
import { format } from './formatters/formatter';
|
||||
import { formatIntl } from './formatters/intl-formatter';
|
||||
import type { DomainsOf, LocaleOf, LocaleType, MessageId, Messages, ParametersOf, RemoveIntlIcuSuffix } from './types';
|
||||
|
||||
let _locale: LocaleType | null = null;
|
||||
let _localeFallbacks: Record<LocaleType, LocaleType> = {};
|
||||
let _throwWhenNotFound = false;
|
||||
export * from './types.d';
|
||||
|
||||
export function setLocale(locale: LocaleType | null) {
|
||||
_locale = locale;
|
||||
}
|
||||
|
||||
export function getLocale(): LocaleType {
|
||||
export function getDefaultLocale(): LocaleType {
|
||||
return (
|
||||
_locale ||
|
||||
document.documentElement.getAttribute('data-symfony-ux-translator-locale') || // <html data-symfony-ux-translator-locale="en_US">
|
||||
(document.documentElement.lang ? document.documentElement.lang.replace('-', '_') : null) || // <html lang="en-US">
|
||||
'en'
|
||||
);
|
||||
}
|
||||
|
||||
export function throwWhenNotFound(enabled: boolean): void {
|
||||
_throwWhenNotFound = enabled;
|
||||
}
|
||||
export function createTranslator<TMessages extends Messages>({
|
||||
messages,
|
||||
locale = getDefaultLocale(),
|
||||
localeFallbacks = {},
|
||||
throwWhenNotFound = false,
|
||||
}: {
|
||||
messages: TMessages;
|
||||
locale?: LocaleType;
|
||||
localeFallbacks?: Record<LocaleType, LocaleType>;
|
||||
throwWhenNotFound?: boolean;
|
||||
}): {
|
||||
setLocale(locale: LocaleType): void;
|
||||
getLocale(): LocaleType;
|
||||
setThrowWhenNotFound(throwWhenNotFound: boolean): void;
|
||||
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;
|
||||
} {
|
||||
const _messages = messages;
|
||||
const _localeFallbacks = localeFallbacks;
|
||||
let _locale = locale;
|
||||
let _throwWhenNotFound = throwWhenNotFound;
|
||||
|
||||
export function setLocaleFallbacks(localeFallbacks: Record<LocaleType, LocaleType>): void {
|
||||
_localeFallbacks = localeFallbacks;
|
||||
}
|
||||
|
||||
export function getLocaleFallbacks(): Record<LocaleType, LocaleType> {
|
||||
return _localeFallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given message, in ICU format (see https://formatjs.io/docs/intl-messageformat) or Symfony format (see below).
|
||||
*
|
||||
* When a number is provided as a parameter named "%count%", the message is parsed for plural
|
||||
* forms and a translation is chosen according to this number using the following rules:
|
||||
*
|
||||
* Given a message with different plural translations separated by a
|
||||
* pipe (|), this method returns the correct portion of the message based
|
||||
* on the given number, locale and the pluralization rules in the message
|
||||
* itself.
|
||||
*
|
||||
* The message supports two different types of pluralization rules:
|
||||
*
|
||||
* interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
|
||||
* indexed: There is one apple|There are %count% apples
|
||||
*
|
||||
* The indexed solution can also contain labels (e.g. one: There is one apple).
|
||||
* This is purely for making the translations more clear - it does not
|
||||
* affect the functionality.
|
||||
*
|
||||
* The two methods can also be mixed:
|
||||
* {0} There are no apples|one: There is one apple|more: There are %count% apples
|
||||
*
|
||||
* An interval can represent a finite set of numbers:
|
||||
* {1,2,3,4}
|
||||
*
|
||||
* An interval can represent numbers between two numbers:
|
||||
* [1, +Inf]
|
||||
* ]-1,2[
|
||||
*
|
||||
* The left delimiter can be [ (inclusive) or ] (exclusive).
|
||||
* The right delimiter can be [ (exclusive) or ] (inclusive).
|
||||
* Beside numbers, you can use -Inf and +Inf for the infinite.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/ISO_31-11
|
||||
*
|
||||
* @param message The message
|
||||
* @param parameters An array of parameters for the message
|
||||
* @param domain The domain for the message or null to use the default
|
||||
* @param locale The locale or null to use the default
|
||||
*/
|
||||
export 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;
|
||||
export function trans<
|
||||
M extends Message<TranslationsType, LocaleType>,
|
||||
D extends DomainsOf<M>,
|
||||
P extends ParametersOf<M, D>,
|
||||
>(
|
||||
message: M,
|
||||
parameters: P = {} as P,
|
||||
domain: RemoveIntlIcuSuffix<DomainsOf<M>> | undefined = 'messages' as RemoveIntlIcuSuffix<DomainsOf<M>>,
|
||||
locale: LocaleOf<M> | null = null
|
||||
): string {
|
||||
if (typeof domain === 'undefined') {
|
||||
domain = 'messages' as RemoveIntlIcuSuffix<DomainsOf<M>>;
|
||||
/**
|
||||
* Sets the locale.
|
||||
*/
|
||||
function setLocale(locale: LocaleType) {
|
||||
_locale = locale;
|
||||
}
|
||||
|
||||
if (typeof locale === 'undefined' || null === locale) {
|
||||
locale = getLocale() as LocaleOf<M>;
|
||||
/**
|
||||
* Returns the current locale.
|
||||
*/
|
||||
function getLocale(): LocaleType {
|
||||
return _locale;
|
||||
}
|
||||
|
||||
if (typeof message.translations === 'undefined') {
|
||||
return message.id;
|
||||
/**
|
||||
* Sets whether an error should be thrown when a translation is not found.
|
||||
*/
|
||||
function setThrowWhenNotFound(throwWhenNotFound: boolean) {
|
||||
_throwWhenNotFound = throwWhenNotFound;
|
||||
}
|
||||
|
||||
const localesFallbacks = getLocaleFallbacks();
|
||||
/**
|
||||
* Translates the given message, in ICU format (see https://formatjs.io/docs/intl-messageformat) or Symfony format (see below).
|
||||
*
|
||||
* When a number is provided as a parameter named "%count%", the message is parsed for plural
|
||||
* forms and a translation is chosen according to this number using the following rules:
|
||||
*
|
||||
* Given a message with different plural translations separated by a
|
||||
* pipe (|), this method returns the correct portion of the message based
|
||||
* on the given number, locale and the pluralization rules in the message
|
||||
* itself.
|
||||
*
|
||||
* The message supports two different types of pluralization rules:
|
||||
*
|
||||
* interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
|
||||
* indexed: There is one apple|There are %count% apples
|
||||
*
|
||||
* The indexed solution can also contain labels (e.g. one: There is one apple).
|
||||
* This is purely for making the translations more clear - it does not
|
||||
* affect the functionality.
|
||||
*
|
||||
* The two methods can also be mixed:
|
||||
* {0} There are no apples|one: There is one apple|more: There are %count% apples
|
||||
*
|
||||
* An interval can represent a finite set of numbers:
|
||||
* {1,2,3,4}
|
||||
*
|
||||
* An interval can represent numbers between two numbers:
|
||||
* [1, +Inf]
|
||||
* ]-1,2[
|
||||
*
|
||||
* The left delimiter can be [ (inclusive) or ] (exclusive).
|
||||
* The right delimiter can be [ (exclusive) or ] (inclusive).
|
||||
* Beside numbers, you can use -Inf and +Inf for the infinite.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/ISO_31-11
|
||||
*
|
||||
* @param id The message ID
|
||||
* @param parameters An array of parameters for the message
|
||||
* @param domain The domain for the message or null to use the default
|
||||
* @param locale The locale or null to use the default
|
||||
*/
|
||||
function trans<
|
||||
TMessageId extends keyof TMessages & MessageId,
|
||||
TMessage extends TMessages[TMessageId],
|
||||
TDomain extends DomainsOf<TMessage>,
|
||||
TParameters extends ParametersOf<TMessage, TDomain>,
|
||||
>(
|
||||
id: TMessageId,
|
||||
parameters: TParameters = {} as TParameters,
|
||||
domain: RemoveIntlIcuSuffix<TDomain> | undefined = 'messages' as RemoveIntlIcuSuffix<TDomain>,
|
||||
locale: LocaleOf<TMessage> | null = null
|
||||
): string {
|
||||
if (typeof domain === 'undefined') {
|
||||
domain = 'messages' as RemoveIntlIcuSuffix<TDomain>;
|
||||
}
|
||||
|
||||
const translationsIntl = message.translations[`${domain}+intl-icu`];
|
||||
if (typeof translationsIntl !== 'undefined') {
|
||||
while (typeof translationsIntl[locale] === 'undefined') {
|
||||
locale = localesFallbacks[locale] as LocaleOf<M>;
|
||||
if (!locale) {
|
||||
break;
|
||||
if (typeof locale === 'undefined' || null === locale) {
|
||||
locale = _locale as LocaleOf<TMessage>;
|
||||
}
|
||||
|
||||
const message = _messages[id] ?? (null as TMessage | null);
|
||||
if (message === null) {
|
||||
return id;
|
||||
}
|
||||
|
||||
const translationsIntl = message.translations[`${domain}+intl-icu`] ?? undefined;
|
||||
if (typeof translationsIntl !== 'undefined') {
|
||||
while (typeof translationsIntl[locale] === 'undefined') {
|
||||
locale = _localeFallbacks[locale] as LocaleOf<TMessage>;
|
||||
if (!locale) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
return formatIntl(translationsIntl[locale], parameters, locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
return formatIntl(translationsIntl[locale], parameters, locale);
|
||||
}
|
||||
}
|
||||
const translations = message.translations[domain] ?? undefined;
|
||||
if (typeof translations !== 'undefined') {
|
||||
while (typeof translations[locale] === 'undefined') {
|
||||
locale = _localeFallbacks[locale] as LocaleOf<TMessage>;
|
||||
if (!locale) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const translations = message.translations[domain];
|
||||
if (typeof translations !== 'undefined') {
|
||||
while (typeof translations[locale] === 'undefined') {
|
||||
locale = localesFallbacks[locale] as LocaleOf<M>;
|
||||
if (!locale) {
|
||||
break;
|
||||
if (locale) {
|
||||
return format(translations[locale], parameters, locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
return format(translations[locale], parameters, locale);
|
||||
if (_throwWhenNotFound) {
|
||||
throw new Error(`No translation message found with id "${id}".`);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
if (_throwWhenNotFound) {
|
||||
throw new Error(`No translation message found with id "${message.id}".`);
|
||||
}
|
||||
|
||||
return message.id;
|
||||
return {
|
||||
setLocale,
|
||||
getLocale,
|
||||
setThrowWhenNotFound,
|
||||
trans,
|
||||
};
|
||||
}
|
||||
|
||||
26
assets/src/types.d.ts
vendored
Normal file
26
assets/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
export type MessageId = string;
|
||||
export type DomainType = string;
|
||||
export type LocaleType = string;
|
||||
|
||||
export type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
|
||||
export type NoParametersType = Record<string, never>;
|
||||
export type ParametersType = Record<string, string | number | Date> | NoParametersType;
|
||||
|
||||
export type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
|
||||
export type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
|
||||
export type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
|
||||
export type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType>
|
||||
? Translations[D] extends { parameters: infer Parameters }
|
||||
? Parameters
|
||||
: never
|
||||
: never;
|
||||
|
||||
export interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
|
||||
translations: {
|
||||
[domain in DomainType]: {
|
||||
[locale in Locale]: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type Messages = Record<MessageId, Message<TranslationsType, LocaleType>>;
|
||||
@@ -1,110 +1,108 @@
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import {
|
||||
getLocale,
|
||||
type Message,
|
||||
type NoParametersType,
|
||||
setLocale,
|
||||
setLocaleFallbacks,
|
||||
throwWhenNotFound,
|
||||
trans,
|
||||
} from '../../src/translator_controller';
|
||||
import { createTranslator } from '../../src/translator_controller';
|
||||
import type { Message, NoParametersType } from '../../src/types';
|
||||
|
||||
describe('Translator', () => {
|
||||
beforeEach(() => {
|
||||
setLocale(null);
|
||||
setLocaleFallbacks({});
|
||||
throwWhenNotFound(false);
|
||||
document.documentElement.lang = '';
|
||||
document.documentElement.removeAttribute('data-symfony-ux-translator-locale');
|
||||
});
|
||||
|
||||
describe('getLocale', () => {
|
||||
describe('create translator with locale', () => {
|
||||
test('default locale', () => {
|
||||
let translator = createTranslator({ messages: {} });
|
||||
|
||||
// 'en' is the default locale
|
||||
expect(getLocale()).toEqual('en');
|
||||
expect(translator.getLocale()).toEqual('en');
|
||||
|
||||
// or the locale from <html lang="...">, if exists
|
||||
document.documentElement.lang = 'fr';
|
||||
expect(getLocale()).toEqual('fr');
|
||||
translator = createTranslator({ messages: {} });
|
||||
expect(translator.getLocale()).toEqual('fr');
|
||||
|
||||
// or the locale from <html data-symfony-ux-translator-locale="...">, if exists
|
||||
document.documentElement.setAttribute('data-symfony-ux-translator-locale', 'it');
|
||||
expect(getLocale()).toEqual('it');
|
||||
|
||||
setLocale('de');
|
||||
expect(getLocale()).toEqual('de');
|
||||
translator = createTranslator({ messages: {} });
|
||||
expect(translator.getLocale()).toEqual('it');
|
||||
});
|
||||
|
||||
test('custom locale', () => {
|
||||
document.documentElement.lang = 'fr';
|
||||
document.documentElement.setAttribute('data-symfony-ux-translator-locale', 'it');
|
||||
|
||||
const translator = createTranslator({ messages: {}, locale: 'de' });
|
||||
expect(translator.getLocale()).toEqual('de');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocale', () => {
|
||||
test('with subcode', () => {
|
||||
// allow format according to W3C
|
||||
document.documentElement.lang = 'de-AT';
|
||||
expect(getLocale()).toEqual('de_AT');
|
||||
const translator = createTranslator({ messages: {} });
|
||||
expect(translator.getLocale()).toEqual('de_AT');
|
||||
|
||||
// or "incorrect" Symfony locale format
|
||||
document.documentElement.lang = 'de_AT';
|
||||
expect(getLocale()).toEqual('de_AT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLocale', () => {
|
||||
test('custom locale', () => {
|
||||
setLocale('fr');
|
||||
|
||||
expect(getLocale()).toEqual('fr');
|
||||
expect(translator.getLocale()).toEqual('de_AT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trans', () => {
|
||||
test('basic message', () => {
|
||||
const MESSAGE_BASIC: Message<{ messages: { parameters: NoParametersType } }, 'en'> = {
|
||||
id: 'message.basic',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A basic message',
|
||||
const messages: {
|
||||
'message.basic': Message<{ messages: { parameters: NoParametersType } }, 'en'>;
|
||||
} = {
|
||||
'message.basic': {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A basic message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(trans(MESSAGE_BASIC)).toEqual('A basic message');
|
||||
expect(trans(MESSAGE_BASIC, {})).toEqual('A basic message');
|
||||
expect(trans(MESSAGE_BASIC, {}, 'messages')).toEqual('A basic message');
|
||||
expect(trans(MESSAGE_BASIC, {}, 'messages', 'en')).toEqual('A basic message');
|
||||
expect(translator.trans('message.basic')).toEqual('A basic message');
|
||||
expect(translator.trans('message.basic', {})).toEqual('A basic message');
|
||||
expect(translator.trans('message.basic', {}, 'messages')).toEqual('A basic message');
|
||||
expect(translator.trans('message.basic', {}, 'messages', 'en')).toEqual('A basic message');
|
||||
|
||||
// @ts-expect-error "%count%" is not a valid parameter
|
||||
expect(trans(MESSAGE_BASIC, { '%count%': 1 })).toEqual('A basic message');
|
||||
expect(translator.trans('message.basic', { '%count%': 1 })).toEqual('A basic message');
|
||||
|
||||
// @ts-expect-error "foo" is not a valid domain
|
||||
expect(trans(MESSAGE_BASIC, {}, 'foo')).toEqual('message.basic');
|
||||
expect(translator.trans('message.basic', {}, 'foo')).toEqual('message.basic');
|
||||
|
||||
// @ts-expect-error "fr" is not a valid locale
|
||||
expect(trans(MESSAGE_BASIC, {}, 'messages', 'fr')).toEqual('message.basic');
|
||||
expect(translator.trans('message.basic', {}, 'messages', 'fr')).toEqual('message.basic');
|
||||
});
|
||||
|
||||
test('basic message with parameters', () => {
|
||||
const MESSAGE_BASIC_WITH_PARAMETERS: Message<
|
||||
{ messages: { parameters: { '%parameter1%': string; '%parameter2%': string } } },
|
||||
'en'
|
||||
> = {
|
||||
id: 'message.basic.with.parameters',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A basic message %parameter1% %parameter2%',
|
||||
const messages: {
|
||||
'message.basic.with.parameters': Message<
|
||||
{ messages: { parameters: { '%parameter1%': string; '%parameter2%': string } } },
|
||||
'en'
|
||||
>;
|
||||
} = {
|
||||
'message.basic.with.parameters': {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A basic message %parameter1% %parameter2%',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(
|
||||
trans(MESSAGE_BASIC_WITH_PARAMETERS, {
|
||||
translator.trans('message.basic.with.parameters', {
|
||||
'%parameter1%': 'foo',
|
||||
'%parameter2%': 'bar',
|
||||
})
|
||||
).toEqual('A basic message foo bar');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
MESSAGE_BASIC_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.basic.with.parameters',
|
||||
{
|
||||
'%parameter1%': 'foo',
|
||||
'%parameter2%': 'bar',
|
||||
@@ -114,8 +112,8 @@ describe('Translator', () => {
|
||||
).toEqual('A basic message foo bar');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
MESSAGE_BASIC_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.basic.with.parameters',
|
||||
{
|
||||
'%parameter1%': 'foo',
|
||||
'%parameter2%': 'bar',
|
||||
@@ -126,82 +124,90 @@ describe('Translator', () => {
|
||||
).toEqual('A basic message foo bar');
|
||||
|
||||
// @ts-expect-error Parameters "%parameter1%" and "%parameter2%" are missing
|
||||
expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, {})).toEqual('A basic message %parameter1% %parameter2%');
|
||||
expect(translator.trans('message.basic.with.parameters', {})).toEqual(
|
||||
'A basic message %parameter1% %parameter2%'
|
||||
);
|
||||
|
||||
// @ts-expect-error Parameter "%parameter2%" is missing
|
||||
expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { '%parameter1%': 'foo' })).toEqual(
|
||||
expect(translator.trans('message.basic.with.parameters', { '%parameter1%': 'foo' })).toEqual(
|
||||
'A basic message foo %parameter2%'
|
||||
);
|
||||
|
||||
expect(
|
||||
trans(
|
||||
MESSAGE_BASIC_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.basic.with.parameters',
|
||||
{
|
||||
'%parameter1%': 'foo',
|
||||
'%parameter2%': 'bar',
|
||||
// @ts-expect-error "foobar" is not a valid domain
|
||||
},
|
||||
// @ts-expect-error "foobar" is not a valid domain
|
||||
'foobar'
|
||||
)
|
||||
).toEqual('message.basic.with.parameters');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
MESSAGE_BASIC_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.basic.with.parameters',
|
||||
{
|
||||
'%parameter1%': 'foo',
|
||||
'%parameter2%': 'bar',
|
||||
// @ts-expect-error "fr" is not a valid locale
|
||||
},
|
||||
'messages',
|
||||
// @ts-expect-error "fr" is not a valid locale
|
||||
'fr'
|
||||
)
|
||||
).toEqual('message.basic.with.parameters');
|
||||
});
|
||||
|
||||
test('intl message', () => {
|
||||
const MESSAGE_INTL: Message<{ 'messages+intl-icu': { parameters: NoParametersType } }, 'en'> = {
|
||||
id: 'message.intl',
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: 'An intl message',
|
||||
const messages: {
|
||||
'message.intl': Message<{ 'messages+intl-icu': { parameters: NoParametersType } }, 'en'>;
|
||||
} = {
|
||||
'message.intl': {
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: 'An intl message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(trans(MESSAGE_INTL)).toEqual('An intl message');
|
||||
expect(trans(MESSAGE_INTL, {})).toEqual('An intl message');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages')).toEqual('An intl message');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'en')).toEqual('An intl message');
|
||||
expect(translator.trans('message.intl')).toEqual('An intl message');
|
||||
expect(translator.trans('message.intl', {})).toEqual('An intl message');
|
||||
expect(translator.trans('message.intl', {}, 'messages')).toEqual('An intl message');
|
||||
expect(translator.trans('message.intl', {}, 'messages', 'en')).toEqual('An intl message');
|
||||
|
||||
// @ts-expect-error "%count%" is not a valid parameter
|
||||
expect(trans(MESSAGE_INTL, { '%count%': 1 })).toEqual('An intl message');
|
||||
expect(translator.trans('message.intl', { '%count%': 1 })).toEqual('An intl message');
|
||||
|
||||
// @ts-expect-error "foo" is not a valid domain
|
||||
expect(trans(MESSAGE_INTL, {}, 'foo')).toEqual('message.intl');
|
||||
expect(translator.trans('message.intl', {}, 'foo')).toEqual('message.intl');
|
||||
|
||||
// @ts-expect-error "fr" is not a valid locale
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'fr')).toEqual('message.intl');
|
||||
expect(translator.trans('message.intl', {}, 'messages', 'fr')).toEqual('message.intl');
|
||||
});
|
||||
|
||||
test('intl message with parameters', () => {
|
||||
const INTL_MESSAGE_WITH_PARAMETERS: Message<
|
||||
{
|
||||
'messages+intl-icu': {
|
||||
parameters: {
|
||||
gender_of_host: 'male' | 'female' | string;
|
||||
num_guests: number;
|
||||
host: string;
|
||||
guest: string;
|
||||
const messages: {
|
||||
'message.intl.with.parameters': Message<
|
||||
{
|
||||
'messages+intl-icu': {
|
||||
parameters: {
|
||||
gender_of_host: 'male' | 'female' | string;
|
||||
num_guests: number;
|
||||
host: string;
|
||||
guest: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
'en'
|
||||
> = {
|
||||
id: 'message.intl.with.parameters',
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: `
|
||||
},
|
||||
'en'
|
||||
>;
|
||||
} = {
|
||||
'message.intl.with.parameters': {
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: `
|
||||
{gender_of_host, select,
|
||||
female {{num_guests, plural, offset:1
|
||||
=0 {{host} does not give a party.}
|
||||
@@ -218,12 +224,14 @@ describe('Translator', () => {
|
||||
=1 {{host} invites {guest} to their party.}
|
||||
=2 {{host} invites {guest} and one other person to their party.}
|
||||
other {{host} invites {guest} as one of the # people invited to their party.}}}}`.trim(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(
|
||||
trans(INTL_MESSAGE_WITH_PARAMETERS, {
|
||||
translator.trans('message.intl.with.parameters', {
|
||||
gender_of_host: 'male',
|
||||
num_guests: 123,
|
||||
host: 'John',
|
||||
@@ -232,8 +240,8 @@ describe('Translator', () => {
|
||||
).toEqual('John invites Mary as one of the 122 people invited to his party.');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
INTL_MESSAGE_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.intl.with.parameters',
|
||||
{
|
||||
gender_of_host: 'female',
|
||||
num_guests: 44,
|
||||
@@ -245,8 +253,8 @@ describe('Translator', () => {
|
||||
).toEqual('Mary invites John as one of the 43 people invited to her party.');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
INTL_MESSAGE_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.intl.with.parameters',
|
||||
{
|
||||
gender_of_host: 'female',
|
||||
num_guests: 1,
|
||||
@@ -260,19 +268,19 @@ describe('Translator', () => {
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error Parameters "gender_of_host", "num_guests", "host", and "guest" are missing
|
||||
trans(INTL_MESSAGE_WITH_PARAMETERS, {});
|
||||
translator.trans('message.intl.with.parameters', {});
|
||||
}).toThrow(/^The intl string context variable "gender_of_host" was not provided/);
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error Parameters "num_guests", "host", and "guest" are missing
|
||||
trans(INTL_MESSAGE_WITH_PARAMETERS, {
|
||||
translator.trans('message.intl.with.parameters', {
|
||||
gender_of_host: 'male',
|
||||
});
|
||||
}).toThrow(/^The intl string context variable "num_guests" was not provided/);
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error Parameters "host", and "guest" are missing
|
||||
trans(INTL_MESSAGE_WITH_PARAMETERS, {
|
||||
translator.trans('message.intl.with.parameters', {
|
||||
gender_of_host: 'male',
|
||||
num_guests: 123,
|
||||
});
|
||||
@@ -280,7 +288,7 @@ describe('Translator', () => {
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error Parameter "guest" is missing
|
||||
trans(INTL_MESSAGE_WITH_PARAMETERS, {
|
||||
translator.trans('message.intl.with.parameters', {
|
||||
gender_of_host: 'male',
|
||||
num_guests: 123,
|
||||
host: 'John',
|
||||
@@ -288,8 +296,8 @@ describe('Translator', () => {
|
||||
}).toThrow(/^The intl string context variable "guest" was not provided/);
|
||||
|
||||
expect(
|
||||
trans(
|
||||
INTL_MESSAGE_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.intl.with.parameters',
|
||||
{
|
||||
gender_of_host: 'male',
|
||||
num_guests: 123,
|
||||
@@ -302,8 +310,8 @@ describe('Translator', () => {
|
||||
).toEqual('message.intl.with.parameters');
|
||||
|
||||
expect(
|
||||
trans(
|
||||
INTL_MESSAGE_WITH_PARAMETERS,
|
||||
translator.trans(
|
||||
'message.intl.with.parameters',
|
||||
{
|
||||
gender_of_host: 'male',
|
||||
num_guests: 123,
|
||||
@@ -318,197 +326,296 @@ describe('Translator', () => {
|
||||
});
|
||||
|
||||
test('same message id for multiple domains', () => {
|
||||
const MESSAGE_MULTI_DOMAINS: Message<
|
||||
{ foobar: { parameters: NoParametersType }; messages: { parameters: NoParametersType } },
|
||||
'en'
|
||||
> = {
|
||||
id: 'message.multi_domains',
|
||||
translations: {
|
||||
foobar: {
|
||||
en: 'A message from foobar catalogue',
|
||||
},
|
||||
messages: {
|
||||
en: 'A message from messages catalogue',
|
||||
const messages: {
|
||||
'message.multi_domains': Message<
|
||||
{ foobar: { parameters: NoParametersType }; messages: { parameters: NoParametersType } },
|
||||
'en'
|
||||
>;
|
||||
} = {
|
||||
'message.multi_domains': {
|
||||
translations: {
|
||||
foobar: {
|
||||
en: 'A message from foobar catalogue',
|
||||
},
|
||||
messages: {
|
||||
en: 'A message from messages catalogue',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS)).toEqual('A message from messages catalogue');
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {})).toEqual('A message from messages catalogue');
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'messages')).toEqual('A message from messages catalogue');
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'foobar')).toEqual('A message from foobar catalogue');
|
||||
expect(translator.trans('message.multi_domains')).toEqual('A message from messages catalogue');
|
||||
expect(translator.trans('message.multi_domains', {})).toEqual('A message from messages catalogue');
|
||||
expect(translator.trans('message.multi_domains', {}, 'messages')).toEqual(
|
||||
'A message from messages catalogue'
|
||||
);
|
||||
expect(translator.trans('message.multi_domains', {}, 'foobar')).toEqual('A message from foobar catalogue');
|
||||
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'messages', 'en')).toEqual('A message from messages catalogue');
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'foobar', 'en')).toEqual('A message from foobar catalogue');
|
||||
expect(translator.trans('message.multi_domains', {}, 'messages', 'en')).toEqual(
|
||||
'A message from messages catalogue'
|
||||
);
|
||||
expect(translator.trans('message.multi_domains', {}, 'foobar', 'en')).toEqual(
|
||||
'A message from foobar catalogue'
|
||||
);
|
||||
|
||||
// @ts-expect-error Domain "acme" is invalid
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'acme', 'fr')).toEqual('message.multi_domains');
|
||||
expect(translator.trans('message.multi_domains', {}, 'acme', 'fr')).toEqual('message.multi_domains');
|
||||
|
||||
// @ts-expect-error Locale "fr" is invalid
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'messages', 'fr')).toEqual('message.multi_domains');
|
||||
expect(translator.trans('message.multi_domains', {}, 'messages', 'fr')).toEqual('message.multi_domains');
|
||||
|
||||
// @ts-expect-error Locale "fr" is invalid
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS, {}, 'foobar', 'fr')).toEqual('message.multi_domains');
|
||||
expect(translator.trans('message.multi_domains', {}, 'foobar', 'fr')).toEqual('message.multi_domains');
|
||||
});
|
||||
|
||||
test('same message id for multiple domains, and different parameters', () => {
|
||||
const MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS: Message<
|
||||
{
|
||||
foobar: { parameters: { '%parameter2%': string } };
|
||||
messages: { parameters: { '%parameter1%': string } };
|
||||
},
|
||||
'en'
|
||||
> = {
|
||||
id: 'message.multi_domains.different_parameters',
|
||||
translations: {
|
||||
foobar: {
|
||||
en: 'A message from foobar catalogue with a parameter %parameter2%',
|
||||
const messages: {
|
||||
'message.multi_domains.different_parameters': Message<
|
||||
{
|
||||
foobar: { parameters: { '%parameter2%': string } };
|
||||
messages: { parameters: { '%parameter1%': string } };
|
||||
},
|
||||
messages: {
|
||||
en: 'A message from messages catalogue with a parameter %parameter1%',
|
||||
'en'
|
||||
>;
|
||||
} = {
|
||||
'message.multi_domains.different_parameters': {
|
||||
translations: {
|
||||
foobar: {
|
||||
en: 'A message from foobar catalogue with a parameter %parameter2%',
|
||||
},
|
||||
messages: {
|
||||
en: 'A message from messages catalogue with a parameter %parameter1%',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' })).toEqual(
|
||||
expect(translator.trans('message.multi_domains.different_parameters', { '%parameter1%': 'foo' })).toEqual(
|
||||
'A message from messages catalogue with a parameter foo'
|
||||
);
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages')).toEqual(
|
||||
'A message from messages catalogue with a parameter foo'
|
||||
);
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages', 'en')).toEqual(
|
||||
'A message from messages catalogue with a parameter foo'
|
||||
);
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter2%': 'foo' }, 'foobar')).toEqual(
|
||||
'A message from foobar catalogue with a parameter foo'
|
||||
);
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter2%': 'foo' }, 'foobar', 'en')).toEqual(
|
||||
'A message from foobar catalogue with a parameter foo'
|
||||
);
|
||||
expect(
|
||||
translator.trans('message.multi_domains.different_parameters', { '%parameter1%': 'foo' }, 'messages')
|
||||
).toEqual('A message from messages catalogue with a parameter foo');
|
||||
expect(
|
||||
translator.trans(
|
||||
'message.multi_domains.different_parameters',
|
||||
{ '%parameter1%': 'foo' },
|
||||
'messages',
|
||||
'en'
|
||||
)
|
||||
).toEqual('A message from messages catalogue with a parameter foo');
|
||||
expect(
|
||||
translator.trans('message.multi_domains.different_parameters', { '%parameter2%': 'foo' }, 'foobar')
|
||||
).toEqual('A message from foobar catalogue with a parameter foo');
|
||||
expect(
|
||||
translator.trans(
|
||||
'message.multi_domains.different_parameters',
|
||||
{ '%parameter2%': 'foo' },
|
||||
'foobar',
|
||||
'en'
|
||||
)
|
||||
).toEqual('A message from foobar catalogue with a parameter foo');
|
||||
|
||||
// @ts-expect-error Parameter "%parameter1%" is missing
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {})).toEqual(
|
||||
expect(translator.trans('message.multi_domains.different_parameters', {})).toEqual(
|
||||
'A message from messages catalogue with a parameter %parameter1%'
|
||||
);
|
||||
|
||||
// @ts-expect-error Domain "baz" is invalid
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'baz')).toEqual(
|
||||
'message.multi_domains.different_parameters'
|
||||
);
|
||||
expect(
|
||||
// @ts-expect-error Domain "baz" is invalid
|
||||
translator.trans('message.multi_domains.different_parameters', { '%parameter1%': 'foo' }, 'baz')
|
||||
).toEqual('message.multi_domains.different_parameters');
|
||||
|
||||
// @ts-expect-error Locale "fr" is invalid
|
||||
expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages', 'fr')).toEqual(
|
||||
'message.multi_domains.different_parameters'
|
||||
);
|
||||
expect(
|
||||
translator.trans(
|
||||
'message.multi_domains.different_parameters',
|
||||
{ '%parameter1%': 'foo' },
|
||||
'messages',
|
||||
// @ts-expect-error Locale "fr" is invalid
|
||||
'fr'
|
||||
)
|
||||
).toEqual('message.multi_domains.different_parameters');
|
||||
});
|
||||
|
||||
test('missing message should return the message id when `throwWhenNotFound` is false', () => {
|
||||
throwWhenNotFound(false);
|
||||
setLocale('fr');
|
||||
|
||||
const MESSAGE_IN_ANOTHER_DOMAIN: Message<{ security: { parameters: NoParametersType } }, 'en'> = {
|
||||
id: 'Invalid credentials.',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'Invalid credentials.',
|
||||
const messages: {
|
||||
'message.id': Message<{ security: { parameters: NoParametersType } }, 'en'>;
|
||||
} = {
|
||||
'message.id': {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'Invalid credentials.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(trans(MESSAGE_IN_ANOTHER_DOMAIN)).toEqual('Invalid credentials.');
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
locale: 'fr',
|
||||
throwWhenNotFound: false,
|
||||
});
|
||||
|
||||
expect(translator.trans('message.id')).toEqual('message.id');
|
||||
});
|
||||
|
||||
test('missing message should throw an error if `throwWhenNotFound` is true', () => {
|
||||
throwWhenNotFound(true);
|
||||
setLocale('fr');
|
||||
|
||||
const MESSAGE_IN_ANOTHER_DOMAIN: Message<{ security: { parameters: NoParametersType } }, 'en'> = {
|
||||
id: 'Invalid credentials.',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'Invalid credentials.',
|
||||
const messages: {
|
||||
'message.id': Message<{ security: { parameters: NoParametersType } }, 'en'>;
|
||||
} = {
|
||||
'message.id': {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'Invalid credentials.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
locale: 'fr',
|
||||
throwWhenNotFound: true,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
trans(MESSAGE_IN_ANOTHER_DOMAIN);
|
||||
}).toThrow(`No translation message found with id "Invalid credentials.".`);
|
||||
translator.trans('message.id');
|
||||
}).toThrow(`No translation message found with id "message.id".`);
|
||||
});
|
||||
|
||||
test('message from intl domain should be prioritized over its non-intl equivalent', () => {
|
||||
const MESSAGE: Message<
|
||||
{ 'messages+intl-icu': { parameters: NoParametersType }; messages: { parameters: NoParametersType } },
|
||||
'en'
|
||||
> = {
|
||||
id: 'message',
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: 'A intl message',
|
||||
const messages: {
|
||||
message: Message<
|
||||
{
|
||||
'messages+intl-icu': { parameters: NoParametersType };
|
||||
messages: { parameters: NoParametersType };
|
||||
},
|
||||
messages: {
|
||||
en: 'A basic message',
|
||||
'en'
|
||||
>;
|
||||
} = {
|
||||
message: {
|
||||
translations: {
|
||||
'messages+intl-icu': {
|
||||
en: 'A intl message',
|
||||
},
|
||||
messages: {
|
||||
en: 'A basic message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
expect(trans(MESSAGE)).toEqual('A intl message');
|
||||
expect(trans(MESSAGE, {})).toEqual('A intl message');
|
||||
expect(trans(MESSAGE, {}, 'messages')).toEqual('A intl message');
|
||||
expect(trans(MESSAGE, {}, 'messages', 'en')).toEqual('A intl message');
|
||||
expect(translator.trans('message')).toEqual('A intl message');
|
||||
expect(translator.trans('message', {})).toEqual('A intl message');
|
||||
expect(translator.trans('message', {}, 'messages')).toEqual('A intl message');
|
||||
expect(translator.trans('message', {}, 'messages', 'en')).toEqual('A intl message');
|
||||
});
|
||||
|
||||
test('fallback behavior', () => {
|
||||
setLocaleFallbacks({ fr_FR: 'fr', fr: 'en', en_US: 'en', en_GB: 'en', de_DE: 'de', de: 'en' });
|
||||
|
||||
const MESSAGE: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'> = {
|
||||
id: 'message',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A message in english',
|
||||
en_US: 'A message in english (US)',
|
||||
fr: 'Un message en français',
|
||||
const messages: {
|
||||
message: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'>;
|
||||
message_intl: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'>;
|
||||
message_french_only: Message<{ messages: { parameters: NoParametersType } }, 'fr'>;
|
||||
} = {
|
||||
message: {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A message in english',
|
||||
en_US: 'A message in english (US)',
|
||||
fr: 'Un message en français',
|
||||
},
|
||||
},
|
||||
},
|
||||
message_intl: {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A intl message in english',
|
||||
en_US: 'A intl message in english (US)',
|
||||
fr: 'Un message intl en français',
|
||||
},
|
||||
},
|
||||
},
|
||||
message_french_only: {
|
||||
translations: {
|
||||
messages: {
|
||||
fr: 'Un message en français uniquement',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
localeFallbacks: {
|
||||
fr_FR: 'fr',
|
||||
fr: 'en',
|
||||
en_US: 'en',
|
||||
en_GB: 'en',
|
||||
de_DE: 'de',
|
||||
de: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
const MESSAGE_INTL: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'> = {
|
||||
id: 'message_intl',
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A intl message in english',
|
||||
en_US: 'A intl message in english (US)',
|
||||
fr: 'Un message intl en français',
|
||||
expect(translator.trans('message', {}, 'messages', 'en')).toEqual('A message in english');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'en')).toEqual('A intl message in english');
|
||||
expect(translator.trans('message', {}, 'messages', 'en_US')).toEqual('A message in english (US)');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'en_US')).toEqual('A intl message in english (US)');
|
||||
expect(translator.trans('message', {}, 'messages', 'en_GB' as 'en')).toEqual('A message in english');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'en_GB' as 'en')).toEqual(
|
||||
'A intl message in english'
|
||||
);
|
||||
|
||||
expect(translator.trans('message', {}, 'messages', 'fr')).toEqual('Un message en français');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'fr')).toEqual('Un message intl en français');
|
||||
expect(translator.trans('message', {}, 'messages', 'fr_FR' as 'fr')).toEqual('Un message en français');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'fr_FR' as 'fr')).toEqual(
|
||||
'Un message intl en français'
|
||||
);
|
||||
|
||||
expect(translator.trans('message', {}, 'messages', 'de_DE' as 'en')).toEqual('A message in english');
|
||||
expect(translator.trans('message_intl', {}, 'messages', 'de_DE' as 'en')).toEqual(
|
||||
'A intl message in english'
|
||||
);
|
||||
|
||||
expect(translator.trans('message_french_only', {}, 'messages', 'fr')).toEqual(
|
||||
'Un message en français uniquement'
|
||||
);
|
||||
expect(translator.trans('message_french_only', {}, 'messages', 'en' as 'fr')).toEqual(
|
||||
'message_french_only'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destructuring', () => {
|
||||
test('createTranslator returns expected methods', () => {
|
||||
const messages: {
|
||||
'message.basic': Message<{ messages: { parameters: NoParametersType } }, 'en' | 'fr'>;
|
||||
} = {
|
||||
'message.basic': {
|
||||
translations: {
|
||||
messages: {
|
||||
en: 'A basic message',
|
||||
fr: 'Un message basique',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const translator = createTranslator({ messages });
|
||||
|
||||
const MESSAGE_FRENCH_ONLY: Message<{ messages: { parameters: NoParametersType } }, 'fr'> = {
|
||||
id: 'message_french_only',
|
||||
translations: {
|
||||
messages: {
|
||||
fr: 'Un message en français uniquement',
|
||||
},
|
||||
},
|
||||
};
|
||||
const { setLocale, getLocale, setThrowWhenNotFound, trans } = translator;
|
||||
|
||||
expect(trans(MESSAGE, {}, 'messages', 'en')).toEqual('A message in english');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'en')).toEqual('A intl message in english');
|
||||
expect(trans(MESSAGE, {}, 'messages', 'en_US')).toEqual('A message in english (US)');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'en_US')).toEqual('A intl message in english (US)');
|
||||
expect(trans(MESSAGE, {}, 'messages', 'en_GB' as 'en')).toEqual('A message in english');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'en_GB' as 'en')).toEqual('A intl message in english');
|
||||
expect(typeof setLocale).toBe('function');
|
||||
expect(typeof getLocale).toBe('function');
|
||||
expect(typeof setThrowWhenNotFound).toBe('function');
|
||||
expect(typeof trans).toBe('function');
|
||||
|
||||
expect(trans(MESSAGE, {}, 'messages', 'fr')).toEqual('Un message en français');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'fr')).toEqual('Un message intl en français');
|
||||
expect(trans(MESSAGE, {}, 'messages', 'fr_FR' as 'fr')).toEqual('Un message en français');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'fr_FR' as 'fr')).toEqual('Un message intl en français');
|
||||
|
||||
expect(trans(MESSAGE, {}, 'messages', 'de_DE' as 'en')).toEqual('A message in english');
|
||||
expect(trans(MESSAGE_INTL, {}, 'messages', 'de_DE' as 'en')).toEqual('A intl message in english');
|
||||
|
||||
expect(trans(MESSAGE_FRENCH_ONLY, {}, 'messages', 'fr')).toEqual('Un message en français uniquement');
|
||||
expect(trans(MESSAGE_FRENCH_ONLY, {}, 'messages', 'en' as 'fr')).toEqual('message_french_only');
|
||||
expect(getLocale()).toEqual('en');
|
||||
expect(trans('message.basic')).toEqual('A basic message');
|
||||
setLocale('fr');
|
||||
expect(getLocale()).toEqual('fr');
|
||||
expect(trans('message.basic')).toEqual('Un message basique');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
138
doc/index.rst
138
doc/index.rst
@@ -15,7 +15,7 @@ Installation
|
||||
.. note::
|
||||
|
||||
This package works best with WebpackEncore. To use it with AssetMapper, see
|
||||
:ref:`Using with AssetMapper <using-with-asset-mapper>`.
|
||||
:ref:`Using with AssetMapper`_.
|
||||
|
||||
.. caution::
|
||||
|
||||
@@ -27,18 +27,6 @@ Install the bundle using Composer and Symfony Flex:
|
||||
|
||||
$ composer require symfony/ux-translator
|
||||
|
||||
If you're using WebpackEncore, install your assets and restart Encore (not
|
||||
needed if you're using AssetMapper):
|
||||
|
||||
.. code-block:: terminal
|
||||
|
||||
$ npm install --force
|
||||
$ npm run watch
|
||||
|
||||
.. note::
|
||||
|
||||
For more complex installation scenarios, you can install the JavaScript assets through the `@symfony/ux-translator npm package`_
|
||||
|
||||
After installing the bundle, the following file should be created, thanks to the Symfony Flex recipe:
|
||||
|
||||
.. code-block:: javascript
|
||||
@@ -54,13 +42,43 @@ After installing the bundle, the following file should be created, thanks to the
|
||||
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
|
||||
*/
|
||||
|
||||
import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
|
||||
import { localeFallbacks } from '../var/translations/configuration';
|
||||
import { createTranslator } from '@symfony/ux-translator';
|
||||
import { messages, localeFallbacks } from '../var/translations/index.js';
|
||||
|
||||
setLocaleFallbacks(localeFallbacks);
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
localeFallbacks,
|
||||
});
|
||||
|
||||
export { trans }
|
||||
export * from '../var/translations';
|
||||
// Allow you to use `import { trans } from './translator';` in your assets
|
||||
export const { trans } = translator;
|
||||
|
||||
Using with WebpackEncore
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you're using WebpackEncore, install your assets and restart Encore (not
|
||||
needed if you're using AssetMapper):
|
||||
|
||||
.. code-block:: terminal
|
||||
|
||||
$ npm install --force
|
||||
$ npm run watch
|
||||
|
||||
.. note::
|
||||
|
||||
For more complex installation scenarios, you can install the JavaScript assets through the `@symfony/ux-translator npm package`_
|
||||
|
||||
Using with AssetMapper
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Using this library with AssetMapper is possible.
|
||||
|
||||
When installing with AssetMapper, Flex will add a new item to your ``importmap.php``
|
||||
file::
|
||||
|
||||
'@symfony/ux-translator' => [
|
||||
'path' => './vendor/symfony/ux-translator/assets/dist/translator_controller.js',
|
||||
],
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -68,8 +86,8 @@ Usage
|
||||
When warming up the Symfony cache, your translations will be dumped as JavaScript into the ``var/translations/`` directory.
|
||||
For a better developer experience, TypeScript types definitions are also generated aside those JavaScript files.
|
||||
|
||||
Then, you will be able to import those JavaScript translations in your assets.
|
||||
Don't worry about your final bundle size, only the translations you use will be included in your final bundle, thanks to the `tree shaking <https://webpack.js.org/guides/tree-shaking/>`_.
|
||||
Then, you will be able to import the ``trans()`` function in your assets and use translation keys as simple strings,
|
||||
exactly as you would in your Symfony PHP code.
|
||||
|
||||
Configuring the dumped translations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -94,7 +112,8 @@ Configuring the default locale
|
||||
|
||||
By default, the default locale is ``en`` (English) that you can configure through many ways (in order of priority):
|
||||
|
||||
#. With ``setLocale('de')`` or ``setLocale('de_AT')`` from ``@symfony/ux-translator`` package
|
||||
#. By passing the locale directly to the ``createTranslator()`` function
|
||||
#. With ``setLocale('de')`` or ``setLocale('de_AT')`` from your ``assets/translator.js`` file
|
||||
#. Or with ``<html data-symfony-ux-translator-locale="{{ app.request.locale }}">`` attribute (e.g., ``de_AT`` or ``de`` using Symfony locale format)
|
||||
#. Or with ``<html lang="{{ app.request.locale|replace({ '_': '-' }) }}">`` attribute (e.g., ``de-AT`` or ``de`` following the `W3C specification on language codes`_)
|
||||
|
||||
@@ -103,86 +122,71 @@ Detecting missing translations
|
||||
|
||||
By default, the translator will return the translation key if the translation is missing.
|
||||
|
||||
You can change this behavior by calling ``throwWhenNotFound(true)``:
|
||||
You can change this behavior by calling ``setThrowWhenNotFound(true)``:
|
||||
|
||||
.. code-block:: diff
|
||||
|
||||
// assets/translator.js
|
||||
|
||||
- import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
|
||||
+ import { trans, getLocale, setLocale, setLocaleFallbacks, throwWhenNotFound } from '@symfony/ux-translator';
|
||||
import { localeFallbacks } from '../var/translations/configuration';
|
||||
import { createTranslator } from '@symfony/ux-translator';
|
||||
import { messages, localeFallbacks } from '../var/translations/index.js';
|
||||
|
||||
setLocaleFallbacks(localeFallbacks);
|
||||
+ throwWhenNotFound(true)
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
localeFallbacks,
|
||||
+ throwWhenNotFound: true, // either when creating the translator
|
||||
});
|
||||
|
||||
export { trans }
|
||||
export * from '../var/translations';
|
||||
+ // Or later in your code
|
||||
export const { trans, setThrowWhenNotFound } = translator;
|
||||
|
||||
Importing and using translations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you use the Symfony Flex recipe, you can import the ``trans()`` function and your translations in your assets from the file ``assets/translator.js``.
|
||||
If you use the Symfony Flex recipe, you can import the ``trans()`` function from the file ``assets/translator.js``.
|
||||
|
||||
Translations are available as named exports, by using the translation's id transformed in uppercase snake-case (e.g.: ``my.translation`` becomes ``MY_TRANSLATION``),
|
||||
so you can import them like this:
|
||||
You can then use translation keys as simple strings, exactly as you would in your Symfony PHP code:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// assets/my_file.js
|
||||
|
||||
import {
|
||||
trans,
|
||||
TRANSLATION_SIMPLE,
|
||||
TRANSLATION_WITH_PARAMETERS,
|
||||
TRANSLATION_MULTI_DOMAINS,
|
||||
TRANSLATION_MULTI_LOCALES,
|
||||
} from './translator';
|
||||
import { trans } from './translator';
|
||||
|
||||
// No parameters, uses the default domain ("messages") and the default locale
|
||||
trans(TRANSLATION_SIMPLE);
|
||||
trans('translation.simple');
|
||||
|
||||
// Two parameters "count" and "foo", uses the default domain ("messages") and the default locale
|
||||
trans(TRANSLATION_WITH_PARAMETERS, { count: 123, foo: 'bar' });
|
||||
trans('translation.with.parameters', { count: 123, foo: 'bar' });
|
||||
|
||||
// No parameters, uses the default domain ("messages") and the default locale
|
||||
trans(TRANSLATION_MULTI_DOMAINS);
|
||||
trans('translation.multi.domains');
|
||||
// Same as above, but uses the "domain2" domain
|
||||
trans(TRANSLATION_MULTI_DOMAINS, {}, 'domain2');
|
||||
trans('translation.multi.domains', {}, 'domain2');
|
||||
// Same as above, but uses the "domain3" domain
|
||||
trans(TRANSLATION_MULTI_DOMAINS, {}, 'domain3');
|
||||
trans('translation.multi.domains', {}, 'domain3');
|
||||
|
||||
// No parameters, uses the default domain ("messages") and the default locale
|
||||
trans(TRANSLATION_MULTI_LOCALES);
|
||||
trans('translation.multi.locales');
|
||||
// Same as above, but uses the "fr" locale
|
||||
trans(TRANSLATION_MULTI_LOCALES, {}, 'messages', 'fr');
|
||||
trans('translation.multi.locales', {}, 'messages', 'fr');
|
||||
// Same as above, but uses the "it" locale
|
||||
trans(TRANSLATION_MULTI_LOCALES, {}, 'messages', 'it');
|
||||
trans('translation.multi.locales', {}, 'messages', 'it');
|
||||
|
||||
.. _using-with-asset-mapper:
|
||||
You will get autocompletion and type-safety for translation keys, parameters, domains, and locales.
|
||||
|
||||
Using with AssetMapper
|
||||
----------------------
|
||||
Q&A
|
||||
---
|
||||
|
||||
Using this library with AssetMapper is possible, but is currently experimental
|
||||
and may not be ready yet for production.
|
||||
What about bundle size?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When installing with AssetMapper, Flex will add a few new items to your ``importmap.php``
|
||||
file. 2 of the new items are::
|
||||
All your translations (extracted from the configured domains) are included in the generated ``var/translations/index.js`` file,
|
||||
which means they will be included in your final JavaScript bundle).
|
||||
|
||||
'@app/translations' => [
|
||||
'path' => 'var/translations/index.js',
|
||||
],
|
||||
'@app/translations/configuration' => [
|
||||
'path' => 'var/translations/configuration.js',
|
||||
],
|
||||
|
||||
These are then imported in your ``assets/translator.js`` file. This setup is
|
||||
very similar to working with WebpackEncore. However, the ``var/translations/index.js``
|
||||
file contains *every* translation in your app, which is not ideal for production
|
||||
and may even leak translations only meant for admin areas. Encore solves this via
|
||||
tree-shaking, but the AssetMapper component does not. There is not, yet, a way to
|
||||
solve this properly with the AssetMapper component.
|
||||
However, modern build tools, caching strategies, and compression techniques (Brotli, gzip)
|
||||
make this negligible in 2025. Additionally, a future feature will allow filtering dumped
|
||||
translations by pattern for those who need to further reduce bundle size.
|
||||
|
||||
Backward Compatibility promise
|
||||
------------------------------
|
||||
|
||||
@@ -17,8 +17,6 @@ use Symfony\UX\Translator\MessageParameters\Extractor\IntlMessageParametersExtra
|
||||
use Symfony\UX\Translator\MessageParameters\Extractor\MessageParametersExtractor;
|
||||
use Symfony\UX\Translator\MessageParameters\Printer\TypeScriptMessageParametersPrinter;
|
||||
|
||||
use function Symfony\Component\String\s;
|
||||
|
||||
/**
|
||||
* @author Hugo Alliaume <hugo@alliau.me>
|
||||
*
|
||||
@@ -34,7 +32,6 @@ class TranslationsDumper
|
||||
{
|
||||
private array $excludedDomains = [];
|
||||
private array $includedDomains = [];
|
||||
private array $alreadyGeneratedConstants = [];
|
||||
|
||||
public function __construct(
|
||||
private string $dumpDir,
|
||||
@@ -48,44 +45,50 @@ class TranslationsDumper
|
||||
public function dump(MessageCatalogueInterface ...$catalogues): void
|
||||
{
|
||||
$this->filesystem->mkdir($this->dumpDir);
|
||||
$this->filesystem->remove($this->dumpDir.'/index.js');
|
||||
$this->filesystem->remove($this->dumpDir.'/index.d.ts');
|
||||
$this->filesystem->remove($this->dumpDir.'/configuration.js');
|
||||
$this->filesystem->remove($this->dumpDir.'/configuration.d.ts');
|
||||
$this->filesystem->remove($fileIndexJs = $this->dumpDir.'/index.js');
|
||||
$this->filesystem->remove($fileIndexDts = $this->dumpDir.'/index.d.ts');
|
||||
|
||||
$translationsJs = '';
|
||||
$translationsTs = "import { Message, NoParametersType } from '@symfony/ux-translator';\n\n";
|
||||
$this->filesystem->appendToFile(
|
||||
$fileIndexJs,
|
||||
\sprintf(<<<'JS'
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
|
||||
foreach ($this->getTranslations(...$catalogues) as $translationId => $translationsByDomainAndLocale) {
|
||||
$constantName = $this->generateConstantName($translationId);
|
||||
export const localeFallbacks = %s;
|
||||
|
||||
$translationsJs .= \sprintf(
|
||||
"export const %s = %s;\n",
|
||||
$constantName,
|
||||
json_encode([
|
||||
'id' => $translationId,
|
||||
'translations' => $translationsByDomainAndLocale,
|
||||
], \JSON_THROW_ON_ERROR),
|
||||
);
|
||||
$translationsTs .= \sprintf(
|
||||
"export declare const %s: %s;\n",
|
||||
$constantName,
|
||||
$this->getTranslationsTypeScriptTypeDefinition($translationsByDomainAndLocale)
|
||||
);
|
||||
}
|
||||
JS,
|
||||
json_encode($this->getLocaleFallbacks(...$catalogues), \JSON_THROW_ON_ERROR)
|
||||
));
|
||||
|
||||
$this->filesystem->dumpFile($this->dumpDir.'/index.js', $translationsJs);
|
||||
$this->filesystem->dumpFile($this->dumpDir.'/index.d.ts', $translationsTs);
|
||||
$this->filesystem->dumpFile($this->dumpDir.'/configuration.js', \sprintf(
|
||||
"export const localeFallbacks = %s;\n",
|
||||
json_encode($this->getLocaleFallbacks(...$catalogues), \JSON_THROW_ON_ERROR)
|
||||
));
|
||||
$this->filesystem->dumpFile($this->dumpDir.'/configuration.d.ts', <<<'TS'
|
||||
import { LocaleType } from '@symfony/ux-translator';
|
||||
$this->filesystem->appendToFile(
|
||||
$fileIndexDts,
|
||||
<<<'TS'
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator';
|
||||
|
||||
export declare const localeFallbacks: Record<LocaleType, LocaleType>;
|
||||
TS
|
||||
export declare const localeFallbacks: Record<LocaleType, LocaleType>;
|
||||
|
||||
TS
|
||||
);
|
||||
|
||||
$this->filesystem->appendToFile($fileIndexJs, 'export const messages = {'."\n");
|
||||
$this->filesystem->appendToFile($fileIndexDts, 'export declare const messages: {'."\n");
|
||||
foreach ($this->getTranslations(...$catalogues) as $translationId => $translationsByDomainAndLocale) {
|
||||
$translationId = str_replace('"', '\\"', $translationId);
|
||||
$this->filesystem->appendToFile($fileIndexJs, \sprintf(
|
||||
' "%s": %s,%s',
|
||||
$translationId,
|
||||
json_encode(['translations' => $translationsByDomainAndLocale], \JSON_THROW_ON_ERROR),
|
||||
"\n"
|
||||
));
|
||||
$this->filesystem->appendToFile($fileIndexDts, \sprintf(
|
||||
' "%s": %s;%s',
|
||||
$translationId,
|
||||
$this->getTranslationsTypeScriptTypeDefinition($translationsByDomainAndLocale),
|
||||
"\n"
|
||||
));
|
||||
}
|
||||
$this->filesystem->appendToFile($fileIndexJs, '};'."\n");
|
||||
$this->filesystem->appendToFile($fileIndexDts, '};'."\n");
|
||||
}
|
||||
|
||||
public function addExcludedDomain(string $domain): void
|
||||
@@ -182,18 +185,4 @@ class TranslationsDumper
|
||||
|
||||
return $localesFallbacks;
|
||||
}
|
||||
|
||||
private function generateConstantName(string $translationId): string
|
||||
{
|
||||
$translationId = s($translationId)->ascii()->snake()->upper()->replaceMatches('/^(\d)/', '_$1')->toString();
|
||||
$prefix = 0;
|
||||
do {
|
||||
$constantName = $translationId.($prefix > 0 ? '_'.$prefix : '');
|
||||
++$prefix;
|
||||
} while ($this->alreadyGeneratedConstants[$constantName] ?? false);
|
||||
|
||||
$this->alreadyGeneratedConstants[$constantName] = true;
|
||||
|
||||
return $constantName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,34 +35,28 @@ class DumpEnabledLocalesTest extends KernelTestCase
|
||||
|
||||
self::assertStringEqualsFile(
|
||||
$translationsDumpDir.'/index.js',
|
||||
<<<JAVASCRIPT
|
||||
export const SYMFONY_UX_GREAT = {"id":"symfony_ux.great","translations":{"messages":{"en":"Symfony UX is awesome","fr":"Symfony UX est g\u00e9nial"}}};
|
||||
<<<JS
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
|
||||
JAVASCRIPT
|
||||
export const localeFallbacks = {"en":null,"fr":"en"};
|
||||
export const messages = {
|
||||
"symfony_ux.great": {"translations":{"messages":{"en":"Symfony UX is awesome","fr":"Symfony UX est g\u00e9nial"}}},
|
||||
};
|
||||
|
||||
JS
|
||||
);
|
||||
self::assertStringEqualsFile(
|
||||
$translationsDumpDir.'/index.d.ts',
|
||||
<<<TYPESCRIPT
|
||||
import { Message, NoParametersType } from '@symfony/ux-translator';
|
||||
|
||||
export declare const SYMFONY_UX_GREAT: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
|
||||
TYPESCRIPT
|
||||
);
|
||||
self::assertStringEqualsFile(
|
||||
$translationsDumpDir.'/configuration.js',
|
||||
<<<JAVASCRIPT
|
||||
export const localeFallbacks = {"en":null,"fr":"en"};
|
||||
|
||||
JAVASCRIPT
|
||||
);
|
||||
self::assertStringEqualsFile(
|
||||
$translationsDumpDir.'/configuration.d.ts',
|
||||
<<<TYPESCRIPT
|
||||
import { LocaleType } from '@symfony/ux-translator';
|
||||
<<<TS
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator';
|
||||
|
||||
export declare const localeFallbacks: Record<LocaleType, LocaleType>;
|
||||
TYPESCRIPT
|
||||
export declare const messages: {
|
||||
"symfony_ux.great": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
};
|
||||
|
||||
TS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,57 +52,66 @@ class TranslationsDumperTest extends TestCase
|
||||
$this->assertFileExists(self::$translationsDumpDir.'/index.js');
|
||||
$this->assertFileExists(self::$translationsDumpDir.'/index.d.ts');
|
||||
|
||||
$this->assertStringEqualsFile(self::$translationsDumpDir.'/index.js', <<<'JAVASCRIPT'
|
||||
export const NOTIFICATION_COMMENT_CREATED = {"id":"notification.comment_created","translations":{"messages+intl-icu":{"en":"Your post received a comment!","fr":"Votre article a re\u00e7u un commentaire !"}}};
|
||||
export const NOTIFICATION_COMMENT_CREATED_DESCRIPTION = {"id":"notification.comment_created.description","translations":{"messages+intl-icu":{"en":"Your post \"{title}\" has received a new comment. You can read the comment by following <a href=\"{link}\">this link<\/a>","fr":"Votre article \"{title}\" a re\u00e7u un nouveau commentaire. Vous pouvez lire le commentaire en suivant <a href=\"{link}\">ce lien<\/a>"}}};
|
||||
export const POST_NUM_COMMENTS = {"id":"post.num_comments","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}}","fr":"{count, plural, one {# commentaire} other {# commentaires}}"},"foobar":{"en":"There is 1 comment|There are %count% comments","fr":"Il y a 1 comment|Il y a %count% comments"}}};
|
||||
export const POST_NUM_COMMENTS_1 = {"id":"post.num_comments.","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
|
||||
export const POST_NUM_COMMENTS_2 = {"id":"post.num_comments..","translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
|
||||
export const SYMFONY_GREAT = {"id":"symfony.great","translations":{"messages":{"en":"Symfony is awesome!","fr":"Symfony est g\u00e9nial !"}}};
|
||||
export const SYMFONY_WHAT = {"id":"symfony.what","translations":{"messages":{"en":"Symfony is %what%!","fr":"Symfony est %what%!"}}};
|
||||
export const SYMFONY_WHAT_1 = {"id":"symfony.what!","translations":{"messages":{"en":"Symfony is %what%! (should not conflict with the previous one.)","fr":"Symfony est %what%! (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
|
||||
export const SYMFONY_WHAT_2 = {"id":"symfony.what.","translations":{"messages":{"en":"Symfony is %what%. (should also not conflict with the previous one.)","fr":"Symfony est %what%. (ne doit pas non plus rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
|
||||
export const APPLES_COUNT0 = {"id":"apples.count.0","translations":{"messages":{"en":"There is 1 apple|There are %count% apples","fr":"Il y a 1 pomme|Il y a %count% pommes"}}};
|
||||
export const APPLES_COUNT1 = {"id":"apples.count.1","translations":{"messages":{"en":"{1} There is one apple|]1,Inf] There are %count% apples","fr":"{1} Il y a une pomme|]1,Inf] Il y a %count% pommes"}}};
|
||||
export const APPLES_COUNT2 = {"id":"apples.count.2","translations":{"messages":{"en":"{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples","fr":"{0} Il n'y a pas de pommes|{1} Il y a une pomme|]1,Inf] Il y a %count% pommes"}}};
|
||||
export const APPLES_COUNT3 = {"id":"apples.count.3","translations":{"messages":{"en":"one: There is one apple|more: There are %count% apples","fr":"one: Il y a une pomme|more: Il y a %count% pommes"}}};
|
||||
export const APPLES_COUNT4 = {"id":"apples.count.4","translations":{"messages":{"en":"one: There is one apple|more: There are more than one apple","fr":"one: Il y a une pomme|more: Il y a plus d'une pomme"}}};
|
||||
export const WHAT_COUNT1 = {"id":"what.count.1","translations":{"messages":{"en":"{1} There is one %what%|]1,Inf] There are %count% %what%","fr":"{1} Il y a une %what%|]1,Inf] Il y a %count% %what%"}}};
|
||||
export const WHAT_COUNT2 = {"id":"what.count.2","translations":{"messages":{"en":"{0} There are no %what%|{1} There is one %what%|]1,Inf] There are %count% %what%","fr":"{0} Il n'y a pas de %what%|{1} Il y a une %what%|]1,Inf] Il y a %count% %what%"}}};
|
||||
export const WHAT_COUNT3 = {"id":"what.count.3","translations":{"messages":{"en":"one: There is one %what%|more: There are %count% %what%","fr":"one: Il y a une %what%|more: Il y a %count% %what%"}}};
|
||||
export const WHAT_COUNT4 = {"id":"what.count.4","translations":{"messages":{"en":"one: There is one %what%|more: There are more than one %what%","fr":"one: Il y a une %what%|more: Il y a more than one %what%"}}};
|
||||
export const ANIMAL_DOG_CAT = {"id":"animal.dog-cat","translations":{"messages":{"en":"Dog and cat","fr":"Chien et chat"}}};
|
||||
export const ANIMAL_DOG_CAT_1 = {"id":"animal.dog_cat","translations":{"messages":{"en":"Dog and cat (should not conflict with the previous one)","fr":"Chien et chat (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}};
|
||||
export const _0STARTS_WITH_NUMERIC = {"id":"0starts.with.numeric","translations":{"messages":{"en":"Key starts with numeric char","fr":"La touche commence par un caract\u00e8re num\u00e9rique"}}};
|
||||
$this->assertStringEqualsFile(self::$translationsDumpDir.'/index.js', <<<'JS'
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
|
||||
JAVASCRIPT);
|
||||
export const localeFallbacks = {"en":null,"fr":null};
|
||||
export const messages = {
|
||||
"notification.comment_created": {"translations":{"messages+intl-icu":{"en":"Your post received a comment!","fr":"Votre article a re\u00e7u un commentaire !"}}},
|
||||
"notification.comment_created.description": {"translations":{"messages+intl-icu":{"en":"Your post \"{title}\" has received a new comment. You can read the comment by following <a href=\"{link}\">this link<\/a>","fr":"Votre article \"{title}\" a re\u00e7u un nouveau commentaire. Vous pouvez lire le commentaire en suivant <a href=\"{link}\">ce lien<\/a>"}}},
|
||||
"post.num_comments": {"translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}}","fr":"{count, plural, one {# commentaire} other {# commentaires}}"},"foobar":{"en":"There is 1 comment|There are %count% comments","fr":"Il y a 1 comment|Il y a %count% comments"}}},
|
||||
"post.num_comments.": {"translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}},
|
||||
"post.num_comments..": {"translations":{"messages+intl-icu":{"en":"{count, plural, one {# comment} other {# comments}} (should not conflict with the previous one.)","fr":"{count, plural, one {# commentaire} other {# commentaires}} (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}},
|
||||
"symfony.great": {"translations":{"messages":{"en":"Symfony is awesome!","fr":"Symfony est g\u00e9nial !"}}},
|
||||
"symfony.what": {"translations":{"messages":{"en":"Symfony is %what%!","fr":"Symfony est %what%!"}}},
|
||||
"symfony.what!": {"translations":{"messages":{"en":"Symfony is %what%! (should not conflict with the previous one.)","fr":"Symfony est %what%! (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}},
|
||||
"symfony.what.": {"translations":{"messages":{"en":"Symfony is %what%. (should also not conflict with the previous one.)","fr":"Symfony est %what%. (ne doit pas non plus rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}},
|
||||
"apples.count.0": {"translations":{"messages":{"en":"There is 1 apple|There are %count% apples","fr":"Il y a 1 pomme|Il y a %count% pommes"}}},
|
||||
"apples.count.1": {"translations":{"messages":{"en":"{1} There is one apple|]1,Inf] There are %count% apples","fr":"{1} Il y a une pomme|]1,Inf] Il y a %count% pommes"}}},
|
||||
"apples.count.2": {"translations":{"messages":{"en":"{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples","fr":"{0} Il n'y a pas de pommes|{1} Il y a une pomme|]1,Inf] Il y a %count% pommes"}}},
|
||||
"apples.count.3": {"translations":{"messages":{"en":"one: There is one apple|more: There are %count% apples","fr":"one: Il y a une pomme|more: Il y a %count% pommes"}}},
|
||||
"apples.count.4": {"translations":{"messages":{"en":"one: There is one apple|more: There are more than one apple","fr":"one: Il y a une pomme|more: Il y a plus d'une pomme"}}},
|
||||
"what.count.1": {"translations":{"messages":{"en":"{1} There is one %what%|]1,Inf] There are %count% %what%","fr":"{1} Il y a une %what%|]1,Inf] Il y a %count% %what%"}}},
|
||||
"what.count.2": {"translations":{"messages":{"en":"{0} There are no %what%|{1} There is one %what%|]1,Inf] There are %count% %what%","fr":"{0} Il n'y a pas de %what%|{1} Il y a une %what%|]1,Inf] Il y a %count% %what%"}}},
|
||||
"what.count.3": {"translations":{"messages":{"en":"one: There is one %what%|more: There are %count% %what%","fr":"one: Il y a une %what%|more: Il y a %count% %what%"}}},
|
||||
"what.count.4": {"translations":{"messages":{"en":"one: There is one %what%|more: There are more than one %what%","fr":"one: Il y a une %what%|more: Il y a more than one %what%"}}},
|
||||
"animal.dog-cat": {"translations":{"messages":{"en":"Dog and cat","fr":"Chien et chat"}}},
|
||||
"animal.dog_cat": {"translations":{"messages":{"en":"Dog and cat (should not conflict with the previous one)","fr":"Chien et chat (ne doit pas rentrer en conflit avec la traduction pr\u00e9c\u00e9dente)"}}},
|
||||
"0starts.with.numeric": {"translations":{"messages":{"en":"Key starts with numeric char","fr":"La touche commence par un caract\u00e8re num\u00e9rique"}}},
|
||||
};
|
||||
|
||||
$this->assertStringEqualsFile(self::$translationsDumpDir.'/index.d.ts', <<<'TYPESCRIPT'
|
||||
import { Message, NoParametersType } from '@symfony/ux-translator';
|
||||
JS);
|
||||
|
||||
export declare const NOTIFICATION_COMMENT_CREATED: Message<{ 'messages+intl-icu': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
export declare const NOTIFICATION_COMMENT_CREATED_DESCRIPTION: Message<{ 'messages+intl-icu': { parameters: { 'title': string, 'link': string } } }, 'en'|'fr'>;
|
||||
export declare const POST_NUM_COMMENTS: Message<{ 'messages+intl-icu': { parameters: { 'count': number } }, 'foobar': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const POST_NUM_COMMENTS_1: Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
|
||||
export declare const POST_NUM_COMMENTS_2: Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
|
||||
export declare const SYMFONY_GREAT: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
export declare const SYMFONY_WHAT: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
export declare const SYMFONY_WHAT_1: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
export declare const SYMFONY_WHAT_2: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
export declare const APPLES_COUNT0: Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const APPLES_COUNT1: Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const APPLES_COUNT2: Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const APPLES_COUNT3: Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const APPLES_COUNT4: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
export declare const WHAT_COUNT1: Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const WHAT_COUNT2: Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const WHAT_COUNT3: Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
export declare const WHAT_COUNT4: Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
export declare const ANIMAL_DOG_CAT: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
export declare const ANIMAL_DOG_CAT_1: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
export declare const _0STARTS_WITH_NUMERIC: Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
$this->assertStringEqualsFile(self::$translationsDumpDir.'/index.d.ts', <<<'TS'
|
||||
// This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
|
||||
import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator';
|
||||
|
||||
TYPESCRIPT);
|
||||
export declare const localeFallbacks: Record<LocaleType, LocaleType>;
|
||||
export declare const messages: {
|
||||
"notification.comment_created": Message<{ 'messages+intl-icu': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
"notification.comment_created.description": Message<{ 'messages+intl-icu': { parameters: { 'title': string, 'link': string } } }, 'en'|'fr'>;
|
||||
"post.num_comments": Message<{ 'messages+intl-icu': { parameters: { 'count': number } }, 'foobar': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
"post.num_comments.": Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
|
||||
"post.num_comments..": Message<{ 'messages+intl-icu': { parameters: { 'count': number } } }, 'en'|'fr'>;
|
||||
"symfony.great": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
"symfony.what": Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
"symfony.what!": Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
"symfony.what.": Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
"apples.count.0": Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
"apples.count.1": Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
"apples.count.2": Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
"apples.count.3": Message<{ 'messages': { parameters: { '%count%': number } } }, 'en'|'fr'>;
|
||||
"apples.count.4": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
"what.count.1": Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
"what.count.2": Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
"what.count.3": Message<{ 'messages': { parameters: { '%what%': string, '%count%': number } } }, 'en'|'fr'>;
|
||||
"what.count.4": Message<{ 'messages': { parameters: { '%what%': string } } }, 'en'|'fr'>;
|
||||
"animal.dog-cat": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
"animal.dog_cat": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
"0starts.with.numeric": Message<{ 'messages': { parameters: NoParametersType } }, 'en'|'fr'>;
|
||||
};
|
||||
|
||||
TS);
|
||||
}
|
||||
|
||||
public function testDumpWithExcludedDomains()
|
||||
|
||||
@@ -20,6 +20,7 @@ require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$kernel = new FrameworkAppKernel('test', true);
|
||||
$application = new Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
// Trigger Symfony Translator and UX Translator cache warmers
|
||||
$application->run(new StringInput('cache:clear'));
|
||||
$application->run(new StringInput('cache:clear -vv'));
|
||||
|
||||
Reference in New Issue
Block a user