diff --git a/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts b/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts index aea057bff10ad..ed552d0514197 100644 --- a/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts +++ b/packages/localize/src/tools/src/translate/asset_files/asset_translation_handler.ts @@ -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. */ diff --git a/packages/localize/src/tools/src/translate/main.ts b/packages/localize/src/tools/src/translate/main.ts index 209426c2d93bd..3d3b9e92dc90d 100644 --- a/packages/localize/src/tools/src/translate/main.ts +++ b/packages/localize/src/tools/src/translate/main.ts @@ -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); diff --git a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts index 391f568eec7aa..0cf4906d41663 100644 --- a/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts +++ b/packages/localize/src/tools/src/translate/source_files/source_file_translation_handler.ts @@ -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. diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_file_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_file_loader.ts deleted file mode 100644 index 04cbc7bb3a51f..0000000000000 --- a/packages/localize/src/tools/src/translate/translation_files/translation_file_loader.ts +++ /dev/null @@ -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}`); - }); - } -} diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts new file mode 100644 index 0000000000000..33096987b111b --- /dev/null +++ b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts @@ -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}.`); + }); + } +} diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json/simple_json_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json/simple_json_translation_parser.ts index d5de42d1529c0..4e9f6d576e78b 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json/simple_json_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json/simple_json_translation_parser.ts @@ -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}; } } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts index 144058d1b3595..802e6dfd7d377 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts @@ -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; } diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser.ts index 7679f618011b3..34e18da458bfb 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser.ts @@ -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 { diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser.ts index 6167c9fcacfd6..57d9c402ec325 100644 --- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser.ts +++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser.ts @@ -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 { } } +interface BundleVisitorContext { + parsedLocale?: string; +} + class Xliff2TranslationBundleVisitor extends BaseVisitor { - private locale: string|undefined; - 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); + 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}); } } } diff --git a/packages/localize/src/tools/src/translate/translator.ts b/packages/localize/src/tools/src/translate/translator.ts index a99f1fdc138bd..1cda8b75d01f1 100644 --- a/packages/localize/src/tools/src/translate/translator.ts +++ b/packages/localize/src/tools/src/translate/translator.ts @@ -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; diff --git a/packages/localize/src/tools/test/translate/integration/main_spec.ts b/packages/localize/src/tools/test/translate/integration/main_spec.ts index bfaecaacf2fb9..8fdb9bdbc2bff 100644 --- a/packages/localize/src/tools/test/translate/integration/main_spec.ts +++ b/packages/localize/src/tools/test/translate/integration/main_spec.ts @@ -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', }); diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts index 9f7497c152e06..0fd0a5c29983c 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts @@ -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 = {}) {} canParse(filePath: string, fileContents: string) { diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser_spec.ts index 972f33dbae81e..f8a25dc3fdc8c 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1/xliff1_translation_parser_spec.ts @@ -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 = ` + + + + + + `; + const parser = new Xliff1TranslationParser(); + const result = parser.parse('/some/file.xlf', XLIFF); + expect(result.locale).toBeUndefined(); + }); + it('should extract basic messages', () => { /** * Source HTML: diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser_spec.ts index d31f56c6d4af2..94bb19e45b8a0 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2/xliff2_translation_parser_spec.ts @@ -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 = ` + + + + `; + const parser = new Xliff2TranslationParser(); + const result = parser.parse('/some/file.xlf', XLIFF); + expect(result.locale).toBeUndefined(); + }); + it('should extract basic messages', () => { /** * Source HTML: