/
localize.ts
210 lines (203 loc) Β· 7.98 KB
/
localize.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface LocalizeFn {
(messageParts: TemplateStringsArray, ...expressions: readonly any[]): string;
/**
* A function that converts an input "message with expressions" into a translated "message with
* expressions".
*
* The conversion may be done in place, modifying the array passed to the function, so
* don't assume that this has no side-effects.
*
* The expressions must be passed in since it might be they need to be reordered for
* different translations.
*/
translate?: TranslateFn;
/**
* The current locale of the translated messages.
*
* The compile-time translation inliner is able to replace the following code:
*
* ```
* $localize && $localize.locale
* ```
*
* with a string literal of the current locale. E.g.
*
* ```
* "fr"
* ```
*/
locale?: string;
}
export interface TranslateFn {
(messageParts: TemplateStringsArray,
expressions: readonly any[]): [TemplateStringsArray, readonly any[]];
}
/**
* Tag a template literal string for localization.
*
* For example:
*
* ```ts
* $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 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:: ${}`
* // anonymous substitution so colon must be escaped
* $localize `${label}\: ${}`
* ```
*
* **Processing localized strings:**
*
* 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.
*
* * **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.
* @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.
*/
export const $localize: LocalizeFn = function(
messageParts: TemplateStringsArray, ...expressions: readonly any[]) {
if ($localize.translate) {
// Don't use array expansion here to avoid the compiler adding `__read()` helper unnecessarily.
const translation = $localize.translate(messageParts, expressions);
messageParts = translation[0];
expressions = translation[1];
}
let message = stripBlock(messageParts[0], messageParts.raw[0]);
for (let i = 1; i < messageParts.length; i++) {
message += expressions[i - 1] + stripBlock(messageParts[i], messageParts.raw[i]);
}
return message;
};
const BLOCK_MARKER = ':';
/**
* Strip a delimited "block" from the start of the `messagePart`, if it is found.
*
* 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 and downleveled by TypeScript to ES5 then its
* raw array will only contain empty strings. This is because the current TypeScript compiler uses
* 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 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 is a limitation if compiling to ES5 in TypeScript but is not a problem if the TypeScript
* output is ES2015 and the code is downleveled by a separate tool as happens in the Angular CLI.
*
* @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.
* @throws an error if the block is unterminated
*/
function stripBlock(messagePart: string, rawMessagePart: string) {
rawMessagePart = rawMessagePart || messagePart;
return rawMessagePart.charAt(0) === BLOCK_MARKER ?
messagePart.substring(findEndOfBlock(messagePart, rawMessagePart) + 1) :
messagePart;
}
/**
* Find the end of a "marked block" indicated by the first non-escaped colon.
*
* @param cooked The cooked string (where escaped chars have been processed)
* @param raw The raw string (where escape sequences are still in place)
*
* @returns the index of the end of block marker
* @throws an error if the block is unterminated
*/
function findEndOfBlock(cooked: string, raw: string): number {
/***********************************************************************************************
* This function is repeated in `src/utils/messages.ts` and the two should be kept in sync.
* The reason is that this file is marked as having side-effects, and if we import `messages.ts`
* into it, the whole of `src/utils` will be included in this bundle and none of the functions
* will be tree shaken.
***********************************************************************************************/
for (let cookedIndex = 1, rawIndex = 1; cookedIndex < cooked.length; cookedIndex++, rawIndex++) {
if (raw[rawIndex] === '\\') {
rawIndex++;
} else if (cooked[cookedIndex] === BLOCK_MARKER) {
return cookedIndex;
}
}
throw new Error(`Unterminated $localize metadata block in "${raw}".`);
}