Skip to content

Commit

Permalink
fix(ivy): i18n - support setting locales for each translation file
Browse files Browse the repository at this point in the history
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.

NOTE to early adopters: if you were using `TranslationParser`s directly
then the return type for the `parse()` method has changed. The properties
of the returned object changed from:

```
interface TranslationBundle {
  locale: string;
  translations: Record<ɵMessageId, ɵParsedTranslation>;
}
```

to

```
interface ParsedTranslationBundle {
  parsedLocale: string|undefined;
  parsedTranslations: Record<ɵMessageId, ɵParsedTranslation>;
}
```

// FW-1644
Fixes #33323
  • Loading branch information
petebacondarwin committed Oct 25, 2019
1 parent dcdb433 commit 0b7b730
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
62 changes: 50 additions & 12 deletions packages/localize/src/tools/src/translate/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -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(
[
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @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 {parsedLocale, parsedTranslations: translations} =
translationParser.parse(filePath, fileContents);
const locale = providedLocale || parsedLocale;
if (locale === undefined) {
throw new Error(
`No target locale provided nor found in the translation file "${filePath}"`);
}
if (parsedLocale !== undefined && providedLocale !== undefined &&
parsedLocale !== providedLocale) {
this.diagnostics.warn(
`The provided locale "${providedLocale}" and the target locale "${parsedLocale}" do not match for translation file "${filePath}".`);
}
return {locale, translations};
}
}
throw new Error(`Unable to parse translation file: ${filePath}`);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 {parsedLocale, parsedTranslations};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
parsedLocale: string|undefined;
parsedTranslations: Record<ɵMessageId, ɵParsedTranslation>;
}

/**
* Implement this interface to provide a class that can parse the contents of a translation file.
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"/;
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -53,8 +52,8 @@ class XliffFileElementVisitor extends BaseVisitor {
visitElement(element: Element): any {
if (element.name === 'file') {
this.bundle = {
locale: getAttrOrThrow(element, 'target-language'),
translations: XliffTranslationVisitor.extractTranslations(element)
parsedLocale: getAttribute(element, 'target-language'),
parsedTranslations: XliffTranslationVisitor.extractTranslations(element)
};
} else {
return visitAll(this, element.children);
Expand Down

0 comments on commit 0b7b730

Please sign in to comment.