fix(ivy): i18n - support setting locales for each translation file (#33381)

Previously the target locale of a translation file had to be extracted
from the contents of the translation file. Therefore it was an error if
the translation file did not provide a target locale.

Now an array of locales can be provided via the `translationFileLocales`
option that overrides any target locale extracted from the file.
This allows us to support translation files that do not have a target
locale specified in their contents.

// FW-1644
Fixes #33323

PR Close #33381
This commit is contained in:
Pete Bacon Darwin 2019-10-24 19:57:13 +01:00 committed by Andrew Kushnir
parent e23bc4991d
commit 62b2840822
14 changed files with 259 additions and 103 deletions

View File

@ -10,8 +10,6 @@ import {FileUtils} from '../../file_utils';
import {OutputPathFn} from '../output_path';
import {TranslationBundle, TranslationHandler} from '../translator';
/**
* Translate an asset file by simply copying it to the appropriate translation output paths.
*/

View File

@ -14,7 +14,7 @@ import {AssetTranslationHandler} from './asset_files/asset_translation_handler';
import {getOutputPathFn, OutputPathFn} from './output_path';
import {SourceFileTranslationHandler} from './source_files/source_file_translation_handler';
import {MissingTranslationStrategy} from './source_files/source_file_utils';
import {TranslationLoader} from './translation_files/translation_file_loader';
import {TranslationLoader} from './translation_files/translation_loader';
import {SimpleJsonTranslationParser} from './translation_files/translation_parsers/simple_json/simple_json_translation_parser';
import {Xliff1TranslationParser} from './translation_files/translation_parsers/xliff1/xliff1_translation_parser';
import {Xliff2TranslationParser} from './translation_files/translation_parsers/xliff2/xliff2_translation_parser';
@ -50,18 +50,21 @@ if (require.main === module) {
describe:
'A glob pattern indicating what translation files to load, either absolute or relative to the current working directory. E.g. `my_proj/src/locale/messages.*.xlf.',
})
.option('o', {
alias: 'outputPath',
required: true,
describe:
'A output path pattern to where the translated files will be written. The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.'
})
.option('m', {
alias: 'missingTranslation',
describe: 'How to handle missing translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
})
.help()
.parse(args);
@ -73,32 +76,67 @@ if (require.main === module) {
const diagnostics = new Diagnostics();
const missingTranslation: MissingTranslationStrategy = options['m'];
const sourceLocale: string|undefined = options['l'];
// For CLI we do not have a way to specify the locale of the translation files
// It must be extracted from the file itself.
const translationFileLocales: string[] = [];
translateFiles({sourceRootPath, sourceFilePaths, translationFilePaths, outputPathFn, diagnostics,
missingTranslation, sourceLocale});
translateFiles({sourceRootPath, sourceFilePaths, translationFilePaths, translationFileLocales,
outputPathFn, diagnostics, missingTranslation, sourceLocale});
diagnostics.messages.forEach(m => console.warn(`${m.type}: ${m.message}`));
process.exit(diagnostics.hasErrors ? 1 : 0);
}
export interface TranslateFilesOptions {
/**
* The root path of the files to translate, either absolute or relative to the current working
* directory. E.g. `dist/en`
*/
sourceRootPath: string;
/**
* The files to translate, relative to the `root` path.
*/
sourceFilePaths: string[];
/**
* An array of paths to the translation files to load, either absolute or relative to the current
* working directory.
*/
translationFilePaths: string[];
/**
* A collection of the target locales for the translation files.
*/
translationFileLocales: (string|undefined)[];
/**
* A function that computes the output path of where the translated files will be written.
* The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.
*/
outputPathFn: OutputPathFn;
/**
* An object that will receive any diagnostics messages due to the processing.
*/
diagnostics: Diagnostics;
/**
* How to handle missing translations.
*/
missingTranslation: MissingTranslationStrategy;
/**
* The locale of the source files.
* If this is provided then a copy of the application will be created with no translation but just
* the `$localize` calls stripped out.
*/
sourceLocale?: string;
}
export function translateFiles({sourceRootPath, sourceFilePaths, translationFilePaths, outputPathFn,
diagnostics, missingTranslation,
sourceLocale}: TranslateFilesOptions) {
const translationLoader = new TranslationLoader([
new Xliff2TranslationParser(),
new Xliff1TranslationParser(),
new SimpleJsonTranslationParser(),
]);
export function translateFiles({sourceRootPath, sourceFilePaths, translationFilePaths,
translationFileLocales, outputPathFn, diagnostics,
missingTranslation, sourceLocale}: TranslateFilesOptions) {
const translationLoader = new TranslationLoader(
[
new Xliff2TranslationParser(),
new Xliff1TranslationParser(),
new SimpleJsonTranslationParser(),
],
diagnostics);
const resourceProcessor = new Translator(
[
@ -107,7 +145,7 @@ export function translateFiles({sourceRootPath, sourceFilePaths, translationFile
],
diagnostics);
const translations = translationLoader.loadBundles(translationFilePaths);
const translations = translationLoader.loadBundles(translationFilePaths, translationFileLocales);
sourceRootPath = resolve(sourceRootPath);
resourceProcessor.translateFiles(
sourceFilePaths, sourceRootPath, outputPathFn, translations, sourceLocale);

View File

@ -5,23 +5,18 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize/private';
import {parseSync, transformFromAstSync} from '@babel/core';
import {File, Program} from '@babel/types';
import {extname, join} from 'path';
import {Diagnostics} from '../../diagnostics';
import {FileUtils} from '../../file_utils';
import {OutputPathFn} from '../output_path';
import {TranslationBundle, TranslationHandler} from '../translator';
import {makeEs2015TranslatePlugin} from './es2015_translate_plugin';
import {makeEs5TranslatePlugin} from './es5_translate_plugin';
import {makeLocalePlugin} from './locale_plugin';
import {TranslatePluginOptions} from './source_file_utils';
/**
* Translate a file by inlining all messages tagged by `$localize` with the appropriate translated
* message.

View File

@ -1,34 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FileUtils} from '../../file_utils';
import {TranslationBundle} from '../translator';
import {TranslationParser} from './translation_parsers/translation_parser';
/**
* Use this class to load a collection of translation files from disk.
*/
export class TranslationLoader {
constructor(private translationParsers: TranslationParser[]) {}
/**
* Load and parse the translation files into a collection of `TranslationBundles`.
*
* @param translationFilePaths A collection of absolute paths to the translation files.
*/
loadBundles(translationFilePaths: string[]): TranslationBundle[] {
return translationFilePaths.map(filePath => {
const fileContents = FileUtils.readFile(filePath);
for (const translationParser of this.translationParsers) {
if (translationParser.canParse(filePath, fileContents)) {
return translationParser.parse(filePath, fileContents);
}
}
throw new Error(`Unable to parse translation file: ${filePath}`);
});
}
}

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Diagnostics} from '../../diagnostics';
import {FileUtils} from '../../file_utils';
import {TranslationBundle} from '../translator';
import {TranslationParser} from './translation_parsers/translation_parser';
/**
* Use this class to load a collection of translation files from disk.
*/
export class TranslationLoader {
constructor(private translationParsers: TranslationParser[], private diagnostics: Diagnostics) {}
/**
* Load and parse the translation files into a collection of `TranslationBundles`.
*
* If there is a locale provided in `translationFileLocales` then this is used rather than the
* locale extracted from the file itself.
* If there is neither a provided locale nor a locale parsed from the file, then an error is
* thrown.
* If there are both a provided locale and a locale parsed from the file, and they are not the
* same, then a warning is reported .
*
* @param translationFilePaths An array of absolute paths to the translation files.
* @param translationFileLocales An array of locales for each of the translation files.
*/
loadBundles(translationFilePaths: string[], translationFileLocales: (string|undefined)[]):
TranslationBundle[] {
return translationFilePaths.map((filePath, index) => {
const fileContents = FileUtils.readFile(filePath);
for (const translationParser of this.translationParsers) {
if (translationParser.canParse(filePath, fileContents)) {
const providedLocale = translationFileLocales[index];
const {locale: parsedLocale, translations} =
translationParser.parse(filePath, fileContents);
const locale = providedLocale || parsedLocale;
if (locale === undefined) {
throw new Error(
`The translation file "${filePath}" does not contain a target locale and no explicit locale was provided for this file.`);
}
if (parsedLocale !== undefined && providedLocale !== undefined &&
parsedLocale !== providedLocale) {
this.diagnostics.warn(
`The provided locale "${providedLocale}" does not match the target locale "${parsedLocale}" found in the translation file "${filePath}".`);
}
return {locale, translations};
}
}
throw new Error(
`There is no "TranslationParser" that can parse this translation file: ${filePath}.`);
});
}
}

View File

@ -7,8 +7,7 @@
*/
import {ɵMessageId, ɵParsedTranslation, ɵparseTranslation} from '@angular/localize';
import {extname} from 'path';
import {TranslationBundle} from '../../../translator';
import {TranslationParser} from '../translation_parser';
import {ParsedTranslationBundle, TranslationParser} from '../translation_parser';
/**
* A translation parser that can parse JSON that has the form:
@ -26,13 +25,13 @@ import {TranslationParser} from '../translation_parser';
export class SimpleJsonTranslationParser implements TranslationParser {
canParse(filePath: string, _contents: string): boolean { return (extname(filePath) === '.json'); }
parse(_filePath: string, contents: string): TranslationBundle {
const {locale, translations} = JSON.parse(contents);
parse(_filePath: string, contents: string): ParsedTranslationBundle {
const {locale: parsedLocale, translations} = JSON.parse(contents);
const parsedTranslations: Record<ɵMessageId, ɵParsedTranslation> = {};
for (const messageId in translations) {
const targetMessage = translations[messageId];
parsedTranslations[messageId] = ɵparseTranslation(targetMessage);
}
return {locale, translations: parsedTranslations};
return {locale: parsedLocale, translations: parsedTranslations};
}
}

View File

@ -5,7 +5,15 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TranslationBundle} from '../../translator';
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize/private';
/**
* An object that holds translations that have been parsed from a translation file.
*/
export interface ParsedTranslationBundle {
locale: string|undefined;
translations: Record<ɵMessageId, ɵParsedTranslation>;
}
/**
* Implement this interface to provide a class that can parse the contents of a translation file.
@ -25,5 +33,5 @@ export interface TranslationParser {
* @param filePath The absolute path to the translation file.
* @param contents The contents of the translation file.
*/
parse(filePath: string, contents: string): TranslationBundle;
parse(filePath: string, contents: string): ParsedTranslationBundle;
}

View File

@ -9,11 +9,10 @@ import {Element, Node, XmlParser, visitAll} from '@angular/compiler';
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
import {extname} from 'path';
import {TargetMessageRenderer} from '../../../message_renderers/target_message_renderer';
import {TranslationBundle} from '../../../translator';
import {BaseVisitor} from '../base_visitor';
import {TranslationParseError} from '../translation_parse_error';
import {TranslationParser} from '../translation_parser';
import {getAttrOrThrow, parseInnerRange} from '../translation_utils';
import {ParsedTranslationBundle, TranslationParser} from '../translation_parser';
import {getAttrOrThrow, getAttribute, parseInnerRange} from '../translation_utils';
import {Xliff1MessageSerializer} from './xliff1_message_serializer';
const XLIFF_1_2_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:1.2"/;
@ -30,7 +29,7 @@ export class Xliff1TranslationParser implements TranslationParser {
return (extname(filePath) === '.xlf') && XLIFF_1_2_NS_REGEX.test(contents);
}
parse(filePath: string, contents: string): TranslationBundle {
parse(filePath: string, contents: string): ParsedTranslationBundle {
const xmlParser = new XmlParser();
const xml = xmlParser.parse(contents, filePath);
const bundle = XliffFileElementVisitor.extractBundle(xml.rootNodes);
@ -42,9 +41,9 @@ export class Xliff1TranslationParser implements TranslationParser {
}
class XliffFileElementVisitor extends BaseVisitor {
private bundle: TranslationBundle|undefined;
private bundle: ParsedTranslationBundle|undefined;
static extractBundle(xliff: Node[]): TranslationBundle|undefined {
static extractBundle(xliff: Node[]): ParsedTranslationBundle|undefined {
const visitor = new this();
visitAll(visitor, xliff);
return visitor.bundle;
@ -53,7 +52,7 @@ class XliffFileElementVisitor extends BaseVisitor {
visitElement(element: Element): any {
if (element.name === 'file') {
this.bundle = {
locale: getAttrOrThrow(element, 'target-language'),
locale: getAttribute(element, 'target-language'),
translations: XliffTranslationVisitor.extractTranslations(element)
};
} else {

View File

@ -9,11 +9,10 @@ import {Element, Node, XmlParser, visitAll} from '@angular/compiler';
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
import {extname} from 'path';
import {TargetMessageRenderer} from '../../../message_renderers/target_message_renderer';
import {TranslationBundle} from '../../../translator';
import {BaseVisitor} from '../base_visitor';
import {TranslationParseError} from '../translation_parse_error';
import {TranslationParser} from '../translation_parser';
import {getAttrOrThrow, parseInnerRange} from '../translation_utils';
import {ParsedTranslationBundle, TranslationParser} from '../translation_parser';
import {getAttrOrThrow, getAttribute, parseInnerRange} from '../translation_utils';
import {Xliff2MessageSerializer} from './xliff2_message_serializer';
const XLIFF_2_0_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:2.0"/;
@ -29,7 +28,7 @@ export class Xliff2TranslationParser implements TranslationParser {
return (extname(filePath) === '.xlf') && XLIFF_2_0_NS_REGEX.test(contents);
}
parse(filePath: string, contents: string): TranslationBundle {
parse(filePath: string, contents: string): ParsedTranslationBundle {
const xmlParser = new XmlParser();
const xml = xmlParser.parse(contents, filePath);
const bundle = Xliff2TranslationBundleVisitor.extractBundle(xml.rootNodes);
@ -40,27 +39,30 @@ export class Xliff2TranslationParser implements TranslationParser {
}
}
class Xliff2TranslationBundleVisitor extends BaseVisitor {
private locale: string|undefined;
private bundle: TranslationBundle|undefined;
interface BundleVisitorContext {
parsedLocale?: string;
}
static extractBundle(xliff: Node[]): TranslationBundle|undefined {
class Xliff2TranslationBundleVisitor extends BaseVisitor {
private bundle: ParsedTranslationBundle|undefined;
static extractBundle(xliff: Node[]): ParsedTranslationBundle|undefined {
const visitor = new this();
visitAll(visitor, xliff);
visitAll(visitor, xliff, {});
return visitor.bundle;
}
visitElement(element: Element): any {
visitElement(element: Element, {parsedLocale}: BundleVisitorContext): any {
if (element.name === 'xliff') {
this.locale = getAttrOrThrow(element, 'trgLang');
return visitAll(this, element.children);
parsedLocale = getAttribute(element, 'trgLang');
return visitAll(this, element.children, {parsedLocale});
} else if (element.name === 'file') {
this.bundle = {
locale: this.locale !,
locale: parsedLocale,
translations: Xliff2TranslationVisitor.extractTranslations(element)
};
} else {
return visitAll(this, element.children);
return visitAll(this, element.children, {parsedLocale});
}
}
}

View File

@ -13,11 +13,8 @@ import {FileUtils} from '../file_utils';
import {OutputPathFn} from './output_path';
/**
* An object that holds translations that have been loaded
* from a translation file.
* An object that holds information to be used to translate files.
*/
export interface TranslationBundle {
locale: string;

View File

@ -30,7 +30,7 @@ describe('translateFiles()', () => {
outputPathFn,
translationFilePaths: resolveAll(
__dirname + '/locales', ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf']),
diagnostics,
translationFileLocales: [], diagnostics,
missingTranslation: 'error'
});
@ -58,7 +58,7 @@ describe('translateFiles()', () => {
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']), outputPathFn,
translationFilePaths: resolveAll(
__dirname + '/locales', ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf']),
diagnostics,
translationFileLocales: [], diagnostics,
missingTranslation: 'error',
});
@ -72,6 +72,33 @@ describe('translateFiles()', () => {
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
});
it('should translate and copy source-code files overriding the locales', () => {
const diagnostics = new Diagnostics();
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
translateFiles({
sourceRootPath: resolve(__dirname, 'test_files'),
sourceFilePaths: resolveAll(__dirname + '/test_files', ['test.js']), outputPathFn,
translationFilePaths: resolveAll(
__dirname + '/locales', ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf']),
translationFileLocales: ['xde', undefined, 'fr'], diagnostics,
missingTranslation: 'error',
});
expect(diagnostics.messages.length).toEqual(1);
expect(diagnostics.messages).toContain({
type: 'warning',
message:
`The provided locale "xde" does not match the target locale "de" found in the translation file "${resolve(__dirname, 'locales', 'messages.de.json')}".`
});
expect(FileUtils.readFile(resolve(testDir, 'xde', 'test.js')))
.toEqual(`var name="World";var message="Guten Tag, "+name+"!";`);
expect(FileUtils.readFile(resolve(testDir, 'es', 'test.js')))
.toEqual(`var name="World";var message="Hola, "+name+"!";`);
expect(FileUtils.readFile(resolve(testDir, 'fr', 'test.js')))
.toEqual(`var name="World";var message="Bonjour, "+name+"!";`);
});
it('should transform and/or copy files to the destination folders', () => {
const diagnostics = new Diagnostics();
const outputPathFn = getOutputPathFn(resolve(testDir, '{{LOCALE}}'));
@ -82,7 +109,7 @@ describe('translateFiles()', () => {
outputPathFn,
translationFilePaths: resolveAll(
__dirname + '/locales', ['messages.de.json', 'messages.es.xlf', 'messages.fr.xlf']),
diagnostics,
translationFileLocales: [], diagnostics,
missingTranslation: 'error',
});

View File

@ -7,8 +7,9 @@
*/
import {ɵParsedTranslation} from '@angular/localize';
import {Diagnostics} from '../../../src/diagnostics';
import {FileUtils} from '../../../src/file_utils';
import {TranslationLoader} from '../../../src/translate/translation_files/translation_file_loader';
import {TranslationLoader} from '../../../src/translate/translation_files/translation_loader';
import {TranslationParser} from '../../../src/translate/translation_files/translation_parsers/translation_parser';
describe('TranslationLoader', () => {
@ -18,9 +19,10 @@ describe('TranslationLoader', () => {
});
it('should `canParse()` and `parse()` for each file', () => {
const parser = new MockTranslationParser(true);
const loader = new TranslationLoader([parser]);
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf']);
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(true, 'fr');
const loader = new TranslationLoader([parser], diagnostics);
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []);
expect(parser.log).toEqual([
'canParse(/src/locale/messages.en.xlf, english messages)',
'parse(/src/locale/messages.en.xlf, english messages)',
@ -30,11 +32,12 @@ describe('TranslationLoader', () => {
});
it('should stop at the first parser that can parse each file', () => {
const diagnostics = new Diagnostics();
const parser1 = new MockTranslationParser(false);
const parser2 = new MockTranslationParser(true);
const parser3 = new MockTranslationParser(true);
const loader = new TranslationLoader([parser1, parser2, parser3]);
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf']);
const parser2 = new MockTranslationParser(true, 'fr');
const parser3 = new MockTranslationParser(true, 'en');
const loader = new TranslationLoader([parser1, parser2, parser3], diagnostics);
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []);
expect(parser1.log).toEqual([
'canParse(/src/locale/messages.en.xlf, english messages)',
'canParse(/src/locale/messages.fr.xlf, french messages)',
@ -49,22 +52,64 @@ describe('TranslationLoader', () => {
it('should return locale and translations parsed from each file', () => {
const translations = {};
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(true, 'pl', translations);
const loader = new TranslationLoader([parser]);
const loader = new TranslationLoader([parser], diagnostics);
const result =
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf']);
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []);
expect(result).toEqual([
{locale: 'pl', translations},
{locale: 'pl', translations},
]);
});
it('should return the provided locale if there is no parsed locale', () => {
const translations = {};
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(true, undefined, translations);
const loader = new TranslationLoader([parser], diagnostics);
const result = loader.loadBundles(
['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], ['en', 'fr']);
expect(result).toEqual([
{locale: 'en', translations},
{locale: 'fr', translations},
]);
});
it('should warn if the provided locales do not match the parsed locales', () => {
const translations = {};
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(true, 'pl', translations);
const loader = new TranslationLoader([parser], diagnostics);
loader.loadBundles(
['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], [undefined, 'FR']);
expect(diagnostics.messages.length).toEqual(1);
expect(diagnostics.messages).toContain({
type: 'warning',
message:
`The provided locale "FR" does not match the target locale "pl" found in the translation file "/src/locale/messages.fr.xlf".`,
}, );
});
it('should throw an error if there is no provided nor parsed target locale', () => {
const translations = {};
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(true, undefined, translations);
const loader = new TranslationLoader([parser], diagnostics);
expect(() => loader.loadBundles(['/src/locale/messages.en.xlf'], []))
.toThrowError(
'The translation file "/src/locale/messages.en.xlf" does not contain a target locale and no explicit locale was provided for this file.');
});
it('should error if none of the parsers can parse the file', () => {
const diagnostics = new Diagnostics();
const parser = new MockTranslationParser(false);
const loader = new TranslationLoader([parser]);
expect(() => loader.loadBundles([
'/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'
])).toThrowError('Unable to parse translation file: /src/locale/messages.en.xlf');
const loader = new TranslationLoader([parser], diagnostics);
expect(
() => loader.loadBundles(
['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []))
.toThrowError(
'There is no "TranslationParser" that can parse this translation file: /src/locale/messages.en.xlf.');
});
});
});
@ -72,7 +117,7 @@ describe('TranslationLoader', () => {
class MockTranslationParser implements TranslationParser {
log: string[] = [];
constructor(
private _canParse: boolean = true, private _locale: string = 'fr',
private _canParse: boolean = true, private _locale?: string,
private _translations: Record<string, ɵParsedTranslation> = {}) {}
canParse(filePath: string, fileContents: string) {

View File

@ -40,6 +40,19 @@ describe('Xliff1TranslationParser', () => {
expect(result.locale).toEqual('fr');
});
it('should return an undefined locale if there is no locale in the file', () => {
const XLIFF = `
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
</body>
</file>
</xliff>`;
const parser = new Xliff1TranslationParser();
const result = parser.parse('/some/file.xlf', XLIFF);
expect(result.locale).toBeUndefined();
});
it('should extract basic messages', () => {
/**
* Source HTML:

View File

@ -39,6 +39,17 @@ describe('Xliff2TranslationParser', () => {
expect(result.locale).toEqual('fr');
});
it('should return undefined locale if there is no locale in the file', () => {
const XLIFF = `
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en">
<file original="ng.template" id="ngi18n">
</file>
</xliff>`;
const parser = new Xliff2TranslationParser();
const result = parser.parse('/some/file.xlf', XLIFF);
expect(result.locale).toBeUndefined();
});
it('should extract basic messages', () => {
/**
* Source HTML: