Skip to content
This repository was archived by the owner on Aug 1, 2024. It is now read-only.

Commit 1762f0a

Browse files
brad4dcopybara-github
authored andcommitted
Add goog.i18n.messages.declareIcuTemplate() to closure-library
This is a new API for declaring messages that use ICU message format. It has long been possible to use `goog.getMsg()` to declare messages that use ICU format, but it wasn't possible to provide example or original_code text for the ICU placeholders. It has been a source of some confusion that `goog.getMsg()` effectively supports 2 quite different placeholder formats, one for compile-time and one for runtime (ICU). Going forward, users should prefer using `declareIcuTemplate()` for ICU-formatted messages. We hope to eventually make it an error to specify an ICU-formatted message to `goog.getMsg()`. RELNOTES[NEW]: Add goog.i18n.messages.declareIcuTemplate() as a better way to declare ICU-formatted messages. PiperOrigin-RevId: 488785352 Change-Id: Ibcdf9745a0ae743353a5c955ac09d6658274e2d0
1 parent 5bfbb9c commit 1762f0a

File tree

3 files changed

+410
-0
lines changed

3 files changed

+410
-0
lines changed

closure/goog/i18n/BUILD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ closure_js_library(
3131
":listsymbolsext",
3232
":localefeature",
3333
":messageformat",
34+
":messages",
3435
":mime",
3536
":numberformat",
3637
":numberformatsymbols",
@@ -301,6 +302,13 @@ closure_js_library(
301302
],
302303
)
303304

305+
closure_js_library(
306+
name = "messages",
307+
srcs = ["messages.js"],
308+
lenient = True,
309+
deps = ["//closure/goog/asserts"],
310+
)
311+
304312
closure_js_library(
305313
name = "mime",
306314
srcs = ["mime.js"],

closure/goog/i18n/messages.js

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/**
2+
* @license
3+
* Copyright The Closure Library Authors.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* @fileoverview Support for declaring localizable messages.
9+
*/
10+
11+
goog.module('goog.i18n.messages');
12+
13+
const {assert} = goog.require('goog.asserts');
14+
15+
/**
16+
* Options bag type for `declareIcuTemplate()` options argument.
17+
*
18+
* It is important to note that these options need to be known at compile time,
19+
* so they must always be provided to `declareIcuTmplate()` as an actual object
20+
* literal in the function call. Otherwise, closure-compiler will report an
21+
* error.
22+
*
23+
* @record
24+
*/
25+
const IcuTemplateOptions = function() {};
26+
27+
/**
28+
* Required text describing how the message will be used.
29+
*
30+
* Translators will use this information to help them understand and translate
31+
* the message.
32+
*
33+
* @type {string}
34+
*/
35+
IcuTemplateOptions.prototype.description;
36+
37+
/**
38+
* Optional text used to differentiate messages with identical original text but
39+
* different meanings.
40+
*
41+
* This field should usually be left undefined.
42+
* It should only be used when there exist 2 or more messages that have
43+
* identical original text but are used in very different ways, so that one
44+
* translation won't work for both.
45+
*
46+
* For example, maybe "close" needs to be translated in one place as "to close
47+
* something" but in another as "these things are close to each other".
48+
*
49+
* In such a case, you would specify clarifying text in the "meaning" field. It
50+
* is different from the "description" field in that "meaning" is used when
51+
* computing the unique message identifier, but "description" is not. Although
52+
* this field may be shown to translators, its primary function is just to
53+
* serve as a point of difference to disambiguate 2 otherwise identical
54+
* messages.
55+
*
56+
* @type {undefined|string}
57+
*/
58+
IcuTemplateOptions.prototype.meaning;
59+
60+
/**
61+
* Associates placeholder names with example values.
62+
*
63+
* closure-compiler uses this as the contents of the `<ex>` tag in the
64+
* XMB file it generates or defaults to `-` for historical reasons.
65+
*
66+
* * Must be an object literal.
67+
* * Ignored at runtime.
68+
* * Keys are placeholder names.
69+
* * Values are string literals containing example placeholder values.
70+
* (e.g. "George McFly" for a name placeholder)
71+
*
72+
* @type {!Object<string, string>|undefined}
73+
*/
74+
IcuTemplateOptions.prototype.example;
75+
76+
/**
77+
* Associates placeholder names with strings showing how their values are
78+
* obtained.
79+
*
80+
* This field is intended for use in automatically generated JS code.
81+
* Human-written code should use meaningful placeholder names instead.
82+
*
83+
* closure-compiler uses this as the contents of the `<ph>` tag in the
84+
* XMB file it generates or defaults to `-` for historical reasons.
85+
*
86+
* * Must be an object literal.
87+
* * Ignored at runtime.
88+
* * Keys are placeholder names.
89+
* * Values are string literals indicating how the value is obtained.
90+
* * Typically this is a snippet of source code.
91+
*
92+
* @type {!Object<string, string>|undefined}
93+
*/
94+
IcuTemplateOptions.prototype.original_code;
95+
96+
/**
97+
* Declare a message template using ICU format.
98+
*
99+
* See https://unicode-org.github.io/icu/userguide/format_parse/messages/
100+
*
101+
* When run uncompiled, the template string and options will be checked for
102+
* easy-to-spot programming errors. If there are none, the template string will
103+
* be returned unmodified. Otherwise, an exception will be thrown.
104+
*
105+
* The template string will probably contain ICU-formatted placeholders
106+
* (e.g. "Hi, {NAME}"). The return value from this function will still contain
107+
* those. It should be passed to a message formatting API, such as
108+
* `goog.i18n.MessageFormat` in order to specify the runtime values to
109+
* substitute and produce the final message to display to users.
110+
*
111+
* IMPORTANT: It is an error to use closure-style placeholder syntax
112+
* (e.g. "Hi, {$NAME}" - note the '{$'), such as is used by `goog.getMsg()` for
113+
* compile-time substitution of placeholder values.
114+
*
115+
* At compile time, closure-compiler will look up the localized form of this
116+
* message in a translation bundle file (XTB), and replace this function call
117+
* with a string literal containing the translated template.
118+
*
119+
* The XTB file is generated by some combination of human translators and
120+
* automated tools based on information they receive in an XMB file.
121+
*
122+
* The closure-compiler message extraction tool will generate an XMB file
123+
* from messages declared in JS source code, including calls to this function.
124+
*
125+
* The message element produced for a call to this function will contain a
126+
* `<ph>` (placeholder) element for each ICU placeholder found in the template
127+
* string.
128+
*
129+
* e.g.
130+
*
131+
* ```javascript
132+
* const MSG_HI = declareIcuTemplate(
133+
* 'Hi, {START_BOLD}{NAME}{END_BOLD}!',
134+
* {
135+
* description: 'Say "Hi" to the user.',
136+
* example: {
137+
* 'NAME': 'Jane'
138+
* },
139+
* original_code: {
140+
* // Shown to make clear how it appears in the XMB file.
141+
* // Hand-written JS code should not use this.
142+
* 'NAME': 'user.getName()'
143+
* }
144+
* });
145+
* ```
146+
*
147+
* May generate an XMB element like this:
148+
*
149+
* ```xml
150+
* <msg
151+
* id='3346156045081344548'
152+
* name='MSG_HI'
153+
* desc='Say "Hi" to the user.'
154+
* >Hi, {START_BOLD}<ph
155+
* name="NAME"><ex>Jane</ex>user.getName()</ph>{END_BOLD}!</msg>
156+
* ```
157+
*
158+
* IMPORTANT: The options argument and the values it contains must all be
159+
* literal values. The message extraction tool is not able to evaluate
160+
* expressions or look up variable values.
161+
*
162+
* Note that in the example above there are ICU placeholders that are not
163+
* converted into `<ph>` placeholder elements in the XMB file. When no example
164+
* or original code text is given, there is no need to create such elements.
165+
* When only one of these two values is given, the other will default to "-"
166+
* for historical reasons.
167+
*
168+
*
169+
* The "original_code" option is intended for use when automatically generating
170+
* JS code from some other source, such as an Angular template, where it is
171+
* difficult or impossible for the human author to specify a meaningful
172+
* placeholder name. Hand-written JS code should just use a meaningful
173+
* placeholder name instead.
174+
*
175+
* The 'original_code' text and the 'example' text are shown to human
176+
* translators to give them more context about the meaning of the message.
177+
* They are not used in any other way by closure-compiler or closure-library.
178+
*
179+
* @param {string} templateString
180+
* @param {!IcuTemplateOptions} options
181+
* @return {string}
182+
*/
183+
function declareIcuTemplate(templateString, options) {
184+
// The options are only really used by the compiler when extracting messages
185+
// into an XMB file for translation. However, we'll check for errors now,
186+
// in order to help the developer discover they've made a mistake ASAP.
187+
// Compilation will completely replace calls to this method with static
188+
// strings, so the cost of these checks will only be paid during testing
189+
// and development.
190+
assertDeclareIcuTemplateParametersAreValid(templateString, options);
191+
return templateString;
192+
}
193+
194+
/**
195+
* Properties that are valid to include in the options bag argument to
196+
* `declareIcuTemplate()`.
197+
*
198+
* @type {!Set<string>}
199+
*/
200+
const VALID_OPTIONS =
201+
new Set(['description', 'meaning', 'example', 'original_code']);
202+
203+
/**
204+
* Throw an exception if the parameters are not valid for a `declareIcuTemplate`
205+
* function call.
206+
*
207+
* @param {string} templateString
208+
* @param {!IcuTemplateOptions} options
209+
*/
210+
function assertDeclareIcuTemplateParametersAreValid(templateString, options) {
211+
// This check exists to alert developers to possible problems with their
212+
// `declareIcuTemplate()` message calls during uncompiled execution.
213+
// During compilation closure-compiler will perform these same checks and
214+
// replace all calls to `declareIcuTemplate()` with string literals.
215+
if (COMPILED) return;
216+
217+
assertNoClosureStylePlaceholdersInIcuTemplate(templateString);
218+
const icuPlaceholderNames = gatherIcuPlaceholderNames(templateString);
219+
220+
const description = options.description;
221+
assert(description, 'no description supplied');
222+
assert(
223+
typeof description == 'string', 'invalid description: "%s"', description);
224+
225+
const meaning = options.meaning;
226+
assert(
227+
!meaning || typeof meaning == 'string', 'invalid meaning: "%s"', meaning);
228+
229+
const exampleMap = options.example;
230+
if (exampleMap) {
231+
assert(
232+
typeof exampleMap == 'object', 'invalid example map: "%s"', exampleMap);
233+
assertValidPlaceholderMap('example', exampleMap, icuPlaceholderNames);
234+
}
235+
236+
const originalCodeMap = options.original_code;
237+
if (originalCodeMap) {
238+
assert(
239+
typeof originalCodeMap == 'object', 'invalid original_code map: "%s"',
240+
originalCodeMap);
241+
assertValidPlaceholderMap(
242+
'original_code', originalCodeMap, icuPlaceholderNames);
243+
}
244+
245+
for (const propName of Object.keys(options)) {
246+
assert(VALID_OPTIONS.has(propName), 'unknown option name: "%s"', propName);
247+
}
248+
}
249+
250+
/**
251+
* Throw an exception if the parameters are not valid for a `declareIcuTemplate`
252+
* function call.
253+
*
254+
* @param {string} mapName 'example' or 'original_code'
255+
* @param {!Object<string>} placeholderMap Map from placeholder names to string
256+
* values
257+
* @param {!Set<string>} allPlaceholderNames All known placeholder names
258+
*/
259+
function assertValidPlaceholderMap(
260+
mapName, placeholderMap, allPlaceholderNames) {
261+
for (const placeholderName of Object.keys(placeholderMap)) {
262+
assert(
263+
allPlaceholderNames.has(placeholderName),
264+
'%s: unknown placeholder: "%s"', mapName, placeholderName);
265+
const placeholderValue = placeholderMap[placeholderName];
266+
assert(
267+
typeof placeholderValue == 'string', 'invalid %s value for %s: "%s"',
268+
mapName, placeholderName, placeholderValue);
269+
}
270+
}
271+
272+
273+
/** Matches a single closure-style placeholder. */
274+
const CLOSURE_PLACEHOLDER_RE = /\{\$([^}]+)}/;
275+
276+
/**
277+
* @param {string} icuTemplate
278+
* @throws an AssertionError if the string contains closure-style placeholders
279+
* (`'{$name} is a closure-style placeholder'`)
280+
*/
281+
function assertNoClosureStylePlaceholdersInIcuTemplate(icuTemplate) {
282+
const match = CLOSURE_PLACEHOLDER_RE.exec(icuTemplate);
283+
assert(
284+
match == null,
285+
'closure-style placeholder: "%s" found in ICU template: "%s"',
286+
match && match[0], icuTemplate);
287+
}
288+
289+
/**
290+
* Matches globally for ICU-style placeholder names.
291+
*
292+
* Capturing group #1 contains the the placeholder name for each match.
293+
*/
294+
const ICU_PLACEHOLDER_GLOBAL_RE = /\{([a-z_]\w*)}/ig;
295+
296+
/**
297+
* @param {string} icuTemplate
298+
* @return {!Set<string>} contains placeholder names found in icuTemplate
299+
*/
300+
function gatherIcuPlaceholderNames(icuTemplate) {
301+
const resultSet = new Set();
302+
let match = null;
303+
304+
while (match = ICU_PLACEHOLDER_GLOBAL_RE.exec(icuTemplate)) {
305+
resultSet.add(match[1]);
306+
}
307+
return resultSet;
308+
}
309+
310+
// TODO(bradfordcsmith): Add another method to this module that will serve to
311+
// replace goog.getMsg(), so we can drop that method from base.js.
312+
exports = {
313+
declareIcuTemplate,
314+
IcuTemplateOptions
315+
};

0 commit comments

Comments
 (0)