diff --git a/packages/localize/init/index.ts b/packages/localize/init/index.ts index 9173b49ce6..536786ba0f 100644 --- a/packages/localize/init/index.ts +++ b/packages/localize/init/index.ts @@ -41,16 +41,26 @@ declare global { * * **Naming placeholders** * - * If the template literal string contains expressions then you can optionally name the - * placeholder - * associated with each expression. Do this by providing the placeholder name wrapped in `:` - * characters directly after the expression. These placeholder names are stripped out of the - * rendered localized string. + * If the template literal string contains expressions, then the expressions will be automatically + * associated with placeholder names for you. * - * For example, to name the `item.length` expression placeholder `itemCount` you write: + * For example: * * ```ts - * $localize `There are ${item.length}:itemCount: items`; + * $localize `Hi ${name}! There are ${items.length} items.`; + * ``` + * + * will generate a message-source of `Hi {$PH}! There are {$PH_1} items`. + * + * The recommended practice is to name the placeholder associated with each expression though. + * + * Do this by providing the placeholder name wrapped in `:` characters directly after the + * expression. These placeholder names are stripped out of the rendered localized string. + * + * For example, to name the `items.length` expression placeholder `itemCount` you write: + * + * ```ts + * $localize `There are ${items.length}:itemCount: items`; * ``` * * **Escaping colon markers** diff --git a/packages/localize/src/localize/src/localize.ts b/packages/localize/src/localize/src/localize.ts index 20b6b5d8cb..043bc4b51c 100644 --- a/packages/localize/src/localize/src/localize.ts +++ b/packages/localize/src/localize/src/localize.ts @@ -36,27 +36,65 @@ export interface TranslateFn { * $localize `some string to localize` * ``` * - * **Naming placeholders** + * **Providing meaning, description and id** * - * If the template literal string contains expressions then you can optionally name the placeholder - * associated with each expression. Do this by providing the placeholder name wrapped in `:` - * characters directly after the expression. These placeholder names are stripped out of the - * rendered localized string. - * - * For example, to name the `item.length` expression placeholder `itemCount` you write: + * You can optionally specify one or more of `meaning`, `description` and `id` for a localized + * string by pre-pending it with a colon delimited block of the form: * * ```ts - * $localize `There are ${item.length}:itemCount: items`; + * $localize`:meaning|description@@id:source message text`; + * + * $localize`:meaning|:source message text`; + * $localize`:description:source message text`; + * $localize`:@@id:source message text`; * ``` * - * If you need to use a `:` character directly an expression you must either provide a name or you - * can escape the `:` by preceding it with a backslash: + * This format is the same as that used for `i18n` markers in Angular templates. See the + * [Angular 18n guide](guide/i18n#template-translations). + * + * **Naming placeholders** + * + * If the template literal string contains expressions, then the expressions will be automatically + * associated with placeholder names for you. * * For example: * * ```ts + * $localize `Hi ${name}! There are ${items.length} items.`; + * ``` + * + * will generate a message-source of `Hi {$PH}! There are {$PH_1} items`. + * + * The recommended practice is to name the placeholder associated with each expression though. + * + * Do this by providing the placeholder name wrapped in `:` characters directly after the + * expression. These placeholder names are stripped out of the rendered localized string. + * + * For example, to name the `items.length` expression placeholder `itemCount` you write: + * + * ```ts + * $localize `There are ${items.length}:itemCount: items`; + * ``` + * + * **Escaping colon markers** + * + * If you need to use a `:` character directly at the start of a tagged string that has no + * metadata block, or directly after a substitution expression that has no name you must escape + * the `:` by preceding it with a backslash: + * + * For example: + * + * ```ts + * // message has a metadata block so no need to escape colon + * $localize `:some description::this message starts with a colon (:)`; + * // no metadata block so the colon must be escaped + * $localize `\:this message starts with a colon (:)`; + * ``` + * + * ```ts + * // named substitution so no need to escape colon * $localize `${label}:label:: ${}` - * // or + * // anonymous substitution so colon must be escaped * $localize `${label}\: ${}` * ``` * @@ -64,19 +102,18 @@ export interface TranslateFn { * * There are three scenarios: * - * * **compile-time inlining**: the `$localize` tag is transformed at compile time by a transpiler, - * removing the tag and replacing the template literal string with a translated literal string - * from a collection of translations provided to the transpilation tool. + * * **compile-time inlining**: the `$localize` tag is transformed at compile time by a + * transpiler, removing the tag and replacing the template literal string with a translated + * literal string from a collection of translations provided to the transpilation tool. * - * * **run-time evaluation**: the `$localize` tag is a run-time function that replaces and reorders - * the parts (static strings and expressions) of the template literal string with strings from a - * collection of translations loaded at run-time. + * * **run-time evaluation**: the `$localize` tag is a run-time function that replaces and + * reorders the parts (static strings and expressions) of the template literal string with strings + * from a collection of translations loaded at run-time. * * * **pass-through evaluation**: the `$localize` tag is a run-time function that simply evaluates - * the original template literal string without applying any translations to the parts. This version - * is used during development or where there is no need to translate the localized template - * literals. - * + * the original template literal string without applying any translations to the parts. This + * version is used during development or where there is no need to translate the localized + * template literals. * @param messageParts a collection of the static parts of the template string. * @param expressions a collection of the values of each placeholder in the template string. * @returns the translated string, with the `messageParts` and `expressions` interleaved together. diff --git a/packages/localize/src/utils/messages.ts b/packages/localize/src/utils/messages.ts index b908a3ed59..6762c96ead 100644 --- a/packages/localize/src/utils/messages.ts +++ b/packages/localize/src/utils/messages.ts @@ -82,7 +82,7 @@ export function parseMessage( const metadata = parseMetadata(messageParts[0], messageParts.raw[0]); let messageString = metadata.text; for (let i = 1; i < messageParts.length; i++) { - const {text: messagePart, block: placeholderName = `ph_${i}`} = + const {text: messagePart, block: placeholderName = computePlaceholderName(i)} = splitBlock(messageParts[i], messageParts.raw[i]); messageString += `{$${placeholderName}}${messagePart}`; if (expressions !== undefined) { @@ -186,3 +186,7 @@ export function splitBlock(cooked: string, raw: string): {text: string, block?: }; } } + +function computePlaceholderName(index: number) { + return index === 1 ? 'PH' : `PH_${index - 1}`; +} diff --git a/packages/localize/test/translate_spec.ts b/packages/localize/test/translate_spec.ts index 93b6b051b9..607a5fbac4 100644 --- a/packages/localize/test/translate_spec.ts +++ b/packages/localize/test/translate_spec.ts @@ -16,10 +16,10 @@ describe('$localize tag with translations', () => { beforeEach(() => { loadTranslations(computeIds({ 'abc': 'abc', - 'abc{$ph_1}': 'abc{$ph_1}', - 'abc{$ph_1}def': 'abc{$ph_1}def', - 'abc{$ph_1}def{$ph_2}': 'abc{$ph_1}def{$ph_2}', - 'Hello, {$ph_1}!': 'Hello, {$ph_1}!', + 'abc{$PH}': 'abc{$PH}', + 'abc{$PH}def': 'abc{$PH}def', + 'abc{$PH}def{$PH_1}': 'abc{$PH}def{$PH_1}', + 'Hello, {$PH}!': 'Hello, {$PH}!', })); }); afterEach(() => { clearTranslations(); }); @@ -38,10 +38,10 @@ describe('$localize tag with translations', () => { beforeEach(() => { loadTranslations(computeIds({ 'abc': 'ABC', - 'abc{$ph_1}': 'ABC{$ph_1}', - 'abc{$ph_1}def': 'ABC{$ph_1}DEF', - 'abc{$ph_1}def{$ph_2}': 'ABC{$ph_1}DEF{$ph_2}', - 'Hello, {$ph_1}!': 'HELLO, {$ph_1}!', + 'abc{$PH}': 'ABC{$PH}', + 'abc{$PH}def': 'ABC{$PH}DEF', + 'abc{$PH}def{$PH_1}': 'ABC{$PH}DEF{$PH_1}', + 'Hello, {$PH}!': 'HELLO, {$PH}!', })); }); afterEach(() => { clearTranslations(); }); @@ -59,7 +59,7 @@ describe('$localize tag with translations', () => { describe('to reverse expressions', () => { beforeEach(() => { loadTranslations(computeIds({ - 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_3}def{$ph_2} - Hello, {$ph_1}!', + 'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH_2}def{$PH_1} - Hello, {$PH}!', })); }); afterEach(() => { clearTranslations(); }); @@ -74,7 +74,7 @@ describe('$localize tag with translations', () => { describe('to remove expressions', () => { beforeEach(() => { loadTranslations(computeIds({ - 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_1} - Hello, {$ph_3}!', + 'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH} - Hello, {$PH_2}!', })); }); afterEach(() => { clearTranslations(); }); diff --git a/packages/localize/test/utils/messages_spec.ts b/packages/localize/test/utils/messages_spec.ts index 17b6444834..d2bad923f4 100644 --- a/packages/localize/test/utils/messages_spec.ts +++ b/packages/localize/test/utils/messages_spec.ts @@ -44,13 +44,13 @@ describe('messages utils', () => { it('should compute the translation key, inferring placeholder names if not given', () => { const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]); - expect(message.messageId).toEqual('3269094494609300850'); + expect(message.messageId).toEqual('8107531564991075946'); }); it('should compute the translation key, ignoring escaped placeholder names', () => { const message = parseMessage( makeTemplateObject(['a', ':one:b', ':two:c'], ['a', '\\:one:b', '\\:two:c']), [1, 2]); - expect(message.messageId).toEqual('529036009514785949'); + expect(message.messageId).toEqual('2623373088949454037'); }); it('should compute the translation key, handling empty raw values', () => { @@ -67,7 +67,7 @@ describe('messages utils', () => { it('should build a map of implied placeholders to expressions', () => { const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]); - expect(message.substitutions).toEqual({ph_1: 1, ph_2: 2}); + expect(message.substitutions).toEqual({PH: 1, PH_1: 2}); }); }); diff --git a/packages/localize/test/utils/translations_spec.ts b/packages/localize/test/utils/translations_spec.ts index a1e0966b3d..bd5cdbac6b 100644 --- a/packages/localize/test/utils/translations_spec.ts +++ b/packages/localize/test/utils/translations_spec.ts @@ -83,10 +83,10 @@ describe('utils', () => { it('(with identity translations) should render template literals as-is', () => { const translations = { 'abc': 'abc', - 'abc{$ph_1}': 'abc{$ph_1}', - 'abc{$ph_1}def': 'abc{$ph_1}def', - 'abc{$ph_1}def{$ph_2}': 'abc{$ph_1}def{$ph_2}', - 'Hello, {$ph_1}!': 'Hello, {$ph_1}!', + 'abc{$PH}': 'abc{$PH}', + 'abc{$PH}def': 'abc{$PH}def', + 'abc{$PH}def{$PH_1}': 'abc{$PH}def{$PH_1}', + 'Hello, {$PH}!': 'Hello, {$PH}!', }; expect(doTranslate(translations, parts `abc`)).toEqual(parts `abc`); expect(doTranslate(translations, parts `abc${1 + 2 + 3}`)).toEqual(parts `abc${1 + 2 + 3}`); @@ -103,10 +103,10 @@ describe('utils', () => { () => { const translations = { 'abc': 'ABC', - 'abc{$ph_1}': 'ABC{$ph_1}', - 'abc{$ph_1}def': 'ABC{$ph_1}DEF', - 'abc{$ph_1}def{$ph_2}': 'ABC{$ph_1}DEF{$ph_2}', - 'Hello, {$ph_1}!': 'HELLO, {$ph_1}!', + 'abc{$PH}': 'ABC{$PH}', + 'abc{$PH}def': 'ABC{$PH}DEF', + 'abc{$PH}def{$PH_1}': 'ABC{$PH}DEF{$PH_1}', + 'Hello, {$PH}!': 'HELLO, {$PH}!', }; expect(doTranslate(translations, parts `abc`)).toEqual(parts `ABC`); expect(doTranslate(translations, parts `abc${1 + 2 + 3}`)) @@ -123,7 +123,7 @@ describe('utils', () => { it('(with translations to reverse expressions) should render template literals with expressions reversed', () => { const translations = { - 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_3}def{$ph_2} - Hello, {$ph_1}!', + 'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH_2}def{$PH_1} - Hello, {$PH}!', }; const getName = () => 'World'; expect(doTranslate( @@ -134,7 +134,7 @@ describe('utils', () => { it('(with translations to remove expressions) should render template literals with expressions removed', () => { const translations = { - 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_1} - Hello, {$ph_3}!', + 'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH} - Hello, {$PH_2}!', }; const getName = () => 'World'; expect(doTranslate(