Compare commits

..

120 Commits

Author SHA1 Message Date
Kim Biesbjerg
05d1917f9d Bump version 6.0.3 2020-03-25 15:10:12 +01:00
Kim Biesbjerg
e50d52003b Fix created vs replaced/merge message 2020-03-25 15:09:29 +01:00
Kim Biesbjerg
cb8731ee0f Bump version to 6.0.2 2020-03-25 14:11:13 +01:00
Kim Biesbjerg
9908681243 Use proper exit codes 2020-03-25 14:10:13 +01:00
Kim Biesbjerg
a83123fb12 better handling when destination is unreadable / cannot be parsed 2020-03-25 12:57:56 +01:00
Kim Biesbjerg
6b740867d6 Update README 2020-03-25 12:42:13 +01:00
Kim Biesbjerg
d0e9a8cd85 Bump version to 6.0.1 2020-03-25 12:32:39 +01:00
Kim Biesbjerg
f157025f0a Require node >=v11.5.0 and make sure npm install fails if requirement is not met 2020-03-25 12:28:20 +01:00
Kim Biesbjerg
ecf629118a Run prettier on code 2020-03-25 11:47:44 +01:00
Kim Biesbjerg
ce399ee717 Update dependencies 2020-03-25 11:44:31 +01:00
Kim Biesbjerg
e8bc023ea6 bump version 2020-03-22 13:35:35 +01:00
Kim Biesbjerg
33d8c26a28 extract strings when TranslateService is accessed directly via constructor parameter. Closes #50 and #106 2020-03-22 13:33:40 +01:00
Kim Biesbjerg
b07d929484 Add more tests 2020-03-22 11:13:16 +01:00
Kim Biesbjerg
a0f2b69f36 Add test. Closes #150 2020-03-22 10:45:52 +01:00
Kim Biesbjerg
13f46a524f Add test for ignoring calculated values. Closes #128 2020-03-22 10:43:08 +01:00
Kim Biesbjerg
6ed962fa6e Remove line breaks and tabs from extracted keys in templates. Closes #144, #167 2020-03-22 10:23:45 +01:00
Kim Biesbjerg
8aa2774eca Bump version 2020-03-21 12:21:14 +01:00
Kim Biesbjerg
37ca29648a Remove files committed by mistake 2020-03-21 12:20:06 +01:00
Kim Biesbjerg
97d844c3d2 Fix paths on Windows. Closes #171 2020-03-21 04:18:07 -07:00
Kim Biesbjerg
e7795c5349 Use rimraf to support Windows 2020-03-21 04:17:17 -07:00
Kim Biesbjerg
9da4939f5d Fix readme 2020-03-19 14:04:00 +01:00
Kim Biesbjerg
8fa7b60d2d Update dependencies 2020-03-19 11:18:39 +01:00
Kim Biesbjerg
d3d6a72d5f Update readme 2020-03-19 11:18:15 +01:00
Kim Biesbjerg
bfd069b755 Change description of default key value 2020-03-19 11:17:55 +01:00
Kim Biesbjerg
72b4fb0545 (chore) clean up tsconfig 2020-03-08 10:26:47 +01:00
Kim Biesbjerg
131713d9db (chore) refactor to use safe navigation operator 2020-03-08 10:07:27 +01:00
Jens Habegger
a17ad9c373 Parse Pipes with Angular Compiler AST, enable ternary operator parsing (#159)
(feature) Use AST-based approach to translate pipe parsing. Also enables parsing translate pipes from any position in a pipe chain. Fixes #111, Fixes #154. (Thanks @TekSiDoT)
2020-03-08 09:54:44 +01:00
Kim Biesbjerg
56a5ab31bf (feature) add StringAsDefaultValue. Closes #40 2020-03-07 09:21:01 +01:00
Kim Biesbjerg
d579114dd2 (chore) update typescript 2020-03-07 09:08:56 +01:00
Kim Biesbjerg
cc45df9b44 prevent conflicting options 2020-03-06 15:54:40 +01:00
Kim Biesbjerg
b813ec0063 (feature) add support for expanding paths on Windows + added more usage examples to cli 2020-03-06 13:53:46 +01:00
Kim Biesbjerg
7b94c9b9a5 (chore) update lint-staged usage 2020-03-06 12:14:53 +01:00
Kim Biesbjerg
a45039ef17 (chore) update dependencies 2020-03-06 12:14:14 +01:00
Robbert Wolfs
5d0c92871e Update flat dependency to the original, but the latest version (#157)
(chore) update flat package
2020-03-06 12:02:39 +01:00
Kim Biesbjerg
9887f9d6ab update tsconfig 2019-09-19 15:24:00 +02:00
Kim Biesbjerg
608c4e8e22 cleanup 2019-09-19 15:22:39 +02:00
Kim Biesbjerg
0345778aa1 refactor 2019-09-19 15:07:06 +02:00
Kim Biesbjerg
77769983d5 refactor 2019-09-19 15:03:46 +02:00
Kim Biesbjerg
306622b9a0 (chore) run prettier 2019-09-18 14:16:47 +02:00
Kim Biesbjerg
7d0d52429f (chore) add husky, lint-staged and prettier 2019-09-18 14:11:12 +02:00
Kim Biesbjerg
2fce357306 fix 2019-09-18 14:04:11 +02:00
Kim Biesbjerg
3827789346 cleanup 2019-09-18 13:47:57 +02:00
Kim Biesbjerg
8c8fe8d131 make ts compiler options more strict 2019-09-18 13:36:08 +02:00
Kim Biesbjerg
7bf0c138b8 (refactor) rename constants 2019-09-17 14:24:01 +02:00
Kim Biesbjerg
16bf5f59e0 Check extracted is an instance of TranslationCollection before merging 2019-09-17 14:15:21 +02:00
Kim Biesbjerg
096fc79a9b Update readme 2019-09-17 08:25:47 +02:00
Kim Biesbjerg
a72dbf0494 (refactor) rename variables 2019-09-17 08:15:51 +02:00
Kim Biesbjerg
8fd157802b update README 2019-09-16 20:42:10 +02:00
Kim Biesbjerg
1db4794ee9 bump version 2019-09-16 20:39:22 +02:00
Kim Biesbjerg
1eb1d0092d remove default boolean values 2019-09-16 20:39:02 +02:00
Kim Biesbjerg
4d3a3529b8 add typeRoots 2019-09-16 20:38:23 +02:00
Kim Biesbjerg
97e8937709 (feat) add argument --null-as-default-value to use null as default value for extracted translations 2019-09-16 17:52:41 +02:00
Kim Biesbjerg
eb7f3f603e (test) (thanks, @adrienverge) 2019-09-16 16:58:21 +02:00
Kim Biesbjerg
ab2b78eaec (chore) rename function 2019-09-16 16:54:24 +02:00
Kim Biesbjerg
75ee2bdfda fix return type 2019-09-16 16:49:47 +02:00
Kim Biesbjerg
4fe3c43624 - (chore) update packages
- (refactor) use tsquery for querying AST
- (feat) autodetect usage of marker function and remove --marker cli argument
- (bugfix) extract strings when TranslateService is declared directly as a class parameter. Closes https://github.com/biesbjerg/ngx-translate-extract/issues/83
- (bugfix) handle split strings: marker('hello ' + 'world') is now extracted as a single string: 'hello world'
2019-09-16 16:40:37 +02:00
Kim Biesbjerg
41bd679fcd (fix) bundle flat dependency. Closes #138 2019-08-29 11:22:40 +02:00
Kim Biesbjerg
24ebd8f428 (bugfix) extract strings encapsulated with backticks. Closes #139 2019-08-26 12:29:52 +02:00
Kim Biesbjerg
e1bb5bfd02 bump version 2019-08-21 10:02:31 +02:00
Kim Biesbjerg
1323c2e6a1 update dependencies 2019-08-21 10:02:07 +02:00
Steven Liekens
0f465014df Use github URL syntax (#135) 2019-08-21 09:58:02 +02:00
Kim Biesbjerg
5d5b07ba2c bump version 2019-08-03 11:30:50 +02:00
Kim Biesbjerg
7eefd6c8d3 fix donation message colors 2019-08-03 11:30:30 +02:00
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
51 changed files with 4432 additions and 836 deletions

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

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

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@ npm-debug.log*
# Compiled files
dist
src/**/*.js
tests/**/*.js
# Extracted strings
strings.json

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

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.

119
README.md
View File

@@ -1,95 +1,102 @@
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
[![Donate](images/donate-badge.png)](https://donate.biesbjerg.com)
# 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
## Install
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/strings.json --key-as-default-value --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
## Usage
**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/strings.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/strings.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**
`ngx-translate-extract --input ./src --output ./src/i18n/{da,en}.json`
`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'`
### 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, you can wrap them in a custom marker function.
### Marker function
If you want to extract strings that are not passed directly to `TranslateService`'s `get()`/`instant()`/`stream()` methods, you can wrap them in a marker function to let `ngx-translate-extract` know you want to extract them.
Install marker function:
`npm install @biesbjerg/ngx-translate-extract-marker`
```ts
import { _ } from '@biesbjerg/ngx-translate-extract';
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
marker('Extract me');
```
You can alias the marker function if needed:
```ts
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
_('Extract me');
```
Add the `marker` argument when running the extract script:
_Note: `ngx-translate-extract` will automatically detect the import name_
`ngx-translate-extract ... -m _`
Modify the scripts arguments as required.
## Commandline arguments
### Commandline arguments
```
Usage:
ngx-translate-extract [options]
Output
--format, -f Format [string] [choices: "json", "namespaced-json", "pot"] [default: "json"]
--format-indentation, --fi Format indentation (JSON/Namedspaced JSON) [string] [default: " "]
--sort, -s Sort strings in alphabetical order [boolean]
--clean, -c Remove obsolete strings after merge [boolean]
--replace, -r Replace the contents of output file if it exists (Merges by default) [boolean]
Extracted key value (defaults to empty string)
--key-as-default-value, -k Use key as default value [boolean]
--null-as-default-value, -n Use null as default value [boolean]
--string-as-default-value, -d Use string as default value [string]
Options:
--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]
--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] [required] [default: ["/Users/kim/apps/ngx-translate-extract"]]
--output, -o Paths where you would like to save extracted strings. You can use path expansion, glob
patterns and multiple paths [array] [required]
Examples:
ngx-translate-extract -i ./src-a/ -i ./src-b/ -o strings.json Extract (ts, html) from multiple paths
ngx-translate-extract -i './{src-a,src-b}/' -o strings.json Extract (ts, html) from multiple paths using brace
expansion
ngx-translate-extract -i ./src/ -o ./i18n/da.json -o ./i18n/en.json Extract (ts, html) and save to da.json and en.json
ngx-translate-extract -i ./src/ -o './i18n/{en,da}.json' Extract (ts, html) and save to da.json and en.json
using brace expansion
ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json Extract from ts, tsx and html
ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o Extract from ts, html, excluding files with ".spec"
strings.json

BIN
images/donate-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

2757
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.3.0",
"version": "6.0.3",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -14,20 +14,34 @@
"scripts": {
"build": "npm run clean && tsc",
"watch": "npm run clean && tsc --watch",
"clean": "rm -rf ./dist",
"clean": "rimraf ./dist",
"lint": "tslint --force './src/**/*.ts'",
"test": "mocha -r ts-node/register tests/**/*.spec.ts"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm test"
}
},
"prettier": {
"trailingComma": "none",
"printWidth": 145,
"useTabs": true,
"singleQuote": true
},
"lint-staged": {
"{src,tests}/**/*.{ts}": [
"tslint --project tsconfig.json -c tslint.commit.json --fix",
"prettier --write"
]
},
"repository": {
"type": "git",
"url": "https://github.com/biesbjerg/ngx-translate-extract.git"
},
"keywords": [
"angular",
"angular2",
"ionic",
"ionic2",
"ng2-translate",
"ngx-translate",
"extract",
"extractor",
@@ -43,34 +57,43 @@
},
"homepage": "https://github.com/biesbjerg/ngx-translate-extract",
"engines": {
"node": ">=4.3.2"
"node": ">=11.15.0"
},
"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": "5.0.0",
"tslint-eslint-rules": "4.0.0",
"typescript": "2.2.2"
"@types/braces": "^3.0.0",
"@types/chai": "^4.2.11",
"@types/flat": "^5.0.0",
"@types/glob": "^7.1.1",
"@types/mkdirp": "^1.0.0",
"@types/mocha": "^7.0.2",
"@types/node": "^12.12.31",
"@types/yargs": "^15.0.4",
"braces": "^3.0.2",
"chai": "^4.2.0",
"husky": "^4.2.3",
"lint-staged": "^10.0.9",
"mocha": "^7.1.1",
"prettier": "^2.0.2",
"rimraf": "^3.0.2",
"ts-node": "^8.8.1",
"tslint": "^6.1.0",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-etc": "^1.10.1"
},
"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": "^9.0.7",
"@phenomnomnominal/tsquery": "^4.0.0",
"boxen": "^4.2.0",
"colorette": "^1.1.0",
"flat": "^5.0.0",
"gettext-parser": "^4.0.3",
"glob": "^7.1.6",
"mkdirp": "^1.0.3",
"path": "^0.12.7",
"terminal-link": "^2.1.1",
"typescript": "^3.8.3",
"yargs": "^15.3.1"
}
}

View File

@@ -1,16 +1,35 @@
import * as yargs from 'yargs';
import { red, green } from 'colorette';
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 { MarkerParser } from '../parsers/marker.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 { NullAsDefaultValuePostProcessor } from '../post-processors/null-as-default-value.post-processor';
import { StringAsDefaultValuePostProcessor } from '../post-processors/string-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 { normalizePaths } from '../utils/fs-helpers';
import { donateMessage } from '../utils/donate';
import * as fs from 'fs';
import * as yargs from 'yargs';
// First parsing pass to be able to access pattern argument for use input/output arguments
const y = yargs.option('patterns', {
alias: 'p',
describe: 'Default patterns',
type: 'array',
default: ['/**/*.html', '/**/*.ts'],
hidden: true
});
export const cli = yargs
const parsed = y.parse();
export const cli = y
.usage('Extract strings from files for translation.\nUsage: $0 [options]')
.version(require(__dirname + '/../../package.json').version)
.alias('version', 'v')
@@ -19,24 +38,14 @@ export const cli = yargs
.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,
default: [process.env.PWD],
type: 'array',
normalize: true
normalize: true,
required: 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']
.coerce('input', (input: string[]) => {
const paths = normalizePaths(input, parsed.patterns);
return paths;
})
.option('output', {
alias: 'o',
@@ -45,68 +54,108 @@ export const cli = yargs
normalize: true,
required: true
})
.option('marker', {
alias: 'm',
describe: 'Extract strings passed to a marker function',
default: false,
type: 'string'
.coerce('output', (output: string[]) => {
const paths = normalizePaths(output, parsed.patterns);
return paths;
})
.option('format', {
alias: 'f',
describe: 'Output format',
describe: 'Format',
default: 'json',
type: 'string',
choices: ['json', 'namespaced-json', 'pot']
})
.option('format-indentation', {
alias: 'fi',
describe: 'Output format indentation',
describe: 'Format indentation (JSON/Namedspaced JSON)',
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,
describe: 'Sort strings in alphabetical order',
type: 'boolean'
})
.option('clean', {
alias: 'c',
describe: 'Remove obsolete strings when merging',
default: false,
describe: 'Remove obsolete strings after merge',
type: 'boolean'
})
.option('key-as-default-value', {
alias: 'k',
describe: 'Use key as default value',
type: 'boolean',
conflicts: ['null-as-default-value', 'string-as-default-value']
})
.option('null-as-default-value', {
alias: 'n',
describe: 'Use null as default value',
type: 'boolean',
conflicts: ['key-as-default-value', 'string-as-default-value']
})
.option('string-as-default-value', {
alias: 'd',
describe: 'Use string as default value',
type: 'string',
conflicts: ['null-as-default-value', 'key-as-default-value']
})
.group(['format', 'format-indentation', 'sort', 'clean', 'replace'], 'Output')
.group(['key-as-default-value', 'null-as-default-value', 'string-as-default-value'], 'Extracted key value (defaults to empty string)')
.conflicts('key-as-default-value', 'null-as-default-value')
.example(`$0 -i ./src-a/ -i ./src-b/ -o strings.json`, 'Extract (ts, html) from multiple paths')
.example(`$0 -i './{src-a,src-b}/' -o strings.json`, 'Extract (ts, html) from multiple paths using brace expansion')
.example(`$0 -i ./src/ -o ./i18n/da.json -o ./i18n/en.json`, 'Extract (ts, html) and save to da.json and en.json')
.example(`$0 -i ./src/ -o './i18n/{en,da}.json'`, 'Extract (ts, html) and save to da.json and en.json using brace expansion')
.example(`$0 -i './src/**/*.{ts,tsx,html}' -o strings.json`, 'Extract from ts, tsx and html')
.example(`$0 -i './src/**/!(*.spec).{ts,html}' -o strings.json`, 'Extract from ts, html, excluding files with ".spec" in filename')
.wrap(110)
.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 extractTask = new ExtractTask(cli.input, cli.output, {
replace: cli.replace
});
// Parsers
const parsers: ParserInterface[] = [new PipeParser(), new DirectiveParser(), new ServiceParser(), new MarkerParser()];
extractTask.setParsers(parsers);
// Post processors
const postProcessors: PostProcessorInterface[] = [];
if (cli.clean) {
postProcessors.push(new PurgeObsoleteKeysPostProcessor());
}
if (cli.keyAsDefaultValue) {
postProcessors.push(new KeyAsDefaultValuePostProcessor());
} else if (cli.nullAsDefaultValue) {
postProcessors.push(new NullAsDefaultValuePostProcessor());
} else if (cli.stringAsDefaultValue) {
postProcessors.push(new StringAsDefaultValuePostProcessor({ defaultValue: cli.stringAsDefaultValue as string }));
}
if (cli.sort) {
postProcessors.push(new SortByKeyPostProcessor());
}
extractTask.setPostProcessors(postProcessors);
// Compiler
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extract.setCompiler(compiler);
extractTask.setCompiler(compiler);
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
new ServiceParser()
];
if (cli.marker) {
parsers.push(new FunctionParser({
identifier: cli.marker
}));
// Run task
try {
extractTask.execute();
console.log(green('\nDone.\n'));
console.log(donateMessage);
process.exit(0);
} catch (e) {
console.log(red(`\nAn error occurred: ${e}\n`));
process.exit(1);
}
extract.setParsers(parsers);
extract.execute();

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, red } from 'colorette';
import * as glob from 'glob';
import * as fs from 'fs';
import * as path from 'path';
@@ -11,135 +12,170 @@ import * as mkdirp from 'mkdirp';
export interface ExtractTaskOptionsInterface {
replace?: boolean;
sort?: boolean;
clean?: boolean;
patterns?: string[];
}
export class ExtractTask implements TaskInterface {
protected _options: ExtractTaskOptionsInterface = {
replace: false,
sort: false,
clean: false,
patterns: []
protected options: ExtractTaskOptionsInterface = {
replace: false
};
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();
this._out(chalk.green('Extracted %d strings\n'), collection.count());
this._save(collection);
this.printEnabledParsers();
this.printEnabledPostProcessors();
this.printEnabledCompiler();
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)) {
try {
existing = this.compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
} catch (e) {
this.out(`%s %s`, dim(`- ${outputPath}`), red(`[ERROR]`));
throw e;
}
}
// merge extracted strings with existing
const draft = extracted.union(existing);
// Run collection through post processors
const final = this.process(draft, extracted, existing);
// Save
try {
let event = 'CREATED';
if (fs.existsSync(outputPath)) {
this.options.replace ? event = 'REPLACED' : event = 'MERGED';
}
this.save(outputPath, final);
this.out(`%s %s`, dim(`- ${outputPath}`), green(`[${event}]`));
} catch (e) {
this.out(`%s %s`, dim(`- ${outputPath}`), red(`[ERROR]`));
throw e;
}
});
}
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...'));
protected extract(): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._input.forEach(dir => {
this._readDir(dir, this._options.patterns).forEach(path => {
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));
this.inputs.forEach((pattern) => {
this.getFiles(pattern).forEach((filePath) => {
this.out(dim('- %s'), filePath);
const contents: string = fs.readFileSync(filePath, 'utf-8');
this.parsers.forEach((parser) => {
const extracted = parser.extract(contents, filePath);
if (extracted instanceof TranslationCollection) {
collection = collection.union(extracted);
}
});
});
});
return collection;
}
/**
* Process collection according to options (merge, clean, sort), compile and save
* @param collection
* Run strings through configured post processors
*/
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!'));
protected process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
this.postProcessors.forEach((postProcessor) => {
draft = postProcessor.process(draft, extracted, existing);
});
return draft;
}
/**
* Get all files in dir matching patterns
* Compile and save translations
* @param collection
*/
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 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));
}
protected _out(...args: any[]): void {
/**
* Get all files matching pattern
*/
protected getFiles(pattern: string): string[] {
return glob.sync(pattern).filter((filePath) => fs.statSync(filePath).isFile());
}
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

@@ -4,14 +4,16 @@ 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}`);
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

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

View File

@@ -1,13 +1,13 @@
import { CompilerInterface } from './compiler.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { stripBOM } from '../utils/utils';
import * as flat from 'flat';
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') {
@@ -20,15 +20,14 @@ export class JsonCompiler implements CompilerInterface {
}
public parse(contents: string): TranslationCollection {
let values: any = JSON.parse(contents);
if (this._isNamespacedJsonFormat(values)) {
values = flat.flatten(values);
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');
protected isNamespacedJsonFormat(values: any): boolean {
return Object.keys(values).some((key) => typeof values[key] === 'object');
}
}

View File

@@ -1,10 +1,10 @@
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 {
public indentation: string = '\t';
public extension = 'json';
@@ -16,15 +16,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

@@ -4,15 +4,14 @@ import { TranslationCollection, TranslationType } from '../utils/translation.col
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 = {
@@ -24,34 +23,37 @@ export class PoCompiler implements CompilerInterface {
},
translations: {
[this.domain]: Object.keys(collection.values).reduce((translations, key) => {
translations[key] = {
msgid: key,
msgstr: collection.get(key)
return {
...translations,
[key]: {
msgid: key,
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;
}
const values = Object.keys(po.translations[this.domain])
.filter(key => key.length > 0)
.reduce((values, key) => {
values[key] = po.translations[this.domain][key].msgstr.pop();
return values;
}, <TranslationType> {});
.filter((key) => key.length > 0)
.reduce((result, key) => {
return {
...result,
[key]: po.translations[this.domain][key].msgstr.pop()
};
}, {} as TranslationType);
return new TranslationCollection(values);
}
}

View File

@@ -6,15 +6,18 @@ 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 './parsers/marker.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

@@ -1,69 +0,0 @@
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, 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);
}
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,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,87 @@
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 {
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
export class DirectiveParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
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(source, filePath);
this.getTranslatableElements(nodes).forEach((element) => {
const key = this.getElementTranslateAttrValue(element) || this.getElementContent(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 content 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?.attributes && node?.children;
}
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?.value ?? '';
}
protected getElementContent(element: TmplAstElement): string {
const content = element.sourceSpan.start.file.content;
const start = element.startSourceSpan.end.offset;
const end = element.endSourceSpan.start.offset;
const val = content.substring(start, end);
return this.cleanKey(val);
}
protected cleanKey(val: string): string {
return val.replace(/\r?\n|\r|\t/g, '');
}
}

View File

@@ -1,61 +0,0 @@
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

@@ -0,0 +1,32 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { getNamedImportAlias, findFunctionCallExpressions, getStringsFromExpression } from '../utils/ast-helpers';
const MARKER_MODULE_NAME = '@biesbjerg/ngx-translate-extract-marker';
const MARKER_IMPORT_NAME = 'marker';
export class MarkerParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);
const markerImportName = getNamedImportAlias(sourceFile, MARKER_MODULE_NAME, MARKER_IMPORT_NAME);
if (!markerImportName) {
return null;
}
let collection: TranslationCollection = new TranslationCollection();
const callExpressions = findFunctionCallExpressions(sourceFile, markerImportName);
callExpressions.forEach((callExpression) => {
const [firstArg] = callExpression.arguments;
if (!firstArg) {
return;
}
const strings = getStringsFromExpression(firstArg);
collection = collection.addKeys(strings);
});
return collection;
}
}

View File

@@ -1,7 +1,5 @@
import { TranslationCollection } from '../utils/translation.collection';
export interface ParserInterface {
extract(contents: string, path?: string): TranslationCollection;
extract(source: string, filePath: string): TranslationCollection | null;
}

View File

@@ -1,27 +1,100 @@
import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler';
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 {
const TRANSLATE_PIPE_NAME = 'translate';
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
export class PipeParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g;
let matches: RegExpExecArray;
while (matches = regExp.exec(template)) {
collection = collection.add(matches[2].replace('\\\'', '\''));
}
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
const pipes: BindingPipe[] = nodes.map((node) => this.findPipesInNode(node)).flat();
pipes.forEach((pipe) => {
this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => {
collection = collection.add(key);
});
});
return collection;
}
protected findPipesInNode(node: any): BindingPipe[] {
let ret: BindingPipe[] = [];
if (node?.children) {
ret = node.children.reduce(
(result: BindingPipe[], childNode: TmplAstNode) => {
const children = this.findPipesInNode(childNode);
return result.concat(children);
},
[ret]
);
}
if (node?.value?.ast?.expressions) {
const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp));
ret.push(...translateables);
}
if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => {
return attr.name === TRANSLATE_PIPE_NAME;
});
ret = [...ret, ...translateableAttributes];
}
if (node?.inputs) {
node.inputs.forEach((input: any) => {
// <element [attrib]="'identifier' | translate">
if (input?.value?.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) {
ret.push(input.value.ast);
}
// <element attrib="{{'identifier' | translate}}>"
if (input?.value?.ast?.expressions) {
input.value.ast.expressions.forEach((exp: BindingPipe) => {
if (this.expressionIsOrHasBindingPipe(exp)) {
ret.push(exp);
}
});
}
});
}
return ret;
}
protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] {
const ret: string[] = [];
if (pipeContent instanceof LiteralPrimitive) {
ret.push(pipeContent.value);
} else if (pipeContent instanceof Conditional) {
const trueExp: LiteralPrimitive | Conditional = pipeContent.trueExp as any;
ret.push(...this.parseTranslationKeysFromPipe(trueExp));
const falseExp: LiteralPrimitive | Conditional = pipeContent.falseExp as any;
ret.push(...this.parseTranslationKeysFromPipe(falseExp));
} else if (pipeContent instanceof BindingPipe) {
ret.push(...this.parseTranslationKeysFromPipe(pipeContent.exp as any));
}
return ret;
}
protected expressionIsOrHasBindingPipe(exp: any): boolean {
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
return true;
}
if (exp.exp && exp.exp instanceof BindingPipe) {
return this.expressionIsOrHasBindingPipe(exp.exp);
}
return false;
}
protected parseTemplate(template: string, path: string): TmplAstNode[] {
return parseTemplate(template, path).nodes;
}
}

View File

@@ -1,120 +1,64 @@
import { ClassDeclaration, CallExpression } from 'typescript';
import { tsquery } from '@phenomnomnominal/tsquery';
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import {
findClassDeclarations,
findClassPropertyByType,
findPropertyCallExpressions,
findMethodCallExpressions,
getStringsFromExpression,
findMethodParameterByType,
findConstructorDeclaration
} from '../utils/ast-helpers';
import * as ts from 'typescript';
const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];
export class ServiceParser extends AbstractAstParser implements ParserInterface {
export class ServiceParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);
protected _sourceFile: ts.SourceFile;
public extract(contents: string, path?: string): TranslationCollection {
this._sourceFile = this._createSourceFile(path, contents);
let collection: TranslationCollection = new TranslationCollection();
const constructorNodes: ts.ConstructorDeclaration[] = this._findConstructorNodes();
constructorNodes.forEach(constructorNode => {
const propertyName: string = this._getPropertyName(constructorNode);
if (!propertyName) {
return;
}
const callNodes = this._findCallNodes(this._sourceFile, propertyName);
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
});
return collection;
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor params
*/
protected _getPropertyName(constructorNode: ts.ConstructorDeclaration): string {
if (!constructorNode) {
const classDeclarations = findClassDeclarations(sourceFile);
if (!classDeclarations) {
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;
}
let collection: TranslationCollection = new TranslationCollection();
// Parameter has no type
if (!parameter.type) {
return false;
}
classDeclarations.forEach((classDeclaration) => {
const callExpressions = [
...this.findConstructorParamCallExpressions(classDeclaration),
...this.findPropertyCallExpressions(classDeclaration)
];
// 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;
}
}
/**
* Find constructor nodes
*/
protected _findConstructorNodes(): ts.ConstructorDeclaration[] {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors;
}
}
/**
* 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;
callExpressions.forEach((callExpression) => {
const [firstArg] = callExpression.arguments;
if (!firstArg) {
return;
}
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')) {
return false;
}
return true;
const strings = getStringsFromExpression(firstArg);
collection = collection.addKeys(strings);
});
return callNodes;
});
return collection;
}
protected findConstructorParamCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const constructorDeclaration = findConstructorDeclaration(classDeclaration);
if (!constructorDeclaration) {
return [];
}
const paramName = findMethodParameterByType(constructorDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
return findMethodCallExpressions(constructorDeclaration, paramName, TRANSLATE_SERVICE_METHOD_NAMES);
}
protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
if (!propName) {
return [];
}
return findPropertyCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES);
}
}

View File

@@ -0,0 +1,10 @@
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,10 @@
import { TranslationCollection } from '../utils/translation.collection';
import { PostProcessorInterface } from './post-processor.interface';
export class NullAsDefaultValuePostProcessor implements PostProcessorInterface {
public name: string = 'NullAsDefaultValue';
public process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
return draft.map((key, val) => (existing.get(key) === undefined ? null : val));
}
}

View File

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

View File

@@ -0,0 +1,10 @@
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,10 @@
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

@@ -0,0 +1,16 @@
import { TranslationCollection } from '../utils/translation.collection';
import { PostProcessorInterface } from './post-processor.interface';
interface Options {
defaultValue: string;
}
export class StringAsDefaultValuePostProcessor implements PostProcessorInterface {
public name: string = 'StringAsDefaultValue';
public constructor(protected options: Options) {}
public process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
return draft.map((key, val) => (existing.get(key) === undefined ? this.options.defaultValue : val));
}
}

157
src/utils/ast-helpers.ts Normal file
View File

@@ -0,0 +1,157 @@
import { tsquery } from '@phenomnomnominal/tsquery';
import {
SyntaxKind,
Node,
NamedImports,
Identifier,
ClassDeclaration,
ConstructorDeclaration,
isStringLiteralLike,
isArrayLiteralExpression,
CallExpression,
Expression,
isBinaryExpression,
isConditionalExpression,
PropertyAccessExpression
} from 'typescript';
export function getNamedImports(node: Node, moduleName: string): NamedImports[] {
const query = `ImportDeclaration[moduleSpecifier.text="${moduleName}"] NamedImports`;
return tsquery<NamedImports>(node, query);
}
export function getNamedImportAlias(node: Node, moduleName: string, importName: string): string | null {
const [namedImportNode] = getNamedImports(node, moduleName);
if (!namedImportNode) {
return null;
}
const query = `ImportSpecifier:has(Identifier[name="${importName}"]) > Identifier`;
const identifiers = tsquery<Identifier>(namedImportNode, query);
if (identifiers.length === 1) {
return identifiers[0].text;
}
if (identifiers.length > 1) {
return identifiers[identifiers.length - 1].text;
}
return null;
}
export function findClassDeclarations(node: Node): ClassDeclaration[] {
const query = 'ClassDeclaration';
return tsquery<ClassDeclaration>(node, query);
}
export function findClassPropertyByType(node: ClassDeclaration, type: string): string | null {
return findClassPropertyConstructorParameterByType(node, type) || findClassPropertyDeclarationByType(node, type);
}
export function findConstructorDeclaration(node: ClassDeclaration): ConstructorDeclaration {
const query = `Constructor`;
const [result] = tsquery<ConstructorDeclaration>(node, query);
return result;
}
export function findMethodParameterByType(node: Node, type: string): string | null {
const query = `Parameter:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const [result] = tsquery<Identifier>(node, query);
if (result) {
return result.text;
}
return null;
}
export function findMethodCallExpressions(node: Node, propName: string, fnName: string | string[]): CallExpression[] {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${propName}"]):not(:has(ThisKeyword)))`;
const nodes = tsquery<PropertyAccessExpression>(node, query).map((n) => n.parent as CallExpression);
return nodes;
}
export function findClassPropertyConstructorParameterByType(node: ClassDeclaration, type: string): string | null {
const query = `Constructor Parameter:has(TypeReference > Identifier[name="${type}"]):has(PublicKeyword,ProtectedKeyword,PrivateKeyword) > Identifier`;
const [result] = tsquery<Identifier>(node, query);
if (result) {
return result.text;
}
return null;
}
export function findClassPropertyDeclarationByType(node: ClassDeclaration, type: string): string | null {
const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const [result] = tsquery<Identifier>(node, query);
if (result) {
return result.text;
}
return null;
}
export function findFunctionCallExpressions(node: Node, fnName: string | string[]): CallExpression[] {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression:has(Identifier[name="${fnName}"]):not(:has(PropertyAccessExpression))`;
const nodes = tsquery<CallExpression>(node, query);
return nodes;
}
export function findPropertyCallExpressions(node: Node, prop: string, fnName: string | string[]): CallExpression[] {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${prop}"]):has(ThisKeyword))`;
const nodes = tsquery<PropertyAccessExpression>(node, query).map((n) => n.parent as CallExpression);
return nodes;
}
export function getStringsFromExpression(expression: Expression): string[] {
if (isStringLiteralLike(expression)) {
return [expression.text];
}
if (isArrayLiteralExpression(expression)) {
return expression.elements.reduce((result: string[], element: Expression) => {
const strings = getStringsFromExpression(element);
return [...result, ...strings];
}, []);
}
if (isBinaryExpression(expression)) {
const [left] = getStringsFromExpression(expression.left);
const [right] = getStringsFromExpression(expression.right);
if (expression.operatorToken.kind === SyntaxKind.PlusToken) {
if (typeof left === 'string' && typeof right === 'string') {
return [left + right];
}
}
if (expression.operatorToken.kind === SyntaxKind.BarBarToken) {
const result = [];
if (typeof left === 'string') {
result.push(left);
}
if (typeof right === 'string') {
result.push(right);
}
return result;
}
}
if (isConditionalExpression(expression)) {
const [whenTrue] = getStringsFromExpression(expression.whenTrue);
const [whenFalse] = getStringsFromExpression(expression.whenFalse);
const result = [];
if (typeof whenTrue === 'string') {
result.push(whenTrue);
}
if (typeof whenFalse === 'string') {
result.push(whenFalse);
}
return result;
}
return [];
}

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

@@ -0,0 +1,17 @@
import { yellow } from 'colorette';
import * as boxen from 'boxen';
import * as terminalLink from 'terminal-link';
const url = 'https://donate.biesbjerg.com';
const link = terminalLink(url, url);
const message = `
If this tool saves you or your company time, please consider making a
donation to support my work and the continued maintainence and development:
${yellow(link)}`;
export const donateMessage = boxen(message.trim(), {
padding: 1,
margin: 0,
dimBorder: true
});

36
src/utils/fs-helpers.ts Normal file
View File

@@ -0,0 +1,36 @@
import * as os from 'os';
import * as fs from 'fs';
import * as braces from 'braces';
declare module 'braces' {
interface Options {
keepEscaping?: boolean; // Workaround for option not present in @types/braces 3.0.0
}
}
export function normalizeHomeDir(path: string): string {
if (path.substring(0, 1) === '~') {
return `${os.homedir()}/${path.substring(1)}`;
}
return path;
}
export function expandPattern(pattern: string): string[] {
return braces(pattern, { expand: true, keepEscaping: true });
}
export function normalizePaths(patterns: string[], defaultPatterns: string[] = []): string[] {
return patterns
.map((pattern) =>
expandPattern(pattern)
.map((path) => {
path = normalizeHomeDir(path);
if (fs.existsSync(path) && fs.statSync(path).isDirectory()) {
return defaultPatterns.map((defaultPattern) => path + defaultPattern);
}
return path;
})
.flat()
)
.flat();
}

View File

@@ -1,9 +1,8 @@
export interface TranslationType {
[key: string]: string
[key: string]: string;
}
export class TranslationCollection {
public values: TranslationType = {};
public constructor(values: TranslationType = {}) {
@@ -11,29 +10,28 @@ 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));
return { ...results, [key]: '' };
}, {} as TranslationType);
return new TranslationCollection({ ...this.values, ...values });
}
public remove(key: string): TranslationCollection {
return this.filter(k => key !== k);
return this.filter((k) => key !== k);
}
public forEach(callback: (key?: string, val?: string) => void): TranslationCollection {
Object.keys(this.values).forEach(key => callback.call(this, key, this.values[key]));
Object.keys(this.values).forEach((key) => callback.call(this, key, this.values[key]));
return this;
}
public filter(callback: (key?: string, val?: string) => boolean): TranslationCollection {
let values: TranslationType = {};
this.forEach((key: string, val: string) => {
const values: TranslationType = {};
this.forEach((key, val) => {
if (callback.call(this, key, val)) {
values[key] = val;
}
@@ -41,16 +39,23 @@ export class TranslationCollection {
return new TranslationCollection(values);
}
public map(callback: (key?: string, val?: string) => string): TranslationCollection {
const values: TranslationType = {};
this.forEach((key, val) => {
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 {
let values: TranslationType = {};
this.filter(key => collection.has(key))
.forEach((key: string, val: string) => {
values[key] = val;
});
const values: TranslationType = {};
this.filter((key) => collection.has(key)).forEach((key, val) => {
values[key] = val;
});
return new TranslationCollection(values);
}
@@ -76,10 +81,12 @@ export class TranslationCollection {
}
public sort(compareFn?: (a: string, b: string) => number): TranslationCollection {
let values: TranslationType = {};
this.keys().sort(compareFn).forEach((key) => {
values[key] = this.get(key);
});
const values: TranslationType = {};
this.keys()
.sort(compareFn)
.forEach((key) => {
values[key] = this.get(key);
});
return new TranslationCollection(values);
}

View File

@@ -1,3 +1,22 @@
export function _(key: string | string[]): string | string[] {
return key;
/**
* 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

@@ -4,7 +4,6 @@ import { TranslationCollection } from '../../src/utils/translation.collection';
import { NamespacedJsonCompiler } from '../../src/compilers/namespaced-json.compiler';
describe('NamespacedJsonCompiler', () => {
let compiler: NamespacedJsonCompiler;
beforeEach(() => {
@@ -23,7 +22,10 @@ describe('NamespacedJsonCompiler', () => {
}
`;
const collection: TranslationCollection = compiler.parse(contents);
expect(collection.values).to.deep.equal({'NAMESPACE.KEY.FIRST_KEY': '', 'NAMESPACE.KEY.SECOND_KEY': 'VALUE' });
expect(collection.values).to.deep.equal({
'NAMESPACE.KEY.FIRST_KEY': '',
'NAMESPACE.KEY.SECOND_KEY': 'VALUE'
});
});
it('should unflatten keys on compile', () => {
@@ -37,9 +39,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 +59,12 @@ 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,38 @@ 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 +48,73 @@ 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([]);
});
it('should extract contents without line breaks', () => {
const contents = `
<p translate>
Please leave a message for your client letting them know why you
rejected the field and what they need to do to fix it.
</p>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([
'Please leave a message for your client letting them know why you rejected the field and what they need to do to fix it.'
]);
});
it('should extract contents without indent spaces', () => {
const contents = `
<div *ngIf="!isLoading && studentsToGrid && studentsToGrid.length == 0" class="no-students" mt-rtl translate>There
are currently no students in this class. The good news is, adding students is really easy! Just use the options
at the top.
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([
'There are currently no students in this class. The good news is, adding students is really easy! Just use the options at the top.'
]);
});
it('should extract contents without indent spaces', () => {
const contents = `<button mat-button (click)="search()" translate>client.search.searchBtn</button>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['client.search.searchBtn']);
});
});

View File

@@ -1,27 +0,0 @@
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

@@ -0,0 +1,53 @@
import { expect } from 'chai';
import { MarkerParser } from '../../src/parsers/marker.parser';
describe('MarkerParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: MarkerParser;
beforeEach(() => {
parser = new MarkerParser();
});
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']);
});
it('should extract split strings', () => {
const contents = `
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
_('Hello ' + 'world');
_('This is a ' + 'very ' + 'very ' + 'very ' + 'very ' + 'long line.');
_('Mix ' + \`of \` + 'different ' + \`types\`);
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello world', 'This is a very very very very long line.', 'Mix of different types']);
});
it('should extract the strings', () => {
const contents = `
import { marker } from '@biesbjerg/ngx-translate-extract-marker';
export class AppModule {
constructor() {
marker('DYNAMIC_TRAD.val1');
marker('DYNAMIC_TRAD.val2');
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['DYNAMIC_TRAD.val1', 'DYNAMIC_TRAD.val2']);
});
});

View File

@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { PipeParser } from '../../src/parsers/pipe.parser';
describe('PipeParser', () => {
const templateFilename: string = 'test.template.html';
let parser: PipeParser;
@@ -30,36 +29,78 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['World']);
});
it('should extract interpolated strings when translate pipe is used before other pipes', () => {
const contents = `Hello {{ 'World' | translate | upper }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract interpolated strings when translate pipe is used after other pipes', () => {
const contents = `Hello {{ 'World' | upper | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings from ternary operators inside interpolations', () => {
const contents = `{{ (condition ? 'Hello' : 'World') | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from nested ternary operators ', () => {
const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'Nested', 'World']);
});
it('should extract strings from ternary operators inside attribute interpolations', () => {
const contents = `<span attr="{{(condition ? 'Hello' : 'World') | translate}}"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings with escaped quotes', () => {
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']);
});
it('should not use a greedy regular expression', () => {
it('should extract multiple entries from nodes', () => {
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>
@@ -69,7 +110,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']);
});
@@ -79,7 +120,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')">
@@ -88,11 +129,34 @@ 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']);
});
it('should ignore calculated values', () => {
const contents = `{{ 'SOURCES.' + source.name + '.NAME_PLURAL' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should not extract pipe argument', () => {
const contents = `{{ value | valueToTranslationKey: 'argument' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
});

View File

@@ -2,39 +2,56 @@ import { expect } from 'chai';
import { ServiceParser } from '../../src/parsers/service.parser';
class TestServiceParser extends ServiceParser {
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
describe('ServiceParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: TestServiceParser;
let parser: ServiceParser;
beforeEach(() => {
parser = new TestServiceParser();
parser = new ServiceParser();
});
/*it('should extract variable used for TranslateService', () => {
it('should extract strings when TranslateService is accessed directly via constructor parameter', () => {
const contents = `
@Component({ })
export class MyComponent {
public constructor(protected translateService: TranslateService) {
translateService.get('It works!');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['It works!']);
});
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 extract strings in TranslateService\'s get() method', () => {
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 = `
@Component({ })
export class AppComponent {
@@ -47,7 +64,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s instant() method', () => {
it("should extract strings in TranslateService's instant() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -60,7 +77,20 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
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({ })
export class AppComponent {
@@ -73,7 +103,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract array of strings in TranslateService\'s instant() method', () => {
it("should extract array of strings in TranslateService's instant() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -86,7 +116,33 @@ 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 extract string arrays encapsulated in backticks', () => {
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 not extract strings in get()/instant()/stream() methods of other services', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -97,6 +153,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();
@@ -164,25 +221,88 @@ describe('ServiceParser', () => {
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) {
console.log(this.translate.instant("Hello!"));
this.translate.instant("Hello!");
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello!']);
expect(keys).to.deep.equal(['Extract me!', 'Hello!']);
});
it('should extract strings when TranslateService is declared as a property', () => {
const contents = `
export class MyComponent {
protected translateService: TranslateService;
public constructor() {
this.translateService = new 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 strings passed to TranslateServices methods only', () => {
const contents = `
export class AppComponent implements OnInit {
constructor(protected config: Config, protected translateService: TranslateService) {}
public ngOnInit(): void {
this.localizeBackButton();
}
protected localizeBackButton(): void {
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.config.set('backButtonText', this.translateService.instant('Back'));
});
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Back']);
});
});

View File

@@ -1,39 +1,20 @@
import { expect } from 'chai';
import { AbstractTemplateParser } from '../../src/parsers/abstract-template.parser';
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();
});
import { isPathAngularComponent, extractComponentInlineTemplate } from '../../src/utils/utils';
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 +26,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,8 +59,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,29 @@
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,40 @@
import { expect } from 'chai';
import { PostProcessorInterface } from '../../src/post-processors/post-processor.interface';
import { NullAsDefaultValuePostProcessor } from '../../src/post-processors/null-as-default-value.post-processor';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('NullAsDefaultValuePostProcessor', () => {
let processor: PostProcessorInterface;
beforeEach(() => {
processor = new NullAsDefaultValuePostProcessor();
});
it('should use null as default value', () => {
const draft = new TranslationCollection({ 'String A': '' });
const extracted = new TranslationCollection({ 'String A': '' });
const existing = new TranslationCollection();
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': null
});
});
it('should keep existing value even if it is an empty string', () => {
const draft = new TranslationCollection({ 'String A': '' });
const extracted = new TranslationCollection({ 'String A': '' });
const existing = new TranslationCollection({ 'String A': '' });
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': ''
});
});
it('should keep existing value', () => {
const draft = new TranslationCollection({ 'String A': 'Streng A' });
const extracted = new TranslationCollection({ 'String A': 'Streng A' });
const existing = new TranslationCollection({ 'String A': 'Streng A' });
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': 'Streng A'
});
});
});

View File

@@ -0,0 +1,34 @@
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 postProcessor: PostProcessorInterface;
beforeEach(() => {
postProcessor = new PurgeObsoleteKeysPostProcessor();
});
it('should purge obsolete keys', () => {
const draft = 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(postProcessor.process(draft, extracted, existing).values).to.deep.equal({
'I am completely new': '',
'I already exist': ''
});
});
});

View File

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,40 @@
import { expect } from 'chai';
import { PostProcessorInterface } from '../../src/post-processors/post-processor.interface';
import { StringAsDefaultValuePostProcessor } from '../../src/post-processors/string-as-default-value.post-processor';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('StringAsDefaultValuePostProcessor', () => {
let processor: PostProcessorInterface;
beforeEach(() => {
processor = new StringAsDefaultValuePostProcessor({ defaultValue: 'default' });
});
it('should use string as default value', () => {
const draft = new TranslationCollection({ 'String A': '' });
const extracted = new TranslationCollection({ 'String A': '' });
const existing = new TranslationCollection();
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': 'default'
});
});
it('should keep existing value even if it is an empty string', () => {
const draft = new TranslationCollection({ 'String A': '' });
const extracted = new TranslationCollection({ 'String A': '' });
const existing = new TranslationCollection({ 'String A': '' });
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': ''
});
});
it('should keep existing value', () => {
const draft = new TranslationCollection({ 'String A': 'Streng A' });
const extracted = new TranslationCollection({ 'String A': 'Streng A' });
const existing = new TranslationCollection({ 'String A': 'Streng A' });
expect(processor.process(draft, extracted, existing).values).to.deep.equal({
'String A': 'Streng A'
});
});
});

View File

@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('StringCollection', () => {
let collection: TranslationCollection;
beforeEach(() => {
@@ -64,12 +63,15 @@ describe('StringCollection', () => {
it('should merge with other collection', () => {
collection = collection.add('oldKey', 'oldVal');
const newCollection = new TranslationCollection({ newKey: 'newVal' });
expect(collection.union(newCollection).values).to.deep.equal({ oldKey: 'oldVal', newKey: 'newVal' });
expect(collection.union(newCollection).values).to.deep.equal({
oldKey: 'oldVal',
newKey: 'newVal'
});
});
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 +81,19 @@ 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

@@ -3,21 +3,24 @@
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitUseStrict": true,
"removeComments": true,
"declaration": true,
"target": "es5",
"target": "es2019",
"lib": [
"dom",
"es2015"
"es2019"
],
"module": "commonjs",
"outDir": "./dist/",
"sourceMap": true
"outDir": "dist",
"sourceMap": true,
"typeRoots" : [
"node_modules/@types"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": []
}

8
tslint.commit.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": ["./tslint.json", "tslint-etc"],
"jsRules": {},
"rules": {
"ordered-imports": false,
"no-unused-declaration": true
}
}

View File

@@ -1,53 +1,117 @@
{
"rulesDirectory": [
"node_modules/tslint-eslint-rules/dist/rules"
],
"rules": {
"indent": [true, "tabs"],
"semicolon": [true, "always", "ignore-interfaces"],
"quotemark": [true, "single", "avoid-escape"],
"only-arrow-functions": false,
"no-duplicate-variable": true,
"member-access": true,
"member-ordering": [
true,
{
"order": [
"public-static-field",
"public-static-method",
"protected-static-field",
"protected-static-method",
"private-static-field",
"private-static-method",
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"constructor",
"public-instance-method",
"protected-instance-method",
"private-instance-method"
]
}
],
"curly": true,
"eofline": true,
"no-trailing-whitespace": true,
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-type",
"check-typecast"
]
}
}
"defaultSeverity": "error",
"extends": [
"tslint-config-prettier"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-spacing": true,
"indent": [
true,
"tabs"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
220
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
false,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": [
true,
"ban-keywords",
"allow-pascal-case",
"check-format"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}