Skip to content

Commit

Permalink
fix(localize): update ng add schematic to support Angular CLI version 15
Browse files Browse the repository at this point in the history
Prior to this, the `@angular/localize/init` was added as a polyfill which caused the `@angular/localize` types not to be included in the TypeScript program which caused errors such as the below:

```
Error: src/app/app.component.ts:9:11 - error TS2304: Cannot find name '$localize'.
```

With the recent changes in the CLI (angular/angular-cli#24032), adding `@angular/localize/init` as polyfil or in the `main.server.ts` is no longer necessary. Instead we add this as a TypeScript type. When users are running in JIT mode, we add `@angular/localize/init` as an additional entrypoint.

This change also exposes the `$localize` method as a global when importing `@angular/localize`.

Closes #47677
  • Loading branch information
alan-agius4 committed Oct 13, 2022
1 parent 3a9c452 commit f0f0b38
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 327 deletions.
97 changes: 0 additions & 97 deletions packages/localize/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,100 +11,3 @@ export {$localize, LocalizeFn, TranslateFn};

// 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`
* ```
*
* **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;
}
98 changes: 98 additions & 0 deletions packages/localize/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,107 @@
*/

// 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;
}
1 change: 1 addition & 0 deletions packages/localize/schematics/ng-add/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ts_library(
deps = [
":ng-add",
"@npm//@angular-devkit/schematics",
"@npm//typescript",
],
)

Expand Down
6 changes: 3 additions & 3 deletions packages/localize/schematics/ng-add/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

This schematic will be executed when an Angular CLI user runs `ng add @angular/localize`.

It will search their `angular.json` file, and find polyfills and main files for server builders.
Then it will add the `@angular/localize/init` polyfill that `@angular/localize` needs to work.
It will search their `angular.json` file, and add `types: ["@angular/localize"]` in the TypeScript
configuration files of the project.

If the user specifies that they want to use `$localize` at runtime then the dependency will be
added to the `depdendencies` section of `package.json` rather than in the `devDependencies` which
is the default.
is the default.
117 changes: 48 additions & 69 deletions packages/localize/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,105 +5,85 @@
* 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
*
* @fileoverview Schematics for ng-new project that builds with Bazel.
* @fileoverview Schematics for `ng add @angular/localize` schematic.
*/

import {tags} from '@angular-devkit/core';
import {chain, noop, Rule, SchematicContext, SchematicsException, Tree,} from '@angular-devkit/schematics';
import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks';
import {addPackageJsonDependency, NodeDependencyType, removePackageJsonDependency,} from '@schematics/angular/utility/dependencies';
import {allTargetOptions, getWorkspace, updateWorkspace,} from '@schematics/angular/utility/workspace';
import {JSONFile, JSONPath} from '@schematics/angular/utility/json-file';
import {getWorkspace} from '@schematics/angular/utility/workspace';
import {Builders} from '@schematics/angular/utility/workspace-models';

import {Schema} from './schema';

export const localizePolyfill = `@angular/localize/init`;
const localizeType = `@angular/localize`;

function prependToMainFiles(projectName: string): Rule {
function addTypeScriptConfigTypes(projectName: string): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const project = workspace.projects.get(projectName);
if (!project) {
throw new SchematicsException(`Invalid project name (${projectName})`);
throw new SchematicsException(`Invalid project name '${projectName}'.`);
}

const fileList = new Set<string>();
// We add the root workspace tsconfig for better IDE support.
const tsConfigFiles = new Set<string>(['tsconfig.json']);
for (const target of project.targets.values()) {
if (target.builder !== Builders.Server) {
continue;
}

for (const [, options] of allTargetOptions(target)) {
const value = options['main'];
if (typeof value === 'string') {
fileList.add(value);
}
switch (target.builder) {
case Builders.Karma:
case Builders.Server:
case Builders.Browser:
const value = target.options?.['tsConfig'];
if (typeof value === 'string') {
tsConfigFiles.add(value);
}

break;
}
}

for (const path of fileList) {
const content = host.readText(path);
if (content.includes(localizePolyfill)) {
// If the file already contains the polyfill (or variations), ignore it too.
const typesJsonPath: JSONPath = ['compilerOptions', 'types'];
for (const path of tsConfigFiles) {
if (!host.exists(path)) {
continue;
}

// Add string at the start of the file.
const recorder = host.beginUpdate(path);

const localizeStr =
tags.stripIndents`/***************************************************************************************************
* Load \`$localize\` onto the global scope - used if i18n tags appear in Angular templates.
*/
import '${localizePolyfill}';
`;
recorder.insertLeft(0, localizeStr);
host.commitUpdate(recorder);
}
};
}

function addToPolyfillsOption(projectName: string): Rule {
return updateWorkspace((workspace) => {
const project = workspace.projects.get(projectName);
if (!project) {
throw new SchematicsException(`Invalid project name (${projectName})`);
}
const json = new JSONFile(host, path);
const types = json.get(typesJsonPath) ?? [];
if (!Array.isArray(types)) {
throw new SchematicsException(`TypeScript configuration file '${
path}' has an invalid 'types' property. It must be an array.`);
}

for (const target of project.targets.values()) {
if (target.builder !== Builders.Browser && target.builder !== Builders.Karma) {
const hasLocalizeType =
types.some((t) => t === localizeType || t === '@angular/localize/init');
if (hasLocalizeType) {
// Skip has already localize type.
continue;
}

target.options ??= {};
target.options['polyfills'] ??= [localizePolyfill];

for (const [, options] of allTargetOptions(target)) {
// Convert polyfills option to array.
const polyfillsValue = typeof options['polyfills'] === 'string' ? [options['polyfills']] :
options['polyfills'];
if (Array.isArray(polyfillsValue) && !polyfillsValue.includes(localizePolyfill)) {
options['polyfills'] = [...polyfillsValue, localizePolyfill];
}
}
json.modify(typesJsonPath, [...types, localizeType]);
}
});
};
}

function moveToDependencies(host: Tree, context: SchematicContext): void {
if (host.exists('package.json')) {
// Remove the previous dependency and add in a new one under the desired type.
removePackageJsonDependency(host, '@angular/localize');
addPackageJsonDependency(host, {
name: '@angular/localize',
type: NodeDependencyType.Default,
version: `~0.0.0-PLACEHOLDER`,
});

// Add a task to run the package manager. This is necessary because we updated
// "package.json" and we want lock files to reflect this.
context.addTask(new NodePackageInstallTask());
if (!host.exists('package.json')) {
return;
}

// Remove the previous dependency and add in a new one under the desired type.
removePackageJsonDependency(host, '@angular/localize');
addPackageJsonDependency(host, {
name: '@angular/localize',
type: NodeDependencyType.Default,
version: `~0.0.0-PLACEHOLDER`,
});

// Add a task to run the package manager. This is necessary because we updated
// "package.json" and we want lock files to reflect this.
context.addTask(new NodePackageInstallTask());
}

export default function(options: Schema): Rule {
Expand All @@ -117,8 +97,7 @@ export default function(options: Schema): Rule {
}

return chain([
prependToMainFiles(projectName),
addToPolyfillsOption(projectName),
addTypeScriptConfigTypes(projectName),
// If `$localize` will be used at runtime then must install `@angular/localize`
// into `dependencies`, rather than the default of `devDependencies`.
options.useAtRuntime ? moveToDependencies : noop(),
Expand Down

0 comments on commit f0f0b38

Please sign in to comment.