From 881ecb09362184f2f9166c7dff8a78643e416cd7 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 15 Dec 2022 09:06:36 +0000 Subject: [PATCH] fix(localize): add triple slash type reference on `@angular/localize` on `ng add (#48502) This commits add a triple slash type reference to the `main.ts` of the project when running `ng add @angular/localize`. This is purely needed for IDE purposes so that `$localize` is available globally. For the compilations `@angular/localize` types are adding the the respective TS configs files. This commits also add better support for using `@angular/localize` in `/// `. To support this we need to move the global definition outside of a barrel file and into the index.ts file. Without this change the `$localize` method will not available globally when using triple slash type references. Closes #48434 PR Close #48502 --- packages/localize/index.ts | 103 ++++++++++++++++++ packages/localize/localize.ts | 98 ----------------- packages/localize/schematics/ng-add/index.ts | 17 ++- .../localize/schematics/ng-add/index_spec.ts | 41 +++---- 4 files changed, 136 insertions(+), 123 deletions(-) diff --git a/packages/localize/index.ts b/packages/localize/index.ts index 7e531e0d29d..e58ae357021 100644 --- a/packages/localize/index.ts +++ b/packages/localize/index.ts @@ -11,3 +11,106 @@ // public_api_guard rules export * from './localize'; + +// The global declaration must be in the index.d.ts as otherwise it will not be picked up when used +// with +// /// + +import {LocalizeFn} from './src/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` + * ``` + * + * **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 i18n guide](guide/i18n-common-prepare#mark-text-in-component-template). + * + * **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. + */ + const $localize: LocalizeFn; +} diff --git a/packages/localize/localize.ts b/packages/localize/localize.ts index ad2b7b1a8b0..90ae8da520f 100644 --- a/packages/localize/localize.ts +++ b/packages/localize/localize.ts @@ -7,107 +7,9 @@ */ // This file contains the public API of the `@angular/localize` entry-point -import {LocalizeFn} from './src/localize'; export {clearTranslations, loadTranslations} from './src/translate'; export {MessageId, TargetMessage} from './src/utils'; // Exports that are not part of the public API export * from './private'; - -// `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` - * ``` - * - * **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 i18n guide](guide/i18n-common-prepare#mark-text-in-component-template). - * - * **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. - */ - const $localize: LocalizeFn; -} diff --git a/packages/localize/schematics/ng-add/index.ts b/packages/localize/schematics/ng-add/index.ts index 462166d57c9..829bd0428a0 100644 --- a/packages/localize/schematics/ng-add/index.ts +++ b/packages/localize/schematics/ng-add/index.ts @@ -18,6 +18,7 @@ import {Builders} from '@schematics/angular/utility/workspace-models'; import {Schema} from './schema'; const localizeType = `@angular/localize`; +const localizeTripleSlashType = `/// `; function addTypeScriptConfigTypes(projectName: string): Rule { return async (host: Tree) => { @@ -28,7 +29,7 @@ function addTypeScriptConfigTypes(projectName: string): Rule { } // We add the root workspace tsconfig for better IDE support. - const tsConfigFiles = new Set(['tsconfig.json']); + const tsConfigFiles = new Set(); for (const target of project.targets.values()) { switch (target.builder) { case Builders.Karma: @@ -41,6 +42,13 @@ function addTypeScriptConfigTypes(projectName: string): Rule { break; } + + if (target.builder === Builders.Browser) { + const value = target.options?.['main']; + if (typeof value === 'string') { + addTripleSlashType(host, value); + } + } } const typesJsonPath: JSONPath = ['compilerOptions', 'types']; @@ -68,6 +76,13 @@ function addTypeScriptConfigTypes(projectName: string): Rule { }; } +function addTripleSlashType(host: Tree, path: string): void { + const content = host.readText(path); + if (!content.includes(localizeTripleSlashType)) { + host.overwrite(path, localizeTripleSlashType + '\n\n' + content); + } +} + function moveToDependencies(host: Tree, context: SchematicContext): void { if (!host.exists('package.json')) { return; diff --git a/packages/localize/schematics/ng-add/index_spec.ts b/packages/localize/schematics/ng-add/index_spec.ts index 1f92baaa7e1..de1567ee2c5 100644 --- a/packages/localize/schematics/ng-add/index_spec.ts +++ b/packages/localize/schematics/ng-add/index_spec.ts @@ -16,7 +16,8 @@ interface TsConfig { } describe('ng-add schematic', () => { - const localizeType = '@angular/localize'; + const localizeTripleSlashType = `/// `; + const defaultOptions = {project: 'demo'}; const schematicRunner = new SchematicTestRunner( '@angular/localize', runfiles.resolvePackageRelative('../collection.json')); @@ -33,6 +34,11 @@ describe('ng-add schematic', () => { }, })); + host.create('main.ts', ` + import { enableProdMode } from '@angular/core'; + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + `); + host.create('angular.json', JSON.stringify({ version: 1, projects: { @@ -42,6 +48,7 @@ describe('ng-add schematic', () => { build: { builder: '@angular-devkit/build-angular:browser', options: { + main: './main.ts', tsConfig: './tsconfig.app.json', }, }, @@ -69,36 +76,22 @@ describe('ng-add schematic', () => { })); }); - it(`should add '@angular/localize' in 'types' in the root level 'tsconfig.json'`, async () => { - host.create('tsconfig.json', JSON.stringify({ - compilerOptions: { - types: ['node'], - }, - })); - + it(`should add '@angular/localize' type reference in 'main.ts'`, async () => { host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const {compilerOptions} = host.readJson('tsconfig.json') as TsConfig; - const types = compilerOptions?.types; - expect(types).toContain(localizeType); - expect(types).toHaveSize(2); + expect(host.readText('main.ts')).toContain(localizeTripleSlashType); }); - it(`should not add '@angular/localize' in 'types' tsconfig when '@angular/localize/init' is present`, + it(`should not add '@angular/localize' type reference in 'main.ts' if already present`, async () => { - host.create('tsconfig.json', JSON.stringify({ - compilerOptions: { - types: ['node', '@angular/localize/init'], - }, - })); - + const mainContentInput = ` + ${localizeTripleSlashType} + import { enableProdMode } from '@angular/core'; + `; + host.overwrite('main.ts', mainContentInput); host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const {compilerOptions} = host.readJson('tsconfig.json') as TsConfig; - const types = compilerOptions?.types; - expect(types).not.toContain(localizeType); - expect(types).toHaveSize(2); + expect(host.readText('main.ts')).toBe(mainContentInput); }); - it(`should not add '@angular/localize' in 'types' tsconfigs referenced in non official builders`, async () => { const tsConfig = JSON.stringify({