Compare commits

...

143 Commits

Author SHA1 Message Date
Kim Biesbjerg
567a0586dc Update deps 2021-04-14 10:08:21 +02:00
Jabi
17dfbbed84 enable piped argument on function calls (#233) 2021-04-14 10:00:44 +02:00
Kim Biesbjerg
acdffe0121 Merge branch 'master' of https://github.com/biesbjerg/ngx-translate-extract into master 2020-09-29 11:56:31 +02:00
Kim Biesbjerg
116133ba32 Bump version 7.0.3 2020-09-29 11:56:03 +02:00
Kim Biesbjerg
50b2ca6f4a (chore) Update deps, allow newer version of typescript and angular compiler 2020-09-29 11:55:17 +02:00
Kim Biesbjerg
b46a914756 Update README.md 2020-08-05 12:59:00 +02:00
Kim Biesbjerg
bc3e5fbe2f fix typo 2020-06-25 10:52:24 +02:00
Kim Biesbjerg
ea990d6f9d update deps, bump version 2020-06-25 10:43:59 +02:00
Kim Biesbjerg
c60705d5fa run prettier on code 2020-05-28 19:36:21 +02:00
Kim Biesbjerg
85cd1e4a46 fix(directive-parser) add support for bound attributes 2020-05-28 19:35:26 +02:00
Kim Biesbjerg
8afbb2f3a9 bump version 2020-05-28 00:29:39 +02:00
Kim Biesbjerg
329c24d962 update deps 2020-05-28 00:29:02 +02:00
Kim Biesbjerg
a30a6f9215 clean up some tests 2020-05-28 00:28:13 +02:00
Kim Biesbjerg
2adec54c00 fix(directive-parser) refactor + correct handling of whitespace 2020-05-28 00:09:53 +02:00
Alexander von Weiss
619b3c56ea fix(pipe-parser): add support for more sophisticated expressions that worked in version 4.2.0 with the regex based parser (#185)
* fix(pipe-parser): add support for more sophisticated expressions
2020-05-20 15:18:31 +02:00
Kim Biesbjerg
5e0da552b0 Add username to GitHub sponsors 2020-04-16 15:00:34 +02:00
Kim Biesbjerg
5f2eb2a7a0 Update package.json example scripts 2020-04-16 14:57:51 +02:00
Kim Biesbjerg
90b59793a7 Bump version to 6.0.4 2020-04-16 09:07:02 +02:00
Kim Biesbjerg
deb6b2373b Remove noUnusedLocals since they are removed by prettier on commit anyway 2020-04-16 09:06:40 +02:00
Esteban Gehring
17dec7deb8 fix(npm-package): move typescript and angular to peer-dependencies (#183)
fix(npm-package): move typescript and angular to peer-dependencies. Fixes #173
2020-04-16 08:58:51 +02:00
Lorent Lempereur
71f4f42b33 Tests about support of HTML tags in translations keys with GetText (#172)
- Verify that html tags are supported in translation keys
- Add typed support of gettext-parser
2020-04-01 12:19:30 +02:00
Kim Biesbjerg
73f39d625c add single quote rule 2020-04-01 10:21:11 +02:00
Kim Biesbjerg
3bf2aaca4e Trim leading/trailing whitespace. Closes #175 2020-03-31 12:24:57 +02:00
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
53 changed files with 4253 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 # Compiled files
dist dist
src/**/*.js
tests/**/*.js
# Extracted strings # Extracted strings
strings.json 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.

125
README.md
View File

@@ -1,95 +1,108 @@
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 # ngx-translate-extract
Extract translatable (ngx-translate) strings and save as a JSON or Gettext pot file. Extract translatable (ngx-translate) strings and save as a JSON or Gettext pot file.
Merges with existing strings if the output file already exists. Merges with existing strings if the output file already exists.
## Usage ## Install
Install the package in your project: Install the package in your project:
`npm install @biesbjerg/ngx-translate-extract --save-dev` `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": { "scripts": {
"extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/*.json --clean --sort --format namespaced-json" "i18n:init": "ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/{en,da,de,fi,nb,nl,sv}.json --clean --format 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** **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** **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` ### JSON indentation
Tabs are used by default for indentation when saving extracted strings in json formats:
**or (update only)**
`ngx-translate-extract -i ./src -o ./src/i18n/*.json`
**or (update only)**
## Custom indentation
By default, tabs are used for indentation when writing extracted strings to json formats:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation $'\t'`
If you want to use spaces instead, you can do the following: 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 ### 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. 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 ```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'); _('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 _` ### Commandline arguments
Modify the scripts arguments as required.
## Commandline arguments
``` ```
Usage: Usage:
ngx-translate-extract [options] 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: Options:
--version, -v Show version number [boolean] --version, -v Show version number [boolean]
--help, -h Show help [boolean] --help, -h Show help [boolean]
--input, -i Paths you would like to extract strings from. You --input, -i Paths you would like to extract strings from. You can use path expansion, glob patterns and
can use path expansion, glob patterns and multiple multiple paths [array] [required] [default: ["/Users/kim/apps/ngx-translate-extract"]]
paths --output, -o Paths where you would like to save extracted strings. You can use path expansion, glob
[array] [default: current working path] patterns and multiple paths [array] [required]
--patterns, -p Extract strings from the following file patterns
[array] [default: ["/**/*.html","/**/*.ts"]] Examples:
--output, -o Paths where you would like to save extracted ngx-translate-extract -i ./src-a/ -i ./src-b/ -o strings.json Extract (ts, html) from multiple paths
strings. You can use path expansion, glob patterns ngx-translate-extract -i './{src-a,src-b}/' -o strings.json Extract (ts, html) from multiple paths using brace
and multiple paths [array] [required] expansion
--marker, -m Extract strings passed to a marker function ngx-translate-extract -i ./src/ -o ./i18n/da.json -o ./i18n/en.json Extract (ts, html) and save to da.json and en.json
[string] [default: false] ngx-translate-extract -i ./src/ -o './i18n/{en,da}.json' Extract (ts, html) and save to da.json and en.json
--format, -f Output format using brace expansion
[string] [choices: "json", "namespaced-json", "pot"] [default: "json"] ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json Extract from ts, tsx and html
--format-indentation, --fi Output format indentation [string] [default: "\t"] ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o Extract from ts, html, excluding files with ".spec"
--replace, -r Replace the contents of output file if it exists strings.json
(Merges by default) [boolean] [default: false] ```
--sort, -s Sort strings in alphabetical order when saving
[boolean] [default: false] ## Note for GetText users
--clean, -c Remove obsolete strings when merging
[boolean] [default: false] Please pay attention of which version of `gettext-parser` you actually use in your project. For instance, `gettext-parser:1.2.2` does not support HTML tags in translation keys.

BIN
images/donate-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

2281
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", "name": "@biesbjerg/ngx-translate-extract",
"version": "2.3.0", "version": "7.0.3",
"description": "Extract strings from projects using ngx-translate", "description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
@@ -14,20 +14,34 @@
"scripts": { "scripts": {
"build": "npm run clean && tsc", "build": "npm run clean && tsc",
"watch": "npm run clean && tsc --watch", "watch": "npm run clean && tsc --watch",
"clean": "rm -rf ./dist", "clean": "rimraf ./dist",
"lint": "tslint --force './src/**/*.ts'", "lint": "tslint --force './src/**/*.ts'",
"test": "mocha -r ts-node/register tests/**/*.spec.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": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/biesbjerg/ngx-translate-extract.git" "url": "https://github.com/biesbjerg/ngx-translate-extract.git"
}, },
"keywords": [ "keywords": [
"angular", "angular",
"angular2",
"ionic", "ionic",
"ionic2",
"ng2-translate",
"ngx-translate", "ngx-translate",
"extract", "extract",
"extractor", "extractor",
@@ -43,34 +57,48 @@
}, },
"homepage": "https://github.com/biesbjerg/ngx-translate-extract", "homepage": "https://github.com/biesbjerg/ngx-translate-extract",
"engines": { "engines": {
"node": ">=4.3.2" "node": ">=11.15.0"
}, },
"config": {}, "config": {},
"devDependencies": { "devDependencies": {
"@types/chai": "3.4.35", "@angular/compiler": "^11.2.9",
"@types/glob": "5.0.30", "@types/braces": "^3.0.0",
"@types/mocha": "2.2.40", "@types/chai": "^4.2.16",
"@types/cheerio": "0.22.1", "@types/flat": "^5.0.1",
"@types/chalk": "0.4.31", "@types/gettext-parser": "4.0.0",
"@types/flat": "0.0.28", "@types/glob": "^7.1.3",
"@types/yargs": "6.6.0", "@types/mkdirp": "^1.0.1",
"@types/mkdirp": "0.3.29", "@types/mocha": "^8.2.2",
"chai": "3.5.0", "@types/node": "^14.14.37",
"mocha": "3.2.0", "@types/yargs": "^16.0.1",
"ts-node": "3.0.2", "braces": "^3.0.2",
"tslint": "5.0.0", "chai": "^4.3.4",
"tslint-eslint-rules": "4.0.0", "husky": "^6.0.0",
"typescript": "2.2.2" "lint-staged": "^10.5.4",
"mocha": "^8.3.2",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-etc": "^1.13.9",
"typescript": "^4.2.4"
},
"peerDependencies": {
"@angular/compiler": ">=8.0.0",
"typescript": ">=3.0.0"
}, },
"dependencies": { "dependencies": {
"chalk": "1.1.3", "@phenomnomnominal/tsquery": "^4.1.1",
"yargs": "7.0.2", "boxen": "^5.0.1",
"cheerio": "0.22.0", "colorette": "^1.2.2",
"fs": "0.0.1-security", "flat": "^5.0.2",
"gettext-parser": "1.2.2", "gettext-parser": "^4.0.4",
"glob": "7.1.1", "glob": "^7.1.6",
"path": "0.12.7", "mkdirp": "^1.0.4",
"mkdirp": "0.5.1", "path": "^0.12.7",
"flat": "2.0.1" "terminal-link": "^2.1.1",
"yargs": "^16.2.0"
} }
} }

View File

@@ -1,16 +1,35 @@
import * as yargs from 'yargs';
import { red, green } from 'colorette';
import { ExtractTask } from './tasks/extract.task'; import { ExtractTask } from './tasks/extract.task';
import { ParserInterface } from '../parsers/parser.interface'; import { ParserInterface } from '../parsers/parser.interface';
import { PipeParser } from '../parsers/pipe.parser'; import { PipeParser } from '../parsers/pipe.parser';
import { DirectiveParser } from '../parsers/directive.parser'; import { DirectiveParser } from '../parsers/directive.parser';
import { ServiceParser } from '../parsers/service.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 { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory'; import { CompilerFactory } from '../compilers/compiler.factory';
import { normalizePaths } from '../utils/fs-helpers';
import { donateMessage } from '../utils/donate';
import * as fs from 'fs'; // First parsing pass to be able to access pattern argument for use input/output arguments
import * as yargs from 'yargs'; 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]') .usage('Extract strings from files for translation.\nUsage: $0 [options]')
.version(require(__dirname + '/../../package.json').version) .version(require(__dirname + '/../../package.json').version)
.alias('version', 'v') .alias('version', 'v')
@@ -19,24 +38,14 @@ export const cli = yargs
.option('input', { .option('input', {
alias: 'i', alias: 'i',
describe: 'Paths you would like to extract strings from. You can use path expansion, glob patterns and multiple paths', 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', type: 'array',
normalize: true normalize: true,
required: true
}) })
.check(options => { .coerce('input', (input: string[]) => {
options.input.forEach((dir: string) => { const paths = normalizePaths(input, parsed.patterns);
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { return paths;
throw new Error(`The path you supplied was not found: '${dir}'`);
}
});
return true;
})
.option('patterns', {
alias: 'p',
describe: 'Extract strings from the following file patterns',
type: 'array',
default: ['/**/*.html', '/**/*.ts']
}) })
.option('output', { .option('output', {
alias: 'o', alias: 'o',
@@ -45,68 +54,108 @@ export const cli = yargs
normalize: true, normalize: true,
required: true required: true
}) })
.option('marker', { .coerce('output', (output: string[]) => {
alias: 'm', const paths = normalizePaths(output, parsed.patterns);
describe: 'Extract strings passed to a marker function', return paths;
default: false,
type: 'string'
}) })
.option('format', { .option('format', {
alias: 'f', alias: 'f',
describe: 'Output format', describe: 'Format',
default: 'json', default: 'json',
type: 'string', type: 'string',
choices: ['json', 'namespaced-json', 'pot'] choices: ['json', 'namespaced-json', 'pot']
}) })
.option('format-indentation', { .option('format-indentation', {
alias: 'fi', alias: 'fi',
describe: 'Output format indentation', describe: 'Format indentation (JSON/Namedspaced JSON)',
default: '\t', default: '\t',
type: 'string' type: 'string'
}) })
.option('replace', { .option('replace', {
alias: 'r', alias: 'r',
describe: 'Replace the contents of output file if it exists (Merges by default)', describe: 'Replace the contents of output file if it exists (Merges by default)',
default: false,
type: 'boolean' type: 'boolean'
}) })
.option('sort', { .option('sort', {
alias: 's', alias: 's',
describe: 'Sort strings in alphabetical order when saving', describe: 'Sort strings in alphabetical order',
default: false,
type: 'boolean' type: 'boolean'
}) })
.option('clean', { .option('clean', {
alias: 'c', alias: 'c',
describe: 'Remove obsolete strings when merging', describe: 'Remove obsolete strings after merge',
default: false,
type: 'boolean' 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) .exitProcess(true)
.parse(process.argv); .parse(process.argv);
const extract = new ExtractTask(cli.input, cli.output, { const extractTask = new ExtractTask(cli.input, cli.output, {
replace: cli.replace, replace: cli.replace
sort: cli.sort,
clean: cli.clean,
patterns: cli.patterns
}); });
// 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, { const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation indentation: cli.formatIndentation
}); });
extract.setCompiler(compiler); extractTask.setCompiler(compiler);
const parsers: ParserInterface[] = [ // Run task
new PipeParser(), try {
new DirectiveParser(), extractTask.execute();
new ServiceParser() console.log(green('\nDone.\n'));
]; console.log(donateMessage);
if (cli.marker) { process.exit(0);
parsers.push(new FunctionParser({ } catch (e) {
identifier: cli.marker 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 { TranslationCollection } from '../../utils/translation.collection';
import { TaskInterface } from './task.interface'; import { TaskInterface } from './task.interface';
import { ParserInterface } from '../../parsers/parser.interface'; import { ParserInterface } from '../../parsers/parser.interface';
import { PostProcessorInterface } from '../../post-processors/post-processor.interface';
import { CompilerInterface } from '../../compilers/compiler.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 glob from 'glob';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
@@ -11,135 +12,170 @@ import * as mkdirp from 'mkdirp';
export interface ExtractTaskOptionsInterface { export interface ExtractTaskOptionsInterface {
replace?: boolean; replace?: boolean;
sort?: boolean;
clean?: boolean;
patterns?: string[];
} }
export class ExtractTask implements TaskInterface { export class ExtractTask implements TaskInterface {
protected options: ExtractTaskOptionsInterface = {
protected _options: ExtractTaskOptionsInterface = { replace: false
replace: false,
sort: false,
clean: false,
patterns: []
}; };
protected _parsers: ParserInterface[] = []; protected parsers: ParserInterface[] = [];
protected _compiler: CompilerInterface; protected postProcessors: PostProcessorInterface[] = [];
protected compiler: CompilerInterface;
public constructor(protected _input: string[], protected _output: string[], options?: ExtractTaskOptionsInterface) { public constructor(protected inputs: string[], protected outputs: string[], options?: ExtractTaskOptionsInterface) {
this._options = Object.assign({}, this._options, options); this.inputs = inputs.map((input) => path.resolve(input));
this.outputs = outputs.map((output) => path.resolve(output));
this.options = { ...this.options, ...options };
} }
public execute(): void { public execute(): void {
if (!this._parsers) { if (!this.compiler) {
throw new Error('No parsers configured');
}
if (!this._compiler) {
throw new Error('No compiler configured'); throw new Error('No compiler configured');
} }
const collection = this._extract(); this.printEnabledParsers();
this._out(chalk.green('Extracted %d strings\n'), collection.count()); this.printEnabledPostProcessors();
this._save(collection); 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 { public setParsers(parsers: ParserInterface[]): this {
this._parsers = parsers; this.parsers = parsers;
return this;
}
public setPostProcessors(postProcessors: PostProcessorInterface[]): this {
this.postProcessors = postProcessors;
return this; return this;
} }
public setCompiler(compiler: CompilerInterface): this { public setCompiler(compiler: CompilerInterface): this {
this._compiler = compiler; this.compiler = compiler;
return this; return this;
} }
/** /**
* Extract strings from input dirs using configured parsers * Extract strings from specified input dirs using configured parsers
*/ */
protected _extract(): TranslationCollection { protected extract(): TranslationCollection {
this._out(chalk.bold('Extracting strings...'));
let collection: TranslationCollection = new TranslationCollection(); let collection: TranslationCollection = new TranslationCollection();
this._input.forEach(dir => { this.inputs.forEach((pattern) => {
this._readDir(dir, this._options.patterns).forEach(path => { this.getFiles(pattern).forEach((filePath) => {
this._out(chalk.gray('- %s'), path); this.out(dim('- %s'), filePath);
const contents: string = fs.readFileSync(path, 'utf-8'); const contents: string = fs.readFileSync(filePath, 'utf-8');
this._parsers.forEach((parser: ParserInterface) => { this.parsers.forEach((parser) => {
collection = collection.union(parser.extract(contents, path)); const extracted = parser.extract(contents, filePath);
if (extracted instanceof TranslationCollection) {
collection = collection.union(extracted);
}
}); });
}); });
}); });
return collection; return collection;
} }
/** /**
* Process collection according to options (merge, clean, sort), compile and save * Run strings through configured post processors
* @param collection
*/ */
protected _save(collection: TranslationCollection): void { protected process(draft: TranslationCollection, extracted: TranslationCollection, existing: TranslationCollection): TranslationCollection {
this._output.forEach(output => { this.postProcessors.forEach((postProcessor) => {
const normalizedOutput: string = path.resolve(output); draft = postProcessor.process(draft, extracted, existing);
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!'));
}); });
return draft;
} }
/** /**
* Get all files in dir matching patterns * Compile and save translations
* @param collection
*/ */
protected _readDir(dir: string, patterns: string[]): string[] { protected save(output: string, collection: TranslationCollection): void {
return patterns.reduce((results, pattern) => { const dir = path.dirname(output);
return glob.sync(dir + pattern) if (!fs.existsSync(dir)) {
.filter(path => fs.statSync(path).isFile()) mkdirp.sync(dir);
.concat(results); }
}, []); 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); 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'; import { PoCompiler } from '../compilers/po.compiler';
export class CompilerFactory { export class CompilerFactory {
public static create(format: string, options?: {}): CompilerInterface { public static create(format: string, options?: {}): CompilerInterface {
switch (format) { switch (format) {
case 'pot': return new PoCompiler(options); case 'pot':
case 'json': return new JsonCompiler(options); return new PoCompiler(options);
case 'namespaced-json': return new NamespacedJsonCompiler(options); case 'json':
default: throw new Error(`Unknown format: ${format}`); 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'; import { TranslationCollection } from '../utils/translation.collection';
export interface CompilerInterface { export interface CompilerInterface {
extension: string; extension: string;
compile(collection: TranslationCollection): string; compile(collection: TranslationCollection): string;
parse(contents: string): TranslationCollection; parse(contents: string): TranslationCollection;
} }

View File

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

View File

@@ -1,10 +1,10 @@
import { CompilerInterface } from './compiler.interface'; import { CompilerInterface } from './compiler.interface';
import { TranslationCollection } from '../utils/translation.collection'; 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 { export class NamespacedJsonCompiler implements CompilerInterface {
public indentation: string = '\t'; public indentation: string = '\t';
public extension = 'json'; public extension = 'json';
@@ -16,15 +16,14 @@ export class NamespacedJsonCompiler implements CompilerInterface {
} }
public compile(collection: TranslationCollection): string { public compile(collection: TranslationCollection): string {
const values: {} = flat.unflatten(collection.values, { const values: {} = unflatten(collection.values, {
object: true object: true
}); });
return JSON.stringify(values, null, this.indentation); return JSON.stringify(values, null, this.indentation);
} }
public parse(contents: string): TranslationCollection { public parse(contents: string): TranslationCollection {
const values: {} = flat.flatten(JSON.parse(contents)); const values: {} = flatten(JSON.parse(stripBOM(contents)));
return new TranslationCollection(values); return new TranslationCollection(values);
} }
} }

View File

@@ -1,18 +1,17 @@
import { CompilerInterface } from './compiler.interface'; import { CompilerInterface } from './compiler.interface';
import { TranslationCollection, TranslationType } from '../utils/translation.collection'; import { TranslationCollection, TranslationType } from '../utils/translation.collection';
import * as gettext from 'gettext-parser'; import { po } from 'gettext-parser';
export class PoCompiler implements CompilerInterface { export class PoCompiler implements CompilerInterface {
public extension: string = 'po';
public extension = 'po';
/** /**
* Translation domain * Translation domain
*/ */
public domain = ''; public domain: string = '';
public constructor(options?: any) { } public constructor(options?: any) {}
public compile(collection: TranslationCollection): string { public compile(collection: TranslationCollection): string {
const data = { const data = {
@@ -24,34 +23,38 @@ export class PoCompiler implements CompilerInterface {
}, },
translations: { translations: {
[this.domain]: Object.keys(collection.values).reduce((translations, key) => { [this.domain]: Object.keys(collection.values).reduce((translations, key) => {
translations[key] = { return {
msgid: key, ...translations,
msgstr: collection.get(key) [key]: {
msgid: key,
msgstr: collection.get(key)
}
}; };
return translations; }, {} as any)
}, <any> {})
} }
}; };
return gettext.po.compile(data, 'utf-8'); return po.compile(data).toString('utf8');
} }
public parse(contents: string): TranslationCollection { public parse(contents: string): TranslationCollection {
const collection = new TranslationCollection(); const collection = new TranslationCollection();
const po = gettext.po.parse(contents, 'utf-8'); const parsedPo = po.parse(contents, 'utf8');
if (!po.translations.hasOwnProperty(this.domain)) {
if (!parsedPo.translations.hasOwnProperty(this.domain)) {
return collection; return collection;
} }
const values = Object.keys(po.translations[this.domain]) const values = Object.keys(parsedPo.translations[this.domain])
.filter(key => key.length > 0) .filter((key) => key.length > 0)
.reduce((values, key) => { .reduce((result, key) => {
values[key] = po.translations[this.domain][key].msgstr.pop(); return {
return values; ...result,
}, <TranslationType> {}); [key]: parsedPo.translations[this.domain][key].msgstr.pop()
};
}, {} as TranslationType);
return new TranslationCollection(values); return new TranslationCollection(values);
} }
} }

View File

@@ -1 +0,0 @@
declare module 'gettext-parser';

View File

@@ -6,15 +6,18 @@ export * from './cli/tasks/task.interface';
export * from './cli/tasks/extract.task'; export * from './cli/tasks/extract.task';
export * from './parsers/parser.interface'; 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/directive.parser';
export * from './parsers/pipe.parser'; export * from './parsers/pipe.parser';
export * from './parsers/service.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.interface';
export * from './compilers/compiler.factory'; export * from './compilers/compiler.factory';
export * from './compilers/json.compiler'; export * from './compilers/json.compiler';
export * from './compilers/namespaced-json.compiler'; export * from './compilers/namespaced-json.compiler';
export * from './compilers/po.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,180 @@
import {
parseTemplate,
TmplAstNode as Node,
TmplAstElement as Element,
TmplAstText as Text,
TmplAstTemplate as Template,
TmplAstTextAttribute as TextAttribute,
TmplAstBoundAttribute as BoundAttribute,
AST,
ASTWithSource,
LiteralPrimitive,
Conditional,
Binary,
BindingPipe,
Interpolation,
LiteralArray,
LiteralMap
} from '@angular/compiler';
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection'; import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
import * as $ from 'cheerio'; const TRANSLATE_ATTR_NAME = 'translate';
type ElementLike = Element | Template;
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface { export class DirectiveParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection(); let collection: TranslationCollection = new TranslationCollection();
template = this._normalizeTemplateAttributes(template); if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
const nodes: Node[] = this.parseTemplate(source, filePath);
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);
const selector = '[translate], [ng2-translate]'; elements.forEach((element) => {
$(template) const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
.find(selector) if (attribute?.value) {
.addBack(selector) collection = collection.add(attribute.value);
.each((i: number, element: CheerioElement) => { return;
const $element = $(element); }
const attr = $element.attr('translate') || $element.attr('ng2-translate');
if (attr) { const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME);
collection = collection.add(attr); if (boundAttribute?.value) {
} else { this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
$element collection = collection.add(literalPrimitive.value);
.contents() });
.toArray() return;
.filter(node => node.type === 'text') }
.map(node => node.nodeValue.trim())
.filter(text => text.length > 0) const textNodes = this.getTextNodes(element);
.forEach(text => collection = collection.add(text)); textNodes.forEach((textNode) => {
} collection = collection.add(textNode.value.trim());
}); });
});
return collection; return collection;
} }
/** /**
* Angular's `[attr]="'val'"` syntax is not valid HTML, * Find all ElementLike nodes with a translate attribute
* so it can't be parsed by standard HTML parsers. * @param nodes
* This method replaces `[attr]="'val'""` with `attr="val"`
*/ */
protected _normalizeTemplateAttributes(template: string): string { protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
return template.replace(/\[([^\]]+)\]="'([^']*)'"/g, '$1="$2"'); let elements: ElementLike[] = [];
nodes.filter(this.isElementLike).forEach((element) => {
if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
const childElements = this.getElementsWithTranslateAttribute(element.children);
if (childElements.length) {
elements = [...elements, ...childElements];
}
});
return elements;
} }
/**
* Get direct child nodes of type Text
* @param element
*/
protected getTextNodes(element: ElementLike): Text[] {
return element.children.filter(this.isText);
}
/**
* Check if attribute is present on element
* @param element
*/
protected hasAttribute(element: ElementLike, name: string): boolean {
return this.getAttribute(element, name) !== undefined;
}
/**
* Get attribute value if present on element
* @param element
*/
protected getAttribute(element: ElementLike, name: string): TextAttribute {
return element.attributes.find((attribute) => attribute.name === name);
}
/**
* Check if bound attribute is present on element
* @param element
* @param name
*/
protected hasBoundAttribute(element: ElementLike, name: string): boolean {
return this.getBoundAttribute(element, name) !== undefined;
}
/**
* Get bound attribute if present on element
* @param element
* @param name
*/
protected getBoundAttribute(element: ElementLike, name: string): BoundAttribute {
return element.inputs.find((input) => input.name === name);
}
/**
* Get literal primitives from expression
* @param exp
*/
protected getLiteralPrimitives(exp: AST): LiteralPrimitive[] {
if (exp instanceof LiteralPrimitive) {
return [exp];
}
let visit: AST[] = [];
if (exp instanceof Interpolation) {
visit = exp.expressions;
} else if (exp instanceof LiteralArray) {
visit = exp.expressions;
} else if (exp instanceof LiteralMap) {
visit = exp.values;
} else if (exp instanceof BindingPipe) {
visit = [exp.exp];
} else if (exp instanceof Conditional) {
visit = [exp.trueExp, exp.falseExp];
} else if (exp instanceof Binary) {
visit = [exp.left, exp.right];
} else if (exp instanceof ASTWithSource) {
visit = [exp.ast];
}
let results: LiteralPrimitive[] = [];
visit.forEach((child) => {
results = [...results, ...this.getLiteralPrimitives(child)];
});
return results;
}
/**
* Check if node type is ElementLike
* @param node
*/
protected isElementLike(node: Node): node is ElementLike {
return node instanceof Element || node instanceof Template;
}
/**
* Check if node type is Text
* @param node
*/
protected isText(node: Node): node is Text {
return node instanceof Text;
}
/**
* Parse a template into nodes
* @param template
* @param path
*/
protected parseTemplate(template: string, path: string): Node[] {
return parseTemplate(template, path).nodes;
}
} }

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'; import { TranslationCollection } from '../utils/translation.collection';
export interface ParserInterface { export interface ParserInterface {
extract(source: string, filePath: string): TranslationCollection | null;
extract(contents: string, path?: string): TranslationCollection;
} }

View File

@@ -1,27 +1,163 @@
import {
AST,
TmplAstNode,
parseTemplate,
BindingPipe,
LiteralPrimitive,
Conditional,
TmplAstTextAttribute,
Binary,
LiteralMap,
LiteralArray,
Interpolation,
MethodCall
} from '@angular/compiler';
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection'; 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 { export class PipeParser implements ParserInterface {
if (path && this._isAngularComponent(path)) { public extract(source: string, filePath: string): TranslationCollection | null {
contents = this._extractInlineTemplate(contents); if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
} }
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection(); let collection: TranslationCollection = new TranslationCollection();
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g; const pipes: BindingPipe[] = nodes.map((node) => this.findPipesInNode(node)).flat();
let matches: RegExpExecArray; pipes.forEach((pipe) => {
while (matches = regExp.exec(template)) { this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => {
collection = collection.add(matches[2].replace('\\\'', '\'')); collection = collection.add(key);
} });
});
return collection; 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) {
ret.push(...this.getTranslatablesFromAst(node.value.ast));
}
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) {
ret.push(...this.getTranslatablesFromAst(input.value.ast));
}
});
}
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 getTranslatablesFromAst(ast: AST): BindingPipe[] {
// the entire expression is the translate pipe, e.g.:
// - 'foo' | translate
// - (condition ? 'foo' : 'bar') | translate
if (this.expressionIsOrHasBindingPipe(ast)) {
return [ast];
}
// angular double curly bracket interpolation, e.g.:
// - {{ expressions }}
if (ast instanceof Interpolation) {
return this.getTranslatablesFromAsts(ast.expressions);
}
// ternary operator, e.g.:
// - condition ? null : ('foo' | translate)
// - condition ? ('foo' | translate) : null
if (ast instanceof Conditional) {
return this.getTranslatablesFromAsts([ast.trueExp, ast.falseExp]);
}
// string concatenation, e.g.:
// - 'foo' + 'bar' + ('baz' | translate)
if (ast instanceof Binary) {
return this.getTranslatablesFromAsts([ast.left, ast.right]);
}
// a pipe on the outer expression, but not the translate pipe - ignore the pipe, visit the expression, e.g.:
// - { foo: 'Hello' | translate } | json
if (ast instanceof BindingPipe) {
return this.getTranslatablesFromAst(ast.exp);
}
// object - ignore the keys, visit all values, e.g.:
// - { key1: 'value1' | translate, key2: 'value2' | translate }
if (ast instanceof LiteralMap) {
return this.getTranslatablesFromAsts(ast.values);
}
// array - visit all its values, e.g.:
// - [ 'value1' | translate, 'value2' | translate ]
if (ast instanceof LiteralArray) {
return this.getTranslatablesFromAsts(ast.expressions);
}
if (ast instanceof MethodCall) {
return this.getTranslatablesFromAsts(ast.args);
}
return [];
}
protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] {
return this.flatten(asts.map((ast) => this.getTranslatablesFromAst(ast)));
}
protected flatten<T extends AST>(array: T[][]): T[] {
return [].concat(...array);
}
protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
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 { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection'; 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; const classDeclarations = findClassDeclarations(sourceFile);
if (!classDeclarations) {
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) {
return null; return null;
} }
const result = constructorNode.parameters.find(parameter => { let collection: TranslationCollection = new TranslationCollection();
// Skip if visibility modifier is not present (we want it set as an instance property)
if (!parameter.modifiers) {
return false;
}
// Parameter has no type classDeclarations.forEach((classDeclaration) => {
if (!parameter.type) { const callExpressions = [
return false; ...this.findConstructorParamCallExpressions(classDeclaration),
} ...this.findPropertyCallExpressions(classDeclaration)
];
// Make sure className is of the correct type callExpressions.forEach((callExpression) => {
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier; const [firstArg] = callExpression.arguments;
if (!parameterType) { if (!firstArg) {
return false; return;
}
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;
} }
const strings = getStringsFromExpression(firstArg);
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression; collection = collection.addKeys(strings);
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;
}); });
});
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 { export interface TranslationType {
[key: string]: string [key: string]: string;
} }
export class TranslationCollection { export class TranslationCollection {
public values: TranslationType = {}; public values: TranslationType = {};
public constructor(values: TranslationType = {}) { public constructor(values: TranslationType = {}) {
@@ -11,29 +10,28 @@ export class TranslationCollection {
} }
public add(key: string, val: string = ''): 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 { public addKeys(keys: string[]): TranslationCollection {
const values = keys.reduce((results, key) => { const values = keys.reduce((results, key) => {
results[key] = ''; return { ...results, [key]: '' };
return results; }, {} as TranslationType);
}, <TranslationType> {}); return new TranslationCollection({ ...this.values, ...values });
return new TranslationCollection(Object.assign({}, this.values, values));
} }
public remove(key: string): TranslationCollection { 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 { 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; return this;
} }
public filter(callback: (key?: string, val?: string) => boolean): TranslationCollection { public filter(callback: (key?: string, val?: string) => boolean): TranslationCollection {
let values: TranslationType = {}; const values: TranslationType = {};
this.forEach((key: string, val: string) => { this.forEach((key, val) => {
if (callback.call(this, key, val)) { if (callback.call(this, key, val)) {
values[key] = val; values[key] = val;
} }
@@ -41,16 +39,23 @@ export class TranslationCollection {
return new TranslationCollection(values); 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 { 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 { public intersect(collection: TranslationCollection): TranslationCollection {
let values: TranslationType = {}; const values: TranslationType = {};
this.filter(key => collection.has(key)) this.filter((key) => collection.has(key)).forEach((key, val) => {
.forEach((key: string, val: string) => { values[key] = val;
values[key] = val; });
});
return new TranslationCollection(values); return new TranslationCollection(values);
} }
@@ -76,10 +81,12 @@ export class TranslationCollection {
} }
public sort(compareFn?: (a: string, b: string) => number): TranslationCollection { public sort(compareFn?: (a: string, b: string) => number): TranslationCollection {
let values: TranslationType = {}; const values: TranslationType = {};
this.keys().sort(compareFn).forEach((key) => { this.keys()
values[key] = this.get(key); .sort(compareFn)
}); .forEach((key) => {
values[key] = this.get(key);
});
return new TranslationCollection(values); 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'; import { NamespacedJsonCompiler } from '../../src/compilers/namespaced-json.compiler';
describe('NamespacedJsonCompiler', () => { describe('NamespacedJsonCompiler', () => {
let compiler: NamespacedJsonCompiler; let compiler: NamespacedJsonCompiler;
beforeEach(() => { beforeEach(() => {
@@ -23,7 +22,10 @@ describe('NamespacedJsonCompiler', () => {
} }
`; `;
const collection: TranslationCollection = compiler.parse(contents); 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', () => { it('should unflatten keys on compile', () => {
@@ -37,9 +39,9 @@ describe('NamespacedJsonCompiler', () => {
it('should preserve numeric values on compile', () => { it('should preserve numeric values on compile', () => {
const collection = new TranslationCollection({ const collection = new TranslationCollection({
"option.0": '', 'option.0': '',
"option.1": '', 'option.1': '',
"option.2": '' 'option.2': ''
}); });
const result: string = compiler.compile(collection); 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}'); 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}'); 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

@@ -0,0 +1,24 @@
import { expect } from 'chai';
import { TranslationCollection } from '../../src/utils/translation.collection';
import { PoCompiler } from '../../src/compilers/po.compiler';
describe('PoCompiler', () => {
let compiler: PoCompiler;
beforeEach(() => {
compiler = new PoCompiler();
});
it('should still include html ', () => {
const collection = new TranslationCollection({
'A <strong>test</strong>': 'Un <strong>test</strong>',
'With a lot of <em>html</em> included': 'Avec beaucoup d\'<em>html</em> inclus'
});
const result: Buffer = Buffer.from(compiler.compile(collection));
expect(result.toString('utf8')).to.equal('msgid ""\nmsgstr ""\n"mime-version: 1.0\\n"\n"Content-Type: text/plain; charset=utf-8\\n"\n"Content-Transfer-Encoding: 8bit\\n"\n\nmsgid "A <strong>test</strong>"\nmsgstr "Un <strong>test</strong>"\n\nmsgid "With a lot of <em>html</em> included"\nmsgstr "Avec beaucoup d\'<em>html</em> inclus"');
});
});

View File

@@ -2,78 +2,99 @@ import { expect } from 'chai';
import { DirectiveParser } from '../../src/parsers/directive.parser'; import { DirectiveParser } from '../../src/parsers/directive.parser';
class TestDirectiveParser extends DirectiveParser {
public normalizeTemplateAttributes(template: string): string {
return this._normalizeTemplateAttributes(template);
}
}
describe('DirectiveParser', () => { describe('DirectiveParser', () => {
const templateFilename: string = 'test.template.html'; const templateFilename: string = 'test.template.html';
const componentFilename: string = 'test.component.ts'; const componentFilename: string = 'test.component.ts';
let parser: TestDirectiveParser; let parser: DirectiveParser;
beforeEach(() => { beforeEach(() => {
parser = new TestDirectiveParser(); parser = new DirectiveParser();
}); });
it('should extract contents when no translate attribute value is provided', () => {
it('should extract keys when using literal map in bound attribute', () => {
const contents = `<div [translate]="{ key1: 'value1' | translate, key2: 'value2' | translate }"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['value1', 'value2']);
});
it('should extract keys when using literal arrays in bound attribute', () => {
const contents = `<div [translate]="[ 'value1' | translate, 'value2' | translate ]"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['value1', 'value2']);
});
it('should extract keys when using binding pipe in bound attribute', () => {
const contents = `<div [translate]="'KEY1' | withPipe"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using binary expression in bound attribute', () => {
const contents = `<div [translate]="keyVar || 'KEY1'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using literal primitive in bound attribute', () => {
const contents = `<div [translate]="'KEY1'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using conditional in bound attribute', () => {
const contents = `<div [translate]="condition ? 'KEY1' : 'KEY2'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1', 'KEY2']);
});
it('should extract keys when using nested conditionals in bound attribute', () => {
const contents = `<div [translate]="isSunny ? (isWarm ? 'Sunny and warm' : 'Sunny but cold') : 'Not sunny'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Sunny and warm', 'Sunny but cold', 'Not sunny']);
});
it('should extract keys when using interpolation', () => {
const contents = `<div translate="{{ 'KEY1' + key2 + 'KEY3' }}"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1', 'KEY3']);
});
it('should extract keys keeping proper whitespace', () => {
const contents = `
<div translate>
Wubba
Lubba
Dub Dub
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Wubba Lubba Dub Dub']);
});
it('should use element contents as key when no translate attribute value is present', () => {
const contents = '<div translate>Hello World</div>'; const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract translate attribute if provided', () => { it('should use translate attribute value as key when present', () => {
const contents = '<div translate="KEY">Hello World<div>'; const contents = '<div translate="MY_KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys(); 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', () => { it('should extract keys from child elements when translate attribute is present', () => {
const contents = `<div [translate]="'KEY'">Hello World<div>`; const contents = `<div translate>Hello <strong translate>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['Hello', 'World']);
}); });
it('should extract direct text nodes when no translate attribute value is provided', () => { it('should not extract keys from child elements when translate attribute is not present', () => {
const contents = ` const contents = `<div translate>Hello <strong>World</strong></div>`;
<div translate>
<span>&#10003;</span>
Hello <strong>World</strong>
Hi <em>there</em>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'Hi']); expect(keys).to.deep.equal(['Hello']);
});
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']);
}); });
it('should extract and parse inline template', () => { it('should extract and parse inline template', () => {
@@ -88,34 +109,65 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract contents when no ng2-translate attribute value is provided', () => { it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div ng2-translate>Hello World</div>'; const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract ng2-translate attribute if provided', () => { it('should extract translate attribute value if provided', () => {
const contents = '<div ng2-translate="KEY">Hello World<div>'; const contents = '<div 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>`;
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['KEY']);
}); });
it('should not extract translate pipe in html tag', () => { 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); const collection = parser.extract(contents, templateFilename);
expect(collection.values).to.deep.equal({}); expect(collection.values).to.deep.equal({});
}); });
it('should normalize bound attributes', () => { it('should extract contents from custom elements', () => {
const contents = `<p [translate]="'KEY'">Hello World</p>`; const contents = `<custom-table><tbody><tr><td translate>Hello World</td></tr></tbody></custom-table>`;
const template = parser.normalizeTemplateAttributes(contents); const keys = parser.extract(contents, templateFilename).keys();
expect(template).to.equal('<p translate="KEY">Hello World</p>'); expect(keys).to.deep.equal(['Hello World']);
});
it('should extract from template without leading/trailing whitespace', () => {
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 keys from element without leading/trailing whitespace', () => {
const contents = `
<div translate>
this is an example
of a long label
</div>
<div>
<p translate>
this is an example
of another a long label
</p>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['this is an example of a long label', 'this is an example of another a long label']);
});
it('should collapse excessive whitespace', () => {
const contents = '<p translate>this is an example</p>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['this is an example']);
}); });
}); });

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,64 @@
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 split strings while keeping html tags', () => {
const contents = `
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
_('Hello ' + 'world');
_('This <em>is</em> 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 <em>is</em> 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'; import { PipeParser } from '../../src/parsers/pipe.parser';
describe('PipeParser', () => { describe('PipeParser', () => {
const templateFilename: string = 'test.template.html'; const templateFilename: string = 'test.template.html';
let parser: PipeParser; let parser: PipeParser;
@@ -30,36 +29,114 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['World']); 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 right expression', () => {
const contents = `{{ condition ? null : ('World' | translate) }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="condition ? null : ('World' | translate)"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings from ternary operators left expression', () => {
const contents = `{{ condition ? ('World' | translate) : null }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings inside string concatenation', () => {
const contents = `{{ 'a' + ('Hello' | translate) + 'b' + 'c' + ('World' | translate) + 'd' }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from object', () => {
const contents = `{{ { foo: 'Hello' | translate, bar: ['World' | translate], deep: { nested: { baz: 'Yes' | translate } } } | json }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World', 'Yes']);
});
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 expressions', () => {
const contents = `<span [attr]="{ foo: ['a' + ((condition ? 'Hello' : 'World') | translate) + 'b'] }"></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', () => { it('should extract strings with escaped quotes', () => {
const contents = `Hello {{ 'World\\'s largest potato' | translate }}`; const contents = `Hello {{ 'World\\'s largest potato' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`World's largest potato`]); 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', () => { 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(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract bound strings using translate pipe in attributes', () => { 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(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should not use a greedy regular expression', () => { it('should extract multiple entries from nodes', () => {
const contents = ` const contents = `
<ion-header> <ion-header>
<ion-navbar color="brand"> <ion-navbar color="brand">
<ion-title>{{ 'Info' | translate }}</ion-title> <ion-title>{{ 'Info' | translate }}</ion-title>
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<content-loading *ngIf="isLoading"> <content-loading *ngIf="isLoading">
{{ 'Loading...' | translate }} {{ 'Loading...' | translate }}
</content-loading> </content-loading>
</ion-content> </ion-content>
@@ -69,7 +146,7 @@ describe('PipeParser', () => {
}); });
it('should extract strings on same line', () => { 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(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']); expect(keys).to.deep.equal(['Hello', 'World']);
}); });
@@ -79,7 +156,7 @@ describe('PipeParser', () => {
<ion-list inset> <ion-list inset>
<ion-item> <ion-item>
<ion-icon item-left name="person" color="dark"></ion-icon> <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>
<ion-item> <ion-item>
<p color="danger" danger *ngFor="let error of form.get('name').getError('remote')"> <p color="danger" danger *ngFor="let error of form.get('name').getError('remote')">
@@ -88,11 +165,40 @@ describe('PipeParser', () => {
</ion-item> </ion-item>
</ion-list> </ion-list>
<div class="form-actions"> <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> </div>
`; `;
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Name', 'Create account']); 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([]);
});
it('should extract strings from piped arguments inside a function calls on templates', () => {
const contents = `{{ callMe('Hello' | translate, 'World' | translate ) }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`Hello`, `World`]);
});
}); });

View File

@@ -2,39 +2,56 @@ import { expect } from 'chai';
import { ServiceParser } from '../../src/parsers/service.parser'; import { ServiceParser } from '../../src/parsers/service.parser';
class TestServiceParser extends ServiceParser {
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
describe('ServiceParser', () => { describe('ServiceParser', () => {
const componentFilename: string = 'test.component.ts'; const componentFilename: string = 'test.component.ts';
let parser: TestServiceParser; let parser: ServiceParser;
beforeEach(() => { 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
public constructor( public constructor(protected _translateService: TranslateService) { }
_serviceA: ServiceA, public test() {
public _serviceB: ServiceB, const message = 'The Message';
protected _translateService: TranslateService this._translateService.get(message || 'Fallback message');
) { } }
`; `;
const name = parser.getInstancePropertyName(); const keys = parser.extract(contents, componentFilename).keys();
expect(name).to.equal('_translateService'); 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
@@ -47,7 +64,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']); 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
@@ -60,7 +77,20 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']); 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
@@ -73,7 +103,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello', 'World']); 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
@@ -86,7 +116,33 @@ describe('ServiceParser', () => {
expect(key).to.deep.equal(['Hello', 'World']); 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 = ` const contents = `
@Component({ }) @Component({ })
export class AppComponent { export class AppComponent {
@@ -97,6 +153,7 @@ describe('ServiceParser', () => {
public test() { public test() {
this._otherService.get('Hello World'); this._otherService.get('Hello World');
this._otherService.instant('Hi there'); this._otherService.instant('Hi there');
this._otherService.stream('Hi there');
} }
`; `;
const keys = parser.extract(contents, componentFilename).keys(); const keys = parser.extract(contents, componentFilename).keys();
@@ -164,25 +221,88 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal([]); 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', () => { it('should extract strings from all classes in the file', () => {
const contents = ` const contents = `
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
export class Stuff { export class Stuff {
thing: string; thing: string;
translate: any;
constructor(thing: string) { constructor(thing: string) {
this.translate.get('Not me');
this.thing = thing; this.thing = thing;
} }
} }
@Injectable() @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 { export class AuthService {
constructor(public translate: TranslateService) { constructor(public translate: TranslateService) {
console.log(this.translate.instant("Hello!")); this.translate.instant("Hello!");
} }
} }
`; `;
const keys = parser.extract(contents, componentFilename).keys(); 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 { expect } from 'chai';
import { AbstractTemplateParser } from '../../src/parsers/abstract-template.parser'; import { isPathAngularComponent, extractComponentInlineTemplate } from '../../src/utils/utils';
class TestTemplateParser extends AbstractTemplateParser {
public isAngularComponent(filePath: string): boolean {
return this._isAngularComponent(filePath);
}
public extractInlineTemplate(contents: string): string {
return this._extractInlineTemplate(contents);
}
}
describe('AbstractTemplateParser', () => {
let parser: TestTemplateParser;
beforeEach(() => {
parser = new TestTemplateParser();
});
describe('Utils', () => {
it('should recognize js extension as angular component', () => { it('should recognize js extension as angular component', () => {
const result = parser.isAngularComponent('test.js'); const result = isPathAngularComponent('test.js');
expect(result).to.equal(true); expect(result).to.equal(true);
}); });
it('should recognize ts extension as angular component', () => { it('should recognize ts extension as angular component', () => {
const result = parser.isAngularComponent('test.ts'); const result = isPathAngularComponent('test.ts');
expect(result).to.equal(true); expect(result).to.equal(true);
}); });
it('should not recognize html extension as angular component', () => { 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); expect(result).to.equal(false);
}); });
@@ -45,10 +26,22 @@ describe('AbstractTemplateParser', () => {
}) })
export class TestComponent { } export class TestComponent { }
`; `;
const template = parser.extractInlineTemplate(contents); const template = extractComponentInlineTemplate(contents);
expect(template).to.equal('<p translate>Hello World</p>'); 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', () => { it('should extract inline template spanning multiple lines', () => {
const contents = ` const contents = `
@Component({ @Component({
@@ -66,8 +59,7 @@ describe('AbstractTemplateParser', () => {
}) })
export class TestComponent { } 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'); 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'; import { TranslationCollection } from '../../src/utils/translation.collection';
describe('StringCollection', () => { describe('StringCollection', () => {
let collection: TranslationCollection; let collection: TranslationCollection;
beforeEach(() => { beforeEach(() => {
@@ -64,12 +63,15 @@ describe('StringCollection', () => {
it('should merge with other collection', () => { it('should merge with other collection', () => {
collection = collection.add('oldKey', 'oldVal'); collection = collection.add('oldKey', 'oldVal');
const newCollection = new TranslationCollection({ newKey: 'newVal' }); 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', () => { it('should intersect with passed collection', () => {
collection = collection.addKeys(['red', 'green', 'blue']); 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: '' }); 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å' }); 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 = new TranslationCollection({ red: 'rød', green: 'grøn', blue: 'blå' });
collection = collection.sort(); collection = collection.sort();
expect(collection.keys()).deep.equal(['blue', 'green', 'red']); 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

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

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,118 @@
{ {
"rulesDirectory": [ "defaultSeverity": "error",
"node_modules/tslint-eslint-rules/dist/rules" "extends": [
], "tslint-config-prettier"
"rules": { ],
"indent": [true, "tabs"], "rules": {
"semicolon": [true, "always", "ignore-interfaces"], "arrow-return-shorthand": true,
"quotemark": [true, "single", "avoid-escape"], "callable-types": true,
"only-arrow-functions": false, "class-name": true,
"no-duplicate-variable": true, "comment-format": [
"member-access": true, true,
"member-ordering": [ "check-space"
true, ],
{ "curly": true,
"order": [ "deprecation": {
"public-static-field", "severity": "warn"
"public-static-method", },
"protected-static-field", "eofline": true,
"protected-static-method", "forin": true,
"private-static-field", "import-spacing": true,
"private-static-method", "indent": [
"public-instance-field", true,
"protected-instance-field", "tabs"
"private-instance-field", ],
"constructor", "interface-over-type-literal": true,
"public-instance-method", "label-position": true,
"protected-instance-method", "max-line-length": [
"private-instance-method" true,
] 220
} ],
], "member-access": false,
"curly": true, "member-ordering": [
"eofline": true, true,
"no-trailing-whitespace": true, {
"trailing-comma": [ "order": [
true, "static-field",
{ "instance-field",
"multiline": "never", "static-method",
"singleline": "never" "instance-method"
} ]
], }
"whitespace": [ ],
true, "no-arg": true,
"check-branch", "no-bitwise": true,
"check-decl", "no-console": [
"check-operator", true,
"check-module", "debug",
"check-separator", "info",
"check-type", "time",
"check-typecast" "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"
],
"quotemark": [true, "single"]
}
}