Skip to content

fix(@angular-devkit/build-angular): insert locale data when localizing #15989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 7, 2019
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
56 changes: 48 additions & 8 deletions packages/angular_devkit/build_angular/src/utils/i18n-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import { createTranslationLoader } from './load-translations';
export interface I18nOptions {
inlineLocales: Set<string>;
sourceLocale: string;
locales: Record<string, { file: string; format?: string; translation?: unknown }>;
locales: Record<
string,
{ file: string; format?: string; translation?: unknown; dataPath?: string }
>;
flatOutput?: boolean;
readonly shouldInline: boolean;
}
Expand Down Expand Up @@ -127,9 +130,16 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
}

if (i18n.inlineLocales.size > 0) {
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
const localeDataBasePath = findLocaleDataBasePath(projectRoot);
if (!localeDataBasePath) {
throw new Error(
`Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`,
);
}

// Load locales
const loader = await createTranslationLoader();
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
const usedFormats = new Set<string>();
for (const [locale, desc] of Object.entries(i18n.locales)) {
if (i18n.inlineLocales.has(locale) && desc.file) {
Expand All @@ -145,19 +155,22 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server

desc.format = result.format;
desc.translation = result.translation;

const localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
if (!localeDataPath) {
context.logger.warn(
`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
);
} else {
desc.dataPath = localeDataPath;
}
}
}

// Legacy message id's require the format of the translations
if (usedFormats.size > 0) {
buildOptions.i18nFormat = [...usedFormats][0];
}

// If only one locale is specified set the deprecated option to enable the webpack plugin
// transform to register the locale directly in the output bundle.
if (i18n.inlineLocales.size === 1) {
buildOptions.i18nLocale = [...i18n.inlineLocales][0];
}
}

// If inlining store the output in a temporary location to facilitate post-processing
Expand Down Expand Up @@ -202,3 +215,30 @@ function mergeDeprecatedI18nOptions(

return i18n;
}

function findLocaleDataBasePath(projectRoot: string): string | null {
try {
const commonPath = path.dirname(
require.resolve('@angular/common/package.json', { paths: [projectRoot] }),
);
const localesPath = path.join(commonPath, 'locales/global');

if (!fs.existsSync(localesPath)) {
return null;
}

return localesPath;
} catch {
return null;
}
}

function findLocaleDataPath(locale: string, basePath: string): string | null {
const localeDataPath = path.join(basePath, locale + '.js');

if (!fs.existsSync(localeDataPath)) {
return null;
}

return localeDataPath;
}
25 changes: 25 additions & 0 deletions packages/angular_devkit/build_angular/src/utils/process-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,14 @@ export async function inlineLocales(options: InlineOptions) {
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});`;
contentClone = content.clone();
content.prepend(setLocaleText);

// If locale data is provided, load it and prepend to file
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
if (localeDataPath) {
const localDataContent = loadLocaleData(localeDataPath, true);
// The semicolon ensures that there is no syntax error between statements
content.prepend(localDataContent + ';');
}
}

const output = content.toString();
Expand Down Expand Up @@ -669,3 +677,20 @@ function findLocalizePositions(

return positions;
}

function loadLocaleData(path: string, optimize: boolean): string {
// The path is validated during option processing before the build starts
const content = fs.readFileSync(path, 'utf8');

// NOTE: This can be removed once the locale data files are preprocessed in the framework
if (optimize) {
const result = terserMangle(content, {
compress: true,
ecma: 5,
});

return result.code;
}

return content;
}
11 changes: 11 additions & 0 deletions tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ export default async function () {
await expectFileToMatch(`${outputPath}/main-es2015.js`, translation.helloPartial);
await expectToFail(() => expectFileToMatch(`${outputPath}/main-es5.js`, '$localize`'));
await expectToFail(() => expectFileToMatch(`${outputPath}/main-es2015.js`, '$localize`'));

// Verify the locale ID is present
await expectFileToMatch(`${outputPath}/main-es5.js`, lang);
await expectFileToMatch(`${outputPath}/main-es2015.js`, lang);

// Verify the locale data is registered using the global files
await expectFileToMatch(`${outputPath}/main-es5.js`, '.ng.common.locales');
await expectFileToMatch(`${outputPath}/main-es2015.js`, '.ng.common.locales');

const server = externalServer(outputPath);
try {
// Execute without a devserver.
Expand All @@ -40,6 +46,11 @@ export default async function () {
}
}

// Verify deprecated locale data registration is not present
await ng('build', '--configuration=fr', '--optimization=false');
await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es5.js`, 'registerLocaleData('));
await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es2015.js`, 'registerLocaleData('));

// Verify missing translation behaviour.
await appendToFile('src/app/app.component.html', '<p i18n>Other content</p>');
await ng('build', '--i18n-missing-translation', 'ignore');
Expand Down