Skip to content

Commit 6e0e14e

Browse files
clydindgp1130
authored andcommitted
fix(@angular-devkit/build-angular): augment base HREF when localizing
All locale i18n options now support an object form which allows a base HREF to be defined for the locale. Each locale can now optionally define a custom base HREF that will be combined with the base HREF defined for the build configuration. By default if the shorthand form for the locale is used or the field is not present in the longhand form, the locale code will be used as the base HREF. To disable automatic augmentation a base HREF value of an empty string (`""`) can be used. This will prevent anything from being added to the existing base HREF. For common scenarios, the shorthand form will result in the preferred and recommended outcome of each built locale variant of the application containing a defined base HREF containing the locale code. (cherry picked from commit c37eaee)
1 parent 9590a5b commit 6e0e14e

File tree

10 files changed

+296
-40
lines changed

10 files changed

+296
-40
lines changed

packages/angular/cli/lib/config/schema.json

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,17 +376,57 @@
376376
"type": "object",
377377
"properties": {
378378
"sourceLocale": {
379-
"type": "string",
380-
"description": "Specifies the source language of the application.",
381-
"default": "en-US"
379+
"oneOf": [
380+
{
381+
"type": "string",
382+
"description": "Specifies the source locale of the application.",
383+
"default": "en-US",
384+
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
385+
},
386+
{
387+
"type": "object",
388+
"description": "Localization options to use for the source locale",
389+
"properties": {
390+
"code": {
391+
"type": "string",
392+
"description": "Specifies the locale code of the source locale",
393+
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
394+
},
395+
"baseHref": {
396+
"type": "string",
397+
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
398+
}
399+
},
400+
"additionalProperties": false
401+
}
402+
]
382403
},
383404
"locales": {
384405
"type": "object",
385406
"additionalProperties": false,
386407
"patternProperties": {
387408
"^[a-z]{2}(-[a-zA-Z]{2,})?$": {
388-
"type": "string",
389-
"description": "Localization file to use for i18n"
409+
"oneOf": [
410+
{
411+
"type": "string",
412+
"description": "Localization file to use for i18n"
413+
},
414+
{
415+
"type": "object",
416+
"description": "Localization options to use for the locale",
417+
"properties": {
418+
"translation": {
419+
"type": "string",
420+
"description": "Localization file to use for i18n"
421+
},
422+
"baseHref": {
423+
"type": "string",
424+
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
425+
}
426+
},
427+
"additionalProperties": false
428+
}
429+
]
390430
}
391431
}
392432
}

packages/angular_devkit/build_angular/src/browser/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,16 @@ export function buildWebpackBrowser(
673673

674674
if (options.index) {
675675
for (const [locale, outputPath] of outputPaths.entries()) {
676+
let localeBaseHref;
677+
if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
678+
localeBaseHref = path.posix.join(
679+
options.baseHref || '',
680+
i18n.locales[locale].baseHref === undefined
681+
? `/${locale}/`
682+
: i18n.locales[locale].baseHref,
683+
);
684+
}
685+
676686
try {
677687
await generateIndex(
678688
outputPath,
@@ -684,6 +694,7 @@ export function buildWebpackBrowser(
684694
transforms.indexHtml,
685695
// i18nLocale is used when Ivy is disabled
686696
locale || options.i18nLocale,
697+
localeBaseHref || options.baseHref,
687698
);
688699
} catch (err) {
689700
return { success: false, error: mapErrorToMessage(err) };
@@ -734,6 +745,7 @@ function generateIndex(
734745
moduleFiles: EmittedFiles[] | undefined,
735746
transformer?: IndexHtmlTransform,
736747
locale?: string,
748+
baseHref?: string,
737749
): Promise<void> {
738750
const host = new NodeJsSyncHost();
739751

@@ -744,7 +756,7 @@ function generateIndex(
744756
files,
745757
noModuleFiles,
746758
moduleFiles,
747-
baseHref: options.baseHref,
759+
baseHref,
748760
deployUrl: options.deployUrl,
749761
sri: options.subresourceIntegrity,
750762
scripts: options.scripts,

packages/angular_devkit/build_angular/src/utils/i18n-options.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ export interface I18nOptions {
2121
sourceLocale: string;
2222
locales: Record<
2323
string,
24-
{ file: string; format?: string; translation?: unknown; dataPath?: string, integrity?: string }
24+
{
25+
file: string;
26+
format?: string;
27+
translation?: unknown;
28+
dataPath?: string;
29+
integrity?: string;
30+
baseHref?: string;
31+
}
2532
>;
2633
flatOutput?: boolean;
2734
readonly shouldInline: boolean;
@@ -32,49 +39,79 @@ export function createI18nOptions(
3239
metadata: json.JsonObject,
3340
inline?: boolean | string[],
3441
): I18nOptions {
35-
if (
36-
metadata.i18n !== undefined &&
37-
(typeof metadata.i18n !== 'object' || !metadata.i18n || Array.isArray(metadata.i18n))
38-
) {
42+
if (metadata.i18n !== undefined && !json.isJsonObject(metadata.i18n)) {
3943
throw new Error('Project i18n field is malformed. Expected an object.');
4044
}
4145
metadata = metadata.i18n || {};
4246

43-
if (metadata.sourceLocale !== undefined && typeof metadata.sourceLocale !== 'string') {
44-
throw new Error('Project i18n sourceLocale field is malformed. Expected a string.');
45-
}
46-
4747
const i18n: I18nOptions = {
4848
inlineLocales: new Set<string>(),
4949
// en-US is the default locale added to Angular applications (https://angular.io/guide/i18n#i18n-pipes)
50-
sourceLocale: metadata.sourceLocale || 'en-US',
50+
sourceLocale: 'en-US',
5151
locales: {},
5252
get shouldInline() {
5353
return this.inlineLocales.size > 0;
5454
},
5555
};
5656

57-
if (
58-
metadata.locales !== undefined &&
59-
(!metadata.locales || typeof metadata.locales !== 'object' || Array.isArray(metadata.locales))
60-
) {
57+
let rawSourceLocale;
58+
let rawSourceLocaleBaseHref;
59+
if (json.isJsonObject(metadata.sourceLocale)) {
60+
rawSourceLocale = metadata.sourceLocale.code;
61+
if (metadata.sourceLocale.baseHref !== undefined && typeof metadata.sourceLocale.baseHref !== 'string') {
62+
throw new Error('Project i18n sourceLocale baseHref field is malformed. Expected a string.');
63+
}
64+
rawSourceLocaleBaseHref = metadata.sourceLocale.baseHref;
65+
} else {
66+
rawSourceLocale = metadata.sourceLocale;
67+
}
68+
69+
if (rawSourceLocale !== undefined) {
70+
if (typeof rawSourceLocale !== 'string') {
71+
throw new Error('Project i18n sourceLocale field is malformed. Expected a string.');
72+
}
73+
74+
i18n.sourceLocale = rawSourceLocale;
75+
}
76+
77+
i18n.locales[i18n.sourceLocale] = {
78+
file: '',
79+
baseHref: rawSourceLocaleBaseHref,
80+
};
81+
82+
if (metadata.locales !== undefined && !json.isJsonObject(metadata.locales)) {
6183
throw new Error('Project i18n locales field is malformed. Expected an object.');
6284
} else if (metadata.locales) {
63-
for (const [locale, translationFile] of Object.entries(metadata.locales)) {
64-
if (typeof translationFile !== 'string') {
85+
for (const [locale, options] of Object.entries(metadata.locales)) {
86+
let translationFile;
87+
let baseHref;
88+
if (json.isJsonObject(options)) {
89+
if (typeof options.translation !== 'string') {
90+
throw new Error(
91+
`Project i18n locales translation field value for '${locale}' is malformed. Expected a string.`,
92+
);
93+
}
94+
translationFile = options.translation;
95+
if (typeof options.baseHref === 'string') {
96+
baseHref = options.baseHref;
97+
}
98+
} else if (typeof options !== 'string') {
6599
throw new Error(
66-
`Project i18n locales field value for '${locale}' is malformed. Expected a string.`,
100+
`Project i18n locales field value for '${locale}' is malformed. Expected a string or object.`,
67101
);
102+
} else {
103+
translationFile = options;
68104
}
69105

70106
if (locale === i18n.sourceLocale) {
71107
throw new Error(
72-
`An i18n locale identifier ('${locale}') cannot both be a source locale and provide a translation.`,
108+
`An i18n locale ('${locale}') cannot both be a source locale and provide a translation.`,
73109
);
74110
}
75111

76112
i18n.locales[locale] = {
77113
file: translationFile,
114+
baseHref,
78115
};
79116
}
80117
}
@@ -252,11 +289,12 @@ function mergeDeprecatedI18nOptions(
252289
i18n.inlineLocales.add(i18nLocale);
253290

254291
if (i18nFile !== undefined) {
255-
i18n.locales[i18nLocale] = { file: i18nFile };
292+
i18n.locales[i18nLocale] = { file: i18nFile, baseHref: '' };
256293
} else {
257294
// If no file, treat the locale as the source locale
258295
// This mimics deprecated behavior
259296
i18n.sourceLocale = i18nLocale;
297+
i18n.locales[i18nLocale] = { file: '', baseHref: '' };
260298
}
261299

262300
i18n.flatOutput = true;

packages/angular_devkit/core/src/experimental/workspace/workspace-schema.json

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,57 @@
131131
"type": "object",
132132
"properties": {
133133
"sourceLocale": {
134-
"type": "string",
135-
"description": "Specifies the source language of the application.",
136-
"default": "en-US"
134+
"oneOf": [
135+
{
136+
"type": "string",
137+
"description": "Specifies the source locale of the application.",
138+
"default": "en-US",
139+
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
140+
},
141+
{
142+
"type": "object",
143+
"description": "Localization options to use for the source locale",
144+
"properties": {
145+
"code": {
146+
"type": "string",
147+
"description": "Specifies the locale code of the source locale",
148+
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
149+
},
150+
"baseHref": {
151+
"type": "string",
152+
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
153+
}
154+
},
155+
"additionalProperties": false
156+
}
157+
]
137158
},
138159
"locales": {
139160
"type": "object",
140161
"additionalProperties": false,
141162
"patternProperties": {
142163
"^[a-z]{2}(-[a-zA-Z]{2,})?$": {
143-
"type": "string",
144-
"description": "Localization file to use for i18n."
164+
"oneOf": [
165+
{
166+
"type": "string",
167+
"description": "Localization file to use for i18n"
168+
},
169+
{
170+
"type": "object",
171+
"description": "Localization options to use for the locale",
172+
"properties": {
173+
"translation": {
174+
"type": "string",
175+
"description": "Localization file to use for i18n"
176+
},
177+
"baseHref": {
178+
"type": "string",
179+
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
180+
}
181+
},
182+
"additionalProperties": false
183+
}
184+
]
145185
}
146186
}
147187
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 { expectFileToMatch } from '../../utils/fs';
9+
import { ng } from '../../utils/process';
10+
import { updateJsonFile } from '../../utils/project';
11+
import { externalServer, langTranslations, setupI18nConfig } from './legacy';
12+
13+
const baseHrefs = {
14+
'en-US': '/en/',
15+
fr: '/fr-FR/',
16+
de: '',
17+
};
18+
19+
export default async function() {
20+
// Setup i18n tests and config.
21+
await setupI18nConfig(true);
22+
23+
// Update angular.json
24+
await updateJsonFile('angular.json', workspaceJson => {
25+
const appProject = workspaceJson.projects['test-project'];
26+
// tslint:disable-next-line: no-any
27+
const i18n: Record<string, any> = appProject.i18n;
28+
29+
i18n.sourceLocale = {
30+
baseHref: baseHrefs['en-US'],
31+
};
32+
33+
i18n.locales['fr'] = {
34+
translation: i18n.locales['fr'],
35+
baseHref: baseHrefs['fr'],
36+
};
37+
38+
i18n.locales['de'] = {
39+
translation: i18n.locales['de'],
40+
baseHref: baseHrefs['de'],
41+
};
42+
});
43+
44+
// Build each locale and verify the output.
45+
await ng('build');
46+
for (const { lang, outputPath } of langTranslations) {
47+
if (baseHrefs[lang] === undefined) {
48+
throw new Error('Invalid E2E test setup: unexpected locale ' + lang);
49+
}
50+
51+
// Verify the HTML lang attribute is present
52+
await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`);
53+
54+
// Verify the HTML base HREF attribute is present
55+
await expectFileToMatch(`${outputPath}/index.html`, `href="${baseHrefs[lang] || '/'}"`);
56+
57+
// Execute Application E2E tests with dev server
58+
await ng('e2e', `--configuration=${lang}`, '--port=0');
59+
60+
// Execute Application E2E tests for a production build without dev server
61+
const server = externalServer(outputPath, baseHrefs[lang] || '/');
62+
try {
63+
await ng(
64+
'e2e',
65+
`--configuration=${lang}`,
66+
'--devServerTarget=',
67+
`--baseUrl=http://localhost:4200${baseHrefs[lang] || '/'}`,
68+
);
69+
} finally {
70+
server.close();
71+
}
72+
}
73+
74+
// Update angular.json
75+
await updateJsonFile('angular.json', workspaceJson => {
76+
const appArchitect = workspaceJson.projects['test-project'].architect;
77+
78+
appArchitect['build'].options.baseHref = '/test/';
79+
});
80+
81+
// Build each locale and verify the output.
82+
await ng('build');
83+
for (const { lang, outputPath } of langTranslations) {
84+
// Verify the HTML base HREF attribute is present
85+
await expectFileToMatch(`${outputPath}/index.html`, `href="/test${baseHrefs[lang] || '/'}"`);
86+
87+
// Execute Application E2E tests with dev server
88+
await ng('e2e', `--configuration=${lang}`, '--port=0');
89+
90+
// Execute Application E2E tests for a production build without dev server
91+
const server = externalServer(outputPath, '/test' + (baseHrefs[lang] || '/'));
92+
try {
93+
await ng(
94+
'e2e',
95+
`--configuration=${lang}`,
96+
'--devServerTarget=',
97+
`--baseUrl=http://localhost:4200/test${baseHrefs[lang] || '/'}`,
98+
);
99+
} finally {
100+
server.close();
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)