Compare commits

...

38 Commits

Author SHA1 Message Date
Kim Biesbjerg
64ebb5e6e8 Bump version 2017-11-08 14:02:41 +01:00
Kim Biesbjerg
40051f4144 Bump version 2017-11-07 15:17:38 +01:00
Sean G. Wright
14eb09f947 feat(cli): add verbose (vb) flag that can control output of all file … (#74)
* feat(cli): add verbose (vb) flag that can control output of all file paths to console

* docs(README): add -vb description
2017-11-07 15:14:31 +01:00
Tiago Dionesto Willrich da Silva
8d1e2c5a2f Change typescript to be a dependency (#75)
Typescript is being imported by the parser files, so it should be a direct dependency instead of just a development one.

Without that change, it's impossible to use the extractor programatically or with npx.
2017-11-07 15:13:35 +01:00
Dominik Herbst
4892ea5146 Configured cheerio to work with non-HTML standard elements to fix issues with custom component tags. (#79) 2017-11-07 15:13:01 +01:00
Kim Biesbjerg
ee28fe2a64 Create LICENSE 2017-07-08 14:10:54 +02:00
Kim Biesbjerg
7c06b66974 Bump version 2017-07-05 15:46:05 +02:00
Kim Biesbjerg
b2ae17697d Replace all occurences of escaped quotes. 2017-07-05 15:43:54 +02:00
Kim Biesbjerg
5259da8fe3 Add support for TranslateService's stream method. Closes #60 2017-07-05 15:18:41 +02:00
Kim Biesbjerg
2d73f056ff Update dependencies 2017-07-05 15:16:17 +02:00
Kim Biesbjerg
fde5245731 Bump version 2017-05-10 14:10:36 +02:00
Kim Biesbjerg
4ee7258a31 Fix potential bug when extracting strings from file containing multiple classes 2017-05-10 14:10:13 +02:00
Kim Biesbjerg
a6c7af0630 Update dependencies 2017-05-10 14:07:45 +02:00
Kim Biesbjerg
0949bf765b Bump version 2017-05-09 20:09:03 +02:00
Kim Biesbjerg
d416c6b9fd Add support for extracting strings from multiple classes per file. Closes #46 2017-05-09 20:08:44 +02:00
Kim Biesbjerg
4e351405fb Bump version 2017-05-09 14:40:06 +02:00
Kim Biesbjerg
39a335638b Add support for parsing NamespacedJson in Json compiler. Closes #44 2017-05-09 14:39:39 +02:00
Kim Biesbjerg
3b9561916b Fix crash when constructor parameter has no type. Closes #38 2017-05-05 11:31:30 +02:00
Kim Biesbjerg
5cef383f3b Forgot to build v2.2.2 before publishing to npm 2017-05-05 11:18:51 +02:00
Kim Biesbjerg
677d2a35ca Update dependencies. Bump version 2017-04-06 08:52:22 +02:00
cvaliere
262a89206d fix parser regexp (#31)
- fix template parser regexp (Closes #15)
2017-04-06 08:50:23 +02:00
Kim Biesbjerg
bcb4a9c069 Fix bug where obsolete strings were not removed when --clean was used. Closes #29 2017-03-31 08:31:14 +02:00
Kim Biesbjerg
5ad1fe6a18 Remove unused import 2017-03-31 08:21:28 +02:00
Kim Biesbjerg
bc5ce7e80d Add marker argument to readme 2017-03-30 14:42:03 +02:00
Kim Biesbjerg
030ab145d6 Add return types 2017-03-30 14:40:51 +02:00
Kim Biesbjerg
daaebede6f Add support for marker functions, to be able to extract strings not directly passed to TranslateService. Closes #10 2017-03-30 14:37:30 +02:00
Kim Biesbjerg
42a6568d47 Fix lint error 2017-03-29 14:20:35 +02:00
Kim Biesbjerg
9a2108e9a9 Add cli option to customize compiler indentation. Closes #22 2017-03-29 14:20:01 +02:00
Kim Biesbjerg
ff1c91010e Fix namespaced-json compiler not preserving numeric values. Fixes #28 2017-03-29 12:52:14 +02:00
Kim Biesbjerg
7654397bb3 Update README 2017-03-21 15:59:18 +01:00
Kim Biesbjerg
7b10b246e6 Update cli option descriptions. Change default filename to strings.format 2017-03-21 15:59:03 +01:00
Kim Biesbjerg
2f68bbc660 Fix package exports 2017-03-21 15:30:04 +01:00
Kim Biesbjerg
371a14d73e Read version directly from package.json 2017-03-21 15:27:24 +01:00
Kim Biesbjerg
4537c1224a Refactored Extractor and cli 2017-03-21 15:21:39 +01:00
Kim Biesbjerg
f61cdc4064 Rename cli script filename. Rename option 'dir' to 'input' 2017-03-20 15:51:38 +01:00
Kim Biesbjerg
72739f2e9a Rename Test class for ServiceParser 2017-03-20 15:32:49 +01:00
Kim Biesbjerg
3facc0c287 - Added typings for packages where typings existed
- Remove regexp ServiceParser and make AstServiceParser the default. #23
- Replaced CLI parser to add support for multiple input/output paths (supports file expansion, glob patterns and multiple values/parameters) #7
2017-03-20 15:29:41 +01:00
Kim Biesbjerg
c5d68cfcaa Check if parameter has a type before attempting to access. Should fix #26 2017-03-20 15:17:46 +01:00
34 changed files with 2124 additions and 565 deletions

4
.gitignore vendored
View File

@@ -10,8 +10,8 @@ npm-debug.log*
dist
# Extracted strings
template.json
template.pot
strings.json
strings.pot
# Dependency directory
node_modules

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Kim Biesbjerg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -12,29 +12,86 @@ Install the package in your project:
Add an `extract` script to your project's `package.json`:
```
"scripts": {
"extract": "ngx-translate-extract --dir ./src --output ./ --format=json --clean"
"extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/*.json --clean --sort --format namespaced-json"
}
```
You can now run `npm run extract` to extract strings from your project's `src` dir. The extracted strings are saved in `JSON`-format in your project's root.
You can now run `npm run extract` to extract strings.
## Extract examples
**Extract from dir and save to file**
`ngx-translate-extract -i ./src -o ./src/i18n/strings.json`
**Extract from multiple dirs**
`ngx-translate-extract -i ./src/folder-a ./src/folder-b -o ./src/i18n/strings.json`
**Extract and save to multiple files**
`ngx-translate-extract -i ./src -o ./src/i18n/{da,en,fr}.json`
**or**
`ngx-translate-extract -i ./src -o ./src/i18n/da.json ./src/i18n/en.json ./src/i18n/fr.json`
**or (update only)**
`ngx-translate-extract -i ./src -o ./src/i18n/*.json`
**or (update only)**
## Custom indentation
By default, tabs are used for indentation when writing extracted strings to json formats:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation $'\t'`
If you want to use spaces instead, you can do the following:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation ' '`
## Mark strings for extraction using a marker function
If, for some reason, you want to extract strings not passed directly to TranslateService, you can wrap them in a custom marker function.
```ts
import { _ } from '@biesbjerg/ngx-translate-extract';
_('Extract me');
```
Add the `marker` argument when running the extract script:
`ngx-translate-extract ... -m _`
Modify the scripts arguments as required.
## Commandline arguments
```
Usage:
ngx-translate-extract [OPTIONS] [ARGS]
ngx-translate-extract [options]
Options:
-d, --dir [DIR] Directory path you would like to extract strings from (Default is current directory)
-o, --output [DIR] Directory path you would like to save extracted
strings (Default is current directory/template.json)
-f, --format [VALUE] Output format. VALUE must be either
[json|namespaced-json|pot] (Default is json)
-r, --replace BOOLEAN Replace the contents of output file if it exists
(Merges by default)
-s, --sort BOOLEAN Sort translations in the output file in alphabetical
order
-c, --clean BOOLEAN Remove obsolete strings when merging
-e, --experimental BOOLEAN Use experimental AST Service Parser
-h, --help Display help and usage details
```
--version, -v Show version number [boolean]
--help, -h Show help [boolean]
--input, -i Paths you would like to extract strings from. You
can use path expansion, glob patterns and multiple
paths
[array] [default: current working path]
--patterns, -p Extract strings from the following file patterns
[array] [default: ["/**/*.html","/**/*.ts"]]
--output, -o Paths where you would like to save extracted
strings. You can use path expansion, glob patterns
and multiple paths [array] [required]
--marker, -m Extract strings passed to a marker function
[string] [default: false]
--format, -f Output format
[string] [choices: "json", "namespaced-json", "pot"] [default: "json"]
--format-indentation, --fi Output format indentation [string] [default: "\t"]
--replace, -r Replace the contents of output file if it exists
(Merges by default) [boolean] [default: false]
--sort, -s Sort strings in alphabetical order when saving
[boolean] [default: false]
--clean, -c Remove obsolete strings when merging
[boolean] [default: false]
--verbose, -vb If true, prints all processed file paths to console
[boolean] [default: true]

3
bin/cli.js Executable file
View File

@@ -0,0 +1,3 @@
#! /usr/bin/env node
require('../dist/cli/cli');

View File

@@ -1,3 +0,0 @@
#! /usr/bin/env node
require('../dist/cli/extract');

1278
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@biesbjerg/ngx-translate-extract",
"version": "1.0.0",
"version": "2.3.4",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -9,7 +9,7 @@
"dist/"
],
"bin": {
"ngx-translate-extract": "bin/extract.js"
"ngx-translate-extract": "bin/cli.js"
},
"scripts": {
"build": "npm run clean && tsc",
@@ -47,24 +47,30 @@
},
"config": {},
"devDependencies": {
"@types/chai": "3.4.35",
"@types/cheerio": "0.17.31",
"@types/chai": "4.0.1",
"@types/glob": "5.0.30",
"@types/mocha": "2.2.39",
"chai": "3.5.0",
"mocha": "3.2.0",
"ts-node": "2.1.0",
"tslint": "4.5.1",
"tslint-eslint-rules": "3.4.0",
"typescript": "2.2.1"
"@types/mocha": "2.2.41",
"@types/cheerio": "0.22.1",
"@types/chalk": "0.4.31",
"@types/flat": "0.0.28",
"@types/yargs": "8.0.0",
"@types/mkdirp": "0.3.29",
"chai": "4.0.2",
"mocha": "3.4.2",
"ts-node": "3.1.0",
"tslint": "5.4.3",
"tslint-eslint-rules": "4.1.1"
},
"dependencies": {
"cheerio": "0.22.0",
"cli": "1.0.1",
"chalk": "2.0.1",
"yargs": "8.0.2",
"cheerio": "1.0.0-rc.2",
"fs": "0.0.1-security",
"gettext-parser": "1.2.2",
"glob": "7.1.1",
"glob": "7.1.2",
"path": "0.12.7",
"flat": "2.0.1"
"mkdirp": "0.5.1",
"flat": "2.0.1",
"typescript": "2.4.1"
}
}

118
src/cli/cli.ts Executable file
View File

@@ -0,0 +1,118 @@
import { ExtractTask } from './tasks/extract.task';
import { ParserInterface } from '../parsers/parser.interface';
import { PipeParser } from '../parsers/pipe.parser';
import { DirectiveParser } from '../parsers/directive.parser';
import { ServiceParser } from '../parsers/service.parser';
import { FunctionParser } from '../parsers/function.parser';
import { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory';
import * as fs from 'fs';
import * as yargs from 'yargs';
export const cli = yargs
.usage('Extract strings from files for translation.\nUsage: $0 [options]')
.version(require(__dirname + '/../../package.json').version)
.alias('version', 'v')
.help('help')
.alias('help', 'h')
.option('input', {
alias: 'i',
describe: 'Paths you would like to extract strings from. You can use path expansion, glob patterns and multiple paths',
default: process.env.PWD,
type: 'array',
normalize: true
})
.check(options => {
options.input.forEach((dir: string) => {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
throw new Error(`The path you supplied was not found: '${dir}'`);
}
});
return true;
})
.option('patterns', {
alias: 'p',
describe: 'Extract strings from the following file patterns',
type: 'array',
default: ['/**/*.html', '/**/*.ts']
})
.option('output', {
alias: 'o',
describe: 'Paths where you would like to save extracted strings. You can use path expansion, glob patterns and multiple paths',
type: 'array',
normalize: true,
required: true
})
.option('marker', {
alias: 'm',
describe: 'Extract strings passed to a marker function',
default: false,
type: 'string'
})
.option('format', {
alias: 'f',
describe: 'Output format',
default: 'json',
type: 'string',
choices: ['json', 'namespaced-json', 'pot']
})
.option('format-indentation', {
alias: 'fi',
describe: 'Output format indentation',
default: '\t',
type: 'string'
})
.option('replace', {
alias: 'r',
describe: 'Replace the contents of output file if it exists (Merges by default)',
default: false,
type: 'boolean'
})
.option('sort', {
alias: 's',
describe: 'Sort strings in alphabetical order when saving',
default: false,
type: 'boolean'
})
.option('clean', {
alias: 'c',
describe: 'Remove obsolete strings when merging',
default: false,
type: 'boolean'
})
.option('verbose', {
alias: 'vb',
describe: 'Log all output to console',
default: true,
type: 'boolean'
})
.exitProcess(true)
.parse(process.argv);
const extract = new ExtractTask(cli.input, cli.output, {
replace: cli.replace,
sort: cli.sort,
clean: cli.clean,
patterns: cli.patterns
});
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extract.setCompiler(compiler);
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
new ServiceParser()
];
if (cli.marker) {
parsers.push(new FunctionParser({
identifier: cli.marker
}));
}
extract.setParsers(parsers);
extract.execute();

View File

@@ -1,105 +0,0 @@
import { Extractor } from '../utils/extractor';
import { TranslationCollection } from '../utils/translation.collection';
import { ParserInterface } from '../parsers/parser.interface';
import { PipeParser } from '../parsers/pipe.parser';
import { DirectiveParser } from '../parsers/directive.parser';
import { ServiceParser } from '../parsers/service.parser';
import { AstServiceParser } from '../parsers/ast-service.parser';
import { CompilerInterface } from '../compilers/compiler.interface';
import { JsonCompiler } from '../compilers/json.compiler';
import { NamespacedJsonCompiler } from '../compilers/namespaced-json.compiler';
import { PoCompiler } from '../compilers/po.compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as cli from 'cli';
const options = cli.parse({
dir: ['d', 'Path you would like to extract strings from', 'dir', process.env.PWD],
output: ['o', 'Path you would like to save extracted strings to', 'dir', process.env.PWD],
format: ['f', 'Output format', ['json', 'namespaced-json', 'pot'], 'json'],
replace: ['r', 'Replace the contents of output file if it exists (Merges by default)', 'boolean', false],
sort: ['s', 'Sort translations in the output file in alphabetical order', 'boolean', false],
clean: ['c', 'Remove obsolete strings when merging', 'boolean', false],
experimental: ['e', 'Use experimental AST Service Parser', 'boolean', false]
});
const patterns: string[] = [
'/**/*.html',
'/**/*.ts'
];
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
options.experimental ? new AstServiceParser() : new ServiceParser()
];
let compiler: CompilerInterface;
let ext: string;
switch (options.format) {
case 'pot':
compiler = new PoCompiler();
ext = 'pot';
break;
case 'json':
compiler = new JsonCompiler();
ext = 'json';
break;
case 'namespaced-json':
compiler = new NamespacedJsonCompiler();
ext = 'json';
break;
}
const normalizedDir: string = path.resolve(options.dir);
const normalizedOutput: string = path.resolve(options.output);
let outputDir: string = normalizedOutput;
let outputFilename: string = `template.${ext}`;
if (!fs.existsSync(normalizedOutput) || !fs.statSync(normalizedOutput).isDirectory()) {
outputDir = path.dirname(normalizedOutput);
outputFilename = path.basename(normalizedOutput);
}
const outputPath: string = path.join(outputDir, outputFilename);
[normalizedDir, outputDir].forEach(dir => {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
cli.fatal(`The path you supplied was not found: '${dir}'`);
}
});
try {
const extractor: Extractor = new Extractor(parsers, patterns);
cli.info(`Extracting strings from '${normalizedDir}'`);
const extracted: TranslationCollection = extractor.process(normalizedDir);
cli.ok(`* Extracted ${extracted.count()} strings`);
let collection: TranslationCollection = extracted;
if (!options.replace && fs.existsSync(outputPath)) {
const existing: TranslationCollection = compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
if (existing.count() > 0) {
collection = extracted.union(existing);
cli.ok(`* Merged with ${existing.count()} existing strings`);
}
if (options.clean) {
const collectionCount = collection.count();
collection = collection.intersect(extracted);
const removeCount = collectionCount - collection.count();
if (removeCount > 0) {
cli.ok(`* Removed ${removeCount} obsolete strings`);
}
}
}
if (options.sort) {
collection = collection.sort();
}
fs.writeFileSync(outputPath, compiler.compile(collection));
cli.ok(`* Saved to '${outputPath}'`);
} catch (e) {
cli.fatal(e.toString());
}

View File

@@ -0,0 +1,147 @@
import { TranslationCollection } from '../../utils/translation.collection';
import { TaskInterface } from './task.interface';
import { ParserInterface } from '../../parsers/parser.interface';
import { CompilerInterface } from '../../compilers/compiler.interface';
import * as chalk from 'chalk';
import * as glob from 'glob';
import * as fs from 'fs';
import * as path from 'path';
import * as mkdirp from 'mkdirp';
export interface ExtractTaskOptionsInterface {
replace?: boolean;
sort?: boolean;
clean?: boolean;
patterns?: string[];
verbose?: boolean;
}
export class ExtractTask implements TaskInterface {
protected _options: ExtractTaskOptionsInterface = {
replace: false,
sort: false,
clean: false,
patterns: [],
verbose: true
};
protected _parsers: ParserInterface[] = [];
protected _compiler: CompilerInterface;
public constructor(protected _input: string[], protected _output: string[], options?: ExtractTaskOptionsInterface) {
this._options = Object.assign({}, this._options, options);
}
public execute(): void {
if (!this._parsers) {
throw new Error('No parsers configured');
}
if (!this._compiler) {
throw new Error('No compiler configured');
}
const collection = this._extract();
this._out(chalk.green('Extracted %d strings\n'), collection.count());
this._save(collection);
}
public setParsers(parsers: ParserInterface[]): this {
this._parsers = parsers;
return this;
}
public setCompiler(compiler: CompilerInterface): this {
this._compiler = compiler;
return this;
}
/**
* Extract strings from input dirs using configured parsers
*/
protected _extract(): TranslationCollection {
this._out(chalk.bold('Extracting strings...'));
let collection: TranslationCollection = new TranslationCollection();
this._input.forEach(dir => {
this._readDir(dir, this._options.patterns).forEach(path => {
this._options.verbose && this._out(chalk.gray('- %s'), path);
const contents: string = fs.readFileSync(path, 'utf-8');
this._parsers.forEach((parser: ParserInterface) => {
collection = collection.union(parser.extract(contents, path));
});
});
});
return collection;
}
/**
* Process collection according to options (merge, clean, sort), compile and save
* @param collection
*/
protected _save(collection: TranslationCollection): void {
this._output.forEach(output => {
const normalizedOutput: string = path.resolve(output);
let dir: string = normalizedOutput;
let filename: string = `strings.${this._compiler.extension}`;
if (!fs.existsSync(normalizedOutput) || !fs.statSync(normalizedOutput).isDirectory()) {
dir = path.dirname(normalizedOutput);
filename = path.basename(normalizedOutput);
}
const outputPath: string = path.join(dir, filename);
let processedCollection: TranslationCollection = collection;
this._out(chalk.bold('\nSaving: %s'), outputPath);
if (fs.existsSync(outputPath) && !this._options.replace) {
const existingCollection: TranslationCollection = this._compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
if (!existingCollection.isEmpty()) {
processedCollection = processedCollection.union(existingCollection);
this._out(chalk.dim('- merged with %d existing strings'), existingCollection.count());
}
if (this._options.clean) {
const collectionCount = processedCollection.count();
processedCollection = processedCollection.intersect(collection);
const removeCount = collectionCount - processedCollection.count();
if (removeCount > 0) {
this._out(chalk.dim('- removed %d obsolete strings'), removeCount);
}
}
}
if (this._options.sort) {
processedCollection = processedCollection.sort();
this._out(chalk.dim('- sorted strings'));
}
if (!fs.existsSync(dir)) {
mkdirp.sync(dir);
this._out(chalk.dim('- created dir: %s'), dir);
}
fs.writeFileSync(outputPath, this._compiler.compile(processedCollection));
this._out(chalk.green('Done!'));
});
}
/**
* Get all files in dir matching patterns
*/
protected _readDir(dir: string, patterns: string[]): string[] {
return patterns.reduce((results, pattern) => {
return glob.sync(dir + pattern)
.filter(path => fs.statSync(path).isFile())
.concat(results);
}, []);
}
protected _out(...args: any[]): void {
console.log.apply(this, arguments);
}
}

View File

@@ -0,0 +1,3 @@
export interface TaskInterface {
execute(): void;
}

View File

@@ -0,0 +1,17 @@
import { CompilerInterface } from '../compilers/compiler.interface';
import { JsonCompiler } from '../compilers/json.compiler';
import { NamespacedJsonCompiler } from '../compilers/namespaced-json.compiler';
import { PoCompiler } from '../compilers/po.compiler';
export class CompilerFactory {
public static create(format: string, options?: {}): CompilerInterface {
switch (format) {
case 'pot': return new PoCompiler(options);
case 'json': return new JsonCompiler(options);
case 'namespaced-json': return new NamespacedJsonCompiler(options);
default: throw new Error(`Unknown format: ${format}`);
}
}
}

View File

@@ -2,6 +2,8 @@ import { TranslationCollection } from '../utils/translation.collection';
export interface CompilerInterface {
extension: string;
compile(collection: TranslationCollection): string;
parse(contents: string): TranslationCollection;

View File

@@ -1,14 +1,34 @@
import { CompilerInterface } from './compiler.interface';
import { TranslationCollection } from '../utils/translation.collection';
import * as flat from 'flat';
export class JsonCompiler implements CompilerInterface {
public indentation: string = '\t';
public extension = 'json';
public constructor(options?: any) {
if (options && typeof options.indentation !== 'undefined') {
this.indentation = options.indentation;
}
}
public compile(collection: TranslationCollection): string {
return JSON.stringify(collection.values, null, '\t');
return JSON.stringify(collection.values, null, this.indentation);
}
public parse(contents: string): TranslationCollection {
return new TranslationCollection(JSON.parse(contents));
let values: any = JSON.parse(contents);
if (this._isNamespacedJsonFormat(values)) {
values = flat.flatten(values);
}
return new TranslationCollection(values);
}
protected _isNamespacedJsonFormat(values: any): boolean {
return Object.keys(values).some(key => typeof values[key] === 'object');
}
}

View File

@@ -5,13 +5,25 @@ import * as flat from 'flat';
export class NamespacedJsonCompiler implements CompilerInterface {
public indentation: string = '\t';
public extension = 'json';
public constructor(options?: any) {
if (options && typeof options.indentation !== 'undefined') {
this.indentation = options.indentation;
}
}
public compile(collection: TranslationCollection): string {
const values = flat.unflatten(collection.values);
return JSON.stringify(values, null, '\t');
const values: {} = flat.unflatten(collection.values, {
object: true
});
return JSON.stringify(values, null, this.indentation);
}
public parse(contents: string): TranslationCollection {
const values = flat.flatten(JSON.parse(contents));
const values: {} = flat.flatten(JSON.parse(contents));
return new TranslationCollection(values);
}

View File

@@ -5,11 +5,15 @@ import * as gettext from 'gettext-parser';
export class PoCompiler implements CompilerInterface {
public extension = 'po';
/**
* Translation domain
*/
public domain = '';
public constructor(options?: any) { }
public compile(collection: TranslationCollection): string {
const data = {
charset: 'utf-8',

View File

@@ -1,3 +1 @@
declare module 'cli';
declare module 'flat';
declare module 'gettext-parser';

View File

@@ -1,12 +1,20 @@
export * from './utils/extractor';
export * from './utils/translation.collection';
export * from './utils/utils';
export * from './cli/cli';
export * from './cli/tasks/task.interface';
export * from './cli/tasks/extract.task';
export * from './parsers/parser.interface';
export * from './parsers/abstract-template.parser';
export * from './parsers/abstract-ast.parser';
export * from './parsers/directive.parser';
export * from './parsers/pipe.parser';
export * from './parsers/service.parser';
export * from './parsers/function.parser';
export * from './compilers/compiler.interface';
export * from './compilers/compiler.factory';
export * from './compilers/json.compiler';
export * from './compilers/namespaced-json.compiler';
export * from './compilers/po.compiler';

View File

@@ -0,0 +1,65 @@
import * as ts from 'typescript';
export abstract class AbstractAstParser {
protected _sourceFile: ts.SourceFile;
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${this._syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind): ts.Node[] {
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
protected _syntaxKindToName(kind: ts.SyntaxKind): string {
return ts.SyntaxKind[kind];
}
protected _printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0): void {
console.log(
new Array(depth + 1).join('----'),
`[${node.kind}]`,
this._syntaxKindToName(node.kind),
`[pos: ${node.pos}-${node.end}]`,
':\t\t\t',
node.getFullText(sourceFile).trim()
);
depth++;
node.getChildren(sourceFile).forEach(childNode => this._printAllChildren(sourceFile, childNode, depth));
}
}

View File

@@ -1,159 +0,0 @@
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
import * as ts from 'typescript';
export class AstServiceParser implements ParserInterface {
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
this._instancePropertyName = this._getInstancePropertyName();
if (!this._instancePropertyName) {
return collection;
}
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor params
*/
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
if (!constructorNode) {
return null;
}
const result = constructorNode.parameters.find(parameter => {
// Skip if visibility modifier is not present (we want it set as an instance property)
if (!parameter.modifiers) {
return false;
}
// Make sure className is of the correct type
const className: string = ((parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier).text;
if (className !== this._serviceClassName) {
return false;
}
return true;
});
if (result) {
return (result.name as ts.Identifier).text;
}
}
/**
* Find first constructor
*/
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
// Only call expressions with arguments
.filter(callNode => callNode.arguments.length > 0)
// More filters
.filter(callNode => {
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== this._instancePropertyName) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
return false;
}
return true;
});
return callNodes;
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] {
if (node.kind === kind && onlyOne) {
return [node];
}
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
}

View File

@@ -2,7 +2,9 @@ import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection';
import * as $ from 'cheerio';
import * as cheerio from 'cheerio';
const $ = cheerio.load('', {xmlMode: true});
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {

View File

@@ -0,0 +1,61 @@
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import * as ts from 'typescript';
export class FunctionParser extends AbstractAstParser implements ParserInterface {
protected _functionIdentifier: string = '_';
public constructor(options?: any) {
super();
if (options && typeof options.identifier !== 'undefined') {
this._functionIdentifier = options.identifier;
}
}
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
/**
* Find all calls to marker function
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const identifier = (callNode.getChildAt(0) as ts.Identifier).text;
if (identifier !== this._functionIdentifier) {
return false;
}
return true;
});
return callNodes;
}
}

View File

@@ -15,10 +15,10 @@ export class PipeParser extends AbstractTemplateParser implements ParserInterfac
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const regExp: RegExp = /(['"`])([^>\1\r\n]*?)\1\s*\|\s*translate/g;
const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g;
let matches: RegExpExecArray;
while (matches = regExp.exec(template)) {
collection = collection.add(matches[2]);
collection = collection.add(matches[2].split('\\\'').join('\''));
}
return collection;

View File

@@ -1,59 +1,131 @@
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
export class ServiceParser implements ParserInterface {
import * as ts from 'typescript';
export class ServiceParser extends AbstractAstParser implements ParserInterface {
protected _sourceFile: ts.SourceFile;
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) {
return collection;
}
const methodRegExp: RegExp = /(?:get|instant)\s*\(\s*(\[?\s*(['"`])([^\1\r\n]*)\2\s*\]?)/;
const regExp: RegExp = new RegExp(`\\.${translateServiceVar}\\.${methodRegExp.source}`, 'g');
let matches: RegExpExecArray;
while (matches = regExp.exec(contents)) {
if (this._stringContainsArray(matches[1])) {
collection = collection.addKeys(this._stringToArray(matches[1]));
} else {
collection = collection.add(matches[3]);
this._sourceFile = this._createSourceFile(path, contents);
const classNodes = this._findClassNodes(this._sourceFile);
classNodes.forEach(classNode => {
const constructorNode = this._findConstructorNode(classNode);
if (!constructorNode) {
return;
}
}
const propertyName: string = this._findTranslateServicePropertyName(constructorNode);
if (!propertyName) {
return;
}
const callNodes = this._findCallNodes(classNode, propertyName);
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
});
return collection;
}
/**
* Extracts name of TranslateService variable for use in patterns
* Detect what the TranslateService instance property
* is called by inspecting constructor arguments
*/
protected _extractTranslateServiceVar(contents: string): string {
const matches = contents.match(/([a-z0-9_]+)\s*:\s*TranslateService/i);
if (matches === null) {
return '';
protected _findTranslateServicePropertyName(constructorNode: ts.ConstructorDeclaration): string {
if (!constructorNode) {
return null;
}
return matches[1];
const result = constructorNode.parameters.find(parameter => {
// Skip if visibility modifier is not present (we want it set as an instance property)
if (!parameter.modifiers) {
return false;
}
// Parameter has no type
if (!parameter.type) {
return false;
}
// Make sure className is of the correct type
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== 'TranslateService') {
return false;
}
return true;
});
if (result) {
return (result.name as ts.Identifier).text;
}
}
/**
* Checks if string contains an array
* Find class nodes
*/
protected _stringContainsArray(input: string): boolean {
return input.startsWith('[') && input.endsWith(']');
protected _findClassNodes(node: ts.Node): ts.ClassDeclaration[] {
return this._findNodes(node, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration[];
}
/**
* Converts string to array
* Find constructor
*/
protected _stringToArray(input: string): string[] {
if (this._stringContainsArray(input)) {
return eval(input);
protected _findConstructorNode(node: ts.ClassDeclaration): ts.ConstructorDeclaration {
const constructorNodes = this._findNodes(node, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration[];
if (constructorNodes) {
return constructorNodes[0];
}
}
return [];
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node: ts.Node, propertyIdentifier: string): ts.CallExpression[] {
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== propertyIdentifier) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || (methodAccess.name.text !== 'get' && methodAccess.name.text !== 'instant' && methodAccess.name.text !== 'stream')) {
return false;
}
return true;
});
return callNodes;
}
}

View File

@@ -1,19 +0,0 @@
import * as ts from 'typescript';
export function printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0) {
console.log(
new Array(depth + 1).join('----'),
`[${node.kind}]`,
syntaxKindToName(node.kind),
`[pos: ${node.pos}-${node.end}]`,
':\t\t\t',
node.getFullText(sourceFile).trim()
);
depth++;
node.getChildren(sourceFile).forEach(childNode => printAllChildren(sourceFile, childNode, depth));
}
export function syntaxKindToName(kind: ts.SyntaxKind) {
return ts.SyntaxKind[kind];
}

View File

@@ -1,38 +0,0 @@
import { ParserInterface } from '../parsers/parser.interface';
import { TranslationCollection } from './translation.collection';
import * as glob from 'glob';
import * as fs from 'fs';
export class Extractor {
public constructor(public parsers: ParserInterface[], public patterns: string[]) { }
/**
* Extract strings from dir
*/
public process(dir: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._readDir(dir, this.patterns).forEach(path => {
const contents: string = fs.readFileSync(path, 'utf-8');
this.parsers.forEach((parser: ParserInterface) => {
collection = collection.union(parser.extract(contents, path));
});
});
return collection;
}
/**
* Get all files in dir matching patterns
*/
protected _readDir(dir: string, patterns: string[]): string[] {
return patterns.reduce((results, pattern) => {
return glob.sync(dir + pattern)
.filter(path => fs.statSync(path).isFile())
.concat(results);
}, []);
}
}

View File

@@ -1,6 +1,6 @@
export interface TranslationType {
[key: string]: string
};
}
export class TranslationCollection {

3
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
export function _(key: string | string[]): string | string[] {
return key;
}

View File

@@ -35,4 +35,26 @@ describe('NamespacedJsonCompiler', () => {
expect(result).to.equal('{\n\t"NAMESPACE": {\n\t\t"KEY": {\n\t\t\t"FIRST_KEY": "",\n\t\t\t"SECOND_KEY": "VALUE"\n\t\t}\n\t}\n}');
});
it('should preserve numeric values on compile', () => {
const collection = new TranslationCollection({
"option.0": '',
"option.1": '',
"option.2": ''
});
const result: string = compiler.compile(collection);
expect(result).to.equal('{\n\t"option": {\n\t\t"0": "",\n\t\t"1": "",\n\t\t"2": ""\n\t}\n}');
});
it('should use custom indentation chars', () => {
const collection = new TranslationCollection({
'NAMESPACE.KEY.FIRST_KEY': '',
'NAMESPACE.KEY.SECOND_KEY': 'VALUE'
});
const customCompiler = new NamespacedJsonCompiler({
indentation: ' '
});
const result: string = customCompiler.compile(collection);
expect(result).to.equal('{\n "NAMESPACE": {\n "KEY": {\n "FIRST_KEY": "",\n "SECOND_KEY": "VALUE"\n }\n }\n}');
});
});

View File

@@ -1,154 +0,0 @@
import { expect } from 'chai';
import { AstServiceParser } from '../../src/parsers/ast-service.parser';
class TestAstServiceParser extends AstServiceParser {
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
describe('AstServiceParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: TestAstServiceParser;
beforeEach(() => {
parser = new TestAstServiceParser();
});
/*it('should extract variable used for TranslateService', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(
_serviceA: ServiceA,
public _serviceB: ServiceB,
protected _translateService: TranslateService
) { }
`;
const name = parser.getInstancePropertyName();
expect(name).to.equal('_translateService');
});*/
it('should extract strings in TranslateService\'s get() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.get('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s instant() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.instant('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.get(['Hello', 'World']);
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract array of strings in TranslateService\'s instant() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.instant(['Hello', 'World']);
}
`;
const key = parser.extract(contents, componentFilename).keys();
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant() methods of other services', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(
protected _translateService: TranslateService,
protected _otherService: OtherService
) { }
public test() {
this._otherService.get('Hello World');
this._otherService.instant('Hi there');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should extract strings with liberal spacing', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(
protected _translateService: TranslateService,
protected _otherService: OtherService
) { }
public test() {
this._translateService.instant('Hello');
this._translateService.get ( 'World' );
this._translateService.instant ( ['How'] );
this._translateService.get([ 'Are' ]);
this._translateService.get([ 'You' , 'Today' ]);
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World', 'How', 'Are', 'You', 'Today']);
});
it('should not extract string when not accessing property', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected trans: TranslateService) { }
public test() {
trans.get("You are expected at {{time}}", {time: moment.format('H:mm')}).subscribe();
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should extract string with params on same line', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.get('You are expected at {{time}}', {time: moment.format('H:mm')});
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['You are expected at {{time}}']);
});
});

View File

@@ -118,4 +118,10 @@ describe('DirectiveParser', () => {
expect(template).to.equal('<p translate="KEY">Hello World</p>');
});
it('should extract contents from within custom tags', () => {
const contents = `<custom-table><tbody><tr><td translate>Hello World</td></tr></tbody></custom-table>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
});

View File

@@ -0,0 +1,27 @@
import { expect } from 'chai';
import { FunctionParser } from '../../src/parsers/function.parser';
describe('FunctionParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: FunctionParser;
beforeEach(() => {
parser = new FunctionParser();
});
it('should extract strings using marker function', () => {
const contents = `
import { _ } from '@biesbjerg/ngx-translate-extract';
_('Hello world');
_(['I', 'am', 'extracted']);
otherFunction('But I am not');
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted']);
});
});

View File

@@ -18,6 +18,12 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['SomeKey_NotWorking']);
});
it('should extract string using pipe, but between quotes only', () => {
const contents = `<input class="form-control" type="text" placeholder="{{'user.settings.form.phone.placeholder' | translate}}" [formControl]="settingsForm.controls['phone']">`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['user.settings.form.phone.placeholder']);
});
it('should extract interpolated strings using translate pipe', () => {
const contents = `Hello {{ 'World' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
@@ -25,11 +31,17 @@ describe('PipeParser', () => {
});
it('should extract strings with escaped quotes', () => {
const contents = `Hello {{ 'World\'s largest potato' | translate }}`;
const contents = `Hello {{ 'World\\'s largest potato' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`World's largest potato`]);
});
it('should extract strings with multiple escaped quotes', () => {
const contents = `{{ 'C\\'est ok. C\\'est ok' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`C'est ok. C'est ok`]);
});
it('should extract interpolated strings using translate pipe in attributes', () => {
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const keys = parser.extract(contents, templateFilename).keys();

View File

@@ -4,9 +4,9 @@ import { ServiceParser } from '../../src/parsers/service.parser';
class TestServiceParser extends ServiceParser {
public extractTranslateServiceVar(contents: string): string {
return this._extractTranslateServiceVar(contents);
}
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
@@ -20,7 +20,7 @@ describe('ServiceParser', () => {
parser = new TestServiceParser();
});
it('should extract variable used for TranslateService', () => {
/*it('should extract variable used for TranslateService', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -30,9 +30,9 @@ describe('ServiceParser', () => {
protected _translateService: TranslateService
) { }
`;
const name = parser.extractTranslateServiceVar(contents);
const name = parser.getInstancePropertyName();
expect(name).to.equal('_translateService');
});
});*/
it('should extract strings in TranslateService\'s get() method', () => {
const contents = `
@@ -60,6 +60,19 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s stream() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
const contents = `
@Component({ })
@@ -86,7 +99,20 @@ describe('ServiceParser', () => {
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant() methods of other services', () => {
it('should extract array of strings in TranslateService\'s stream() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream(['Hello', 'World']);
}
`;
const key = parser.extract(contents, componentFilename).keys();
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant()/stream() methods of other services', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -97,6 +123,7 @@ describe('ServiceParser', () => {
public test() {
this._otherService.get('Hello World');
this._otherService.instant('Hi there');
this._otherService.stream('Hi there');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
@@ -129,7 +156,7 @@ describe('ServiceParser', () => {
export class AppComponent {
public constructor(protected trans: TranslateService) { }
public test() {
trans.get('Hello World');
trans.get("You are expected at {{time}}", {time: moment.format('H:mm')}).subscribe();
}
}
`;
@@ -137,8 +164,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal([]);
});
// FAILS (Use AstServiceParser)
/*it('should extract string with params on same line', () => {
it('should extract string with params on same line', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -150,6 +176,53 @@ describe('ServiceParser', () => {
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['You are expected at {{time}}']);
});*/
});
it('should not crash when constructor parameter has no type', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService) { }
public test() {
this._translateService.instant('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should extract strings from all classes in the file', () => {
const contents = `
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
export class Stuff {
thing: string;
translate: any;
constructor(thing: string) {
this.translate.get('Not me');
this.thing = thing;
}
}
@Injectable()
export class MyComponent {
constructor(public translate: TranslateService) {
this.translate.instant("Extract me!");
}
}
export class OtherClass {
constructor(thing: string, _translate: TranslateService) {
this._translate.get("Do not extract me");
}
}
@Injectable()
export class AuthService {
constructor(public translate: TranslateService) {
this.translate.instant("Hello!");
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Extract me!', 'Hello!']);
});
});