Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions packages/angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,17 +376,57 @@
"type": "object",
"properties": {
"sourceLocale": {
"type": "string",
"description": "Specifies the source language of the application.",
"default": "en-US"
"oneOf": [
{
"type": "string",
"description": "Specifies the source locale of the application.",
"default": "en-US",
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
},
{
"type": "object",
"description": "Localization options to use for the source locale",
"properties": {
"code": {
"type": "string",
"description": "Specifies the locale code of the source locale",
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
},
"locales": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z]{2}(-[a-zA-Z]{2,})?$": {
"type": "string",
"description": "Localization file to use for i18n"
"oneOf": [
{
"type": "string",
"description": "Localization file to use for i18n"
},
{
"type": "object",
"description": "Localization options to use for the locale",
"properties": {
"translation": {
"type": "string",
"description": "Localization file to use for i18n"
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion packages/angular_devkit/build_angular/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,16 @@ export function buildWebpackBrowser(

if (options.index) {
for (const [locale, outputPath] of outputPaths.entries()) {
let localeBaseHref;
if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
localeBaseHref = path.posix.join(
options.baseHref || '',
i18n.locales[locale].baseHref === undefined
? `/${locale}/`
: i18n.locales[locale].baseHref,
);
}

try {
await generateIndex(
outputPath,
Expand All @@ -684,6 +694,7 @@ export function buildWebpackBrowser(
transforms.indexHtml,
// i18nLocale is used when Ivy is disabled
locale || options.i18nLocale,
localeBaseHref || options.baseHref,
);
} catch (err) {
return { success: false, error: mapErrorToMessage(err) };
Expand Down Expand Up @@ -734,6 +745,7 @@ function generateIndex(
moduleFiles: EmittedFiles[] | undefined,
transformer?: IndexHtmlTransform,
locale?: string,
baseHref?: string,
): Promise<void> {
const host = new NodeJsSyncHost();

Expand All @@ -744,7 +756,7 @@ function generateIndex(
files,
noModuleFiles,
moduleFiles,
baseHref: options.baseHref,
baseHref,
deployUrl: options.deployUrl,
sri: options.subresourceIntegrity,
scripts: options.scripts,
Expand Down
76 changes: 57 additions & 19 deletions packages/angular_devkit/build_angular/src/utils/i18n-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ export interface I18nOptions {
sourceLocale: string;
locales: Record<
string,
{ file: string; format?: string; translation?: unknown; dataPath?: string, integrity?: string }
{
file: string;
format?: string;
translation?: unknown;
dataPath?: string;
integrity?: string;
baseHref?: string;
}
>;
flatOutput?: boolean;
readonly shouldInline: boolean;
Expand All @@ -32,49 +39,79 @@ export function createI18nOptions(
metadata: json.JsonObject,
inline?: boolean | string[],
): I18nOptions {
if (
metadata.i18n !== undefined &&
(typeof metadata.i18n !== 'object' || !metadata.i18n || Array.isArray(metadata.i18n))
) {
if (metadata.i18n !== undefined && !json.isJsonObject(metadata.i18n)) {
throw new Error('Project i18n field is malformed. Expected an object.');
}
metadata = metadata.i18n || {};

if (metadata.sourceLocale !== undefined && typeof metadata.sourceLocale !== 'string') {
throw new Error('Project i18n sourceLocale field is malformed. Expected a string.');
}

const i18n: I18nOptions = {
inlineLocales: new Set<string>(),
// en-US is the default locale added to Angular applications (https://angular.io/guide/i18n#i18n-pipes)
sourceLocale: metadata.sourceLocale || 'en-US',
sourceLocale: 'en-US',
locales: {},
get shouldInline() {
return this.inlineLocales.size > 0;
},
};

if (
metadata.locales !== undefined &&
(!metadata.locales || typeof metadata.locales !== 'object' || Array.isArray(metadata.locales))
) {
let rawSourceLocale;
let rawSourceLocaleBaseHref;
if (json.isJsonObject(metadata.sourceLocale)) {
rawSourceLocale = metadata.sourceLocale.code;
if (metadata.sourceLocale.baseHref !== undefined && typeof metadata.sourceLocale.baseHref !== 'string') {
throw new Error('Project i18n sourceLocale baseHref field is malformed. Expected a string.');
}
rawSourceLocaleBaseHref = metadata.sourceLocale.baseHref;
} else {
rawSourceLocale = metadata.sourceLocale;
}

if (rawSourceLocale !== undefined) {
if (typeof rawSourceLocale !== 'string') {
throw new Error('Project i18n sourceLocale field is malformed. Expected a string.');
}

i18n.sourceLocale = rawSourceLocale;
}

i18n.locales[i18n.sourceLocale] = {
file: '',
baseHref: rawSourceLocaleBaseHref,
};

if (metadata.locales !== undefined && !json.isJsonObject(metadata.locales)) {
throw new Error('Project i18n locales field is malformed. Expected an object.');
} else if (metadata.locales) {
for (const [locale, translationFile] of Object.entries(metadata.locales)) {
if (typeof translationFile !== 'string') {
for (const [locale, options] of Object.entries(metadata.locales)) {
let translationFile;
let baseHref;
if (json.isJsonObject(options)) {
if (typeof options.translation !== 'string') {
throw new Error(
`Project i18n locales translation field value for '${locale}' is malformed. Expected a string.`,
);
}
translationFile = options.translation;
if (typeof options.baseHref === 'string') {
baseHref = options.baseHref;
}
} else if (typeof options !== 'string') {
throw new Error(
`Project i18n locales field value for '${locale}' is malformed. Expected a string.`,
`Project i18n locales field value for '${locale}' is malformed. Expected a string or object.`,
);
} else {
translationFile = options;
}

if (locale === i18n.sourceLocale) {
throw new Error(
`An i18n locale identifier ('${locale}') cannot both be a source locale and provide a translation.`,
`An i18n locale ('${locale}') cannot both be a source locale and provide a translation.`,
);
}

i18n.locales[locale] = {
file: translationFile,
baseHref,
};
}
}
Expand Down Expand Up @@ -252,11 +289,12 @@ function mergeDeprecatedI18nOptions(
i18n.inlineLocales.add(i18nLocale);

if (i18nFile !== undefined) {
i18n.locales[i18nLocale] = { file: i18nFile };
i18n.locales[i18nLocale] = { file: i18nFile, baseHref: '' };
} else {
// If no file, treat the locale as the source locale
// This mimics deprecated behavior
i18n.sourceLocale = i18nLocale;
i18n.locales[i18nLocale] = { file: '', baseHref: '' };
}

i18n.flatOutput = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,57 @@
"type": "object",
"properties": {
"sourceLocale": {
"type": "string",
"description": "Specifies the source language of the application.",
"default": "en-US"
"oneOf": [
{
"type": "string",
"description": "Specifies the source locale of the application.",
"default": "en-US",
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
},
{
"type": "object",
"description": "Localization options to use for the source locale",
"properties": {
"code": {
"type": "string",
"description": "Specifies the locale code of the source locale",
"pattern": "^[a-z]{2}(-[a-zA-Z]{2,})?$"
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
},
"locales": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-z]{2}(-[a-zA-Z]{2,})?$": {
"type": "string",
"description": "Localization file to use for i18n."
"oneOf": [
{
"type": "string",
"description": "Localization file to use for i18n"
},
{
"type": "object",
"description": "Localization options to use for the locale",
"properties": {
"translation": {
"type": "string",
"description": "Localization file to use for i18n"
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function addProjectI18NOptions(
}

// browser builder options
let locales: Record<string, string> | undefined;
let locales: Record<string, string | { translation: string; baseHref: string }> | undefined;
const options = getAllOptions(browserConfig);
for (const option of options) {
const localeId = findPropertyInAstObject(option, 'i18nLocale');
Expand All @@ -87,12 +87,35 @@ function addProjectI18NOptions(
const localIdValue = localeId.value;
const localeFileValue = localeFile.value;

const baseHref = findPropertyInAstObject(option, 'baseHref');
let baseHrefValue;
if (baseHref) {
if (baseHref.kind === 'string' && baseHref.value !== `/${localIdValue}/`) {
baseHrefValue = baseHref.value;
}
} else {
// If the configuration does not contain a baseHref, ensure the main option value is used.
baseHrefValue = '';
}

if (!locales) {
locales = {
[localIdValue]: localeFileValue,
[localIdValue]:
baseHrefValue === undefined
? localeFileValue
: {
translation: localeFileValue,
baseHref: baseHrefValue,
},
};
} else {
locales[localIdValue] = localeFileValue;
locales[localIdValue] =
baseHrefValue === undefined
? localeFileValue
: {
translation: localeFileValue,
baseHref: baseHrefValue,
};
}
}

Expand Down Expand Up @@ -127,6 +150,13 @@ function addProjectI18NOptions(

function addBuilderI18NOptions(recorder: UpdateRecorder, builderConfig: JsonAstObject, projectConfig: JsonAstObject) {
const options = getAllOptions(builderConfig);
const mainOptions = findPropertyInAstObject(builderConfig, 'options');
const mainBaseHref =
mainOptions &&
mainOptions.kind === 'object' &&
findPropertyInAstObject(mainOptions, 'baseHref');
const hasMainBaseHref =
!!mainBaseHref && mainBaseHref.kind === 'string' && mainBaseHref.value !== '/';

for (const option of options) {
const localeId = findPropertyInAstObject(option, 'i18nLocale');
Expand All @@ -145,6 +175,18 @@ function addBuilderI18NOptions(recorder: UpdateRecorder, builderConfig: JsonAstO
if (i18nFormat) {
removePropertyInAstObject(recorder, option, 'i18nFormat');
}

// localize base HREF values are controlled by the i18n configuration
const baseHref = findPropertyInAstObject(option, 'baseHref');
if (localeId && i18nFile && baseHref) {
removePropertyInAstObject(recorder, option, 'baseHref');

// if the main option set has a non-default base href,
// ensure that the augmented base href has the correct base value
if (hasMainBaseHref) {
insertPropertyInAstObjectInOrder(recorder, option, 'baseHref', '/', 12);
}
}
}
}

Expand Down
Loading