Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): use source locale with non-locali…
Browse files Browse the repository at this point in the history
…zed dev serving

The source locale was intended to be used when building an application; even when not specifically localizing.  This includes setting the HTML `lang` attribute, injecting locale data, and setting `LOCALE_ID` within the application.
  • Loading branch information
clydin authored and filipesilva committed Oct 30, 2020
1 parent 4169e30 commit 05cd4d6
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 101 deletions.
39 changes: 28 additions & 11 deletions packages/angular_devkit/build_angular/src/dev-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,33 @@ export function serveWebpackBrowser(
`);
}

let locale: string | undefined;
if (browserOptions.i18nLocale) {
// Deprecated VE option
locale = browserOptions.i18nLocale;
} else if (i18n.shouldInline) {
// Dev-server only supports one locale
locale = [...i18n.inlineLocales][0];
} else if (i18n.hasDefinedSourceLocale) {
// use source locale if not localizing
locale = i18n.sourceLocale;
}

let webpackConfig = config;
const tsConfig = readTsconfig(browserOptions.tsConfig, workspaceRoot);
if (i18n.shouldInline && tsConfig.options.enableIvy !== false) {
if (i18n.inlineLocales.size > 1) {
throw new Error(
'The development server only supports localizing a single locale per build.',
);
}

await setupLocalize(i18n, browserOptions, webpackConfig);
// If a locale is defined, setup localization
if (locale) {
// Only supported with Ivy
const tsConfig = readTsconfig(browserOptions.tsConfig, workspaceRoot);
if (tsConfig.options.enableIvy !== false) {
if (i18n.inlineLocales.size > 1) {
throw new Error(
'The development server only supports localizing a single locale per build.',
);
}

await setupLocalize(locale, i18n, browserOptions, webpackConfig);
}
}

if (transforms.webpackConfiguration) {
Expand All @@ -226,8 +243,7 @@ export function serveWebpackBrowser(
browserOptions,
webpackConfig,
projectRoot,
locale:
browserOptions.i18nLocale || (i18n.shouldInline ? [...i18n.inlineLocales][0] : undefined),
locale,
};
}

Expand Down Expand Up @@ -323,16 +339,17 @@ export function serveWebpackBrowser(
}

async function setupLocalize(
locale: string,
i18n: I18nOptions,
browserOptions: BrowserBuilderSchema,
webpackConfig: webpack.Configuration,
) {
const locale = [...i18n.inlineLocales][0];
const localeDescription = i18n.locales[locale];
const { plugins, diagnostics } = await createI18nPlugins(
locale,
localeDescription?.translation,
browserOptions.i18nMissingTranslation || 'ignore',
i18n.shouldInline,
);

// Modify main entrypoint to include locale data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { Architect, BuilderRun } from '@angular-devkit/architect';
import { DevServerBuilderOutput } from '@angular-devkit/build-angular';
import { logging } from '@angular-devkit/core';
import { logging, normalize, virtualFs } from '@angular-devkit/core';
import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies
import { createArchitect, host } from '../test-utils';

Expand Down Expand Up @@ -112,4 +112,30 @@ describe('Dev Server Builder', () => {
expect(response.headers.get('X-Header')).toBe('Hello World');
}, 30000);

it('uses source locale when not localizing', async () => {
const config = host.scopedSync().read(normalize('angular.json'));
const jsonConfig = JSON.parse(virtualFs.fileBufferToString(config));
const applicationProject = jsonConfig.projects.app;

applicationProject.i18n = { sourceLocale: 'fr' };

host.writeMultipleFiles({
'angular.json': JSON.stringify(jsonConfig),
});

const architect = (await createArchitect(host.root())).architect;
const run = await architect.scheduleTarget(target);
const output = await run.result;
expect(output.success).toBe(true);

const indexResponse = await fetch('http://localhost:4200/index.html');
expect(await indexResponse.text()).toContain('lang="fr"');
const vendorResponse = await fetch('http://localhost:4200/vendor.js');
const vendorText = await vendorResponse.text();
expect(vendorText).toContain('fr');
expect(vendorText).toContain('octobre');

await run.stop();
});

});
147 changes: 78 additions & 69 deletions packages/angular_devkit/build_angular/src/utils/i18n-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface I18nOptions {
flatOutput?: boolean;
readonly shouldInline: boolean;
veCompatLocale?: string;
hasDefinedSourceLocale?: boolean;
}

function normalizeTranslationFileOption(
Expand Down Expand Up @@ -93,6 +94,7 @@ export function createI18nOptions(
}

i18n.sourceLocale = rawSourceLocale;
i18n.hasDefinedSourceLocale = true;
}

i18n.locales[i18n.sourceLocale] = {
Expand Down Expand Up @@ -202,91 +204,98 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
buildOptions.i18nLocale = undefined;
}

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.`,
);
// No additional processing needed if no inlining requested and no source locale defined.
if (!i18n.shouldInline && !i18n.hasDefinedSourceLocale) {
return { buildOptions, i18n };
}

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 locale data and translations (if present)
let loader;
const usedFormats = new Set<string>();
for (const [locale, desc] of Object.entries(i18n.locales)) {
if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) {
continue;
}

// Load locales
const loader = await createTranslationLoader();
const usedFormats = new Set<string>();
for (const [locale, desc] of Object.entries(i18n.locales)) {
if (!i18n.inlineLocales.has(locale)) {
continue;
let localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
if (!localeDataPath) {
const [first] = locale.split('-');
if (first) {
localeDataPath = findLocaleDataPath(first.toLowerCase(), localeDataBasePath);
if (localeDataPath) {
context.logger.warn(
`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
);
}
}
}
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;
}

let localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
if (!localeDataPath) {
const [first] = locale.split('-');
if (first) {
localeDataPath = findLocaleDataPath(first.toLowerCase(), localeDataBasePath);
if (localeDataPath) {
context.logger.warn(
`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
);
}
if (!desc.files.length) {
continue;
}

if (!loader) {
loader = await createTranslationLoader();
}

for (const file of desc.files) {
const loadResult = loader(path.join(context.workspaceRoot, file.path));

for (const diagnostics of loadResult.diagnostics.messages) {
if (diagnostics.type === 'error') {
throw new Error(
`Error parsing translation file '${file.path}': ${diagnostics.message}`,
);
} else {
context.logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
}
}
if (!localeDataPath) {

if (loadResult.locale !== undefined && loadResult.locale !== locale) {
context.logger.warn(
`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
`WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
);
} else {
desc.dataPath = localeDataPath;
}

if (!desc.files.length) {
continue;
usedFormats.add(loadResult.format);
if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
// This limitation is only for legacy message id support (defaults to true as of 9.0)
throw new Error(
'Localization currently only supports using one type of translation file format for the entire application.',
);
}

for (const file of desc.files) {
const loadResult = loader(path.join(context.workspaceRoot, file.path));
file.format = loadResult.format;
file.integrity = loadResult.integrity;

for (const diagnostics of loadResult.diagnostics.messages) {
if (diagnostics.type === 'error') {
throw new Error(
`Error parsing translation file '${file.path}': ${diagnostics.message}`,
if (desc.translation) {
// Merge translations
for (const [id, message] of Object.entries(loadResult.translations)) {
if (desc.translation[id] !== undefined) {
context.logger.warn(
`WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
);
} else {
context.logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
}
desc.translation[id] = message;
}

if (loadResult.locale !== undefined && loadResult.locale !== locale) {
context.logger.warn(
`WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
);
}

usedFormats.add(loadResult.format);
if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
// This limitation is only for legacy message id support (defaults to true as of 9.0)
throw new Error(
'Localization currently only supports using one type of translation file format for the entire application.',
);
}

file.format = loadResult.format;
file.integrity = loadResult.integrity;

if (desc.translation) {
// Merge translations
for (const [id, message] of Object.entries(loadResult.translations)) {
if (desc.translation[id] !== undefined) {
context.logger.warn(
`WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
);
}
desc.translation[id] = message;
}
} else {
// First or only translation file
desc.translation = loadResult.translations;
}
} else {
// First or only translation file
desc.translation = loadResult.translations;
}
}

Expand Down
44 changes: 24 additions & 20 deletions packages/angular_devkit/build_angular/src/utils/process-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,34 +552,37 @@ export async function createI18nPlugins(
locale: string,
translation: unknown | undefined,
missingTranslation: 'error' | 'warning' | 'ignore',
shouldInline: boolean,
localeDataContent?: string,
) {
const plugins = [];
const localizeDiag = await import('@angular/localize/src/tools/src/diagnostics');

const diagnostics = new localizeDiag.Diagnostics();

const es2015 = await import(
// tslint:disable-next-line: trailing-comma
'@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin'
);
plugins.push(
// tslint:disable-next-line: no-any
es2015.makeEs2015TranslatePlugin(diagnostics, (translation || {}) as any, {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}),
);
if (shouldInline) {
const es2015 = await import(
// tslint:disable-next-line: trailing-comma
'@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin'
);
plugins.push(
// tslint:disable-next-line: no-any
es2015.makeEs2015TranslatePlugin(diagnostics, (translation || {}) as any, {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}),
);

const es5 = await import(
// tslint:disable-next-line: trailing-comma
'@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin'
);
plugins.push(
// tslint:disable-next-line: no-any
es5.makeEs5TranslatePlugin(diagnostics, (translation || {}) as any, {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}),
);
const es5 = await import(
// tslint:disable-next-line: trailing-comma
'@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin'
);
plugins.push(
// tslint:disable-next-line: no-any
es5.makeEs5TranslatePlugin(diagnostics, (translation || {}) as any, {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}),
);
}

const inlineLocale = await import(
// tslint:disable-next-line: trailing-comma
Expand Down Expand Up @@ -678,6 +681,7 @@ export async function inlineLocales(options: InlineOptions) {
locale,
translations,
isSourceLocale ? 'ignore' : options.missingTranslation || 'warning',
true,
localeDataContent,
);
const transformResult = await transformFromAstSync(ast, options.code, {
Expand Down

0 comments on commit 05cd4d6

Please sign in to comment.