diff --git a/integration/cli-hello-world-ivy-i18n/src/polyfills.ts b/integration/cli-hello-world-ivy-i18n/src/polyfills.ts index ec4e202bec..93bcf8a403 100644 --- a/integration/cli-hello-world-ivy-i18n/src/polyfills.ts +++ b/integration/cli-hello-world-ivy-i18n/src/polyfills.ts @@ -84,7 +84,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. */ -import '@angular/localize'; +import '@angular/localize/init'; /*************************************************************************************************** * APPLICATION IMPORTS diff --git a/integration/side-effects/snapshots/core/esm2015.js b/integration/side-effects/snapshots/core/esm2015.js index 3c9b0a8fee..4a4b8bf84e 100644 --- a/integration/side-effects/snapshots/core/esm2015.js +++ b/integration/side-effects/snapshots/core/esm2015.js @@ -13,5 +13,5 @@ const __global = "undefined" !== typeof global && global; const _global = __globalThis || __global || __window || __self; if (ngDevMode) _global.$localize = _global.$localize || function() { - throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize';` to your polyfills.ts file."); + throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize/init';` to your polyfills.ts file."); }; diff --git a/integration/side-effects/snapshots/core/esm5.js b/integration/side-effects/snapshots/core/esm5.js index ba58f35bd7..e969f1091b 100644 --- a/integration/side-effects/snapshots/core/esm5.js +++ b/integration/side-effects/snapshots/core/esm5.js @@ -15,5 +15,5 @@ var __global = "undefined" !== typeof global && global; var _global = __globalThis || __global || __window || __self; if (ngDevMode) _global.$localize = _global.$localize || function() { - throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize';` to your polyfills.ts file."); + throw new Error("It looks like your application or one of its dependencies is using i18n.\n" + "Angular 9 introduced a global `$localize()` function that needs to be loaded.\n" + "Please add `import '@angular/localize/init';` to your polyfills.ts file."); }; diff --git a/modules/benchmarks/src/expanding_rows/BUILD.bazel b/modules/benchmarks/src/expanding_rows/BUILD.bazel index 9b1f32f75f..9360501f9b 100644 --- a/modules/benchmarks/src/expanding_rows/BUILD.bazel +++ b/modules/benchmarks/src/expanding_rows/BUILD.bazel @@ -14,7 +14,7 @@ ng_module( "//packages:types", "//packages/common", "//packages/core", - "//packages/localize", + "//packages/localize/init", "//packages/platform-browser", "@npm//rxjs", ], diff --git a/modules/benchmarks/src/expanding_rows/index_aot.ts b/modules/benchmarks/src/expanding_rows/index_aot.ts index f46ffce318..4ba9a19a1e 100644 --- a/modules/benchmarks/src/expanding_rows/index_aot.ts +++ b/modules/benchmarks/src/expanding_rows/index_aot.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ // This benchmark uses i18n in its `ExpandingRowSummary` component so `$localize` must be loaded. -import '@angular/localize'; +import '@angular/localize/init'; import {enableProdMode} from '@angular/core'; import {platformBrowser} from '@angular/platform-browser'; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 4b66d5f5f3..11511b64ce 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -47,6 +47,6 @@ if (ngDevMode) { throw new Error( 'It looks like your application or one of its dependencies is using i18n.\n' + 'Angular 9 introduced a global `$localize()` function that needs to be loaded.\n' + - 'Please add `import \'@angular/localize\';` to your polyfills.ts file.'); + 'Please add `import \'@angular/localize/init\';` to your polyfills.ts file.'); }; } diff --git a/packages/core/src/render3/i18n.md b/packages/core/src/render3/i18n.md index f079fe2a22..f0469261a9 100644 --- a/packages/core/src/render3/i18n.md +++ b/packages/core/src/render3/i18n.md @@ -1052,7 +1052,7 @@ The generated code needs work with: The solution is to take advantage of compile time constants (e.g. `CLOSURE`) like so: ```typescript -import '@angular/localize'; +import '@angular/localize/init'; let MSG_hello; if (CLOSURE) { diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index 52f0a455c1..a249241bb6 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -27,7 +27,7 @@ ts_library( "//packages/core/src/reflection", "//packages/core/src/util", "//packages/core/testing", - "//packages/localize", + "//packages/localize/init", "//packages/platform-browser", "//packages/platform-browser-dynamic", "//packages/platform-browser/animations", diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel index db2a0e28be..862c5eb34b 100644 --- a/packages/core/test/acceptance/BUILD.bazel +++ b/packages/core/test/acceptance/BUILD.bazel @@ -20,7 +20,7 @@ ts_library( "//packages/core/src/util", "//packages/core/testing", "//packages/localize", - "//packages/localize/run_time", + "//packages/localize/init", "//packages/platform-browser", "//packages/platform-browser-dynamic", "//packages/platform-browser/animations", diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 41dca47aea..ac77ee1dde 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -5,13 +5,15 @@ * 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 '@angular/localize'; +// Make the `$localize()` global function available to the compiled templates, and the direct calls +// below. This would normally be done inside the application `polyfills.ts` file. +import '@angular/localize/init'; import {registerLocaleData} from '@angular/common'; import localeRo from '@angular/common/locales/ro'; import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, Pipe, PipeTransform} from '@angular/core'; import {setDelayProjection} from '@angular/core/src/render3/instructions/projection'; import {TestBed} from '@angular/core/testing'; -import {loadTranslations} from '@angular/localize/run_time'; +import {loadTranslations} from '@angular/localize'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index efa28c70b0..80d7231b1d 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -11,7 +11,7 @@ import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, Eleme import {Input} from '@angular/core/src/metadata'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed, TestComponentRenderer} from '@angular/core/testing'; -import {loadTranslations} from '@angular/localize/run_time'; +import {loadTranslations} from '@angular/localize'; import {By, DomSanitizer} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; diff --git a/packages/core/test/bundling/hello_world_i18n/BUILD.bazel b/packages/core/test/bundling/hello_world_i18n/BUILD.bazel index acd83c5928..83d0657776 100644 --- a/packages/core/test/bundling/hello_world_i18n/BUILD.bazel +++ b/packages/core/test/bundling/hello_world_i18n/BUILD.bazel @@ -11,7 +11,8 @@ ng_module( ], deps = [ "//packages/core", - "//packages/localize/run_time", + "//packages/localize", + "//packages/localize/init", ], ) diff --git a/packages/core/test/bundling/hello_world_i18n/translations.ts b/packages/core/test/bundling/hello_world_i18n/translations.ts index 7d1fb21f54..7ffe25a657 100644 --- a/packages/core/test/bundling/hello_world_i18n/translations.ts +++ b/packages/core/test/bundling/hello_world_i18n/translations.ts @@ -5,8 +5,10 @@ * 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 {loadTranslations} from '@angular/localize/run_time'; +// Make the `$localize()` global function available to the compiled templates, and the direct calls +// below. This would normally be done inside the application `polyfills.ts` file. +import '@angular/localize/init'; +import {loadTranslations} from '@angular/localize'; const translations = { 'Hello World!': 'Bonjour Monde!', diff --git a/packages/core/test/bundling/todo_i18n/BUILD.bazel b/packages/core/test/bundling/todo_i18n/BUILD.bazel index 93dc3994b2..628e9849a3 100644 --- a/packages/core/test/bundling/todo_i18n/BUILD.bazel +++ b/packages/core/test/bundling/todo_i18n/BUILD.bazel @@ -17,7 +17,8 @@ ng_module( "//packages/common", "//packages/core", "//packages/core/test/bundling/util:reflect_metadata", - "//packages/localize/run_time", + "//packages/localize", + "//packages/localize/init", ], ) diff --git a/packages/core/test/bundling/todo_i18n/index.ts b/packages/core/test/bundling/todo_i18n/index.ts index 609c372562..648da7d67f 100644 --- a/packages/core/test/bundling/todo_i18n/index.ts +++ b/packages/core/test/bundling/todo_i18n/index.ts @@ -6,9 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ import '@angular/core/test/bundling/util/src/reflect_metadata'; -// Make the `$localize()` global function available to the compiled templates, and the direct calls -// below. This would normally be done inside the application `polyfills.ts` file. -import '@angular/localize'; import './translations'; import {CommonModule} from '@angular/common'; import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; diff --git a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts index 96adcdc609..74d560ab0e 100644 --- a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts @@ -5,10 +5,12 @@ * 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 '@angular/localize/init'; import '@angular/compiler'; + import {ɵwhenRendered as whenRendered} from '@angular/core'; import {getComponent} from '@angular/core/src/render3'; +import {clearTranslations} from '@angular/localize'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; @@ -19,12 +21,7 @@ describe('functional test for todo i18n', () => { BUNDLES.forEach(bundle => { describe(bundle, () => { it('should render todo i18n', withBody('', async() => { - // We need to delete the dummy `$localize` that was added because of the import of - // `@angular/core` at the top of this file. - // Also to clear out the translations from the previous test. - // This would not be needed in normal applications since the import of - // `@angular/localize` would be in polyfill.ts before any other import. - ($localize as any) = undefined; + clearTranslations(); require(path.join(PACKAGE, bundle)); const toDoAppComponent = getComponent(document.querySelector('todo-app') !); expect(document.body.textContent).toContain('liste de tâches'); diff --git a/packages/core/test/bundling/todo_i18n/translations.ts b/packages/core/test/bundling/todo_i18n/translations.ts index f0871aa370..370bad234a 100644 --- a/packages/core/test/bundling/todo_i18n/translations.ts +++ b/packages/core/test/bundling/todo_i18n/translations.ts @@ -5,8 +5,10 @@ * 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 {loadTranslations} from '@angular/localize/run_time'; +// Make the `$localize()` global function available to the compiled templates, and the direct calls +// below. This would normally be done inside the application `polyfills.ts` file. +import '@angular/localize/init'; +import {loadTranslations} from '@angular/localize'; export const translations = { 'What needs to be done?': `Qu'y a-t-il à faire ?`, diff --git a/packages/core/test/linker/ng_container_integration_spec.ts b/packages/core/test/linker/ng_container_integration_spec.ts index 70fbdb91c3..6f7fca54a2 100644 --- a/packages/core/test/linker/ng_container_integration_spec.ts +++ b/packages/core/test/linker/ng_container_integration_spec.ts @@ -5,7 +5,9 @@ * 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 '@angular/localize'; +// Make the `$localize()` global function available to the compiled templates, and the direct calls +// below. This would normally be done inside the application `polyfills.ts` file. +import '@angular/localize/init'; import {AfterContentInit, AfterViewInit, Component, ContentChildren, Directive, Input, QueryList, ViewChildren, ɵivyEnabled as ivyEnabled} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {isCommentNode} from '@angular/platform-browser/testing/src/browser_util'; diff --git a/packages/localize/BUILD.bazel b/packages/localize/BUILD.bazel index c27a8aa673..b05ebec99e 100644 --- a/packages/localize/BUILD.bazel +++ b/packages/localize/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( ), module_name = "@angular/localize", deps = [ + "//packages/localize/src/localize", "@npm//@types/node", ], ) @@ -20,7 +21,7 @@ ng_package( name = "npm_package", srcs = [ "package.json", - "//packages/localize/run_time:package.json", + "//packages/localize/init:package.json", ], entry_point = ":index.ts", tags = [ @@ -28,6 +29,6 @@ ng_package( ], deps = [ ":localize", - "//packages/localize/run_time", + "//packages/localize/init", ], ) diff --git a/packages/localize/index.ts b/packages/localize/index.ts index 6b76f3b6a8..1f5ed0ec2c 100644 --- a/packages/localize/index.ts +++ b/packages/localize/index.ts @@ -5,73 +5,9 @@ * 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; +// DO NOT ADD public exports to this file. +// The public API exports are specified in the `./localize` module, which is checked by the +// public_api_guard rules -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; -} +export * from './localize'; \ No newline at end of file diff --git a/packages/localize/run_time/BUILD.bazel b/packages/localize/init/BUILD.bazel similarity index 71% rename from packages/localize/run_time/BUILD.bazel rename to packages/localize/init/BUILD.bazel index 0fedff66bf..0ffd78b265 100644 --- a/packages/localize/run_time/BUILD.bazel +++ b/packages/localize/init/BUILD.bazel @@ -5,15 +5,15 @@ package(default_visibility = ["//visibility:public"]) exports_files(["package.json"]) ts_library( - name = "run_time", + name = "init", srcs = glob( [ "**/*.ts", ], ), - module_name = "@angular/localize/run_time", + module_name = "@angular/localize/init", deps = [ - "//packages/localize", + "//packages/localize/src/localize", "@npm//@types/node", ], ) diff --git a/packages/localize/init/index.ts b/packages/localize/init/index.ts new file mode 100644 index 0000000000..610b327657 --- /dev/null +++ b/packages/localize/init/index.ts @@ -0,0 +1,76 @@ +/** + * @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 {$localize, LocalizeFn, _global} from '../src/localize'; + +export {LocalizeFn, TranslateFn} from '../src/localize'; + +// Attach $localize to the global context, as a side-effect of this module. +_global.$localize = $localize; + +// `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; +} diff --git a/packages/localize/init/package.json b/packages/localize/init/package.json new file mode 100644 index 0000000000..e63f637630 --- /dev/null +++ b/packages/localize/init/package.json @@ -0,0 +1,11 @@ +{ + "name": "@angular/localize/init", + "typings": "./index.d.ts", + "main": "../bundles/localize-init.umd.js", + "module": "../fesm5/init.js", + "es2015": "../fesm2015/init.js", + "esm5": "../esm5/init/index.js", + "esm2015": "../esm2015/init/index.js", + "fesm5": "../fesm5/init.js", + "fesm2015": "../fesm2015/init.js" +} diff --git a/packages/localize/run_time/index.ts b/packages/localize/localize.ts similarity index 76% rename from packages/localize/run_time/index.ts rename to packages/localize/localize.ts index 24b88c6c0a..c8eb15048d 100644 --- a/packages/localize/run_time/index.ts +++ b/packages/localize/localize.ts @@ -6,4 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {clearTranslations, loadTranslations} from './src/translate'; +// This file contains the public API of the `@angular/localize` entry-point + +export {clearTranslations, loadTranslations} from './src/translate'; \ No newline at end of file diff --git a/packages/localize/package.json b/packages/localize/package.json index 879a415146..e988e5334f 100644 --- a/packages/localize/package.json +++ b/packages/localize/package.json @@ -5,8 +5,8 @@ "main": "./bundles/localize.umd.js", "module": "./fesm5/localize.js", "es2015": "./fesm2015/localize.js", - "esm5": "./esm5/localize.js", - "esm2015": "./esm2015/localize.js", + "esm5": "./esm5/index.js", + "esm2015": "./esm2015/index.js", "fesm5": "./fesm5/localize.js", "fesm2015": "./fesm2015/localize.js", "typings": "./index.d.ts", @@ -19,7 +19,11 @@ "ng-update": { "packageGroup": "NG_UPDATE_PACKAGE_GROUP" }, - "sideEffects": true, + "sideEffects": [ + "**/init/index.js", + "**/init.js", + "**/localize-init.umd.js" + ], "engines": { "node": ">=8.0" } diff --git a/packages/localize/run_time/package.json b/packages/localize/run_time/package.json deleted file mode 100644 index e3380819f8..0000000000 --- a/packages/localize/run_time/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 -} diff --git a/packages/localize/run_time/src/translate.ts b/packages/localize/run_time/src/translate.ts deleted file mode 100644 index 27c926e0ce..0000000000 --- a/packages/localize/run_time/src/translate.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * @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; -} diff --git a/packages/localize/src/localize/BUILD.bazel b/packages/localize/src/localize/BUILD.bazel new file mode 100644 index 0000000000..8bf81c7357 --- /dev/null +++ b/packages/localize/src/localize/BUILD.bazel @@ -0,0 +1,16 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "localize", + srcs = glob( + [ + "**/*.ts", + ], + ), + module_name = "@angular/localize/src/localize", + deps = [ + "@npm//@types/node", + ], +) diff --git a/packages/localize/src/localize/index.ts b/packages/localize/src/localize/index.ts new file mode 100644 index 0000000000..0f9d07ab32 --- /dev/null +++ b/packages/localize/src/localize/index.ts @@ -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 {_global} from './src/global'; +export {$localize, LocalizeFn, TranslateFn} from './src/localize'; diff --git a/packages/localize/src/global.ts b/packages/localize/src/localize/src/global.ts similarity index 100% rename from packages/localize/src/global.ts rename to packages/localize/src/localize/src/global.ts diff --git a/packages/localize/src/localize.ts b/packages/localize/src/localize/src/localize.ts similarity index 100% rename from packages/localize/src/localize.ts rename to packages/localize/src/localize/src/localize.ts diff --git a/packages/localize/run_time/test/BUILD.bazel b/packages/localize/src/localize/test/BUILD.bazel similarity index 85% rename from packages/localize/run_time/test/BUILD.bazel rename to packages/localize/src/localize/test/BUILD.bazel index a1d6e7ddde..94cde285ef 100644 --- a/packages/localize/run_time/test/BUILD.bazel +++ b/packages/localize/src/localize/test/BUILD.bazel @@ -8,8 +8,7 @@ ts_library( ), deps = [ "//packages:types", - "//packages/localize", - "//packages/localize/run_time", + "//packages/localize/src/localize", ], ) diff --git a/packages/localize/test/localize_spec.ts b/packages/localize/src/localize/test/localize_spec.ts similarity index 97% rename from packages/localize/test/localize_spec.ts rename to packages/localize/src/localize/test/localize_spec.ts index 7c67808f8d..29323254a3 100644 --- a/packages/localize/test/localize_spec.ts +++ b/packages/localize/src/localize/test/localize_spec.ts @@ -5,8 +5,7 @@ * 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 '..'; // Ensure $localize is attached to the global scope -import {TranslateFn} from '../src/localize'; +import {$localize, TranslateFn} from '../src/localize'; describe('$localize tag', () => { describe('with no `translate()` defined (the default)', () => { diff --git a/packages/localize/src/translate.ts b/packages/localize/src/translate.ts new file mode 100644 index 0000000000..1db3e5885e --- /dev/null +++ b/packages/localize/src/translate.ts @@ -0,0 +1,57 @@ +/** + * @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 './localize'; +import {ParsedTranslation, TargetMessage, TranslationKey, parseTranslation, translate as _translate} from './utils/translations'; + +/** + * 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: Record}; + +/** + * 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. + * + * @publicApi + */ +export function loadTranslations(translations: Record) { + // 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`. + * + * @publicApi + */ +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[]] { + return _translate($localize.TRANSLATIONS, messageParts, substitutions); +} diff --git a/packages/localize/src/utils/constants.ts b/packages/localize/src/utils/constants.ts new file mode 100644 index 0000000000..efcd0d7ab2 --- /dev/null +++ b/packages/localize/src/utils/constants.ts @@ -0,0 +1,19 @@ +/** + * @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 + */ + +/** + * The character used to mark the start and end of a placeholder name in a `$localize` tagged + * string. + * + * For example: + * + * ``` + * $localize`Hello, ${title}:title:!`; + * ``` + */ +export const PLACEHOLDER_NAME_MARKER = ':'; diff --git a/packages/localize/src/utils/messages.ts b/packages/localize/src/utils/messages.ts new file mode 100644 index 0000000000..9eed8dca4f --- /dev/null +++ b/packages/localize/src/utils/messages.ts @@ -0,0 +1,81 @@ +/** + * @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 {PLACEHOLDER_NAME_MARKER} from './constants'; +import {TranslationKey} from './translations'; + +/** + * A string containing a translation source message. + * + * I.E. the message that indicates what will be translated from. + * + * Uses `{$placeholder-name}` to indicate a placeholder. + */ +export type SourceMessage = string; + +/** + * Information parsed from a `$localize` tagged string that is used to translate it. + * + * For example: + * + * ``` + * const name = 'Jo Bloggs'; + * $localize`Hello ${name}:title!`; + * ``` + * + * May be parsed into: + * + * ``` + * { + * translationKey: 'Hello {$title}!', + * substitutions: { title: 'Jo Bloggs' }, + * } + * ``` + */ +export interface ParsedMessage { + /** + * The key used to look up the appropriate translation target. + */ + translationKey: TranslationKey; + /** + * A mapping of placeholder names to substitution values. + */ + substitutions: Record; +} + +/** + * Parse a `$localize` tagged string into a structure that can be used for translation. + * + * See `ParsedMessage` for an example. + */ +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}; +} diff --git a/packages/localize/src/utils/translations.ts b/packages/localize/src/utils/translations.ts new file mode 100644 index 0000000000..8345929989 --- /dev/null +++ b/packages/localize/src/utils/translations.ts @@ -0,0 +1,97 @@ +/** + * @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 {PLACEHOLDER_NAME_MARKER} from './constants'; +import {SourceMessage, parseMessage} from './messages'; + +/** + * A key used to lookup a `TargetMessage` in a hash map. + */ +export type TranslationKey = SourceMessage; + +/** + * A string containing a translation target message. + * + * I.E. the message that indicates what will be translated to. + * + * Uses `{$placeholder-name}` to indicate a placeholder. + */ +export type TargetMessage = string; + +/** + * A translation message that has been processed to extract the message parts and placeholders. + */ +export interface ParsedTranslation { + messageParts: TemplateStringsArray; + placeholderNames: string[]; +} + +/** + * The internal structure used by the runtime localization to translate messages. + */ +export type ParsedTranslations = Record; + + +/** + * Translate the text of the `$localize` tagged-string (i.e. `messageParts` and + * `substitutions`) using the given `translations`. + * + * The tagged-string is parsed to extract its `translationKey` which is used to find an appropriate + * `ParsedTranslation`. + * + * If one is found then it is used to translate the message into a new set of `messageParts` and + * `substitutions`. + * The translation may reorder (or remove) substitutions as appropriate. + * + * If no translation matches then the original `messageParts` and `substitutions` are returned + */ +export function translate( + translations: Record, messageParts: TemplateStringsArray, + substitutions: readonly any[]): [TemplateStringsArray, readonly any[]] { + const message = parseMessage(messageParts, substitutions); + const translation = translations[message.translationKey]; + if (translation !== undefined) { + return [ + translation.messageParts, + translation.placeholderNames.map(placeholder => message.substitutions[placeholder]) + ]; + } else { + return [messageParts, substitutions]; + } +} + +/** + * Parse the `messageParts` and `placeholderNames` out of a target `message`. + * + * Used by `loadTranslations()` to convert target message strings into a structure that is more + * appropriate for doing translation. + * + * @param message the message to be parsed. + */ +export function parseTranslation(message: TargetMessage): ParsedTranslation { + const parts = message.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}; +} + +/** + * Create the specialized array that is passed to tagged-string tag functions. + * + * @param cooked The message parts with their escape codes processed. + * @param raw The message parts with their escaped codes as-is. + */ +export function makeTemplateObject(cooked: string[], raw: string[]): TemplateStringsArray { + Object.defineProperty(cooked, 'raw', {value: raw}); + return cooked as any; +} diff --git a/packages/localize/test/BUILD.bazel b/packages/localize/test/BUILD.bazel index aaf6c460eb..5a12e06dc4 100644 --- a/packages/localize/test/BUILD.bazel +++ b/packages/localize/test/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( deps = [ "//packages:types", "//packages/localize", + "//packages/localize/init", ], ) diff --git a/packages/localize/run_time/test/translate_spec.ts b/packages/localize/test/translate_spec.ts similarity index 97% rename from packages/localize/run_time/test/translate_spec.ts rename to packages/localize/test/translate_spec.ts index 78eaa8422f..d54f2793cc 100644 --- a/packages/localize/run_time/test/translate_spec.ts +++ b/packages/localize/test/translate_spec.ts @@ -5,8 +5,7 @@ * 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 */ -// Ensure that `$localize` is loaded to the global scope. -import '@angular/localize'; +import '@angular/localize/init'; import {clearTranslations, loadTranslations} from '../src/translate'; describe('$localize tag with translations', () => { diff --git a/packages/localize/test/utils/messages_spec.ts b/packages/localize/test/utils/messages_spec.ts new file mode 100644 index 0000000000..9eb8cdcd3f --- /dev/null +++ b/packages/localize/test/utils/messages_spec.ts @@ -0,0 +1,47 @@ +/** + * @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 {parseMessage} from '../../src/utils/messages'; +import {makeTemplateObject} from '../../src/utils/translations'; + +describe('messages utils', () => { + describe('parseMessage', () => { + it('should compute the translation key', () => { + const message = parseMessage( + makeTemplateObject(['a', ':one:b', ':two:c'], ['a', ':one:b', ':two:c']), [1, 2]); + expect(message.translationKey).toEqual('a{$one}b{$two}c'); + }); + + it('should compute the translation key, inferring placeholder names if not given', () => { + const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]); + expect(message.translationKey).toEqual('a{$ph_1}b{$ph_2}c'); + }); + + it('should compute the translation key, ignoring escaped placeholder names', () => { + const message = parseMessage( + makeTemplateObject(['a', ':one:b', ':two:c'], ['a', '\\:one:b', '\\:two:c']), [1, 2]); + expect(message.translationKey).toEqual('a{$ph_1}:one:b{$ph_2}:two:c'); + }); + + it('should compute the translation key, handling empty raw values', () => { + const message = + parseMessage(makeTemplateObject(['a', ':one:b', ':two:c'], ['', '', '']), [1, 2]); + expect(message.translationKey).toEqual('a{$one}b{$two}c'); + }); + + it('should build a map of named placeholders to expressions', () => { + const message = parseMessage( + makeTemplateObject(['a', ':one:b', ':two:c'], ['a', ':one:b', ':two:c']), [1, 2]); + expect(message.substitutions).toEqual({one: 1, two: 2}); + }); + + it('should build a map of implied placeholders to expressions', () => { + const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]); + expect(message.substitutions).toEqual({ph_1: 1, ph_2: 2}); + }); + }); +}); diff --git a/packages/localize/test/utils/translations_spec.ts b/packages/localize/test/utils/translations_spec.ts new file mode 100644 index 0000000000..a9158f0c15 --- /dev/null +++ b/packages/localize/test/utils/translations_spec.ts @@ -0,0 +1,164 @@ +/** + * @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 {ParsedTranslation, TargetMessage, TranslationKey, makeTemplateObject, parseTranslation, translate} from '../../src/utils/translations'; + +describe('utils', () => { + describe('makeTemplateObject', () => { + it('should return an array containing the cooked items', () => { + const template = + makeTemplateObject(['cooked-a', 'cooked-b', 'cooked-c'], ['raw-a', 'raw-b', 'raw-c']); + expect(template).toEqual(['cooked-a', 'cooked-b', 'cooked-c']); + }); + + it('should return an array that has a raw property containing the raw items', () => { + const template = + makeTemplateObject(['cooked-a', 'cooked-b', 'cooked-c'], ['raw-a', 'raw-b', 'raw-c']); + expect(template.raw).toEqual(['raw-a', 'raw-b', 'raw-c']); + }); + }); + + describe('parseTranslation', () => { + it('should extract the messageParts as a TemplateStringsArray', () => { + const translation = parseTranslation('a{$one}b{$two}c'); + expect(translation.messageParts).toEqual(['a', 'b', 'c']); + expect(translation.messageParts.raw).toEqual(['a', 'b', 'c']); + }); + + it('should extract the messageParts with leading expression as a TemplateStringsArray', () => { + const translation = parseTranslation('{$one}a{$two}b'); + expect(translation.messageParts).toEqual(['', 'a', 'b']); + expect(translation.messageParts.raw).toEqual(['', 'a', 'b']); + }); + + it('should extract the messageParts with trailing expression as a TemplateStringsArray', () => { + const translation = parseTranslation('a{$one}b{$two}'); + expect(translation.messageParts).toEqual(['a', 'b', '']); + expect(translation.messageParts.raw).toEqual(['a', 'b', '']); + }); + + it('should extract the messageParts with escaped characters as a TemplateStringsArray', () => { + const translation = parseTranslation('a{$one}\nb\n{$two}c'); + expect(translation.messageParts).toEqual(['a', '\nb\n', 'c']); + // `messageParts.raw` are not actually escaped as they are not generally used by `$localize`. + // See the "escaped placeholders" test below... + expect(translation.messageParts.raw).toEqual(['a', '\nb\n', 'c']); + }); + + it('should extract the messageParts with escaped placeholders as a TemplateStringsArray', + () => { + const translation = parseTranslation('a{$one}:marker:b{$two}c'); + expect(translation.messageParts).toEqual(['a', ':marker:b', 'c']); + // A `messagePart` that starts with a placeholder marker does get escaped in + // `messageParts.raw` as this is used by `$localize`. + expect(translation.messageParts.raw).toEqual(['a', '\\:marker:b', 'c']); + }); + + it('should extract the placeholder names, in order', () => { + const translation = parseTranslation('a{$one}b{$two}c'); + expect(translation.placeholderNames).toEqual(['one', 'two']); + }); + + it('should handle a translation with no substitutions', () => { + const translation = parseTranslation('abc'); + expect(translation.messageParts).toEqual(['abc']); + expect(translation.messageParts.raw).toEqual(['abc']); + expect(translation.placeholderNames).toEqual([]); + }); + + it('should handle a translation with only substitutions', () => { + const translation = parseTranslation('{$one}{$two}'); + expect(translation.messageParts).toEqual(['', '', '']); + expect(translation.messageParts.raw).toEqual(['', '', '']); + expect(translation.placeholderNames).toEqual(['one', 'two']); + }); + }); + + describe('translate', () => { + it('(with identity translations) should render template literals as-is', () => { + const translations = { + 'abc': 'abc', + 'abc{$ph_1}': 'abc{$ph_1}', + 'abc{$ph_1}def': 'abc{$ph_1}def', + 'abc{$ph_1}def{$ph_2}': 'abc{$ph_1}def{$ph_2}', + 'Hello, {$ph_1}!': 'Hello, {$ph_1}!', + }; + expect(doTranslate(translations, parts `abc`)).toEqual(parts `abc`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}`)).toEqual(parts `abc${1 + 2 + 3}`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}def`)) + .toEqual(parts `abc${1 + 2 + 3}def`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6}`)) + .toEqual(parts `abc${1 + 2 + 3}def${4 + 5 + 6}`); + const getName = () => 'World'; + expect(doTranslate(translations, parts `Hello, ${getName()}!`)) + .toEqual(parts `Hello, ${'World'}!`); + }); + + it('(with upper-casing translations) should render template literals with messages upper-cased', + () => { + const translations = { + 'abc': 'ABC', + 'abc{$ph_1}': 'ABC{$ph_1}', + 'abc{$ph_1}def': 'ABC{$ph_1}DEF', + 'abc{$ph_1}def{$ph_2}': 'ABC{$ph_1}DEF{$ph_2}', + 'Hello, {$ph_1}!': 'HELLO, {$ph_1}!', + }; + expect(doTranslate(translations, parts `abc`)).toEqual(parts `ABC`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}`)) + .toEqual(parts `ABC${1 + 2 + 3}`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}def`)) + .toEqual(parts `ABC${1 + 2 + 3}DEF`); + expect(doTranslate(translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6}`)) + .toEqual(parts `ABC${1 + 2 + 3}DEF${4 + 5 + 6}`); + const getName = () => 'World'; + expect(doTranslate(translations, parts `Hello, ${getName()}!`)) + .toEqual(parts `HELLO, ${'World'}!`); + }); + + it('(with translations to reverse expressions) should render template literals with expressions reversed', + () => { + const translations = { + 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_3}def{$ph_2} - Hello, {$ph_1}!', + }; + const getName = () => 'World'; + expect(doTranslate( + translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6} - Hello, ${getName()}!`)) + .toEqual(parts `abc${'World'}def${4 + 5 + 6} - Hello, ${1 + 2 + 3}!`); + }); + + it('(with translations to remove expressions) should render template literals with expressions removed', + () => { + const translations = { + 'abc{$ph_1}def{$ph_2} - Hello, {$ph_3}!': 'abc{$ph_1} - Hello, {$ph_3}!', + }; + const getName = () => 'World'; + expect(doTranslate( + translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6} - Hello, ${getName()}!`)) + .toEqual(parts `abc${1 + 2 + 3} - Hello, ${'World'}!`); + }); + + function parts(messageParts: TemplateStringsArray, ...substitutions: any[]): + [TemplateStringsArray, any[]] { + return [messageParts, substitutions]; + } + + function parseTranslations(translations: Record): + Record { + const parsedTranslations: Record = {}; + Object.keys(translations).forEach(key => { + parsedTranslations[key] = parseTranslation(translations[key]); + }); + return parsedTranslations; + } + + function doTranslate( + translations: Record, + message: [TemplateStringsArray, any[]]): [TemplateStringsArray, readonly any[]] { + return translate(parseTranslations(translations), message[0], message[1]); + } + }); +}); diff --git a/test-main.js b/test-main.js index 1fea998c6a..45bbc1c456 100644 --- a/test-main.js +++ b/test-main.js @@ -51,7 +51,8 @@ System.config({ '@angular/router': {main: 'index.js', defaultExtension: 'js'}, '@angular/http/testing': {main: 'index.js', defaultExtension: 'js'}, '@angular/http': {main: 'index.js', defaultExtension: 'js'}, - '@angular/localize/run_time': {main: 'index.js', defaultExtension: 'js'}, + '@angular/localize/src/localize': {main: 'index.js', defaultExtension: 'js'}, + '@angular/localize/init': {main: 'index.js', defaultExtension: 'js'}, '@angular/localize': {main: 'index.js', defaultExtension: 'js'}, '@angular/upgrade/static/testing': {main: 'index.js', defaultExtension: 'js'}, '@angular/upgrade/static': {main: 'index.js', defaultExtension: 'js'}, diff --git a/tools/public_api_guard/localize/localize.d.ts b/tools/public_api_guard/localize/localize.d.ts new file mode 100644 index 0000000000..c3d5f94d6e --- /dev/null +++ b/tools/public_api_guard/localize/localize.d.ts @@ -0,0 +1,3 @@ +export declare function clearTranslations(): void; + +export declare function loadTranslations(translations: Record): void;