From c7abb7d1967bbeb96e48857b3408fa7075506f85 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Fri, 13 Sep 2019 12:46:05 +0100 Subject: [PATCH] feat(ivy): i18n - add syntax support for `$localize` metadata block (#32594) This commit documents and extends the basic `$localize` implementation to support adding a metadata block to the start of a tagged message. All the "pass-though" version does is to strip this block out, similar to what it does to placeholder name blocks. PR Close #32594 --- packages/localize/index.ts | 2 +- packages/localize/init/index.ts | 48 ++++++++++++++----- .../localize/src/localize/src/localize.ts | 40 ++++++++-------- .../src/localize/test/localize_spec.ts | 10 ++++ 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/packages/localize/index.ts b/packages/localize/index.ts index 1f5ed0ec2c..f75ccf8076 100644 --- a/packages/localize/index.ts +++ b/packages/localize/index.ts @@ -10,4 +10,4 @@ // The public API exports are specified in the `./localize` module, which is checked by the // public_api_guard rules -export * from './localize'; \ No newline at end of file +export * from './localize'; diff --git a/packages/localize/init/index.ts b/packages/localize/init/index.ts index 610b327657..9173b49ce6 100644 --- a/packages/localize/init/index.ts +++ b/packages/localize/init/index.ts @@ -23,6 +23,22 @@ declare global { * $localize `some string to localize` * ``` * + * **Providing meaning, description and id** + * + * 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`:meaning|description@@id:source message text`; + * + * $localize`:meaning|:source message text`; + * $localize`:description:source message text`; + * $localize`:@@id:source message text`; + * ``` + * + * 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 you can optionally name the @@ -37,14 +53,25 @@ declare global { * $localize `There are ${item.length}:itemCount: items`; * ``` * - * 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: + * **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}\: ${}` * ``` * @@ -53,20 +80,17 @@ declare global { * 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. + * 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. + * 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. + * 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. diff --git a/packages/localize/src/localize/src/localize.ts b/packages/localize/src/localize/src/localize.ts index 952e68acab..20b6b5d8cb 100644 --- a/packages/localize/src/localize/src/localize.ts +++ b/packages/localize/src/localize/src/localize.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -const PLACEHOLDER_NAME_MARKER = ':'; - export interface LocalizeFn { (messageParts: TemplateStringsArray, ...expressions: readonly any[]): string; @@ -91,37 +89,39 @@ export const $localize: LocalizeFn = function( messageParts = translation[0]; expressions = translation[1]; } - let message = messageParts[0]; + let message = stripBlock(messageParts[0], messageParts.raw[0]); for (let i = 1; i < messageParts.length; i++) { - message += expressions[i - 1] + stripPlaceholderName(messageParts[i], messageParts.raw[i]); + message += expressions[i - 1] + stripBlock(messageParts[i], messageParts.raw[i]); } return message; }; +const BLOCK_MARKER = ':'; + /** - * Strip the placeholder name from the start of the `messagePart`, if it is found. + * Strip a delimited "block" from the start of the `messagePart`, if it is found. * - * Placeholder marker characters (:) may appear after a substitution that does not provide an - * explicit placeholder name. In this case the character must be escaped with a backslash, `\:`. - * We can check for this by looking at the `raw` messagePart, which should still contain the - * backslash. + * If a marker character (:) actually appears in the content at the start of a tagged string or + * after a substitution expression, where a block has not been provided the character must be + * escaped with a backslash, `\:`. This function checks for this by looking at the `raw` + * messagePart, which should still contain the backslash. * - * If the template literal was synthesized then its raw array will only contain empty strings. - * This is because TS needs the original source code to find the raw text and in the case of - * synthesize AST nodes, there is no source code. + * If the template literal was synthesized, rather than appearing in original source code, then its + * raw array will only contain empty strings. This is because the current TypeScript compiler use + * the original source code to find the raw text and in the case of synthesized AST nodes, there is + * no source code to draw upon. * - * The workaround is to assume that the template literal did not contain an escaped placeholder - * name, and fall back on checking the cooked array instead. - * - * This should be OK because synthesized nodes (from the Angular template compiler) will always - * provide explicit placeholder names and so will never need to escape placeholder name markers. + * The workaround in this function is to assume that the template literal did not contain an escaped + * placeholder name, and fall back on checking the cooked array instead. This should be OK because + * synthesized nodes (from the Angular template compiler) will always provide explicit delimited + * blocks and so will never need to escape placeholder name markers. * * @param messagePart The cooked message part to process. * @param rawMessagePart The raw message part to check. * @returns the message part with the placeholder name stripped, if found. */ -function stripPlaceholderName(messagePart: string, rawMessagePart: string) { - return (rawMessagePart || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER ? - messagePart.substring(messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1) + 1) : +function stripBlock(messagePart: string, rawMessagePart: string) { + return (rawMessagePart || messagePart).charAt(0) === BLOCK_MARKER ? + messagePart.substring(messagePart.indexOf(BLOCK_MARKER, 1) + 1) : messagePart; } diff --git a/packages/localize/src/localize/test/localize_spec.ts b/packages/localize/src/localize/test/localize_spec.ts index 29323254a3..c910a177d4 100644 --- a/packages/localize/src/localize/test/localize_spec.ts +++ b/packages/localize/src/localize/test/localize_spec.ts @@ -19,6 +19,16 @@ describe('$localize tag', () => { expect($localize `Hello, ${getName()}!`).toEqual('Hello, World!'); }); + it('should strip metadata block from message parts', () => { + expect($localize.translate).toBeUndefined(); + expect($localize `:meaning|description@@custom-id:abcdef`).toEqual('abcdef'); + }); + + it('should ignore escaped metadata block marker', () => { + expect($localize.translate).toBeUndefined(); + expect($localize `\:abc:def`).toEqual(':abc:def'); + }); + it('should strip placeholder names from message parts', () => { expect($localize.translate).toBeUndefined(); expect($localize `abc${1 + 2 + 3}:ph1:def${4 + 5 + 6}:ph2:`).toEqual('abc6def15');