Skip to content

Commit c257515

Browse files
clydinvikerman
authored andcommitted
refactor(@schematics/angular): separate i18n workspace 9.0 migration
1 parent 17e237b commit c257515

File tree

5 files changed

+510
-418
lines changed

5 files changed

+510
-418
lines changed

packages/schematics/angular/migrations/update-9/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import { updateNGSWConfig } from './ngsw-config';
1414
import { removeTsickle } from './remove-tsickle';
1515
import { updateApplicationTsConfigs } from './update-app-tsconfigs';
1616
import { updateDependencies } from './update-dependencies';
17+
import { updateI18nConfig } from './update-i18n';
1718
import { updateServerMainFile } from './update-server-main-file';
1819
import { updateWorkspaceConfig } from './update-workspace-config';
1920

2021
export default function(): Rule {
2122
return () => {
2223
return chain([
2324
updateWorkspaceConfig(),
25+
updateI18nConfig(),
2426
updateLibraries(),
2527
updateNGSWConfig(),
2628
updateApplicationTsConfigs(),
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { JsonAstObject, logging } from '@angular-devkit/core';
9+
import { Rule, Tree, UpdateRecorder } from '@angular-devkit/schematics';
10+
import { posix } from 'path';
11+
import { getWorkspacePath } from '../../utility/config';
12+
import { NodeDependencyType, addPackageJsonDependency, getPackageJsonDependency } from '../../utility/dependencies';
13+
import {
14+
findPropertyInAstObject,
15+
insertPropertyInAstObjectInOrder,
16+
removePropertyInAstObject,
17+
} from '../../utility/json-utils';
18+
import { latestVersions } from '../../utility/latest-versions';
19+
import { Builders } from '../../utility/workspace-models';
20+
import { getAllOptions, getProjectTarget, getTargets, getWorkspace } from './utils';
21+
22+
export function updateI18nConfig(): Rule {
23+
return (tree, context) => {
24+
const workspacePath = getWorkspacePath(tree);
25+
const workspace = getWorkspace(tree);
26+
const recorder = tree.beginUpdate(workspacePath);
27+
28+
for (const { target } of getTargets(workspace, 'build', Builders.Browser)) {
29+
addBuilderI18NOptions(recorder, target, context.logger);
30+
}
31+
32+
for (const { target } of getTargets(workspace, 'test', Builders.Karma)) {
33+
addBuilderI18NOptions(recorder, target, context.logger);
34+
}
35+
36+
for (const { target, project } of getTargets(workspace, 'extract-i18n', Builders.ExtractI18n)) {
37+
addProjectI18NOptions(recorder, tree, target, project);
38+
removeExtracti18nDeprecatedOptions(recorder, target);
39+
}
40+
41+
tree.commitUpdate(recorder);
42+
43+
return tree;
44+
};
45+
}
46+
47+
function addProjectI18NOptions(
48+
recorder: UpdateRecorder,
49+
tree: Tree,
50+
builderConfig: JsonAstObject,
51+
projectConfig: JsonAstObject,
52+
) {
53+
const browserConfig = getProjectTarget(projectConfig, 'build', Builders.Browser);
54+
if (!browserConfig || browserConfig.kind !== 'object') {
55+
return;
56+
}
57+
58+
// browser builder options
59+
let locales: Record<string, string | { translation: string; baseHref: string }> | undefined;
60+
const options = getAllOptions(browserConfig);
61+
for (const option of options) {
62+
const localeId = findPropertyInAstObject(option, 'i18nLocale');
63+
if (!localeId || localeId.kind !== 'string') {
64+
continue;
65+
}
66+
67+
const localeFile = findPropertyInAstObject(option, 'i18nFile');
68+
if (!localeFile || localeFile.kind !== 'string') {
69+
continue;
70+
}
71+
72+
const localIdValue = localeId.value;
73+
const localeFileValue = localeFile.value;
74+
75+
const baseHref = findPropertyInAstObject(option, 'baseHref');
76+
let baseHrefValue;
77+
if (baseHref) {
78+
if (baseHref.kind === 'string' && baseHref.value !== `/${localIdValue}/`) {
79+
baseHrefValue = baseHref.value;
80+
}
81+
} else {
82+
// If the configuration does not contain a baseHref, ensure the main option value is used.
83+
baseHrefValue = '';
84+
}
85+
86+
if (!locales) {
87+
locales = {
88+
[localIdValue]:
89+
baseHrefValue === undefined
90+
? localeFileValue
91+
: {
92+
translation: localeFileValue,
93+
baseHref: baseHrefValue,
94+
},
95+
};
96+
} else {
97+
locales[localIdValue] =
98+
baseHrefValue === undefined
99+
? localeFileValue
100+
: {
101+
translation: localeFileValue,
102+
baseHref: baseHrefValue,
103+
};
104+
}
105+
}
106+
107+
if (locales) {
108+
// Get sourceLocale from extract-i18n builder
109+
const i18nOptions = getAllOptions(builderConfig);
110+
const sourceLocale = i18nOptions
111+
.map(o => {
112+
const sourceLocale = findPropertyInAstObject(o, 'i18nLocale');
113+
114+
return sourceLocale && sourceLocale.value;
115+
})
116+
.find(x => !!x);
117+
118+
// Add i18n project configuration
119+
insertPropertyInAstObjectInOrder(recorder, projectConfig, 'i18n', {
120+
locales,
121+
// tslint:disable-next-line: no-any
122+
sourceLocale: sourceLocale as any,
123+
}, 6);
124+
125+
// Add @angular/localize if not already a dependency
126+
if (!getPackageJsonDependency(tree, '@angular/localize')) {
127+
addPackageJsonDependency(tree, {
128+
name: '@angular/localize',
129+
version: latestVersions.Angular,
130+
type: NodeDependencyType.Default,
131+
});
132+
}
133+
}
134+
}
135+
136+
function addBuilderI18NOptions(
137+
recorder: UpdateRecorder,
138+
builderConfig: JsonAstObject,
139+
logger: logging.LoggerApi,
140+
) {
141+
const options = getAllOptions(builderConfig);
142+
const mainOptions = findPropertyInAstObject(builderConfig, 'options');
143+
const mainBaseHref =
144+
mainOptions &&
145+
mainOptions.kind === 'object' &&
146+
findPropertyInAstObject(mainOptions, 'baseHref');
147+
const hasMainBaseHref =
148+
!!mainBaseHref && mainBaseHref.kind === 'string' && mainBaseHref.value !== '/';
149+
150+
for (const option of options) {
151+
const localeId = findPropertyInAstObject(option, 'i18nLocale');
152+
const i18nFile = findPropertyInAstObject(option, 'i18nFile');
153+
154+
// The format is always auto-detected now
155+
const i18nFormat = findPropertyInAstObject(option, 'i18nFormat');
156+
if (i18nFormat) {
157+
removePropertyInAstObject(recorder, option, 'i18nFormat');
158+
}
159+
160+
const outputPath = findPropertyInAstObject(option, 'outputPath');
161+
if (
162+
localeId &&
163+
localeId.kind === 'string' &&
164+
i18nFile &&
165+
outputPath &&
166+
outputPath.kind === 'string'
167+
) {
168+
// This first block was intended to remove the redundant output path field
169+
// but due to defects in the recorder, removing the option will cause malformed json
170+
// if (
171+
// mainOutputPathValue &&
172+
// outputPath.value.match(
173+
// new RegExp(`[/\\\\]?${mainOutputPathValue}[/\\\\]${localeId.value}[/\\\\]?$`),
174+
// )
175+
// ) {
176+
// removePropertyInAstObject(recorder, option, 'outputPath');
177+
// } else
178+
if (outputPath.value.match(new RegExp(`[/\\\\]${localeId.value}[/\\\\]?$`))) {
179+
const newOutputPath = outputPath.value.replace(
180+
new RegExp(`[/\\\\]${localeId.value}[/\\\\]?$`),
181+
'',
182+
);
183+
const { start, end } = outputPath;
184+
recorder.remove(start.offset, end.offset - start.offset);
185+
recorder.insertLeft(start.offset, `"${newOutputPath}"`);
186+
} else {
187+
logger.warn(
188+
`Output path value "${outputPath.value}" for locale "${localeId.value}" is not supported with the new localization system. ` +
189+
`With the current value, the localized output would be written to "${posix.join(
190+
outputPath.value,
191+
localeId.value,
192+
)}". ` +
193+
`Keeping existing options for the target configuration of locale "${localeId.value}".`,
194+
);
195+
196+
continue;
197+
}
198+
}
199+
200+
if (localeId && localeId.kind === 'string') {
201+
// add new localize option
202+
insertPropertyInAstObjectInOrder(recorder, option, 'localize', [localeId.value], 12);
203+
removePropertyInAstObject(recorder, option, 'i18nLocale');
204+
}
205+
206+
if (i18nFile) {
207+
removePropertyInAstObject(recorder, option, 'i18nFile');
208+
}
209+
210+
// localize base HREF values are controlled by the i18n configuration
211+
const baseHref = findPropertyInAstObject(option, 'baseHref');
212+
if (localeId && i18nFile && baseHref) {
213+
// if the main option set has a non-default base href,
214+
// ensure that the augmented base href has the correct base value
215+
if (hasMainBaseHref) {
216+
const { start, end } = baseHref;
217+
recorder.remove(start.offset, end.offset - start.offset);
218+
recorder.insertLeft(start.offset, `"/"`);
219+
} else {
220+
removePropertyInAstObject(recorder, option, 'baseHref');
221+
}
222+
}
223+
}
224+
}
225+
226+
function removeExtracti18nDeprecatedOptions(recorder: UpdateRecorder, builderConfig: JsonAstObject) {
227+
const options = getAllOptions(builderConfig);
228+
229+
for (const option of options) {
230+
// deprecated options
231+
removePropertyInAstObject(recorder, option, 'i18nLocale');
232+
const i18nFormat = option.properties.find(({ key }) => key.value === 'i18nFormat');
233+
234+
if (i18nFormat) {
235+
// i18nFormat has been changed to format
236+
const key = i18nFormat.key;
237+
const offset = key.start.offset + 1;
238+
recorder.remove(offset, key.value.length);
239+
recorder.insertLeft(offset, 'format');
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)