feat(compiler-cli): add a `locale` option to ng-xi18n
Fixes #12303 Closes #14537
This commit is contained in:
parent
e99d721612
commit
234f05996c
|
@ -42,7 +42,7 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
<file source-language="fr" datatype="plaintext" original="ng2.template">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" datatype="html">
|
<trans-unit id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" datatype="html">
|
||||||
<source>translate me</source>
|
<source>translate me</source>
|
||||||
|
|
|
@ -131,6 +131,7 @@ function i18nTest() {
|
||||||
compilerOptions: config.parsed.options, program, host,
|
compilerOptions: config.parsed.options, program, host,
|
||||||
angularCompilerOptions: config.ngOptions,
|
angularCompilerOptions: config.ngOptions,
|
||||||
i18nFormat: 'xlf',
|
i18nFormat: 'xlf',
|
||||||
|
locale: null,
|
||||||
readResource: (fileName: string) => {
|
readResource: (fileName: string) => {
|
||||||
readResources.push(fileName);
|
readResources.push(fileName);
|
||||||
return hostContext.readResource(fileName);
|
return hostContext.readResource(fileName);
|
||||||
|
|
|
@ -22,7 +22,8 @@ import {Extractor} from './extractor';
|
||||||
function extract(
|
function extract(
|
||||||
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions,
|
ngOptions: tsc.AngularCompilerOptions, cliOptions: tsc.I18nExtractionCliOptions,
|
||||||
program: ts.Program, host: ts.CompilerHost): Promise<void> {
|
program: ts.Program, host: ts.CompilerHost): Promise<void> {
|
||||||
return Extractor.create(ngOptions, program, host).extract(cliOptions.i18nFormat);
|
return Extractor.create(ngOptions, program, host, cliOptions.locale)
|
||||||
|
.extract(cliOptions.i18nFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
|
|
|
@ -73,7 +73,8 @@ export class Extractor {
|
||||||
|
|
||||||
static create(
|
static create(
|
||||||
options: tsc.AngularCompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost,
|
options: tsc.AngularCompilerOptions, program: ts.Program, tsCompilerHost: ts.CompilerHost,
|
||||||
compilerHostContext?: CompilerHostContext, ngCompilerHost?: CompilerHost): Extractor {
|
locale?: string|null, compilerHostContext?: CompilerHostContext,
|
||||||
|
ngCompilerHost?: CompilerHost): Extractor {
|
||||||
if (!ngCompilerHost) {
|
if (!ngCompilerHost) {
|
||||||
const usePathMapping = !!options.rootDirs && options.rootDirs.length > 0;
|
const usePathMapping = !!options.rootDirs && options.rootDirs.length > 0;
|
||||||
const context = compilerHostContext || new ModuleResolutionHostAdapter(tsCompilerHost);
|
const context = compilerHostContext || new ModuleResolutionHostAdapter(tsCompilerHost);
|
||||||
|
@ -81,7 +82,7 @@ export class Extractor {
|
||||||
new CompilerHost(program, options, context);
|
new CompilerHost(program, options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {extractor: ngExtractor} = compiler.Extractor.create(ngCompilerHost);
|
const {extractor: ngExtractor} = compiler.Extractor.create(ngCompilerHost, locale || null);
|
||||||
|
|
||||||
return new Extractor(options, ngExtractor, tsCompilerHost, ngCompilerHost, program);
|
return new Extractor(options, ngExtractor, tsCompilerHost, ngCompilerHost, program);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ export interface NgTools_InternalApi_NG2_ExtractI18n_Options {
|
||||||
i18nFormat: string;
|
i18nFormat: string;
|
||||||
readResource: (fileName: string) => Promise<string>;
|
readResource: (fileName: string) => Promise<string>;
|
||||||
// Every new property under this line should be optional.
|
// Every new property under this line should be optional.
|
||||||
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,8 +143,9 @@ export class NgTools_InternalApi_NG_2 {
|
||||||
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
|
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
|
||||||
|
|
||||||
// Create the i18n extractor.
|
// Create the i18n extractor.
|
||||||
|
const locale = options.locale || null;
|
||||||
const extractor = Extractor.create(
|
const extractor = Extractor.create(
|
||||||
options.angularCompilerOptions, options.program, options.host, hostContext);
|
options.angularCompilerOptions, options.program, options.host, locale, hostContext);
|
||||||
|
|
||||||
return extractor.extract(options.i18nFormat);
|
return extractor.extract(options.i18nFormat);
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,8 @@ export class Extractor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(host: ExtractorHost): {extractor: Extractor, staticReflector: StaticReflector} {
|
static create(host: ExtractorHost, locale: string|null):
|
||||||
|
{extractor: Extractor, staticReflector: StaticReflector} {
|
||||||
const htmlParser = new I18NHtmlParser(new HtmlParser());
|
const htmlParser = new I18NHtmlParser(new HtmlParser());
|
||||||
|
|
||||||
const urlResolver = createOfflineCompileUrlResolver();
|
const urlResolver = createOfflineCompileUrlResolver();
|
||||||
|
@ -112,7 +113,7 @@ export class Extractor {
|
||||||
symbolCache, staticReflector);
|
symbolCache, staticReflector);
|
||||||
|
|
||||||
// TODO(vicb): implicit tags & attributes
|
// TODO(vicb): implicit tags & attributes
|
||||||
const messageBundle = new MessageBundle(htmlParser, [], {});
|
const messageBundle = new MessageBundle(htmlParser, [], {}, locale);
|
||||||
|
|
||||||
const extractor = new Extractor(host, staticSymbolResolver, messageBundle, resolver);
|
const extractor = new Extractor(host, staticSymbolResolver, messageBundle, resolver);
|
||||||
return {extractor, staticReflector};
|
return {extractor, staticReflector};
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class MessageBundle {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
||||||
private _implicitAttrs: {[k: string]: string[]}) {}
|
private _implicitAttrs: {[k: string]: string[]}, private _locale: string|null = null) {}
|
||||||
|
|
||||||
updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
|
updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
|
||||||
ParseError[] {
|
ParseError[] {
|
||||||
|
@ -67,7 +67,7 @@ export class MessageBundle {
|
||||||
return new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
|
return new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
|
||||||
});
|
});
|
||||||
|
|
||||||
return serializer.write(msgList);
|
return serializer.write(msgList, this._locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,4 +92,4 @@ class MapPlaceholderNames extends i18n.CloneVisitor {
|
||||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, mapper: PlaceholderMapper): i18n.IcuPlaceholder {
|
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, mapper: PlaceholderMapper): i18n.IcuPlaceholder {
|
||||||
return new i18n.IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
|
return new i18n.IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export abstract class Serializer {
|
||||||
// - The `placeholders` and `placeholderToMessage` properties are irrelevant in the input messages
|
// - The `placeholders` and `placeholderToMessage` properties are irrelevant in the input messages
|
||||||
// - The `id` contains the message id that the serializer is expected to use
|
// - The `id` contains the message id that the serializer is expected to use
|
||||||
// - Placeholder names are already map to public names using the provided mapper
|
// - Placeholder names are already map to public names using the provided mapper
|
||||||
abstract write(messages: i18n.Message[]): string;
|
abstract write(messages: i18n.Message[], locale: string|null): string;
|
||||||
|
|
||||||
abstract load(content: string, url: string):
|
abstract load(content: string, url: string):
|
||||||
{locale: string | null, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}};
|
{locale: string | null, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}};
|
||||||
|
|
|
@ -18,7 +18,7 @@ import * as xml from './xml_helper';
|
||||||
const _VERSION = '1.2';
|
const _VERSION = '1.2';
|
||||||
const _XMLNS = 'urn:oasis:names:tc:xliff:document:1.2';
|
const _XMLNS = 'urn:oasis:names:tc:xliff:document:1.2';
|
||||||
// TODO(vicb): make this a param (s/_/-/)
|
// TODO(vicb): make this a param (s/_/-/)
|
||||||
const _SOURCE_LANG = 'en';
|
const _DEFAULT_SOURCE_LANG = 'en';
|
||||||
const _PLACEHOLDER_TAG = 'x';
|
const _PLACEHOLDER_TAG = 'x';
|
||||||
|
|
||||||
const _FILE_TAG = 'file';
|
const _FILE_TAG = 'file';
|
||||||
|
@ -29,7 +29,7 @@ const _UNIT_TAG = 'trans-unit';
|
||||||
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
|
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
|
||||||
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
|
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
|
||||||
export class Xliff extends Serializer {
|
export class Xliff extends Serializer {
|
||||||
write(messages: i18n.Message[]): string {
|
write(messages: i18n.Message[], locale: string|null): string {
|
||||||
const visitor = new _WriteVisitor();
|
const visitor = new _WriteVisitor();
|
||||||
const transUnits: xml.Node[] = [];
|
const transUnits: xml.Node[] = [];
|
||||||
|
|
||||||
|
@ -59,7 +59,11 @@ export class Xliff extends Serializer {
|
||||||
|
|
||||||
const body = new xml.Tag('body', {}, [...transUnits, new xml.CR(4)]);
|
const body = new xml.Tag('body', {}, [...transUnits, new xml.CR(4)]);
|
||||||
const file = new xml.Tag(
|
const file = new xml.Tag(
|
||||||
'file', {'source-language': _SOURCE_LANG, datatype: 'plaintext', original: 'ng2.template'},
|
'file', {
|
||||||
|
'source-language': locale || _DEFAULT_SOURCE_LANG,
|
||||||
|
datatype: 'plaintext',
|
||||||
|
original: 'ng2.template',
|
||||||
|
},
|
||||||
[new xml.CR(4), body, new xml.CR(2)]);
|
[new xml.CR(4), body, new xml.CR(2)]);
|
||||||
const xliff = new xml.Tag(
|
const xliff = new xml.Tag(
|
||||||
'xliff', {version: _VERSION, xmlns: _XMLNS}, [new xml.CR(2), file, new xml.CR()]);
|
'xliff', {version: _VERSION, xmlns: _XMLNS}, [new xml.CR(2), file, new xml.CR()]);
|
||||||
|
@ -283,4 +287,4 @@ function getCtypeForTag(tag: string): string {
|
||||||
default:
|
default:
|
||||||
return `x-${tag}`;
|
return `x-${tag}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
|
||||||
<!ELEMENT ex (#PCDATA)>`;
|
<!ELEMENT ex (#PCDATA)>`;
|
||||||
|
|
||||||
export class Xmb extends Serializer {
|
export class Xmb extends Serializer {
|
||||||
write(messages: i18n.Message[]): string {
|
write(messages: i18n.Message[], locale: string|null): string {
|
||||||
const exampleVisitor = new ExampleVisitor();
|
const exampleVisitor = new ExampleVisitor();
|
||||||
const visitor = new _Visitor();
|
const visitor = new _Visitor();
|
||||||
let rootNode = new xml.Tag(_MESSAGES_TAG);
|
let rootNode = new xml.Tag(_MESSAGES_TAG);
|
||||||
|
@ -161,4 +161,4 @@ class ExampleVisitor implements xml.IVisitor {
|
||||||
// XMB/XTB placeholders can only contain A-Z, 0-9 and _
|
// XMB/XTB placeholders can only contain A-Z, 0-9 and _
|
||||||
export function toPublicName(internalName: string): string {
|
export function toPublicName(internalName: string): string {
|
||||||
return internalName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
|
return internalName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const _TRANSLATION_TAG = 'translation';
|
||||||
const _PLACEHOLDER_TAG = 'ph';
|
const _PLACEHOLDER_TAG = 'ph';
|
||||||
|
|
||||||
export class Xtb extends Serializer {
|
export class Xtb extends Serializer {
|
||||||
write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
|
write(messages: i18n.Message[], locale: string|null): string { throw new Error('Unsupported'); }
|
||||||
|
|
||||||
load(content: string, url: string):
|
load(content: string, url: string):
|
||||||
{locale: string, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}} {
|
{locale: string, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}} {
|
||||||
|
|
|
@ -57,4 +57,4 @@ class _TestSerializer extends Serializer {
|
||||||
|
|
||||||
function humanizeMessages(catalog: MessageBundle): string[] {
|
function humanizeMessages(catalog: MessageBundle): string[] {
|
||||||
return catalog.write(new _TestSerializer()).split('//');
|
return catalog.write(new _TestSerializer()).split('//');
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,8 +107,8 @@ const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
export function main(): void {
|
export function main(): void {
|
||||||
const serializer = new Xliff();
|
const serializer = new Xliff();
|
||||||
|
|
||||||
function toXliff(html: string): string {
|
function toXliff(html: string, locale: string | null = null): string {
|
||||||
const catalog = new MessageBundle(new HtmlParser, [], {});
|
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
|
||||||
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
||||||
return catalog.write(serializer);
|
return catalog.write(serializer);
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,8 @@ export function main(): void {
|
||||||
describe('XLIFF serializer', () => {
|
describe('XLIFF serializer', () => {
|
||||||
describe('write', () => {
|
describe('write', () => {
|
||||||
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
|
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
|
||||||
|
it('should write a valid xliff file with a source language',
|
||||||
|
() => { expect(toXliff(HTML, 'fr')).toContain('file source-language="fr"'); });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('load', () => {
|
describe('load', () => {
|
||||||
|
@ -247,4 +249,4 @@ export function main(): void {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,11 @@ export function main(): void {
|
||||||
</messagebundle>
|
</messagebundle>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
it('should write a valid xmb file', () => { expect(toXmb(HTML)).toEqual(XMB); });
|
it('should write a valid xmb file', () => {
|
||||||
|
expect(toXmb(HTML)).toEqual(XMB);
|
||||||
|
// the locale is not specified in the xmb file
|
||||||
|
expect(toXmb(HTML, 'fr')).toEqual(XMB);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw when trying to load an xmb file', () => {
|
it('should throw when trying to load an xmb file', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
@ -67,8 +71,8 @@ export function main(): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toXmb(html: string): string {
|
function toXmb(html: string, locale: string | null = null): string {
|
||||||
const catalog = new MessageBundle(new HtmlParser, [], {});
|
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
|
||||||
const serializer = new Xmb();
|
const serializer = new Xmb();
|
||||||
|
|
||||||
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
||||||
|
|
|
@ -186,8 +186,8 @@ export function main(): void {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when trying to save an xtb file',
|
it('should throw when trying to save an xtb file',
|
||||||
() => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
|
() => { expect(() => { serializer.write([], null); }).toThrowError(/Unsupported/); });
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ cp -v package.json $TMP
|
||||||
|
|
||||||
./node_modules/.bin/ngc -p tsconfig-build.json --i18nFile=src/messages.fi.xlf --locale=fi --i18nFormat=xlf
|
./node_modules/.bin/ngc -p tsconfig-build.json --i18nFile=src/messages.fi.xlf --locale=fi --i18nFormat=xlf
|
||||||
|
|
||||||
./node_modules/.bin/ng-xi18n -p tsconfig-xi18n.json --i18nFormat=xlf
|
./node_modules/.bin/ng-xi18n -p tsconfig-xi18n.json --i18nFormat=xlf --locale=fr
|
||||||
./node_modules/.bin/ng-xi18n -p tsconfig-xi18n.json --i18nFormat=xmb
|
./node_modules/.bin/ng-xi18n -p tsconfig-xi18n.json --i18nFormat=xmb
|
||||||
|
|
||||||
node test/test_summaries.js
|
node test/test_summaries.js
|
||||||
|
|
|
@ -12,10 +12,12 @@ export class CliOptions {
|
||||||
|
|
||||||
export class I18nExtractionCliOptions extends CliOptions {
|
export class I18nExtractionCliOptions extends CliOptions {
|
||||||
public i18nFormat: string;
|
public i18nFormat: string;
|
||||||
|
public locale: string;
|
||||||
|
|
||||||
constructor({i18nFormat = null}: {i18nFormat?: string}) {
|
constructor({i18nFormat = null, locale = null}: {i18nFormat?: string, locale: string|null}) {
|
||||||
super({});
|
super({});
|
||||||
this.i18nFormat = i18nFormat;
|
this.i18nFormat = i18nFormat;
|
||||||
|
this.locale = locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue