From 750baf92d681faa7f6fe8d1a29aea53133415108 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 10 Oct 2019 19:07:26 +0200 Subject: [PATCH] feat(@schematics/angular): add migration to add new i18n options for Ivy This migration will update current projects by adding the `i18n` project level option and add `localize` option in the server and browser builder configurations when both `i18nLocale` and `i18nFile` are defined. --- .../update-9/update-workspace-config.ts | 74 ++++++++++++++++++- .../update-9/update-workspace-config_spec.ts | 71 ++++++++++++++++++ .../angular/migrations/update-9/utils.ts | 51 ++++++++----- 3 files changed, 176 insertions(+), 20 deletions(-) diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts index 30bfd3d6bb..f74d9730a6 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config.ts @@ -15,7 +15,7 @@ import { removePropertyInAstObject, } from '../../utility/json-utils'; import { Builders } from '../../utility/workspace-models'; -import { getAllOptions, getTargets, getWorkspace, isIvyEnabled } from './utils'; +import { getAllOptions, getProjectTarget, getTargets, getWorkspace, isIvyEnabled } from './utils'; export const ANY_COMPONENT_STYLE_BUDGET = { type: 'anyComponentStyle', @@ -33,6 +33,7 @@ export function updateWorkspaceConfig(): Rule { updateStyleOrScriptOption('scripts', recorder, target); addAnyComponentStyleBudget(recorder, target); updateAotOption(tree, recorder, target); + addBuilderI18NOptions(recorder, target); } for (const { target } of getTargets(workspace, 'test', Builders.Karma)) { @@ -42,6 +43,11 @@ export function updateWorkspaceConfig(): Rule { for (const { target } of getTargets(workspace, 'server', Builders.Server)) { updateOptimizationOption(recorder, target); + addBuilderI18NOptions(recorder, target); + } + + for (const { target, project } of getTargets(workspace, 'extract-i18n', Builders.ExtractI18n)) { + addProjectI18NOptions(recorder, target, project); } tree.commitUpdate(recorder); @@ -50,6 +56,72 @@ export function updateWorkspaceConfig(): Rule { }; } +function addProjectI18NOptions(recorder: UpdateRecorder, builderConfig: JsonAstObject, projectConfig: JsonAstObject) { + const browserConfig = getProjectTarget(projectConfig, 'build', Builders.Browser); + if (!browserConfig || browserConfig.kind !== 'object') { + return; + } + + // browser builder options + let locales: Record | undefined; + const options = getAllOptions(browserConfig); + for (const option of options) { + const localeId = findPropertyInAstObject(option, 'i18nLocale'); + if (!localeId || localeId.kind !== 'string') { + continue; + } + + const localeFile = findPropertyInAstObject(option, 'i18nFile'); + if (!localeFile || localeFile.kind !== 'string') { + continue; + } + + const localIdValue = localeId.value; + const localeFileValue = localeFile.value; + + if (!locales) { + locales = { + [localIdValue]: localeFileValue, + }; + } else { + locales[localIdValue] = localeFileValue; + } + } + + if (locales) { + // Get sourceLocale from extract-i18n builder + const i18nOptions = getAllOptions(builderConfig); + const sourceLocale = i18nOptions + .map(o => { + const sourceLocale = findPropertyInAstObject(o, 'i18nLocale'); + + return sourceLocale && sourceLocale.value; + }) + .find(x => !!x); + + // Add i18n project configuration + insertPropertyInAstObjectInOrder(recorder, projectConfig, 'i18n', { + locales, + // tslint:disable-next-line: no-any + sourceLocale: sourceLocale as any, + }, 6); + } +} + +function addBuilderI18NOptions(recorder: UpdateRecorder, builderConfig: JsonAstObject) { + const options = getAllOptions(builderConfig); + + for (const option of options) { + const localeId = findPropertyInAstObject(option, 'i18nLocale'); + if (!localeId || localeId.kind !== 'string') { + continue; + } + + // add new localize option + insertPropertyInAstObjectInOrder(recorder, option, 'localize', [localeId.value], 12); + } +} + function updateAotOption(tree: Tree, recorder: UpdateRecorder, builderConfig: JsonAstObject) { const options = findPropertyInAstObject(builderConfig, 'options'); if (!options || options.kind !== 'object') { diff --git a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts index a7a2c98a22..55d3261f78 100644 --- a/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts +++ b/packages/schematics/angular/migrations/update-9/update-workspace-config_spec.ts @@ -296,5 +296,76 @@ describe('Migration to version 9', () => { expect(config.production.optimization).toBe(true); }); }); + + describe('i18n configuration', () => { + function getI18NConfig(localId: string): object { + return { + outputPath: `dist/my-project-${localId}/`, + i18nFile: `src/locale/messages.${localId}.xlf`, + i18nFormat: 'xlf', + i18nLocale: localId, + }; + } + + describe('when i18n builder options are set', () => { + it(`should add 'localize' option in configuration`, async () => { + let config = getWorkspaceTargets(tree); + config.build.options.aot = false; + config.build.options = getI18NConfig('fr'); + config.build.configurations.de = getI18NConfig('de'); + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + config = getWorkspaceTargets(tree2).build; + expect(config.options.localize).toEqual(['fr']); + expect(config.configurations.de.localize).toEqual(['de']); + }); + + it(`should add i18n 'sourceLocale' project config when 'extract-i18n' 'i18nLocale' is defined`, async () => { + const config = getWorkspaceTargets(tree); + config.build.options.aot = false; + config.build.options = getI18NConfig('fr'); + config['extract-i18n'].options.i18nLocale = 'en-GB'; + config.build.configurations.de = getI18NConfig('de'); + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const projectConfig = JSON.parse(tree2.readContent(workspacePath)).projects['migration-test']; + expect(projectConfig.i18n.sourceLocale).toBe('en-GB'); + expect(projectConfig.i18n.locales).toBeDefined(); + }); + + it(`should add i18n 'locales' project config`, async () => { + const config = getWorkspaceTargets(tree); + config.build.options.aot = false; + config.build.options = getI18NConfig('fr'); + config.build.configurations.de = getI18NConfig('de'); + updateWorkspaceTargets(tree, config); + + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const projectConfig = JSON.parse(tree2.readContent(workspacePath)).projects['migration-test']; + expect(projectConfig.i18n.sourceLocale).toBeUndefined(); + expect(projectConfig.i18n.locales).toEqual({ + de: 'src/locale/messages.de.xlf', + fr: 'src/locale/messages.fr.xlf', + }); + }); + }); + + describe('when i18n builder options are not set', () => { + it(`should not add 'localize' option`, async () => { + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const config = getWorkspaceTargets(tree2).build; + expect(config.options.localize).toBeUndefined(); + expect(config.configurations.production.localize).toBeUndefined(); + }); + + it('should not add i18n project config', async () => { + const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise(); + const projectConfig = JSON.parse(tree2.readContent(workspacePath)).projects['migration-test']; + expect(projectConfig.i18n).toBeUndefined(); + }); + }); + }); }); }); diff --git a/packages/schematics/angular/migrations/update-9/utils.ts b/packages/schematics/angular/migrations/update-9/utils.ts index f847957468..074cbf4e67 100644 --- a/packages/schematics/angular/migrations/update-9/utils.ts +++ b/packages/schematics/angular/migrations/update-9/utils.ts @@ -12,7 +12,36 @@ import { getWorkspacePath } from '../../utility/config'; import { findPropertyInAstObject } from '../../utility/json-utils'; import { Builders, WorkspaceTargets } from '../../utility/workspace-models'; -/** Get all workspace targets which builder and target names matches the provided. */ +/** Get a project target which builder and target names matches the provided. */ +export function getProjectTarget( + project: JsonAstObject, + targetName: Exclude, + builderName: Builders, +): JsonAstObject | undefined { + const projectRoot = findPropertyInAstObject(project, 'root'); + if (!projectRoot || projectRoot.kind !== 'string') { + return undefined; + } + + const architect = findPropertyInAstObject(project, 'architect'); + if (!architect || architect.kind !== 'object') { + return undefined; + } + + const target = findPropertyInAstObject(architect, targetName); + if (!target || target.kind !== 'object') { + return undefined; + } + + const builder = findPropertyInAstObject(target, 'builder'); + // Projects who's build builder is @angular-devkit/build-ng-packagr + if (builder && builder.kind === 'string' && builder.value === builderName) { + return target; + } + + return undefined; +} + export function getTargets( workspace: JsonAstObject, targetName: Exclude, @@ -30,24 +59,8 @@ export function getTargets( continue; } - const projectRoot = findPropertyInAstObject(projectConfig, 'root'); - if (!projectRoot || projectRoot.kind !== 'string') { - continue; - } - - const architect = findPropertyInAstObject(projectConfig, 'architect'); - if (!architect || architect.kind !== 'object') { - continue; - } - - const target = findPropertyInAstObject(architect, targetName); - if (!target || target.kind !== 'object') { - continue; - } - - const builder = findPropertyInAstObject(target, 'builder'); - // Projects who's build builder is @angular-devkit/build-ng-packagr - if (builder && builder.kind === 'string' && builder.value === builderName) { + const target = getProjectTarget(projectConfig, targetName, builderName); + if (target) { targets.push({ target, project: projectConfig }); } }