refactor: expose new @angular/localize/tools entry-point for CLI usage (#43431)

This wires up the `@angular/localize/tools` entry-point. For context:
This entry-point is being created to avoid deep imports into
`@angular/localize/src/tools/<..>` like the CLI relies on. Deep imports
do not play well with strict ESM, and now that all APF packages are
strict ESM, the tool code needs to be either strict ESM as well.

We use ESBuild to create individual bundles for the CLI entry-points,
and the actual tool entry-point. We use a bundler because this enables
the localize code be ESM compatible. Without a bundler, all relative imports
within the `tools` entry-point would need to explicitly have the `.js`
extension. This would be cumbersome and hard to maintain/enforce or
validate.

One might wonder why this is not a standard APF entry-point then. The
answer is that the APF entry-points do not support exposing the CLI
binaries (like `yarn localize-translate`). This could be done through
tertiary entry-points, but using ESBuild directly gives us more control
for now. We might want to revisit this in the future again.

PR Close #43431
This commit is contained in:
Paul Gschwendtner 2021-09-24 19:19:11 +02:00 committed by Dylan Hunn
parent a18981ab7b
commit 27535bfb79
29 changed files with 478 additions and 373 deletions

View file

@ -1028,7 +1028,8 @@ groups:
- *can-be-global-docs-approved
- >
contains_any_globs(files, [
'packages/compiler-cli/src/tooling.ts'
'packages/compiler-cli/src/tooling.ts',
'packages/localize/tools/index.ts'
])
reviewers:
users:

View file

@ -1,6 +1,6 @@
load("//tools:defaults.bzl", "api_golden_test", "pkg_npm", "ts_config", "ts_library")
load("@npm//@bazel/esbuild:index.bzl", "esbuild", "esbuild_config")
load(":extract_typings_rule.bzl", "extract_typings")
load("//tools:extract_typings_rule.bzl", "extract_typings")
# Load ng_perf_flag explicitly from ng_perf.bzl as it's private API, and not exposed to other
# consumers of @angular/bazel.

View file

@ -25,7 +25,7 @@ ng_package(
],
nested_packages = [
"//packages/localize/schematics:npm_package",
"//packages/localize/src/tools:npm_package",
"//packages/localize/tools:npm_package",
],
tags = [
"release-with-framework",

View file

@ -3,9 +3,15 @@
"version": "0.0.0-PLACEHOLDER",
"description": "Angular - library for localizing messages",
"bin": {
"localize-translate": "./src/tools/src/translate/main.js",
"localize-extract": "./src/tools/src/extract/main.js",
"localize-migrate": "./src/tools/src/migrate/main.js"
"localize-translate": "./tools/bundles/src/translate/cli.js",
"localize-extract": "./tools/bundles/src/extract/cli.js",
"localize-migrate": "./tools/bundles/src/migrate/cli.js"
},
"exports": {
"./tools": {
"types": "./tools/index.d.ts",
"default": "./tools/bundles/index.js"
}
},
"author": "angular",
"license": "MIT",
@ -28,7 +34,8 @@
"**/init.mjs"
],
"dependencies": {
"@babel/core": "7.8.3",
"@babel/core": "7.8.6",
"@babel/types": "7.8.6",
"glob": "7.2.0",
"yargs": "^17.0.0"
},

View file

@ -1,40 +0,0 @@
load("//tools:defaults.bzl", "pkg_npm", "ts_config", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_config(
name = "tsconfig",
src = "tsconfig-build.json",
deps = ["//packages:tsconfig-build.json"],
)
ts_library(
name = "tools",
srcs = glob(
[
"**/*.ts",
],
),
tsconfig = ":tsconfig",
deps = [
"//packages/compiler",
"//packages/compiler-cli/private",
"//packages/localize",
"@npm//@babel/core",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__traverse",
"@npm//@types/glob",
"@npm//@types/node",
"@npm//@types/yargs",
],
)
pkg_npm(
name = "npm_package",
srcs = [
],
deps = [
":tools",
],
)

View file

@ -1,23 +0,0 @@
{
"extends": "../../../tsconfig-build.json",
"compilerOptions": {
"module": "commonjs",
"stripInternal": false,
"target": "es2015",
"lib": [
"es2015",
"es2017.object"
],
"paths": {
"@angular/*": ["./packages/*"]
},
"strict": true,
"types": [
"node"
]
},
"bazelOptions": {
"suppressTsconfigOverrideWarnings": true
}
}

View file

@ -0,0 +1,72 @@
load("@npm//@bazel/esbuild:index.bzl", "esbuild", "esbuild_config")
load("//tools:defaults.bzl", "pkg_npm", "ts_library")
load("//tools:extract_typings_rule.bzl", "extract_typings")
ts_library(
name = "tools",
srcs = glob(
[
"**/*.ts",
],
),
visibility = ["//packages/localize/tools:__subpackages__"],
deps = [
"//packages/compiler",
"//packages/compiler-cli/private",
"//packages/localize",
"@npm//@babel/core",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__traverse",
"@npm//@types/glob",
"@npm//@types/node",
"@npm//@types/yargs",
"@npm//glob",
],
)
esbuild_config(
name = "esbuild_config",
config_file = "esbuild.config.js",
)
esbuild(
name = "bundles",
config = ":esbuild_config",
entry_points = [
":index.ts",
":src/extract/cli.ts",
":src/migrate/cli.ts",
":src/translate/cli.ts",
],
external = [
"@angular/localize",
"@angular/compiler",
"@angular/compiler-cli/private/localize",
"@babel/core",
"@babel/types",
"yargs",
"glob",
],
format = "esm",
platform = "node",
target = "node14",
deps = [
":tools",
],
)
extract_typings(
name = "api_type_definitions",
deps = [":tools"],
)
pkg_npm(
name = "npm_package",
srcs = ["README.md"],
visibility = ["//packages/localize:__pkg__"],
deps = [
":api_type_definitions",
":bundles",
],
)

View file

@ -0,0 +1,4 @@
### Disclaimer
The localize tools are consumed via the Angular CLI. The programmatic APIs are not considered officially
supported though and are not subject to the breaking change guarantees of SemVer.

View file

@ -0,0 +1,21 @@
/**
* @license
* Copyright Google LLC 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
*/
module.exports = {
resolveExtensions: ['.mjs'],
// Note: `@bazel/esbuild` has a bug and does not pass-through the format from Starlark.
format: 'esm',
banner: {
// Workaround for: https://github.com/evanw/esbuild/issues/946
// TODO: Remove this workaround in the future once devmode is ESM as well.
js: `
import {createRequire as __cjsCompatRequire} from 'module';
const require = __cjsCompatRequire(import.meta.url);
`,
},
};

View file

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google LLC 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
*/
// Note: Before changing any exports here, consult with the Angular tooling team
// as the CLI heavily relies on exports declared here.
import {NodeJSFileSystem, setFileSystem} from '@angular/compiler-cli/private/localize';
setFileSystem(new NodeJSFileSystem());
export {DiagnosticHandlingStrategy, Diagnostics} from './src/diagnostics';
export {checkDuplicateMessages} from './src/extract/duplicates';
export {MessageExtractor} from './src/extract/extraction';
export {ArbTranslationSerializer} from './src/extract/translation_files/arb_translation_serializer';
export {SimpleJsonTranslationSerializer} from './src/extract/translation_files/json_translation_serializer';
export {LegacyMessageIdMigrationSerializer} from './src/extract/translation_files/legacy_message_id_migration_serializer';
export {Xliff1TranslationSerializer} from './src/extract/translation_files/xliff1_translation_serializer';
export {Xliff2TranslationSerializer} from './src/extract/translation_files/xliff2_translation_serializer';
export {XmbTranslationSerializer} from './src/extract/translation_files/xmb_translation_serializer';
export {buildLocalizeReplacement, isGlobalIdentifier, translate, unwrapExpressionsFromTemplateLiteral, unwrapMessagePartsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral, unwrapSubstitutionsFromLocalizeCall} from './src/source_file_utils';
export {makeEs2015TranslatePlugin} from './src/translate/source_files/es2015_translate_plugin';
export {makeEs5TranslatePlugin} from './src/translate/source_files/es5_translate_plugin';
export {makeLocalePlugin} from './src/translate/source_files/locale_plugin';
export {ArbTranslationParser} from './src/translate/translation_files/translation_parsers/arb_translation_parser';
export {SimpleJsonTranslationParser} from './src/translate/translation_files/translation_parsers/simple_json_translation_parser';
export {Xliff1TranslationParser} from './src/translate/translation_files/translation_parsers/xliff1_translation_parser';
export {Xliff2TranslationParser} from './src/translate/translation_files/translation_parsers/xliff2_translation_parser';
export {XtbTranslationParser} from './src/translate/translation_files/translation_parsers/xtb_translation_parser';

View file

@ -0,0 +1,116 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC 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 {ConsoleLogger, LogLevel, NodeJSFileSystem, setFileSystem} from '@angular/compiler-cli/private/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {DiagnosticHandlingStrategy} from '../diagnostics';
import {parseFormatOptions} from './translation_files/format_options';
import {extractTranslations} from './index';
process.title = 'Angular Localization Message Extractor (localize-extract)';
const args = process.argv.slice(2);
const options =
yargs
.option('l', {
alias: 'locale',
describe: 'The locale of the source being processed',
default: 'en',
type: 'string',
})
.option('r', {
alias: 'root',
default: '.',
describe: 'The root path for other paths provided in these options.\n' +
'This should either be absolute or relative to the current working directory.',
type: 'string',
})
.option('s', {
alias: 'source',
required: true,
describe:
'A glob pattern indicating what files to search for translations, e.g. `./dist/**/*.js`.\n' +
'This should be relative to the root path.',
type: 'string',
})
.option('f', {
alias: 'format',
required: true,
choices:
['xmb', 'xlf', 'xlif', 'xliff', 'xlf2', 'xlif2', 'xliff2', 'json', 'legacy-migrate'],
describe: 'The format of the translation file.',
type: 'string',
})
.option('formatOptions', {
describe:
'Additional options to pass to the translation file serializer, in the form of JSON formatted key-value string pairs:\n' +
'For example: `--formatOptions {"xml:space":"preserve"}.\n' +
'The meaning of the options is specific to the format being serialized.',
type: 'string'
})
.option('o', {
alias: 'outputPath',
required: true,
describe:
'A path to where the translation file will be written. This should be relative to the root path.',
type: 'string',
})
.option('loglevel', {
describe: 'The lowest severity logging message that should be output.',
choices: ['debug', 'info', 'warn', 'error'],
type: 'string',
})
.option('useSourceMaps', {
type: 'boolean',
default: true,
describe:
'Whether to generate source information in the output files by following source-map mappings found in the source files',
})
.option('useLegacyIds', {
type: 'boolean',
default: true,
describe:
'Whether to use the legacy id format for messages that were extracted from Angular templates.',
})
.option('d', {
alias: 'duplicateMessageHandling',
describe: 'How to handle messages with the same id but different text.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.strict()
.help()
.parse(args);
const fileSystem = new NodeJSFileSystem();
setFileSystem(fileSystem);
const rootPath = options.r;
const sourceFilePaths = glob.sync(options.s, {cwd: rootPath, nodir: true});
const logLevel = options.loglevel as (keyof typeof LogLevel) | undefined;
const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn);
const duplicateMessageHandling = options.d as DiagnosticHandlingStrategy;
const formatOptions = parseFormatOptions(options.formatOptions);
const format = options.f;
extractTranslations({
rootPath,
sourceFilePaths,
sourceLocale: options.l,
format,
outputPath: options.o,
logger,
useSourceMaps: options.useSourceMaps,
useLegacyIds: format === 'legacy-migrate' || options.useLegacyIds,
duplicateMessageHandling,
formatOptions,
fileSystem,
});

View file

@ -1,4 +1,3 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC All Rights Reserved.
@ -6,126 +5,22 @@
* 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 {setFileSystem, NodeJSFileSystem, AbsoluteFsPath, FileSystem, PathManipulation, ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/private/localize';
import {AbsoluteFsPath, FileSystem, Logger, PathManipulation} from '@angular/compiler-cli/private/localize';
import {ɵParsedMessage} from '@angular/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {Diagnostics, DiagnosticHandlingStrategy} from '../diagnostics';
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
import {checkDuplicateMessages} from './duplicates';
import {MessageExtractor} from './extraction';
import {TranslationSerializer} from './translation_files/translation_serializer';
import {ArbTranslationSerializer} from './translation_files/arb_translation_serializer';
import {FormatOptions} from './translation_files/format_options';
import {SimpleJsonTranslationSerializer} from './translation_files/json_translation_serializer';
import {LegacyMessageIdMigrationSerializer} from './translation_files/legacy_message_id_migration_serializer';
import {TranslationSerializer} from './translation_files/translation_serializer';
import {Xliff1TranslationSerializer} from './translation_files/xliff1_translation_serializer';
import {Xliff2TranslationSerializer} from './translation_files/xliff2_translation_serializer';
import {XmbTranslationSerializer} from './translation_files/xmb_translation_serializer';
import {FormatOptions, parseFormatOptions} from './translation_files/format_options';
import {LegacyMessageIdMigrationSerializer} from './translation_files/legacy_message_id_migration_serializer';
if (require.main === module) {
process.title = 'Angular Localization Message Extractor (localize-extract)';
const args = process.argv.slice(2);
const options =
yargs
.option('l', {
alias: 'locale',
describe: 'The locale of the source being processed',
default: 'en',
type: 'string',
})
.option('r', {
alias: 'root',
default: '.',
describe: 'The root path for other paths provided in these options.\n' +
'This should either be absolute or relative to the current working directory.',
type: 'string',
})
.option('s', {
alias: 'source',
required: true,
describe:
'A glob pattern indicating what files to search for translations, e.g. `./dist/**/*.js`.\n' +
'This should be relative to the root path.',
type: 'string',
})
.option('f', {
alias: 'format',
required: true,
choices: [
'xmb', 'xlf', 'xlif', 'xliff', 'xlf2', 'xlif2', 'xliff2', 'json', 'legacy-migrate'
],
describe: 'The format of the translation file.',
type: 'string',
})
.option('formatOptions', {
describe:
'Additional options to pass to the translation file serializer, in the form of JSON formatted key-value string pairs:\n' +
'For example: `--formatOptions {"xml:space":"preserve"}.\n' +
'The meaning of the options is specific to the format being serialized.',
type: 'string'
})
.option('o', {
alias: 'outputPath',
required: true,
describe:
'A path to where the translation file will be written. This should be relative to the root path.',
type: 'string',
})
.option('loglevel', {
describe: 'The lowest severity logging message that should be output.',
choices: ['debug', 'info', 'warn', 'error'],
type: 'string',
})
.option('useSourceMaps', {
type: 'boolean',
default: true,
describe:
'Whether to generate source information in the output files by following source-map mappings found in the source files',
})
.option('useLegacyIds', {
type: 'boolean',
default: true,
describe:
'Whether to use the legacy id format for messages that were extracted from Angular templates.',
})
.option('d', {
alias: 'duplicateMessageHandling',
describe: 'How to handle messages with the same id but different text.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.strict()
.help()
.parse(args);
const fileSystem = new NodeJSFileSystem();
setFileSystem(fileSystem);
const rootPath = options.r;
const sourceFilePaths = glob.sync(options.s, {cwd: rootPath, nodir: true});
const logLevel = options.loglevel as (keyof typeof LogLevel) | undefined;
const logger = new ConsoleLogger(logLevel ? LogLevel[logLevel] : LogLevel.warn);
const duplicateMessageHandling = options.d as DiagnosticHandlingStrategy;
const formatOptions = parseFormatOptions(options.formatOptions);
const format = options.f;
extractTranslations({
rootPath,
sourceFilePaths,
sourceLocale: options.l,
format,
outputPath: options.o,
logger,
useSourceMaps: options.useSourceMaps,
useLegacyIds: format === 'legacy-migrate' || options.useLegacyIds,
duplicateMessageHandling,
formatOptions,
fileSystem,
});
}
export interface ExtractTranslationsOptions {
/**

View file

@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC 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 {ConsoleLogger, LogLevel, NodeJSFileSystem, setFileSystem} from '@angular/compiler-cli/private/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {migrateFiles} from './index';
const args = process.argv.slice(2);
const options =
yargs
.option('r', {
alias: 'root',
default: '.',
describe: 'The root path for other paths provided in these options.\n' +
'This should either be absolute or relative to the current working directory.',
type: 'string',
})
.option('f', {
alias: 'files',
required: true,
describe:
'A glob pattern indicating what files to migrate. This should be relative to the root path',
type: 'string',
})
.option('m', {
alias: 'mapFile',
required: true,
describe:
'Path to the migration mapping file generated by `localize-extract`. This should be relative to the root path.',
type: 'string',
})
.strict()
.help()
.parse(args);
const fs = new NodeJSFileSystem();
setFileSystem(fs);
const rootPath = options.r;
const translationFilePaths = glob.sync(options.f, {cwd: rootPath, nodir: true});
const logger = new ConsoleLogger(LogLevel.warn);
migrateFiles({rootPath, translationFilePaths, mappingFilePath: options.m, logger});
process.exit(0);

View file

@ -1,4 +1,3 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC All Rights Reserved.
@ -6,51 +5,10 @@
* 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 {getFileSystem, NodeJSFileSystem, setFileSystem, ConsoleLogger, Logger, LogLevel} from '@angular/compiler-cli/private/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {getFileSystem, Logger} from '@angular/compiler-cli/private/localize';
import {migrateFile, MigrationMapping} from './migrate';
if (require.main === module) {
const args = process.argv.slice(2);
const options =
yargs
.option('r', {
alias: 'root',
default: '.',
describe: 'The root path for other paths provided in these options.\n' +
'This should either be absolute or relative to the current working directory.',
type: 'string',
})
.option('f', {
alias: 'files',
required: true,
describe:
'A glob pattern indicating what files to migrate. This should be relative to the root path',
type: 'string',
})
.option('m', {
alias: 'mapFile',
required: true,
describe:
'Path to the migration mapping file generated by `localize-extract`. This should be relative to the root path.',
type: 'string',
})
.strict()
.help()
.parse(args);
const fs = new NodeJSFileSystem();
setFileSystem(fs);
const rootPath = options.r;
const translationFilePaths = glob.sync(options.f, {cwd: rootPath, nodir: true});
const logger = new ConsoleLogger(LogLevel.warn);
migrateFiles({rootPath, translationFilePaths, mappingFilePath: options.m, logger});
process.exit(0);
}
export interface MigrateFilesOptions {
/**
* The base path for other paths provided in these options.

View file

@ -0,0 +1,131 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC 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 {NodeJSFileSystem, setFileSystem} from '@angular/compiler-cli/private/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
import {getOutputPathFn} from './output_path';
import {translateFiles} from './index';
process.title = 'Angular Localization Message Translator (localize-translate)';
const args = process.argv.slice(2);
const options =
yargs
.option('r', {
alias: 'root',
required: true,
describe:
'The root path of the files to translate, either absolute or relative to the current working directory. E.g. `dist/en`.',
type: 'string',
})
.option('s', {
alias: 'source',
required: true,
describe:
'A glob pattern indicating what files to translate, relative to the `root` path. E.g. `bundles/**/*`.',
type: 'string',
})
.option('l', {
alias: 'source-locale',
describe:
'The source locale of the application. If this is provided then a copy of the application will be created with no translation but just the `$localize` calls stripped out.',
type: 'string',
})
.option('t', {
alias: 'translations',
required: true,
array: true,
describe:
'A list of paths to the translation files to load, either absolute or relative to the current working directory.\n' +
'E.g. `-t src/locale/messages.en.xlf src/locale/messages.fr.xlf src/locale/messages.de.xlf`.\n' +
'If you want to merge multiple translation files for each locale, then provide the list of files in an array.\n' +
'Note that the arrays must be in double quotes if you include any whitespace within the array.\n' +
'E.g. `-t "[src/locale/messages.en.xlf, src/locale/messages-2.en.xlf]" [src/locale/messages.fr.xlf,src/locale/messages-2.fr.xlf]`',
type: 'string',
})
.option('target-locales', {
array: true,
describe:
'A list of target locales for the translation files, which will override any target locale parsed from the translation file.\n' +
'E.g. "-t en fr de".',
type: 'string',
})
.option('o', {
alias: 'outputPath',
required: true,
describe: 'A output path pattern to where the translated files will be written.\n' +
'The path must be either absolute or relative to the current working directory.\n' +
'The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.',
type: 'string',
})
.option('m', {
alias: 'missingTranslation',
describe: 'How to handle missing translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.option('d', {
alias: 'duplicateTranslation',
describe: 'How to handle duplicate translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.strict()
.help()
.parse(args);
const fs = new NodeJSFileSystem();
setFileSystem(fs);
const sourceRootPath = options.r;
const sourceFilePaths = glob.sync(options.s, {cwd: sourceRootPath, nodir: true});
const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options.t);
const outputPathFn = getOutputPathFn(fs, fs.resolve(options.o));
const diagnostics = new Diagnostics();
const missingTranslation = options.m as DiagnosticHandlingStrategy;
const duplicateTranslation = options.d as DiagnosticHandlingStrategy;
const sourceLocale: string|undefined = options.l;
const translationFileLocales: string[] = options['target-locales'] || [];
translateFiles({
sourceRootPath,
sourceFilePaths,
translationFilePaths,
translationFileLocales,
outputPathFn,
diagnostics,
missingTranslation,
duplicateTranslation,
sourceLocale
});
diagnostics.messages.forEach(m => console.warn(`${m.type}: ${m.message}`));
process.exit(diagnostics.hasErrors ? 1 : 0);
/**
* Parse each of the given string `args` and convert it to an array if it is of the form
* `[abc, def, ghi]`, i.e. it is enclosed in square brackets with comma delimited items.
* @param args The string to potentially convert to arrays.
*/
function convertArraysFromArgs(args: string[]): (string|string[])[] {
return args.map(
arg => (arg.startsWith('[') && arg.endsWith(']')) ?
arg.slice(1, -1).split(',').map(arg => arg.trim()) :
arg);
}

View file

@ -1,4 +1,3 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google LLC All Rights Reserved.
@ -6,13 +5,11 @@
* 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 {getFileSystem, NodeJSFileSystem, setFileSystem, relativeFrom} from '@angular/compiler-cli/private/localize';
import * as glob from 'glob';
import * as yargs from 'yargs';
import {getFileSystem, relativeFrom} from '@angular/compiler-cli/private/localize';
import {DiagnosticHandlingStrategy, Diagnostics} from '../diagnostics';
import {AssetTranslationHandler} from './asset_files/asset_translation_handler';
import {getOutputPathFn, OutputPathFn} from './output_path';
import {OutputPathFn} from './output_path';
import {SourceFileTranslationHandler} from './source_files/source_file_translation_handler';
import {TranslationLoader} from './translation_files/translation_loader';
import {ArbTranslationParser} from './translation_files/translation_parsers/arb_translation_parser';
@ -22,112 +19,6 @@ import {Xliff2TranslationParser} from './translation_files/translation_parsers/x
import {XtbTranslationParser} from './translation_files/translation_parsers/xtb_translation_parser';
import {Translator} from './translator';
if (require.main === module) {
process.title = 'Angular Localization Message Translator (localize-translate)';
const args = process.argv.slice(2);
const options =
yargs
.option('r', {
alias: 'root',
required: true,
describe:
'The root path of the files to translate, either absolute or relative to the current working directory. E.g. `dist/en`.',
type: 'string',
})
.option('s', {
alias: 'source',
required: true,
describe:
'A glob pattern indicating what files to translate, relative to the `root` path. E.g. `bundles/**/*`.',
type: 'string',
})
.option('l', {
alias: 'source-locale',
describe:
'The source locale of the application. If this is provided then a copy of the application will be created with no translation but just the `$localize` calls stripped out.',
type: 'string',
})
.option('t', {
alias: 'translations',
required: true,
array: true,
describe:
'A list of paths to the translation files to load, either absolute or relative to the current working directory.\n' +
'E.g. `-t src/locale/messages.en.xlf src/locale/messages.fr.xlf src/locale/messages.de.xlf`.\n' +
'If you want to merge multiple translation files for each locale, then provide the list of files in an array.\n' +
'Note that the arrays must be in double quotes if you include any whitespace within the array.\n' +
'E.g. `-t "[src/locale/messages.en.xlf, src/locale/messages-2.en.xlf]" [src/locale/messages.fr.xlf,src/locale/messages-2.fr.xlf]`',
type: 'string',
})
.option('target-locales', {
array: true,
describe:
'A list of target locales for the translation files, which will override any target locale parsed from the translation file.\n' +
'E.g. "-t en fr de".',
type: 'string',
})
.option('o', {
alias: 'outputPath',
required: true,
describe: 'A output path pattern to where the translated files will be written.\n' +
'The path must be either absolute or relative to the current working directory.\n' +
'The marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.',
type: 'string',
})
.option('m', {
alias: 'missingTranslation',
describe: 'How to handle missing translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.option('d', {
alias: 'duplicateTranslation',
describe: 'How to handle duplicate translations.',
choices: ['error', 'warning', 'ignore'],
default: 'warning',
type: 'string',
})
.strict()
.help()
.parse(args);
const fs = new NodeJSFileSystem();
setFileSystem(fs);
const sourceRootPath = options.r;
const sourceFilePaths = glob.sync(options.s, {cwd: sourceRootPath, nodir: true});
const translationFilePaths: (string|string[])[] = convertArraysFromArgs(options.t);
const outputPathFn = getOutputPathFn(fs, fs.resolve(options.o));
const diagnostics = new Diagnostics();
const missingTranslation = options.m as DiagnosticHandlingStrategy;
const duplicateTranslation = options.d as DiagnosticHandlingStrategy;
const sourceLocale: string|undefined = options.l;
const translationFileLocales: string[] = options['target-locales'] || [];
translateFiles({
sourceRootPath,
sourceFilePaths,
translationFilePaths,
translationFileLocales,
outputPathFn,
diagnostics,
missingTranslation,
duplicateTranslation,
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
@ -235,15 +126,3 @@ export function translateFiles({
sourceFilePaths.map(relativeFrom), fs.resolve(sourceRootPath), outputPathFn, translations,
sourceLocale);
}
/**
* Parse each of the given string `args` and convert it to an array if it is of the form
* `[abc, def, ghi]`, i.e. it is enclosed in square brackets with comma delimited items.
* @param args The string to potentially convert to arrays.
*/
function convertArraysFromArgs(args: string[]): (string|string[])[] {
return args.map(
arg => (arg.startsWith('[') && arg.endsWith(']')) ?
arg.slice(1, -1).split(',').map(arg => arg.trim()) :
arg);
}

View file

@ -5,7 +5,7 @@
* 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 {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
import {Diagnostics} from '../../../diagnostics';
/**

View file

@ -6,7 +6,7 @@ ts_library(
srcs = glob(
["**/*.ts"],
),
visibility = ["//packages/localize/src/tools/test:__subpackages__"],
visibility = ["//packages/localize/tools/test:__subpackages__"],
deps = [
"//packages:types",
"//packages/compiler",
@ -14,9 +14,9 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/localize",
"//packages/localize/src/tools",
"//packages/localize/src/tools/test/helpers",
"//packages/localize/src/utils",
"//packages/localize/tools",
"//packages/localize/tools/test/helpers",
"@npm//@babel/core",
"@npm//@babel/generator",
"@npm//@babel/template",

View file

@ -13,9 +13,9 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/logging",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/localize/src/tools",
"//packages/localize/src/tools/test:test_lib",
"//packages/localize/src/tools/test/helpers",
"//packages/localize/tools",
"//packages/localize/tools/test:test_lib",
"//packages/localize/tools/test/helpers",
],
)
@ -23,9 +23,9 @@ jasmine_node_test(
name = "integration",
bootstrap = ["//tools/testing:node_no_angular_es5"],
data = [
"//packages/localize/src/tools/test/extract/integration/test_files",
"//packages/localize/src/tools/test/extract/integration/test_files:compile_es2015",
"//packages/localize/src/tools/test/extract/integration/test_files:compile_es5",
"//packages/localize/tools/test/extract/integration/test_files",
"//packages/localize/tools/test/extract/integration/test_files:compile_es2015",
"//packages/localize/tools/test/extract/integration/test_files:compile_es5",
],
deps = [
":test_lib",

View file

@ -10,7 +10,7 @@ import {InvalidFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/src
import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
import {loadTestDirectory} from '@angular/compiler-cli/src/ngtsc/testing';
import {extractTranslations} from '../../../src/extract/main';
import {extractTranslations} from '../../../src/extract/index';
import {FormatOptions} from '../../../src/extract/translation_files/format_options';
import {runInNativeFileSystem} from '../../helpers';
import {toAttributes} from '../translation_files/utils';
@ -398,7 +398,7 @@ runInNativeFileSystem(() => {
` <context-group purpose="location">`,
// These source file paths are due to how Bazel TypeScript compilation source-maps
// work
` <context context-type="sourcefile">../packages/localize/src/tools/test/extract/integration/test_files/src/a.ts</context>`,
` <context context-type="sourcefile">../packages/localize/tools/test/extract/integration/test_files/src/a.ts</context>`,
` <context context-type="linenumber">3,${
target === 'es2015' ? 7 : 5}</context>`,
` </context-group>`,
@ -406,7 +406,7 @@ runInNativeFileSystem(() => {
` <trans-unit id="7829869508202074508" datatype="html">`,
` <source>Message in <x id="b-file" equiv-text="file"/>!</source>`,
` <context-group purpose="location">`,
` <context context-type="sourcefile">../packages/localize/src/tools/test/extract/integration/test_files/src/b.ts</context>`,
` <context context-type="sourcefile">../packages/localize/tools/test/extract/integration/test_files/src/b.ts</context>`,
` <context context-type="linenumber">3</context>`,
` </context-group>`,
` </trans-unit>`,

View file

@ -1,7 +1,7 @@
load("@npm//typescript:index.bzl", "tsc")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
package(default_visibility = ["//packages/localize/src/tools/test/extract/integration:__pkg__"])
package(default_visibility = ["//packages/localize/tools/test/extract/integration:__pkg__"])
tsc(
name = "compile_es5",

View file

@ -6,7 +6,7 @@ ts_library(
srcs = glob(
["**/*.ts"],
),
visibility = ["//packages/localize/src/tools/test:__subpackages__"],
visibility = ["//packages/localize/tools/test:__subpackages__"],
deps = [
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",

View file

@ -14,9 +14,9 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/logging",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/localize/src/tools",
"//packages/localize/src/tools/test:test_lib",
"//packages/localize/src/tools/test/helpers",
"//packages/localize/tools",
"//packages/localize/tools/test:test_lib",
"//packages/localize/tools/test/helpers",
],
)

View file

@ -8,7 +8,7 @@
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {MockLogger} from '@angular/compiler-cli/src/ngtsc/logging/testing';
import {loadTestDirectory} from '@angular/compiler-cli/src/ngtsc/testing';
import {migrateFiles} from '../../../src/migrate/main';
import {migrateFiles} from '../../../src/migrate/index';
import {runInNativeFileSystem} from '../../helpers';
runInNativeFileSystem(() => {

View file

@ -11,8 +11,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/localize/src/tools",
"//packages/localize/src/tools/test/helpers",
"//packages/localize/tools",
"//packages/localize/tools/test/helpers",
],
)
@ -20,8 +20,8 @@ jasmine_node_test(
name = "integration",
bootstrap = ["//tools/testing:node_no_angular_es5"],
data = [
"//packages/localize/src/tools/test/translate/integration/locales",
"//packages/localize/src/tools/test/translate/integration/test_files",
"//packages/localize/tools/test/translate/integration/locales",
"//packages/localize/tools/test/translate/integration/test_files",
],
deps = [
":test_lib",

View file

@ -1,6 +1,6 @@
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
package(default_visibility = ["//packages/localize/src/tools/test/translate/integration:__pkg__"])
package(default_visibility = ["//packages/localize/tools/test/translate/integration:__pkg__"])
# Use copy_to_bin since filegroup doesn't seem to work on Windows.
copy_to_bin(

View file

@ -10,7 +10,7 @@ import {loadTestDirectory} from '@angular/compiler-cli/src/ngtsc/testing';
import {resolve as realResolve} from 'path';
import {Diagnostics} from '../../../src/diagnostics';
import {translateFiles} from '../../../src/translate/main';
import {translateFiles} from '../../../src/translate/index';
import {getOutputPathFn} from '../../../src/translate/output_path';
import {runInNativeFileSystem} from '../../helpers';

View file

@ -1,6 +1,6 @@
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
package(default_visibility = ["//packages/localize/src/tools/test/translate/integration:__pkg__"])
package(default_visibility = ["//packages/localize/tools/test/translate/integration:__pkg__"])
# Use copy_to_bin since filegroup doesn't seem to work on Windows.
copy_to_bin(

View file

@ -14,6 +14,7 @@ def _extract_typings_rule_impl(ctx):
return [DefaultInfo(files = depset(transitive = transitive_depsets))]
# TODO: Move into shared dev-infra package.
extract_typings = rule(
implementation = _extract_typings_rule_impl,
doc = """Rule that extracts all transitive typings of dependencies""",