forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ivy): implement
$localize()
global function (angular#31609)
PR Close angular#31609
- Loading branch information
1 parent
b34bdf5
commit b21397b
Showing
13 changed files
with
723 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
load("//tools:defaults.bzl", "ng_package", "ts_library") | ||
|
||
package(default_visibility = ["//visibility:public"]) | ||
|
||
ts_library( | ||
name = "localize", | ||
srcs = glob( | ||
[ | ||
"*.ts", | ||
"src/**/*.ts", | ||
], | ||
), | ||
module_name = "@angular/localize", | ||
deps = [ | ||
"@npm//@types/node", | ||
], | ||
) | ||
|
||
ng_package( | ||
name = "npm_package", | ||
srcs = [ | ||
"package.json", | ||
"//packages/localize/run_time:package.json", | ||
], | ||
entry_point = ":index.ts", | ||
tags = [ | ||
"release-with-framework", | ||
], | ||
deps = [ | ||
":localize", | ||
"//packages/localize/run_time", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/** | ||
* @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 | ||
*/ | ||
import {_global} from './src/global'; | ||
import {$localize as _localize, LocalizeFn, TranslateFn} from './src/localize'; | ||
|
||
// Attach $localize to the global context, as a side-effect of this module. | ||
_global.$localize = _localize; | ||
|
||
export {LocalizeFn, TranslateFn}; | ||
|
||
// `declare global` allows us to escape the current module and place types on the global namespace | ||
declare global { | ||
/** | ||
* Tag a template literal string for localization. | ||
* | ||
* For example: | ||
* | ||
* ```ts | ||
* $localize `some string to localize` | ||
* ``` | ||
* | ||
* **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. | ||
* | ||
* For example, to name the `item.length` expression placeholder `itemCount` you write: | ||
* | ||
* ```ts | ||
* $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: | ||
* | ||
* For example: | ||
* | ||
* ```ts | ||
* $localize `${label}:label:: ${}` | ||
* // or | ||
* $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. | ||
*/ | ||
const $localize: LocalizeFn; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "@angular/localize", | ||
"version": "0.0.0-PLACEHOLDER", | ||
"description": "Angular - library for localizing messages", | ||
"main": "./bundles/localize.umd.js", | ||
"module": "./fesm5/localize.js", | ||
"es2015": "./fesm2015/localize.js", | ||
"esm5": "./esm5/localize.js", | ||
"esm2015": "./esm2015/localize.js", | ||
"fesm5": "./fesm5/localize.js", | ||
"fesm2015": "./fesm2015/localize.js", | ||
"typings": "./index.d.ts", | ||
"author": "angular", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/angular/angular.git" | ||
}, | ||
"ng-update": { | ||
"packageGroup": "NG_UPDATE_PACKAGE_GROUP" | ||
}, | ||
"sideEffects": true, | ||
"engines": { | ||
"node": ">=8.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
package(default_visibility = ["//visibility:public"]) | ||
|
||
exports_files(["package.json"]) | ||
|
||
ts_library( | ||
name = "run_time", | ||
srcs = glob( | ||
[ | ||
"**/*.ts", | ||
], | ||
), | ||
module_name = "@angular/localize/run_time", | ||
deps = [ | ||
"//packages/localize", | ||
"@npm//@types/node", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* @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 {clearTranslations, loadTranslations} from './src/translate'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"name": "@angular/localize/run_time", | ||
"typings": "./index.d.ts", | ||
"main": "../bundles/localize-run_time.umd.js", | ||
"module": "../fesm5/run_time.js", | ||
"es2015": "../fesm2015/run_time.js", | ||
"esm5": "../esm5/run_time/run_time.js", | ||
"esm2015": "../esm2015/run_time/run_time.js", | ||
"fesm5": "../fesm5/run_time.js", | ||
"fesm2015": "../fesm2015/run_time.js", | ||
"sideEffects": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/** | ||
* @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 | ||
*/ | ||
import {LocalizeFn} from '@angular/localize'; | ||
|
||
/** | ||
* We augment the `$localize` object to also store the translations. | ||
* | ||
* Note that because the TRANSLATIONS are attached to a global object, they will be shared between | ||
* all applications that are running in a single page of the browser. | ||
*/ | ||
declare const $localize: LocalizeFn&{TRANSLATIONS: {[key: string]: ParsedTranslation}}; | ||
|
||
/** | ||
* A map of translations. | ||
* | ||
* The key is the original translation message, the value is the translated message. | ||
* | ||
* The format of these translation message strings uses `{$marker}` to indicate a placeholder. | ||
*/ | ||
export interface Translations { [translationKey: string]: string; } | ||
|
||
/** | ||
* A translation message that has been processed to extract the message parts and placeholders. | ||
* | ||
* This is the format used by the runtime inlining to translate messages. | ||
*/ | ||
export interface ParsedTranslation { | ||
messageParts: TemplateStringsArray; | ||
placeholderNames: string[]; | ||
} | ||
|
||
/** | ||
* A localized message that has been processed to compute the translation key for looking up the | ||
* appropriate translation. | ||
*/ | ||
export interface ParsedMessage { | ||
translationKey: string; | ||
substitutions: {[placeholderName: string]: any}; | ||
} | ||
|
||
/** | ||
* The character used to mark the start and end of a placeholder name. | ||
*/ | ||
const PLACEHOLDER_NAME_MARKER = ':'; | ||
|
||
/** | ||
* Load translations for `$localize`. | ||
* | ||
* The given `translations` are processed and added to a lookup based on their translation key. | ||
* A new translation will overwrite a previous translation if it has the same key. | ||
*/ | ||
export function loadTranslations(translations: Translations) { | ||
// Ensure the translate function exists | ||
if (!$localize.translate) { | ||
$localize.translate = translate; | ||
} | ||
if (!$localize.TRANSLATIONS) { | ||
$localize.TRANSLATIONS = {}; | ||
} | ||
Object.keys(translations).forEach(key => { | ||
$localize.TRANSLATIONS[key] = parseTranslation(translations[key]); | ||
}); | ||
} | ||
|
||
/** | ||
* Remove all translations for `$localize`. | ||
*/ | ||
export function clearTranslations() { | ||
$localize.TRANSLATIONS = {}; | ||
} | ||
|
||
/** | ||
* Translate the text of the given message, using the loaded translations. | ||
* | ||
* This function may reorder (or remove) substitutions as indicated in the matching translation. | ||
*/ | ||
export function translate(messageParts: TemplateStringsArray, substitutions: readonly any[]): | ||
[TemplateStringsArray, readonly any[]] { | ||
const message = parseMessage(messageParts, substitutions); | ||
const translation = $localize.TRANSLATIONS[message.translationKey]; | ||
const result: [TemplateStringsArray, readonly any[]] = | ||
(translation === undefined ? [messageParts, substitutions] : [ | ||
translation.messageParts, | ||
translation.placeholderNames.map(placeholder => message.substitutions[placeholder]) | ||
]); | ||
return result; | ||
} | ||
|
||
///////////// | ||
// Helpers | ||
|
||
/** | ||
* Parse the `messageParts` and `placeholderNames` out of a translation key. | ||
* | ||
* @param translationKey the message to be parsed. | ||
*/ | ||
export function parseTranslation(translationKey: string): ParsedTranslation { | ||
const parts = translationKey.split(/{\$([^}]*)}/); | ||
const messageParts = [parts[0]]; | ||
const placeholderNames: string[] = []; | ||
for (let i = 1; i < parts.length - 1; i += 2) { | ||
placeholderNames.push(parts[i]); | ||
messageParts.push(`${parts[i + 1]}`); | ||
} | ||
const rawMessageParts = | ||
messageParts.map(part => part.charAt(0) === PLACEHOLDER_NAME_MARKER ? '\\' + part : part); | ||
return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames}; | ||
} | ||
|
||
/** | ||
* Process the `messageParts` and `substitutions` that were passed to the `$localize` tag in order | ||
* to match it to a translation. | ||
* | ||
* Specifically this function computes: | ||
* * the `translationKey` for looking up an appropriate translation for this message. | ||
* * a map of placeholder names to substitutions values. | ||
*/ | ||
export function parseMessage( | ||
messageParts: TemplateStringsArray, expressions: readonly any[]): ParsedMessage { | ||
const replacements: {[placeholderName: string]: any} = {}; | ||
let translationKey = messageParts[0]; | ||
for (let i = 1; i < messageParts.length; i++) { | ||
const messagePart = messageParts[i]; | ||
const expression = expressions[i - 1]; | ||
// There is a problem with synthesizing template literals in TS. | ||
// It is not possible to provide raw values for the `messageParts` and TS is not able to compute | ||
// them since this requires access to the string in its original (non-existent) source code. | ||
// Therefore we fall back on the non-raw version if the raw string is empty. | ||
// This should be OK because synthesized nodes only come from the template compiler and they | ||
// will always contain placeholder name information. | ||
// So there will be no escaped placeholder marker character (`:`) directly after a substitution. | ||
if ((messageParts.raw[i] || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER) { | ||
const endOfPlaceholderName = messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1); | ||
const placeholderName = messagePart.substring(1, endOfPlaceholderName); | ||
translationKey += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`; | ||
replacements[placeholderName] = expression; | ||
} else { | ||
const placeholderName = `ph_${i}`; | ||
translationKey += `{$${placeholderName}}${messagePart}`; | ||
replacements[placeholderName] = expression; | ||
} | ||
} | ||
return {translationKey, substitutions: replacements}; | ||
} | ||
|
||
/** | ||
* Make an array of `cooked` strings that also holds the `raw` strings in an additional property. | ||
* | ||
* @param cooked The actual values of the `messagePart` strings. | ||
* @param raw The original raw values of the `messagePart` strings, before escape characters are | ||
* processed. | ||
*/ | ||
function makeTemplateObject(cooked: string[], raw: string[]): TemplateStringsArray { | ||
Object.defineProperty(cooked, 'raw', {value: raw}); | ||
return cooked as any; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") | ||
|
||
ts_library( | ||
name = "test_lib", | ||
testonly = True, | ||
srcs = glob( | ||
["*_spec.ts"], | ||
), | ||
deps = [ | ||
"//packages:types", | ||
"//packages/localize", | ||
"//packages/localize/run_time", | ||
], | ||
) | ||
|
||
jasmine_node_test( | ||
name = "test", | ||
bootstrap = [ | ||
"angular/tools/testing/init_node_no_angular_spec.js", | ||
], | ||
deps = [ | ||
":test_lib", | ||
"//tools/testing:node_no_angular", | ||
], | ||
) |
Oops, something went wrong.