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:
parent
e23bc4991d
commit
62b2840822
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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}.`);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue