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
This commit is contained in:
Pete Bacon Darwin 2019-09-13 12:46:05 +01:00 committed by Andrew Kushnir
parent fd62ed66e3
commit c7abb7d196
4 changed files with 67 additions and 33 deletions

View File

@ -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.

View File

@ -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;
}

View File

@ -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');