Compare commits

...

70 Commits

Author SHA1 Message Date
Kim Biesbjerg
50fd3ae9e2 (chore) bump version 2019-08-02 13:34:45 +02:00
Kim Biesbjerg
a5b8f6e6c6 (bugfix) order of keys was not preserved when using namespaced-json format. Closes #131 2019-08-02 13:34:18 +02:00
Kim Biesbjerg
3cbc20e0a0 update package version 2019-07-31 13:07:18 +02:00
Kim Biesbjerg
7ce01b97e4 add donate info 2019-07-31 13:06:09 +02:00
Kim Biesbjerg
393e1ed03f update packages 2019-07-31 10:33:11 +02:00
Kim Biesbjerg
8b014abf49 update README 2019-07-31 10:28:09 +02:00
Kim Biesbjerg
71cc6e6883 fix tests 2019-07-17 13:00:29 +02:00
Kim Biesbjerg
ceb4be7e3d Change default marker function to 'marker' 2019-07-17 12:52:33 +02:00
Kim Biesbjerg
e41fc88d97 (refactor) rename working to draft 2019-07-17 12:44:21 +02:00
Kim Biesbjerg
ac551b1824 update packages 2019-07-08 15:13:50 +02:00
Kim Biesbjerg
73877a5a35 Add stripBOM 2019-07-08 15:13:07 +02:00
Kim Biesbjerg
6e161c83f8 replace DirectiveParser with new version that uses Angular compiler 2019-07-08 15:12:54 +02:00
Kim Biesbjerg
7d1bcd2a80 fix: strip bom from json files. Closes #94 2019-07-08 15:11:56 +02:00
Kim Biesbjerg
bc2bfac7d7 fix invalid options passed to gettext 2019-07-08 15:10:58 +02:00
Kim Biesbjerg
c7563d4998 re-enable parsers 2019-07-08 14:41:55 +02:00
Kim Biesbjerg
f9b3c63c4c added tests 2019-07-08 14:36:44 +02:00
Kim Biesbjerg
9e5dad362c removed test parsers 2019-07-08 14:36:24 +02:00
Kim Biesbjerg
e133e0ce30 remove console log 2019-07-08 14:35:18 +02:00
Kim Biesbjerg
7ee0b7da71 refactor: use isPropertyAccessExpression 2019-07-08 14:33:35 +02:00
Kim Biesbjerg
c38ca59d43 add types 2019-06-25 13:09:15 +02:00
Kim Biesbjerg
98b84447c7 refactor: use instanceof 2019-06-19 12:49:09 +02:00
Kim Biesbjerg
2507d0cdd2 gray -> dim 2019-06-19 12:42:28 +02:00
Kim Biesbjerg
69047857b2 Remove output 2019-06-19 12:39:54 +02:00
Kim Biesbjerg
04b6684024 Print more info when running extracttask 2019-06-19 12:37:50 +02:00
Kim Biesbjerg
9a8abb3248 refactor(directive parser) remove cheerio in favor of angular's own compiler 2019-06-18 15:54:58 +02:00
Kim Biesbjerg
c8ba1312b5 (refactor) remove extract marker function. will be published as a separate package at @biesbjerg/ngx-translate-extract-marker 2019-06-13 12:36:26 +02:00
Kim Biesbjerg
e0178b5a97 (bugfix) fix unmatched selector error when template didnt contain any html 2019-06-13 12:32:18 +02:00
Kim Biesbjerg
3e43fde1cc (test) fix tests 2019-06-13 12:17:04 +02:00
Kim Biesbjerg
8493015e15 (test) closes #68 2019-06-13 12:03:33 +02:00
Kim Biesbjerg
deb38eb7c3 (test) add test 2019-06-13 11:59:36 +02:00
Kim Biesbjerg
36928e253d (test) add test. closes #104 2019-06-13 11:44:30 +02:00
Kim Biesbjerg
230b13e245 (refactor) rename processor to post processor 2019-06-13 11:44:04 +02:00
Kim Biesbjerg
4fd7efa2dc (refactor) simplify extraction of string literals 2019-06-13 11:23:37 +02:00
Kim Biesbjerg
02fc705bc0 (feat) add support for extracting binary expressions. example: translateService.instant(message || 'text if message is falsey') 2019-06-12 15:04:47 +02:00
Kim Biesbjerg
cb53f6f3b1 (fix) utils import path 2019-06-12 12:59:45 +02:00
Kim Biesbjerg
842b0a7d97 (fix) barrel import/export 2019-06-12 12:57:43 +02:00
Kim Biesbjerg
f32128b2ec add github funding options 2019-06-12 12:52:15 +02:00
Kim Biesbjerg
53eb4d1202 (refactor) get rid of AbstractTemplateParser 2019-06-12 11:50:23 +02:00
Kim Biesbjerg
102286a209 - (feat) add concept of post processors
- (feat) add 'key as default value' post processor (closes #109)
- (chore) move clean functionality to a post processor
- (chore) move sort functionality to a post processor
- (refactor) get rid of leading underscore on protected properties/methods
2019-06-11 23:06:47 +02:00
Kim Biesbjerg
ab29c9ab67 verbose disabled by default 2019-06-11 13:00:10 +02:00
Kim Biesbjerg
590f58fff3 (refactor) use object spread syntax 2019-06-11 12:54:50 +02:00
Kim Biesbjerg
d07d81681e update packages, replace chalk with colorette 2019-06-11 12:50:08 +02:00
Kim Biesbjerg
141eaca7b1 change target to es6 2019-06-11 12:27:58 +02:00
Håkon Drolsum Røkenes
7d5d38e6a1 docs: fixes path for extract npm script example (#89)
The example script gives the following error on windows:

Saving: <project dir>\src\assets\i18n\*.json
- sorted strings
fs.js:652
  return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
                 ^

Error: ENOENT: no such file or directory, open '<project dir>\src\assets\i18n\*.json'
2018-03-03 08:43:58 +01:00
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
39 changed files with 2654 additions and 488 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: #biesbjerg
custom: https://donate.biesbjerg.com

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

@@ -1,60 +1,84 @@
If you like this project please show your support with a GitHub star. Much appreciated!
If this tool saves you time, please consider making a donation towards the continued maintainence and development: https://donate.biesbjerg.com
# ngx-translate-extract
[![Donate](images/donate-badge.png)](https://donate.biesbjerg.com)
# Usage
## ngx-translate-extract
Extract translatable (ngx-translate) strings and save as a JSON or Gettext pot file.
Merges with existing strings if the output file already exists.
## Usage
### Usage
Install the package in your project:
`npm install @biesbjerg/ngx-translate-extract --save-dev`
Add an `extract` script to your project's `package.json`:
```
Add a script to your project's `package.json`:
```json
...
"scripts": {
"extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/*.json --clean --sort --format namespaced-json"
"extract-i18n": "ngx-translate-extract --input ./src --output ./src/assets/i18n/ --clean --sort --format namespaced-json"
}
...
```
You can now run `npm run extract` to extract strings.
You can now run `npm run extract-i18n` and it will extract strings from your project.
## Extract examples
### Examples
**Extract from dir and save to file**
`ngx-translate-extract -i ./src -o ./src/i18n/strings.json`
`ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json`
**Extract from multiple dirs**
`ngx-translate-extract -i ./src/folder-a ./src/folder-b -o ./src/i18n/strings.json`
`ngx-translate-extract --input ./src-a ./src-b --output ./src/assets/i18n/template.json`
**Extract and save to multiple files**
`ngx-translate-extract -i ./src -o ./src/i18n/{da,en,fr}.json`
**Extract and save to multiple files using path expansion**
**or**
_Note: This method does not work on Windows!_
`ngx-translate-extract -i ./src -o ./src/i18n/da.json ./src/i18n/en.json ./src/i18n/fr.json`
`ngx-translate-extract --input ./src --output ./src/i18n/{da,en}.json`
**or (update only)**
On Windows you must specify each output destination individually:
`ngx-translate-extract -i ./src -o ./src/i18n/*.json`
`ngx-translate-extract --input ./src --output ./src/i18n/da.json ./src/i18n/en.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'`
### JSON indentation
Tabs are used by default for indentation when saving extracted strings in json formats:
If you want to use spaces instead, you can do the following:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation ' '`
`ngx-translate-extract --input ./src --output ./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`'s `get()` or `instant()` methods, you can wrap them in a custom marker function to let `ngx-translate-extract` know you want to extract them.
Modify the scripts arguments as required.
Install marker function:
`npm install @biesbjerg/ngx-translate-extract-marker`
```ts
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
marker('Extract me');
```
Add the `marker` argument when running the extract script:
`ngx-translate-extract ... -m extract`
You can alias the marker function if needed:
```ts
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
_('Extract me');
```
`ngx-translate-extract ... -m _`
## Commandline arguments
```
```shell
Usage:
ngx-translate-extract [options]
@@ -64,18 +88,22 @@ Options:
--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]
[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"]
--format-indentation, --fi Output format indentation [string] [default: " "]
--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]
--key-as-default-value, -k Use key as default value for translations
[boolean] [default: false]

BIN
images/donate-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

1635
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": "2.1.0",
"version": "3.0.1",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -24,10 +24,7 @@
},
"keywords": [
"angular",
"angular2",
"ionic",
"ionic2",
"ng2-translate",
"ngx-translate",
"extract",
"extractor",
@@ -47,30 +44,29 @@
},
"config": {},
"devDependencies": {
"@types/chai": "3.4.35",
"@types/glob": "5.0.30",
"@types/mocha": "2.2.40",
"@types/cheerio": "0.22.1",
"@types/chalk": "0.4.31",
"@types/flat": "0.0.28",
"@types/yargs": "6.6.0",
"@types/mkdirp": "0.3.29",
"chai": "3.5.0",
"mocha": "3.2.0",
"ts-node": "3.0.2",
"tslint": "4.5.1",
"tslint-eslint-rules": "3.5.1",
"typescript": "2.2.2"
"@types/chai": "^4.1.7",
"@types/flat": "^0.0.28",
"@types/glob": "^7.1.1",
"@types/mkdirp": "^0.5.2",
"@types/mocha": "^5.2.7",
"@types/yargs": "^13.0.1",
"chai": "^4.2.0",
"mocha": "^6.2.0",
"ts-node": "^8.3.0",
"tslint": "^5.18.0",
"tslint-eslint-rules": "^5.4.0"
},
"dependencies": {
"chalk": "1.1.3",
"yargs": "7.0.2",
"cheerio": "0.22.0",
"fs": "0.0.1-security",
"gettext-parser": "1.2.2",
"glob": "7.1.1",
"path": "0.12.7",
"mkdirp": "0.5.1",
"flat": "2.0.1"
"@angular/compiler": "^8.1.3",
"boxen": "^4.1.0",
"colorette": "^1.1.0",
"flat": "git://github.com/lenchvolodymyr/flat.git#ffe77ef",
"gettext-parser": "^4.0.1",
"glob": "^7.1.4",
"mkdirp": "^0.5.1",
"path": "^0.12.7",
"terminal-link": "^1.3.0",
"typescript": "^3.5.3",
"yargs": "^13.3.0"
}
}

View File

@@ -1,13 +1,19 @@
import * as fs from 'fs';
import * as yargs from 'yargs';
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 { PostProcessorInterface } from '../post-processors/post-processor.interface';
import { SortByKeyPostProcessor } from '../post-processors/sort-by-key.post-processor';
import { KeyAsDefaultValuePostProcessor } from '../post-processors/key-as-default-value.post-processor';
import { PurgeObsoleteKeysPostProcessor } from '../post-processors/purge-obsolete-keys.post-processor';
import { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory';
import * as fs from 'fs';
import * as yargs from 'yargs';
import { donateMessage } from '../utils/donate';
export const cli = yargs
.usage('Extract strings from files for translation.\nUsage: $0 [options]')
@@ -44,6 +50,12 @@ export const cli = yargs
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',
@@ -75,25 +87,52 @@ export const cli = yargs
default: false,
type: 'boolean'
})
.option('key-as-default-value', {
alias: 'k',
describe: 'Use key as default value for translations',
default: false,
type: 'boolean'
})
.exitProcess(true)
.parse(process.argv);
const parsers: ParserInterface[] = [
new ServiceParser(),
new PipeParser(),
new DirectiveParser()
];
const extractTask = new ExtractTask(cli.input, cli.output, {
replace: cli.replace,
patterns: cli.patterns
});
// Parsers
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
new ServiceParser()
];
if (cli.marker) {
parsers.push(new FunctionParser({
identifier: cli.marker
}));
}
extractTask.setParsers(parsers);
// Post processors
const postProcessors: PostProcessorInterface[] = [];
if (cli.clean) {
postProcessors.push(new PurgeObsoleteKeysPostProcessor());
}
if (cli.keyAsDefaultValue) {
postProcessors.push(new KeyAsDefaultValuePostProcessor());
}
if (cli.sort) {
postProcessors.push(new SortByKeyPostProcessor());
}
extractTask.setPostProcessors(postProcessors);
// Compiler
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extractTask.setCompiler(compiler);
new ExtractTask(cli.input, cli.output, {
replace: cli.replace,
sort: cli.sort,
clean: cli.clean,
patterns: cli.patterns
})
.setParsers(parsers)
.setCompiler(compiler)
.execute();
extractTask.execute();
console.log(donateMessage);

View File

@@ -1,9 +1,10 @@
import { TranslationCollection } from '../../utils/translation.collection';
import { TaskInterface } from './task.interface';
import { ParserInterface } from '../../parsers/parser.interface';
import { PostProcessorInterface } from '../../post-processors/post-processor.interface';
import { CompilerInterface } from '../../compilers/compiler.interface';
import * as chalk from 'chalk';
import { cyan, green, bold, dim } from 'colorette';
import * as glob from 'glob';
import * as fs from 'fs';
import * as path from 'path';
@@ -11,131 +12,133 @@ import * as mkdirp from 'mkdirp';
export interface ExtractTaskOptionsInterface {
replace?: boolean;
sort?: boolean;
clean?: boolean;
patterns?: string[];
}
export class ExtractTask implements TaskInterface {
protected _options: ExtractTaskOptionsInterface = {
protected options: ExtractTaskOptionsInterface = {
replace: false,
sort: false,
clean: false,
patterns: []
};
protected _parsers: ParserInterface[] = [];
protected _compiler: CompilerInterface;
protected parsers: ParserInterface[] = [];
protected postProcessors: PostProcessorInterface[] = [];
protected compiler: CompilerInterface;
public constructor(protected _input: string[], protected _output: string[], options?: ExtractTaskOptionsInterface) {
this._options = Object.assign({}, this._options, options);
public constructor(protected inputs: string[], protected outputs: string[], options?: ExtractTaskOptionsInterface) {
this.inputs = inputs.map(input => path.resolve(input));
this.outputs = outputs.map(output => path.resolve(output));
this.options = { ...this.options, ...options };
}
public execute(): void {
if (!this._parsers) {
throw new Error('No parsers configured');
}
if (!this._compiler) {
if (!this.compiler) {
throw new Error('No compiler configured');
}
const collection = this._extract();
if (collection.isEmpty()) {
this._out(chalk.yellow('Did not find any extractable strings\n'));
return;
}
this.printEnabledParsers();
this.printEnabledPostProcessors();
this.printEnabledCompiler();
this._out(chalk.green('Extracted %d strings\n'), collection.count());
this._save(collection);
this.out(bold('Extracting:'));
const extracted = this.extract();
this.out(green(`\nFound %d strings.\n`), extracted.count());
this.out(bold('Saving:'));
this.outputs.forEach(output => {
let dir: string = output;
let filename: string = `strings.${this.compiler.extension}`;
if (!fs.existsSync(output) || !fs.statSync(output).isDirectory()) {
dir = path.dirname(output);
filename = path.basename(output);
}
const outputPath: string = path.join(dir, filename);
let existing: TranslationCollection = new TranslationCollection();
if (!this.options.replace && fs.existsSync(outputPath)) {
existing = this.compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
}
// merge extracted strings with existing
const draft = extracted.union(existing);
if (existing.isEmpty()) {
this.out(dim(`- ${outputPath}`));
} else {
this.out(dim(`- ${outputPath} (merged)`));
}
// Run collection through post processors
const final = this.process(draft, extracted, existing);
// Save to file
this.save(outputPath, final);
});
this.out(green('\nDone.\n'));
}
public setParsers(parsers: ParserInterface[]): this {
this._parsers = parsers;
this.parsers = parsers;
return this;
}
public setPostProcessors(postProcessors: PostProcessorInterface[]): this {
this.postProcessors = postProcessors;
return this;
}
public setCompiler(compiler: CompilerInterface): this {
this._compiler = compiler;
this.compiler = compiler;
return this;
}
/**
* Extract strings from input dirs using configured parsers
* Extract strings from specified 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._out(chalk.gray('- %s'), path);
protected extract(): TranslationCollection {
let extracted: TranslationCollection = new TranslationCollection();
this.inputs.forEach(dir => {
this.readDir(dir, this.options.patterns).forEach(path => {
this.out(dim('- %s'), path);
const contents: string = fs.readFileSync(path, 'utf-8');
this._parsers.forEach((parser: ParserInterface) => {
collection = collection.union(parser.extract(contents, path));
this.parsers.forEach(parser => {
extracted = extracted.union(parser.extract(contents, path));
});
});
});
return collection;
return extracted;
}
/**
* Process collection according to options (merge, clean, sort), compile and save
* Run strings through configured post processors
*/
protected process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
this.postProcessors.forEach(postProcessor => {
draft = postProcessor.process(draft, extracted, existing);
});
return draft;
}
/**
* Compile and save translations
* @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(processedCollection);
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!'));
});
protected save(output: string, collection: TranslationCollection): void {
const dir = path.dirname(output);
if (!fs.existsSync(dir)) {
mkdirp.sync(dir);
}
fs.writeFileSync(output, this.compiler.compile(collection));
}
/**
* Get all files in dir matching patterns
*/
protected _readDir(dir: string, patterns: string[]): string[] {
protected readDir(dir: string, patterns: string[]): string[] {
return patterns.reduce((results, pattern) => {
return glob.sync(dir + pattern)
.filter(path => fs.statSync(path).isFile())
@@ -143,8 +146,34 @@ export class ExtractTask implements TaskInterface {
}, []);
}
protected _out(...args: any[]): void {
protected out(...args: any[]): void {
console.log.apply(this, arguments);
}
protected printEnabledParsers(): void {
this.out(cyan('Enabled parsers:'));
if (this.parsers.length) {
this.out(cyan(dim(this.parsers.map(parser => `- ${parser.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}
protected printEnabledPostProcessors(): void {
this.out(cyan('Enabled post processors:'));
if (this.postProcessors.length) {
this.out(cyan(dim(this.postProcessors.map(postProcessor => `- ${postProcessor.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}
protected printEnabledCompiler(): void {
this.out(cyan('Compiler:'));
this.out(cyan(dim(`- ${this.compiler.constructor.name}`)));
this.out();
}
}

View File

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

View File

@@ -1,7 +1,8 @@
import { CompilerInterface } from './compiler.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { stripBOM } from '../utils/utils';
import * as flat from 'flat';
import { flatten, unflatten } from 'flat';
export class NamespacedJsonCompiler implements CompilerInterface {
@@ -16,14 +17,14 @@ export class NamespacedJsonCompiler implements CompilerInterface {
}
public compile(collection: TranslationCollection): string {
const values: {} = flat.unflatten(collection.values, {
const values: {} = 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: {} = flatten(JSON.parse(stripBOM(contents)));
return new TranslationCollection(values);
}

View File

@@ -5,14 +5,14 @@ import * as gettext from 'gettext-parser';
export class PoCompiler implements CompilerInterface {
public extension = 'po';
public extension: string = 'po';
/**
* Translation domain
*/
public domain = '';
public domain: string = '';
public constructor(options?: any) { }
public constructor(options?: any) {}
public compile(collection: TranslationCollection): string {
const data = {
@@ -29,17 +29,17 @@ export class PoCompiler implements CompilerInterface {
msgstr: collection.get(key)
};
return translations;
}, <any> {})
}, {} as any)
}
};
return gettext.po.compile(data, 'utf-8');
return gettext.po.compile(data);
}
public parse(contents: string): TranslationCollection {
const collection = new TranslationCollection();
const po = gettext.po.parse(contents, 'utf-8');
const po = gettext.po.parse(contents, 'utf8');
if (!po.translations.hasOwnProperty(this.domain)) {
return collection;
}
@@ -49,7 +49,7 @@ export class PoCompiler implements CompilerInterface {
.reduce((values, key) => {
values[key] = po.translations[this.domain][key].msgstr.pop();
return values;
}, <TranslationType> {});
}, {} as TranslationType);
return new TranslationCollection(values);
}

View File

@@ -1,18 +1,24 @@
export * from './utils/translation.collection';
export * from './utils/ast-utils';
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';
export * from './post-processors/post-processor.interface';
export * from './post-processors/key-as-default-value.post-processor';
export * from './post-processors/purge-obsolete-keys.post-processor';
export * from './post-processors/sort-by-key.post-processor';

View File

@@ -0,0 +1,43 @@
import {
createSourceFile,
SourceFile,
CallExpression,
Node,
SyntaxKind,
StringLiteral
} from 'typescript';
export abstract class AbstractAstParser {
protected sourceFile: SourceFile;
protected createSourceFile(path: string, contents: string): SourceFile {
return createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Get strings from function call's first argument
*/
protected getStringLiterals(callNode: CallExpression): string[] {
if (!callNode.arguments.length) {
return[];
}
const firstArg = callNode.arguments[0];
return this.findNodes(firstArg, SyntaxKind.StringLiteral)
.map((node: StringLiteral) => node.text);
}
/**
* Find all child nodes of a kind
*/
protected findNodes(node: Node, kind: SyntaxKind): Node[] {
const childrenNodes: Node[] = node.getChildren(this.sourceFile);
const initialValue: Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: Node[], childNode: Node) => {
return result.concat(this.findNodes(childNode, kind));
}, initialValue);
}
}

View File

@@ -1,24 +0,0 @@
export abstract class AbstractTemplateParser {
/**
* Checks if file is of type javascript or typescript and
* makes the assumption that it is an Angular Component
*/
protected _isAngularComponent(path: string): boolean {
return (/\.ts|js$/i).test(path);
}
/**
* Extracts inline template from components
*/
protected _extractInlineTemplate(contents: string): string {
const regExp: RegExp = /template\s*:\s*(["'`])([^\1]*?)\1/;
const match = regExp.exec(contents);
if (match !== null) {
return match[2];
}
return '';
}
}

View File

@@ -1,55 +1,83 @@
import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
import * as $ from 'cheerio';
import { parseTemplate, TmplAstNode, TmplAstElement, TmplAstTextAttribute } from '@angular/compiler';
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {
export class DirectiveParser implements ParserInterface {
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
public extract(template: string, path: string): TranslationCollection {
if (path && isPathAngularComponent(path)) {
template = extractComponentInlineTemplate(template);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
template = this._normalizeTemplateAttributes(template);
const selector = '[translate], [ng2-translate]';
$(template)
.find(selector)
.addBack(selector)
.each((i: number, element: CheerioElement) => {
const $element = $(element);
const attr = $element.attr('translate') || $element.attr('ng2-translate');
if (attr) {
collection = collection.add(attr);
} else {
$element
.contents()
.toArray()
.filter(node => node.type === 'text')
.map(node => node.nodeValue.trim())
.filter(text => text.length > 0)
.forEach(text => collection = collection.add(text));
}
});
const nodes: TmplAstNode[] = this.parseTemplate(template, path);
this.getTranslatableElements(nodes).forEach(element => {
const key = this.getElementTranslateAttrValue(element) || this.getElementContents(element);
collection = collection.add(key);
});
return collection;
}
/**
* Angular's `[attr]="'val'"` syntax is not valid HTML,
* so it can't be parsed by standard HTML parsers.
* This method replaces `[attr]="'val'""` with `attr="val"`
*/
protected _normalizeTemplateAttributes(template: string): string {
return template.replace(/\[([^\]]+)\]="'([^']*)'"/g, '$1="$2"');
protected getTranslatableElements(nodes: TmplAstNode[]): TmplAstElement[] {
return nodes
.filter(element => this.isElement(element))
.reduce((result: TmplAstElement[], element: TmplAstElement) => {
return result.concat(this.findChildrenElements(element));
}, [])
.filter(element => this.isTranslatable(element));
}
protected findChildrenElements(node: TmplAstNode): TmplAstElement[] {
if (!this.isElement(node)) {
return [];
}
// If element has translate attribute all its contents is translatable
// so we don't need to traverse any deeper
if (this.isTranslatable(node)) {
return [node];
}
return node.children.reduce((result: TmplAstElement[], childNode: TmplAstNode) => {
if (this.isElement(childNode)) {
const children = this.findChildrenElements(childNode);
return result.concat(children);
}
return result;
}, [node]);
}
protected parseTemplate(template: string, path: string): TmplAstNode[] {
return parseTemplate(template, path).nodes;
}
protected isElement(node: any): node is TmplAstElement {
return node
&& node.attributes !== undefined
&& node.children !== undefined;
}
protected isTranslatable(node: TmplAstNode): boolean {
if (this.isElement(node) && node.attributes.some(attribute => attribute.name === 'translate')) {
return true;
}
return false;
}
protected getElementTranslateAttrValue(element: TmplAstElement): string {
const attr: TmplAstTextAttribute = element.attributes.find(attribute => attribute.name === 'translate');
return attr && attr.value || '';
}
protected getElementContents(element: TmplAstElement): string {
const contents = element.sourceSpan.start.file.content;
const start = element.startSourceSpan.end.offset;
const end = element.endSourceSpan.start.offset;
return contents.substring(start, end).trim();
}
}

View File

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

View File

@@ -2,6 +2,6 @@ import { TranslationCollection } from '../utils/translation.collection';
export interface ParserInterface {
extract(contents: string, path?: string): TranslationCollection;
extract(template: string, path: string): TranslationCollection;
}

View File

@@ -1,24 +1,24 @@
import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
export class PipeParser extends AbstractTemplateParser implements ParserInterface {
export class PipeParser implements ParserInterface {
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
public extract(template: string, path: string): TranslationCollection {
if (path && isPathAngularComponent(path)) {
template = extractComponentInlineTemplate(template);
}
return this._parseTemplate(contents);
return this.parseTemplate(template);
}
protected _parseTemplate(template: string): TranslationCollection {
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,48 +1,57 @@
import {
SourceFile,
Node,
ConstructorDeclaration,
Identifier,
TypeReferenceNode,
ClassDeclaration,
SyntaxKind,
CallExpression,
PropertyAccessExpression,
isPropertyAccessExpression
} from 'typescript';
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
import * as ts from 'typescript';
export class ServiceParser extends AbstractAstParser implements ParserInterface {
export class ServiceParser implements ParserInterface {
protected sourceFile: SourceFile;
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
public extract(template: 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);
this.sourceFile = this.createSourceFile(path, template);
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.getStringLiterals(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
* is called by inspecting constructor arguments
*/
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
protected findTranslateServicePropertyName(constructorNode: ConstructorDeclaration): string {
if (!constructorNode) {
return null;
}
@@ -53,13 +62,18 @@ export class ServiceParser implements ParserInterface {
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;
const parameterType: Identifier = (parameter.type as TypeReferenceNode).typeName as Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== this._serviceClassName) {
if (className !== 'TranslateService') {
return false;
}
@@ -67,50 +81,55 @@ export class ServiceParser implements ParserInterface {
});
if (result) {
return (result.name as ts.Identifier).text;
return (result.name as Identifier).text;
}
}
/**
* Find first constructor
* Find class nodes
*/
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
protected findClassNodes(node: Node): ClassDeclaration[] {
return this.findNodes(node, SyntaxKind.ClassDeclaration) as ClassDeclaration[];
}
/**
* Find constructor
*/
protected findConstructorNode(node: ClassDeclaration): ConstructorDeclaration {
const constructorNodes = this.findNodes(node, SyntaxKind.Constructor) as ConstructorDeclaration[];
if (constructorNodes) {
return constructorNodes[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[];
protected findCallNodes(node: Node, propertyIdentifier: string): CallExpression[] {
let callNodes = this.findNodes(node, SyntaxKind.CallExpression) as 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) {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
const propAccess = callNode.getChildAt(0).getChildAt(0) as PropertyAccessExpression;
if (!propAccess || !isPropertyAccessExpression(propAccess)) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== propertyIdentifier) {
return false;
}
const methodAccess = callNode.getChildAt(0) as PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || (methodAccess.name.text !== 'get' && methodAccess.name.text !== 'instant' && methodAccess.name.text !== 'stream')) {
return false;
}
@@ -120,44 +139,4 @@ export class ServiceParser implements ParserInterface {
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

@@ -0,0 +1,12 @@
import { TranslationCollection } from '../utils/translation.collection';
import { PostProcessorInterface } from './post-processor.interface';
export class KeyAsDefaultValuePostProcessor implements PostProcessorInterface {
public name: string = 'KeyAsDefaultValue';
public process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
return draft.map((key, val) => val === '' ? key : val);
}
}

View File

@@ -0,0 +1,9 @@
import { TranslationCollection } from '../utils/translation.collection';
export interface PostProcessorInterface {
name: string;
process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection;
}

View File

@@ -0,0 +1,12 @@
import { TranslationCollection } from '../utils/translation.collection';
import { PostProcessorInterface } from './post-processor.interface';
export class PurgeObsoleteKeysPostProcessor implements PostProcessorInterface {
public name: string = 'PurgeObsoleteKeys';
public process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
return draft.intersect(extracted);
}
}

View File

@@ -0,0 +1,12 @@
import { TranslationCollection } from '../utils/translation.collection';
import { PostProcessorInterface } from './post-processor.interface';
export class SortByKeyPostProcessor implements PostProcessorInterface {
public name: string = 'SortByKey';
public process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
return draft.sort();
}
}

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];
}

19
src/utils/donate.ts Normal file
View File

@@ -0,0 +1,19 @@
import { yellow } from 'colorette';
import * as boxen from 'boxen';
import * as terminalLink from 'terminal-link';
const url = 'https://donate.biesbjerg.com';
const text = `
If this tool saves you time, please consider making a
donation towards the continued maintainence and development:
${yellow(terminalLink(url, url))}
`;
export const donateMessage = boxen(text.trim(), {
padding: 1,
margin: 0,
borderColor: 'yellow',
backgroundColor: 'black',
dimBorder: true
});

View File

@@ -1,6 +1,6 @@
export interface TranslationType {
[key: string]: string
};
[key: string]: string;
}
export class TranslationCollection {
@@ -11,15 +11,15 @@ export class TranslationCollection {
}
public add(key: string, val: string = ''): TranslationCollection {
return new TranslationCollection(Object.assign({}, this.values, { [key]: val }));
return new TranslationCollection({ ...this.values, [key]: val });
}
public addKeys(keys: string[]): TranslationCollection {
const values = keys.reduce((results, key) => {
results[key] = '';
return results;
}, <TranslationType> {});
return new TranslationCollection(Object.assign({}, this.values, values));
}, {} as TranslationType);
return new TranslationCollection({ ...this.values, ...values });
}
public remove(key: string): TranslationCollection {
@@ -41,8 +41,16 @@ export class TranslationCollection {
return new TranslationCollection(values);
}
public map(callback: (key?: string, val?: string) => string): TranslationCollection {
let values: TranslationType = {};
this.forEach((key: string, val: string) => {
values[key] = callback.call(this, key, val);
});
return new TranslationCollection(values);
}
public union(collection: TranslationCollection): TranslationCollection {
return new TranslationCollection(Object.assign({}, this.values, collection.values));
return new TranslationCollection({ ...this.values, ...collection.values });
}
public intersect(collection: TranslationCollection): TranslationCollection {

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

@@ -0,0 +1,22 @@
/**
* Assumes file is an Angular component if type is javascript/typescript
*/
export function isPathAngularComponent(path: string): boolean {
return (/\.ts|js$/i).test(path);
}
/**
* Extract inline template from a component
*/
export function extractComponentInlineTemplate(contents: string): string {
const regExp: RegExp = /template\s*:\s*(["'`])([^\1]*?)\1/;
const match = regExp.exec(contents);
if (match !== null) {
return match[2];
}
return '';
}
export function stripBOM(contents: string): string {
return contents.trim();
}

View File

@@ -37,9 +37,9 @@ describe('NamespacedJsonCompiler', () => {
it('should preserve numeric values on compile', () => {
const collection = new TranslationCollection({
"option.0": '',
"option.1": '',
"option.2": ''
'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}');
@@ -57,4 +57,13 @@ describe('NamespacedJsonCompiler', () => {
expect(result).to.equal('{\n "NAMESPACE": {\n "KEY": {\n "FIRST_KEY": "",\n "SECOND_KEY": "VALUE"\n }\n }\n}');
});
it('should not reorder keys when compiled', () => {
const collection = new TranslationCollection({
'BROWSE': '',
'LOGIN': ''
});
const result: string = compiler.compile(collection);
expect(result).to.equal('{\n\t"BROWSE": "",\n\t"LOGIN": ""\n}');
});
});

View File

@@ -2,78 +2,39 @@ import { expect } from 'chai';
import { DirectiveParser } from '../../src/parsers/directive.parser';
class TestDirectiveParser extends DirectiveParser {
public normalizeTemplateAttributes(template: string): string {
return this._normalizeTemplateAttributes(template);
}
}
describe('DirectiveParser', () => {
const templateFilename: string = 'test.template.html';
const componentFilename: string = 'test.component.ts';
let parser: TestDirectiveParser;
let parser: DirectiveParser;
beforeEach(() => {
parser = new TestDirectiveParser();
parser = new DirectiveParser();
});
it('should extract contents when no translate attribute value is provided', () => {
it('should not choke when no html is present in template', () => {
const contents = 'Hello World';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should use contents as key when there is no translate attribute value provided', () => {
const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract translate attribute if provided', () => {
const contents = '<div translate="KEY">Hello World<div>';
it('should use translate attribute value as key when provided', () => {
const contents = '<div translate="MY_KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
expect(keys).to.deep.equal(['MY_KEY']);
});
it('should extract bound translate attribute as key if provided', () => {
const contents = `<div [translate]="'KEY'">Hello World<div>`;
it('should not process children when translate attribute is present', () => {
const contents = `<div translate>Hello <strong translate>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract direct text nodes when no translate attribute value is provided', () => {
const contents = `
<div translate>
<span>&#10003;</span>
Hello <strong>World</strong>
Hi <em>there</em>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'Hi']);
});
it('should extract direct text nodes of tags with a translate attribute', () => {
const contents = `
<div translate>
<span>&#10003;</span>
Hello World
<div translate>Hi there</div>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World', 'Hi there']);
});
it('should extract translate attribute if provided or direct text nodes if not', () => {
const contents = `
<div translate="KEY">
<span>&#10003;</span>
Hello World
<p translate>Hi there</p>
<p [translate]="'OTHER_KEY'">Lorem Ipsum</p>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']);
expect(keys).to.deep.equal(['Hello <strong translate>World</strong>']);
});
it('should extract and parse inline template', () => {
@@ -88,34 +49,42 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract contents when no ng2-translate attribute value is provided', () => {
const contents = '<div ng2-translate>Hello World</div>';
it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract ng2-translate attribute if provided', () => {
const contents = '<div ng2-translate="KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract bound ng2-translate attribute as key if provided', () => {
const contents = `<div [ng2-translate]="'KEY'">Hello World<div>`;
it('should extract translate attribute value if provided', () => {
const contents = '<div translate="KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should not extract translate pipe in html tag', () => {
const contents = `<p>{{ 'Audiobooks for personal development' | translate }}</p>`;
const contents = `<p>{{ 'Audiobooks for personal development' | translate }}</p>`;
const collection = parser.extract(contents, templateFilename);
expect(collection.values).to.deep.equal({});
});
it('should normalize bound attributes', () => {
const contents = `<p [translate]="'KEY'">Hello World</p>`;
const template = parser.normalizeTemplateAttributes(contents);
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']);
});
it('should not cause error when no html is present in template', () => {
const contents = `
import { Component } from '@angular/core';
@Component({
template: '{{ variable }}'
})
export class MyComponent {
variable: string
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
});

View File

@@ -0,0 +1,30 @@
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 { marker } from '@biesbjerg/ngx-translate-extract-marker';
marker('Hello world');
marker(['I', 'am', 'extracted']);
otherFunction('But I am not');
marker(message || 'binary expression');
marker(message ? message : 'conditional operator');
marker('FOO.bar');
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted', 'binary expression', 'conditional operator', 'FOO.bar']);
});
});

View File

@@ -18,26 +18,44 @@ 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();
expect(keys).to.deep.equal(['World']);
});
it('should extract interpolated strings when translate pipe is used in conjunction with other pipes', () => {
const contents = `Hello {{ 'World' | translate | upper }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
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 contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract bound strings using translate pipe in attributes', () => {
const contents = `<span [attr]="'Hello World' | translate"></span>`;
const contents = `<span [attr]="'Hello World' | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
@@ -46,14 +64,14 @@ describe('PipeParser', () => {
const contents = `
<ion-header>
<ion-navbar color="brand">
<ion-title>{{ 'Info' | translate }}</ion-title>
<ion-title>{{ 'Info' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<content-loading *ngIf="isLoading">
{{ 'Loading...' | translate }}
{{ 'Loading...' | translate }}
</content-loading>
</ion-content>
@@ -63,7 +81,7 @@ describe('PipeParser', () => {
});
it('should extract strings on same line', () => {
const contents = `<span [attr]="'Hello' | translate"></span><span [attr]="'World' | translate"></span>`;
const contents = `<span [attr]="'Hello' | translate"></span><span [attr]="'World' | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
@@ -73,7 +91,7 @@ describe('PipeParser', () => {
<ion-list inset>
<ion-item>
<ion-icon item-left name="person" color="dark"></ion-icon>
<ion-input formControlName="name" type="text" [placeholder]="'Name' | translate"></ion-input>
<ion-input formControlName="name" type="text" [placeholder]="'Name' | translate"></ion-input>
</ion-item>
<ion-item>
<p color="danger" danger *ngFor="let error of form.get('name').getError('remote')">
@@ -82,11 +100,23 @@ describe('PipeParser', () => {
</ion-item>
</ion-list>
<div class="form-actions">
<button ion-button (click)="onSubmit()" color="secondary" block>{{ 'Create account' | translate }}</button>
<button ion-button (click)="onSubmit()" color="secondary" block>{{ 'Create account' | translate }}</button>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Name', 'Create account']);
});
it('should not extract variables', () => {
const contents = '<p>{{ message | translate }}</p>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should be able to extract without html', () => {
const contents = `{{ 'message' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['message']);
});
});

View File

@@ -4,10 +4,6 @@ import { ServiceParser } from '../../src/parsers/service.parser';
class TestServiceParser extends ServiceParser {
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
describe('ServiceParser', () => {
@@ -20,19 +16,33 @@ describe('ServiceParser', () => {
parser = new TestServiceParser();
});
/*it('should extract variable used for TranslateService', () => {
it('should support extracting binary expressions', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(
_serviceA: ServiceA,
public _serviceB: ServiceB,
protected _translateService: TranslateService
) { }
public constructor(protected _translateService: TranslateService) { }
public test() {
const message = 'The Message';
this._translateService.get(message || 'Fallback message');
}
`;
const name = parser.getInstancePropertyName();
expect(name).to.equal('_translateService');
});*/
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Fallback message']);
});
it('should support conditional operator', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
const message = 'The Message';
this._translateService.get(message ? message : 'Fallback message');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Fallback message']);
});
it('should extract strings in TranslateService\'s get() method', () => {
const contents = `
@@ -60,6 +70,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 +109,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 +133,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();
@@ -151,4 +188,66 @@ describe('ServiceParser', () => {
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 not extract variables', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected translateService: TranslateService) { }
public test() {
this.translateService.get(["yes", variable]).then(translations => {
console.log(translations[variable]);
});
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['yes']);
});
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!']);
});
});

View File

@@ -1,39 +1,21 @@
import { expect } from 'chai';
import { AbstractTemplateParser } from '../../src/parsers/abstract-template.parser';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../../src/utils/utils';
class TestTemplateParser extends AbstractTemplateParser {
public isAngularComponent(filePath: string): boolean {
return this._isAngularComponent(filePath);
}
public extractInlineTemplate(contents: string): string {
return this._extractInlineTemplate(contents);
}
}
describe('AbstractTemplateParser', () => {
let parser: TestTemplateParser;
beforeEach(() => {
parser = new TestTemplateParser();
});
describe('Utils', () => {
it('should recognize js extension as angular component', () => {
const result = parser.isAngularComponent('test.js');
const result = isPathAngularComponent('test.js');
expect(result).to.equal(true);
});
it('should recognize ts extension as angular component', () => {
const result = parser.isAngularComponent('test.ts');
const result = isPathAngularComponent('test.ts');
expect(result).to.equal(true);
});
it('should not recognize html extension as angular component', () => {
const result = parser.isAngularComponent('test.html');
const result = isPathAngularComponent('test.html');
expect(result).to.equal(false);
});
@@ -45,10 +27,22 @@ describe('AbstractTemplateParser', () => {
})
export class TestComponent { }
`;
const template = parser.extractInlineTemplate(contents);
const template = extractComponentInlineTemplate(contents);
expect(template).to.equal('<p translate>Hello World</p>');
});
it('should extract inline template without html', () => {
const contents = `
@Component({
selector: 'test',
template: '{{ "Hello World" | translate }}'
})
export class TestComponent { }
`;
const template = extractComponentInlineTemplate(contents);
expect(template).to.equal('{{ "Hello World" | translate }}');
});
it('should extract inline template spanning multiple lines', () => {
const contents = `
@Component({
@@ -66,7 +60,7 @@ describe('AbstractTemplateParser', () => {
})
export class TestComponent { }
`;
const template = parser.extractInlineTemplate(contents);
const template = extractComponentInlineTemplate(contents);
expect(template).to.equal('\n\t\t\t\t\t<p>\n\t\t\t\t\t\tHello World\n\t\t\t\t\t</p>\n\t\t\t\t');
});

View File

@@ -0,0 +1,31 @@
import { expect } from 'chai';
import { PostProcessorInterface } from '../../src/post-processors/post-processor.interface';
import { KeyAsDefaultValuePostProcessor } from '../../src/post-processors/key-as-default-value.post-processor';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('KeyAsDefaultValuePostProcessor', () => {
let processor: PostProcessorInterface;
beforeEach(() => {
processor = new KeyAsDefaultValuePostProcessor();
});
it('should use key as default value', () => {
const collection = new TranslationCollection({
'I have no value': '',
'I am already translated': 'Jeg er allerede oversat',
'Use this key as value as well': ''
});
const extracted = new TranslationCollection();
const existing = new TranslationCollection();
expect(processor.process(collection, extracted, existing).values).to.deep.equal({
'I have no value': 'I have no value',
'I am already translated': 'Jeg er allerede oversat',
'Use this key as value as well': 'Use this key as value as well'
});
});
});

View File

@@ -0,0 +1,36 @@
import { expect } from 'chai';
import { PostProcessorInterface } from '../../src/post-processors/post-processor.interface';
import { PurgeObsoleteKeysPostProcessor } from '../../src/post-processors/purge-obsolete-keys.post-processor';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('PurgeObsoleteKeysPostProcessor', () => {
let processor: PostProcessorInterface;
beforeEach(() => {
processor = new PurgeObsoleteKeysPostProcessor();
});
it('should purge obsolete keys', () => {
const collection = new TranslationCollection({
'I am completely new': '',
'I already exist': '',
'I already exist but was not present in extract': ''
});
const extracted = new TranslationCollection({
'I am completely new': '',
'I already exist': ''
});
const existing = new TranslationCollection({
'I already exist': '',
'I already exist but was not present in extract': ''
});
expect(processor.process(collection, extracted, existing).values).to.deep.equal({
'I am completely new': '',
'I already exist': ''
});
});
});

View File

@@ -0,0 +1,33 @@
import { expect } from 'chai';
import { PostProcessorInterface } from '../../src/post-processors/post-processor.interface';
import { SortByKeyPostProcessor } from '../../src/post-processors/sort-by-key.post-processor';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('SortByKeyPostProcessor', () => {
let processor: PostProcessorInterface;
beforeEach(() => {
processor = new SortByKeyPostProcessor();
});
it('should sort keys alphanumerically', () => {
const collection = new TranslationCollection({
'z': 'last value',
'a': 'a value',
'9': 'a numeric key',
'b': 'another value'
});
const extracted = new TranslationCollection();
const existing = new TranslationCollection();
expect(processor.process(collection, extracted, existing).values).to.deep.equal({
'9': 'a numeric key',
'a': 'a value',
'b': 'another value',
'z': 'last value'
});
});
});

View File

@@ -69,7 +69,7 @@ describe('StringCollection', () => {
it('should intersect with passed collection', () => {
collection = collection.addKeys(['red', 'green', 'blue']);
const newCollection = new TranslationCollection( { red: '', blue: '' });
const newCollection = new TranslationCollection( { red: '', blue: '' });
expect(collection.intersect(newCollection).values).to.deep.equal({ red: '', blue: '' });
});
@@ -79,10 +79,16 @@ describe('StringCollection', () => {
expect(collection.intersect(newCollection).values).to.deep.equal({ red: 'rød', blue: 'blå' });
});
it('should sort translations in alphabetical order', () => {
it('should sort keys alphabetically', () => {
collection = new TranslationCollection({ red: 'rød', green: 'grøn', blue: 'blå' });
collection = collection.sort();
expect(collection.keys()).deep.equal(['blue', 'green', 'red']);
});
it('should map values', () => {
collection = new TranslationCollection({ red: 'rød', green: 'grøn', blue: 'blå' });
collection = collection.map((key, val) => 'mapped value');
expect(collection.values).to.deep.equal({ red: 'mapped value', green: 'mapped value', blue: 'mapped value' });
});
});

View File

@@ -5,7 +5,7 @@
"noImplicitAny": true,
"removeComments": true,
"declaration": true,
"target": "es5",
"target": "es6",
"lib": [
"dom",
"es2015"