Compare commits

...

163 Commits

Author SHA1 Message Date
dependabot[bot]
0a7c2dd5f0 Bump minimatch and mocha
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together.


Updates `minimatch` from 3.0.4 to 3.1.2
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

Updates `mocha` from 8.3.2 to 10.1.0
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v8.3.2...v10.1.0)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
- dependency-name: mocha
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-11 12:29:34 +00:00
dependabot[bot]
a108eb776c Bump ansi-regex from 3.0.0 to 3.0.1 (#3)
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-22 09:13:58 +03:00
dependabot[bot]
a60ea65325 Bump minimist from 1.2.5 to 1.2.6 (#2)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-21 23:49:50 +03:00
dd31950151 Merge pull request #1 from unistack-org/dependabot/npm_and_yarn/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7
2022-01-12 17:36:04 +03:00
dependabot[bot]
f0cc5a2d9f Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 01:30:55 +00:00
Kim Biesbjerg
82eb652e4b Bump version 2021-04-14 10:10:21 +02:00
Kim Biesbjerg
4e91eb5fc5 Update deps (#234) 2021-04-14 10:09:26 +02:00
riot
0809e065ec fixes example call to generate i18n extracts (#225) 2021-04-14 10:03:06 +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
Kim Biesbjerg
0949bf765b Bump version 2017-05-09 20:09:03 +02:00
Kim Biesbjerg
d416c6b9fd Add support for extracting strings from multiple classes per file. Closes #46 2017-05-09 20:08:44 +02:00
Kim Biesbjerg
4e351405fb Bump version 2017-05-09 14:40:06 +02:00
Kim Biesbjerg
39a335638b Add support for parsing NamespacedJson in Json compiler. Closes #44 2017-05-09 14:39:39 +02:00
Kim Biesbjerg
3b9561916b Fix crash when constructor parameter has no type. Closes #38 2017-05-05 11:31:30 +02:00
Kim Biesbjerg
5cef383f3b Forgot to build v2.2.2 before publishing to npm 2017-05-05 11:18:51 +02:00
Kim Biesbjerg
677d2a35ca Update dependencies. Bump version 2017-04-06 08:52:22 +02:00
cvaliere
262a89206d fix parser regexp (#31)
- fix template parser regexp (Closes #15)
2017-04-06 08:50:23 +02:00
Kim Biesbjerg
bcb4a9c069 Fix bug where obsolete strings were not removed when --clean was used. Closes #29 2017-03-31 08:31:14 +02:00
Kim Biesbjerg
5ad1fe6a18 Remove unused import 2017-03-31 08:21:28 +02:00
Kim Biesbjerg
bc5ce7e80d Add marker argument to readme 2017-03-30 14:42:03 +02:00
Kim Biesbjerg
030ab145d6 Add return types 2017-03-30 14:40:51 +02:00
Kim Biesbjerg
daaebede6f Add support for marker functions, to be able to extract strings not directly passed to TranslateService. Closes #10 2017-03-30 14:37:30 +02:00
51 changed files with 4283 additions and 714 deletions

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

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

6
.gitignore vendored
View File

@@ -8,10 +8,12 @@ npm-debug.log*
# Compiled files
dist
src/**/*.js
tests/**/*.js
# Extracted strings
template.json
template.pot
strings.json
strings.pot
# Dependency directory
node_modules

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.

121
README.md
View File

@@ -1,81 +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
Extract translatable (ngx-translate) strings and save as a JSON or Gettext pot file.
Merges with existing strings if the output file already exists.
## Usage
## Install
Install the package in your project:
`npm install @biesbjerg/ngx-translate-extract --save-dev`
Add an `extract` script to your project's `package.json`:
```
Add a script to your project's `package.json`:
```json
...
"scripts": {
"extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/*.json --clean --sort --format namespaced-json"
"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 i18n:extract` and it will extract strings from your project.
## Extract examples
## Usage
**Extract from dir and save to file**
`ngx-translate-extract -i ./src -o ./src/i18n/strings.json`
`ngx-translate-extract --input ./src --output ./src/assets/i18n/strings.json`
**Extract from multiple dirs**
`ngx-translate-extract -i ./src/folder-a ./src/folder-b -o ./src/i18n/strings.json`
`ngx-translate-extract --input ./src-a ./src-b --output ./src/assets/i18n/strings.json`
**Extract and save to multiple files**
`ngx-translate-extract -i ./src -o ./src/i18n/{da,en,fr}.json`
**Extract and save to multiple files using path expansion**
**or**
`ngx-translate-extract --input ./src --output ./src/i18n/{da,en}.json`
`ngx-translate-extract -i ./src -o ./src/i18n/da.json ./src/i18n/en.json ./src/i18n/fr.json`
**or (update only)**
`ngx-translate-extract -i ./src -o ./src/i18n/*.json`
**or (update only)**
## Custom indentation
By default, tabs are used for indentation when writing extracted strings to json formats:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation $'\t'`
### JSON indentation
Tabs are used by default for indentation when saving extracted strings in json formats:
If you want to use spaces instead, you can do the following:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation ' '`
`ngx-translate-extract --input ./src --output ./src/i18n/en.json --format-indentation ' '`
### 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.
Modify the scripts arguments as required.
Install marker function:
`npm install @biesbjerg/ngx-translate-extract-marker`
## Commandline arguments
```ts
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');
```
_Note: `ngx-translate-extract` will automatically detect the import name_
### Commandline arguments
```
Usage:
ngx-translate-extract [options]
Output
--format, -f Format [string] [choices: "json", "namespaced-json", "pot"] [default: "json"]
--format-indentation, --fi Format indentation (JSON/Namedspaced JSON) [string] [default: " "]
--sort, -s Sort strings in alphabetical order [boolean]
--clean, -c Remove obsolete strings after merge [boolean]
--replace, -r Replace the contents of output file if it exists (Merges by default) [boolean]
Extracted key value (defaults to empty string)
--key-as-default-value, -k Use key as default value [boolean]
--null-as-default-value, -n Use null as default value [boolean]
--string-as-default-value, -d Use string as default value [string]
Options:
--version, -v Show version number [boolean]
--help, -h Show help [boolean]
--input, -i Paths you would like to extract strings from. You
can use path expansion, glob patterns and multiple
paths
[array] [default: current working path]
--patterns, -p Extract strings from the following file patterns
[array] [default: ["/**/*.html","/**/*.ts"]]
--output, -o Paths where you would like to save extracted
strings. You can use path expansion, glob patterns
and multiple paths [array] [required]
--format, -f Output format
[string] [choices: "json", "namespaced-json", "pot"] [default: "json"]
--format-indentation, --fi Output format indentation [string] [default: "\t"]
--replace, -r Replace the contents of output file if it exists
(Merges by default) [boolean] [default: false]
--sort, -s Sort strings in alphabetical order when saving
[boolean] [default: false]
--clean, -c Remove obsolete strings when merging
[boolean] [default: false]
--version, -v Show version number [boolean]
--help, -h Show help [boolean]
--input, -i Paths you would like to extract strings from. You can use path expansion, glob patterns and
multiple paths [array] [required] [default: ["/Users/kim/apps/ngx-translate-extract"]]
--output, -o Paths where you would like to save extracted strings. You can use path expansion, glob
patterns and multiple paths [array] [required]
Examples:
ngx-translate-extract -i ./src-a/ -i ./src-b/ -o strings.json Extract (ts, html) from multiple paths
ngx-translate-extract -i './{src-a,src-b}/' -o strings.json Extract (ts, html) from multiple paths using brace
expansion
ngx-translate-extract -i ./src/ -o ./i18n/da.json -o ./i18n/en.json Extract (ts, html) and save to da.json and en.json
ngx-translate-extract -i ./src/ -o './i18n/{en,da}.json' Extract (ts, html) and save to da.json and en.json
using brace expansion
ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json Extract from ts, tsx and html
ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o Extract from ts, html, excluding files with ".spec"
strings.json
```
## Note for GetText users
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

2261
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@biesbjerg/ngx-translate-extract",
"version": "2.1.0",
"version": "7.0.4",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -14,20 +14,34 @@
"scripts": {
"build": "npm run clean && tsc",
"watch": "npm run clean && tsc --watch",
"clean": "rm -rf ./dist",
"clean": "rimraf ./dist",
"lint": "tslint --force './src/**/*.ts'",
"test": "mocha -r ts-node/register tests/**/*.spec.ts"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm test"
}
},
"prettier": {
"trailingComma": "none",
"printWidth": 145,
"useTabs": true,
"singleQuote": true
},
"lint-staged": {
"{src,tests}/**/*.{ts}": [
"tslint --project tsconfig.json -c tslint.commit.json --fix",
"prettier --write"
]
},
"repository": {
"type": "git",
"url": "https://github.com/biesbjerg/ngx-translate-extract.git"
},
"keywords": [
"angular",
"angular2",
"ionic",
"ionic2",
"ng2-translate",
"ngx-translate",
"extract",
"extractor",
@@ -43,34 +57,48 @@
},
"homepage": "https://github.com/biesbjerg/ngx-translate-extract",
"engines": {
"node": ">=4.3.2"
"node": ">=11.15.0"
},
"config": {},
"devDependencies": {
"@types/chai": "3.4.35",
"@types/glob": "5.0.30",
"@types/mocha": "2.2.40",
"@types/cheerio": "0.22.1",
"@types/chalk": "0.4.31",
"@types/flat": "0.0.28",
"@types/yargs": "6.6.0",
"@types/mkdirp": "0.3.29",
"chai": "3.5.0",
"mocha": "3.2.0",
"ts-node": "3.0.2",
"tslint": "4.5.1",
"tslint-eslint-rules": "3.5.1",
"typescript": "2.2.2"
"@angular/compiler": "^11.2.9",
"@types/braces": "^3.0.0",
"@types/chai": "^4.2.16",
"@types/flat": "^5.0.1",
"@types/gettext-parser": "4.0.0",
"@types/glob": "^7.1.3",
"@types/mkdirp": "^1.0.1",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.37",
"@types/yargs": "^16.0.1",
"braces": "^3.0.2",
"chai": "^4.3.4",
"husky": "^6.0.0",
"lint-staged": "^10.5.4",
"mocha": "^10.1.0",
"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": {
"chalk": "1.1.3",
"yargs": "7.0.2",
"cheerio": "0.22.0",
"fs": "0.0.1-security",
"gettext-parser": "1.2.2",
"glob": "7.1.1",
"path": "0.12.7",
"mkdirp": "0.5.1",
"flat": "2.0.1"
"@phenomnomnominal/tsquery": "^4.1.1",
"boxen": "^5.0.1",
"colorette": "^1.2.2",
"flat": "^5.0.2",
"gettext-parser": "^4.0.4",
"glob": "^7.1.6",
"mkdirp": "^1.0.4",
"path": "^0.12.7",
"terminal-link": "^2.1.1",
"yargs": "^16.2.0"
}
}
}

View File

@@ -1,15 +1,35 @@
import * as yargs from 'yargs';
import { red, green } from 'colorette';
import { ExtractTask } from './tasks/extract.task';
import { ParserInterface } from '../parsers/parser.interface';
import { PipeParser } from '../parsers/pipe.parser';
import { DirectiveParser } from '../parsers/directive.parser';
import { ServiceParser } from '../parsers/service.parser';
import { MarkerParser } from '../parsers/marker.parser';
import { PostProcessorInterface } from '../post-processors/post-processor.interface';
import { SortByKeyPostProcessor } from '../post-processors/sort-by-key.post-processor';
import { KeyAsDefaultValuePostProcessor } from '../post-processors/key-as-default-value.post-processor';
import { NullAsDefaultValuePostProcessor } from '../post-processors/null-as-default-value.post-processor';
import { StringAsDefaultValuePostProcessor } from '../post-processors/string-as-default-value.post-processor';
import { PurgeObsoleteKeysPostProcessor } from '../post-processors/purge-obsolete-keys.post-processor';
import { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory';
import { normalizePaths } from '../utils/fs-helpers';
import { donateMessage } from '../utils/donate';
import * as fs from 'fs';
import * as yargs from 'yargs';
// First parsing pass to be able to access pattern argument for use input/output arguments
const y = yargs.option('patterns', {
alias: 'p',
describe: 'Default patterns',
type: 'array',
default: ['/**/*.html', '/**/*.ts'],
hidden: true
});
export const cli = yargs
const parsed = y.parse();
export const cli = y
.usage('Extract strings from files for translation.\nUsage: $0 [options]')
.version(require(__dirname + '/../../package.json').version)
.alias('version', 'v')
@@ -18,24 +38,14 @@ export const cli = yargs
.option('input', {
alias: 'i',
describe: 'Paths you would like to extract strings from. You can use path expansion, glob patterns and multiple paths',
default: process.env.PWD,
default: [process.env.PWD],
type: 'array',
normalize: true
normalize: true,
required: true
})
.check(options => {
options.input.forEach((dir: string) => {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
throw new Error(`The path you supplied was not found: '${dir}'`);
}
});
return true;
})
.option('patterns', {
alias: 'p',
describe: 'Extract strings from the following file patterns',
type: 'array',
default: ['/**/*.html', '/**/*.ts']
.coerce('input', (input: string[]) => {
const paths = normalizePaths(input, parsed.patterns);
return paths;
})
.option('output', {
alias: 'o',
@@ -44,56 +54,108 @@ export const cli = yargs
normalize: true,
required: true
})
.coerce('output', (output: string[]) => {
const paths = normalizePaths(output, parsed.patterns);
return paths;
})
.option('format', {
alias: 'f',
describe: 'Output format',
describe: 'Format',
default: 'json',
type: 'string',
choices: ['json', 'namespaced-json', 'pot']
})
.option('format-indentation', {
alias: 'fi',
describe: 'Output format indentation',
describe: 'Format indentation (JSON/Namedspaced JSON)',
default: '\t',
type: 'string'
})
.option('replace', {
alias: 'r',
describe: 'Replace the contents of output file if it exists (Merges by default)',
default: false,
type: 'boolean'
})
.option('sort', {
alias: 's',
describe: 'Sort strings in alphabetical order when saving',
default: false,
describe: 'Sort strings in alphabetical order',
type: 'boolean'
})
.option('clean', {
alias: 'c',
describe: 'Remove obsolete strings when merging',
default: false,
describe: 'Remove obsolete strings after merge',
type: 'boolean'
})
.option('key-as-default-value', {
alias: 'k',
describe: 'Use key as default value',
type: 'boolean',
conflicts: ['null-as-default-value', 'string-as-default-value']
})
.option('null-as-default-value', {
alias: 'n',
describe: 'Use null as default value',
type: 'boolean',
conflicts: ['key-as-default-value', 'string-as-default-value']
})
.option('string-as-default-value', {
alias: 'd',
describe: 'Use string as default value',
type: 'string',
conflicts: ['null-as-default-value', 'key-as-default-value']
})
.group(['format', 'format-indentation', 'sort', 'clean', 'replace'], 'Output')
.group(['key-as-default-value', 'null-as-default-value', 'string-as-default-value'], 'Extracted key value (defaults to empty string)')
.conflicts('key-as-default-value', 'null-as-default-value')
.example(`$0 -i ./src-a/ -i ./src-b/ -o strings.json`, 'Extract (ts, html) from multiple paths')
.example(`$0 -i './{src-a,src-b}/' -o strings.json`, 'Extract (ts, html) from multiple paths using brace expansion')
.example(`$0 -i ./src/ -o ./i18n/da.json -o ./i18n/en.json`, 'Extract (ts, html) and save to da.json and en.json')
.example(`$0 -i ./src/ -o './i18n/{en,da}.json'`, 'Extract (ts, html) and save to da.json and en.json using brace expansion')
.example(`$0 -i './src/**/*.{ts,tsx,html}' -o strings.json`, 'Extract from ts, tsx and html')
.example(`$0 -i './src/**/!(*.spec).{ts,html}' -o strings.json`, 'Extract from ts, html, excluding files with ".spec" in filename')
.wrap(110)
.exitProcess(true)
.parse(process.argv);
const parsers: ParserInterface[] = [
new ServiceParser(),
new PipeParser(),
new DirectiveParser()
];
const extractTask = new ExtractTask(cli.input, cli.output, {
replace: cli.replace
});
// Parsers
const parsers: ParserInterface[] = [new PipeParser(), new DirectiveParser(), new ServiceParser(), new MarkerParser()];
extractTask.setParsers(parsers);
// Post processors
const postProcessors: PostProcessorInterface[] = [];
if (cli.clean) {
postProcessors.push(new PurgeObsoleteKeysPostProcessor());
}
if (cli.keyAsDefaultValue) {
postProcessors.push(new KeyAsDefaultValuePostProcessor());
} else if (cli.nullAsDefaultValue) {
postProcessors.push(new NullAsDefaultValuePostProcessor());
} else if (cli.stringAsDefaultValue) {
postProcessors.push(new StringAsDefaultValuePostProcessor({ defaultValue: cli.stringAsDefaultValue as string }));
}
if (cli.sort) {
postProcessors.push(new SortByKeyPostProcessor());
}
extractTask.setPostProcessors(postProcessors);
// Compiler
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extractTask.setCompiler(compiler);
new ExtractTask(cli.input, cli.output, {
replace: cli.replace,
sort: cli.sort,
clean: cli.clean,
patterns: cli.patterns
})
.setParsers(parsers)
.setCompiler(compiler)
.execute();
// Run task
try {
extractTask.execute();
console.log(green('\nDone.\n'));
console.log(donateMessage);
process.exit(0);
} catch (e) {
console.log(red(`\nAn error occurred: ${e}\n`));
process.exit(1);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,23 @@
export * from './utils/translation.collection';
export * from './utils/ast-utils';
export * from './utils/utils';
export * from './cli/cli';
export * from './cli/tasks/task.interface';
export * from './cli/tasks/extract.task';
export * from './parsers/parser.interface';
export * from './parsers/abstract-template.parser';
export * from './parsers/directive.parser';
export * from './parsers/pipe.parser';
export * from './parsers/service.parser';
export * from './parsers/marker.parser';
export * from './compilers/compiler.interface';
export * from './compilers/compiler.factory';
export * from './compilers/json.compiler';
export * from './compilers/namespaced-json.compiler';
export * from './compilers/po.compiler';
export * from './post-processors/post-processor.interface';
export * from './post-processors/key-as-default-value.post-processor';
export * from './post-processors/purge-obsolete-keys.post-processor';
export * from './post-processors/sort-by-key.post-processor';

View File

@@ -1,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 { AbstractTemplateParser } from './abstract-template.parser';
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 {
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 {
export class DirectiveParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
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]';
$(template)
.find(selector)
.addBack(selector)
.each((i: number, element: CheerioElement) => {
const $element = $(element);
const attr = $element.attr('translate') || $element.attr('ng2-translate');
elements.forEach((element) => {
const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
if (attribute?.value) {
collection = collection.add(attribute.value);
return;
}
if (attr) {
collection = collection.add(attr);
} else {
$element
.contents()
.toArray()
.filter(node => node.type === 'text')
.map(node => node.nodeValue.trim())
.filter(text => text.length > 0)
.forEach(text => collection = collection.add(text));
}
const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME);
if (boundAttribute?.value) {
this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
collection = collection.add(literalPrimitive.value);
});
return;
}
const textNodes = this.getTextNodes(element);
textNodes.forEach((textNode) => {
collection = collection.add(textNode.value.trim());
});
});
return collection;
}
/**
* Angular's `[attr]="'val'"` syntax is not valid HTML,
* so it can't be parsed by standard HTML parsers.
* This method replaces `[attr]="'val'""` with `attr="val"`
* Find all ElementLike nodes with a translate attribute
* @param nodes
*/
protected _normalizeTemplateAttributes(template: string): string {
return template.replace(/\[([^\]]+)\]="'([^']*)'"/g, '$1="$2"');
protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
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

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

View File

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

View File

@@ -1,27 +1,163 @@
import {
AST,
TmplAstNode,
parseTemplate,
BindingPipe,
LiteralPrimitive,
Conditional,
TmplAstTextAttribute,
Binary,
LiteralMap,
LiteralArray,
Interpolation,
MethodCall
} from '@angular/compiler';
import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
export class PipeParser extends AbstractTemplateParser implements ParserInterface {
const TRANSLATE_PIPE_NAME = 'translate';
public extract(contents: string, path?: string): TranslationCollection {
if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents);
export class PipeParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
return this._parseTemplate(contents);
}
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const regExp: RegExp = /(['"`])([^>\1\r\n]*?)\1\s*\|\s*translate/g;
let matches: RegExpExecArray;
while (matches = regExp.exec(template)) {
collection = collection.add(matches[2]);
}
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
const pipes: BindingPipe[] = nodes.map((node) => this.findPipesInNode(node)).flat();
pipes.forEach((pipe) => {
this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => {
collection = collection.add(key);
});
});
return collection;
}
protected findPipesInNode(node: any): BindingPipe[] {
let ret: BindingPipe[] = [];
if (node?.children) {
ret = node.children.reduce(
(result: BindingPipe[], childNode: TmplAstNode) => {
const children = this.findPipesInNode(childNode);
return result.concat(children);
},
[ret]
);
}
if (node?.value?.ast) {
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,163 +1,64 @@
import { ClassDeclaration, CallExpression } from 'typescript';
import { tsquery } from '@phenomnomnominal/tsquery';
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
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 implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
this._instancePropertyName = this._getInstancePropertyName();
if (!this._instancePropertyName) {
return collection;
}
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor params
*/
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
if (!constructorNode) {
const classDeclarations = findClassDeclarations(sourceFile);
if (!classDeclarations) {
return null;
}
const result = constructorNode.parameters.find(parameter => {
// Skip if visibility modifier is not present (we want it set as an instance property)
if (!parameter.modifiers) {
return false;
}
let collection: TranslationCollection = new TranslationCollection();
// Make sure className is of the correct type
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== this._serviceClassName) {
return false;
}
classDeclarations.forEach((classDeclaration) => {
const callExpressions = [
...this.findConstructorParamCallExpressions(classDeclaration),
...this.findPropertyCallExpressions(classDeclaration)
];
return true;
});
if (result) {
return (result.name as ts.Identifier).text;
}
}
/**
* Find first constructor
*/
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
// Only call expressions with arguments
.filter(callNode => callNode.arguments.length > 0)
// More filters
.filter(callNode => {
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
callExpressions.forEach((callExpression) => {
const [firstArg] = callExpression.arguments;
if (!firstArg) {
return;
}
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== this._instancePropertyName) {
return false;
}
const methodAccess = callNode.getChildAt(0) as ts.PropertyAccessExpression;
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
return false;
}
return true;
const strings = getStringsFromExpression(firstArg);
collection = collection.addKeys(strings);
});
return callNodes;
});
return collection;
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg);
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);
}
/**
* 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];
protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
if (!propName) {
return [];
}
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);
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 [];
}

View File

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

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

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

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

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

View File

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

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

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

View File

@@ -4,7 +4,6 @@ import { TranslationCollection } from '../../src/utils/translation.collection';
import { NamespacedJsonCompiler } from '../../src/compilers/namespaced-json.compiler';
describe('NamespacedJsonCompiler', () => {
let compiler: NamespacedJsonCompiler;
beforeEach(() => {
@@ -23,7 +22,10 @@ describe('NamespacedJsonCompiler', () => {
}
`;
const collection: TranslationCollection = compiler.parse(contents);
expect(collection.values).to.deep.equal({'NAMESPACE.KEY.FIRST_KEY': '', 'NAMESPACE.KEY.SECOND_KEY': 'VALUE' });
expect(collection.values).to.deep.equal({
'NAMESPACE.KEY.FIRST_KEY': '',
'NAMESPACE.KEY.SECOND_KEY': 'VALUE'
});
});
it('should unflatten keys on compile', () => {
@@ -37,9 +39,9 @@ describe('NamespacedJsonCompiler', () => {
it('should preserve numeric values on compile', () => {
const collection = new TranslationCollection({
"option.0": '',
"option.1": '',
"option.2": ''
'option.0': '',
'option.1': '',
'option.2': ''
});
const result: string = compiler.compile(collection);
expect(result).to.equal('{\n\t"option": {\n\t\t"0": "",\n\t\t"1": "",\n\t\t"2": ""\n\t}\n}');
@@ -57,4 +59,12 @@ describe('NamespacedJsonCompiler', () => {
expect(result).to.equal('{\n "NAMESPACE": {\n "KEY": {\n "FIRST_KEY": "",\n "SECOND_KEY": "VALUE"\n }\n }\n}');
});
it('should not reorder keys when compiled', () => {
const collection = new TranslationCollection({
BROWSE: '',
LOGIN: ''
});
const result: string = compiler.compile(collection);
expect(result).to.equal('{\n\t"BROWSE": "",\n\t"LOGIN": ""\n}');
});
});

View File

@@ -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';
class TestDirectiveParser extends DirectiveParser {
public normalizeTemplateAttributes(template: string): string {
return this._normalizeTemplateAttributes(template);
}
}
describe('DirectiveParser', () => {
const templateFilename: string = 'test.template.html';
const componentFilename: string = 'test.component.ts';
let parser: TestDirectiveParser;
let parser: DirectiveParser;
beforeEach(() => {
parser = new TestDirectiveParser();
parser = new DirectiveParser();
});
it('should extract contents when no translate attribute value is provided', () => {
it('should 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 keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract translate attribute if provided', () => {
const contents = '<div translate="KEY">Hello World<div>';
it('should use translate attribute value as key when present', () => {
const contents = '<div translate="MY_KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
expect(keys).to.deep.equal(['MY_KEY']);
});
it('should extract bound translate attribute as key if provided', () => {
const contents = `<div [translate]="'KEY'">Hello World<div>`;
it('should extract keys from child elements when translate attribute is present', () => {
const contents = `<div translate>Hello <strong translate>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract direct text nodes when no translate attribute value is provided', () => {
const contents = `
<div translate>
<span>&#10003;</span>
Hello <strong>World</strong>
Hi <em>there</em>
</div>
`;
it('should not extract keys from child elements when translate attribute is not present', () => {
const contents = `<div translate>Hello <strong>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'Hi']);
});
it('should extract direct text nodes of tags with a translate attribute', () => {
const contents = `
<div translate>
<span>&#10003;</span>
Hello World
<div translate>Hi there</div>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World', 'Hi there']);
});
it('should extract translate attribute if provided or direct text nodes if not', () => {
const contents = `
<div translate="KEY">
<span>&#10003;</span>
Hello World
<p translate>Hi there</p>
<p [translate]="'OTHER_KEY'">Lorem Ipsum</p>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']);
expect(keys).to.deep.equal(['Hello']);
});
it('should extract and parse inline template', () => {
@@ -88,34 +109,65 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract contents when no ng2-translate attribute value is provided', () => {
const contents = '<div ng2-translate>Hello World</div>';
it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract ng2-translate attribute if provided', () => {
const contents = '<div ng2-translate="KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should extract bound ng2-translate attribute as key if provided', () => {
const contents = `<div [ng2-translate]="'KEY'">Hello World<div>`;
it('should extract translate attribute value if provided', () => {
const contents = '<div translate="KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY']);
});
it('should not extract translate pipe in html tag', () => {
const contents = `<p>{{ 'Audiobooks for personal development' | translate }}</p>`;
const contents = `<p>{{ 'Audiobooks for personal development' | translate }}</p>`;
const collection = parser.extract(contents, templateFilename);
expect(collection.values).to.deep.equal({});
});
it('should normalize bound attributes', () => {
const contents = `<p [translate]="'KEY'">Hello World</p>`;
const template = parser.normalizeTemplateAttributes(contents);
expect(template).to.equal('<p translate="KEY">Hello World</p>');
it('should extract contents from custom elements', () => {
const contents = `<custom-table><tbody><tr><td translate>Hello World</td></tr></tbody></custom-table>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should 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

@@ -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';
describe('PipeParser', () => {
const templateFilename: string = 'test.template.html';
let parser: PipeParser;
@@ -18,42 +17,126 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['SomeKey_NotWorking']);
});
it('should extract string using pipe, but between quotes only', () => {
const contents = `<input class="form-control" type="text" placeholder="{{'user.settings.form.phone.placeholder' | translate}}" [formControl]="settingsForm.controls['phone']">`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['user.settings.form.phone.placeholder']);
});
it('should extract interpolated strings using translate pipe', () => {
const contents = `Hello {{ 'World' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract interpolated strings when translate pipe is used 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', () => {
const contents = `Hello {{ 'World\'s largest potato' | translate }}`;
const contents = `Hello {{ 'World\\'s largest potato' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`World's largest potato`]);
});
it('should extract strings with multiple escaped quotes', () => {
const contents = `{{ 'C\\'est ok. C\\'est ok' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`C'est ok. C'est ok`]);
});
it('should extract interpolated strings using translate pipe in attributes', () => {
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract bound strings using translate pipe in attributes', () => {
const contents = `<span [attr]="'Hello World' | translate"></span>`;
const contents = `<span [attr]="'Hello World' | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should not use a greedy regular expression', () => {
it('should extract multiple entries from nodes', () => {
const contents = `
<ion-header>
<ion-navbar color="brand">
<ion-title>{{ 'Info' | translate }}</ion-title>
<ion-title>{{ 'Info' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<content-loading *ngIf="isLoading">
{{ 'Loading...' | translate }}
{{ 'Loading...' | translate }}
</content-loading>
</ion-content>
@@ -63,7 +146,7 @@ describe('PipeParser', () => {
});
it('should extract strings on same line', () => {
const contents = `<span [attr]="'Hello' | translate"></span><span [attr]="'World' | translate"></span>`;
const contents = `<span [attr]="'Hello' | translate"></span><span [attr]="'World' | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
@@ -73,7 +156,7 @@ describe('PipeParser', () => {
<ion-list inset>
<ion-item>
<ion-icon item-left name="person" color="dark"></ion-icon>
<ion-input formControlName="name" type="text" [placeholder]="'Name' | translate"></ion-input>
<ion-input formControlName="name" type="text" [placeholder]="'Name' | translate"></ion-input>
</ion-item>
<ion-item>
<p color="danger" danger *ngFor="let error of form.get('name').getError('remote')">
@@ -82,11 +165,40 @@ describe('PipeParser', () => {
</ion-item>
</ion-list>
<div class="form-actions">
<button ion-button (click)="onSubmit()" color="secondary" block>{{ 'Create account' | translate }}</button>
<button ion-button (click)="onSubmit()" color="secondary" block>{{ 'Create account' | translate }}</button>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Name', 'Create account']);
});
it('should not extract variables', () => {
const contents = '<p>{{ message | translate }}</p>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should be able to extract without html', () => {
const contents = `{{ 'message' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['message']);
});
it('should ignore calculated values', () => {
const contents = `{{ 'SOURCES.' + source.name + '.NAME_PLURAL' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should not extract pipe argument', () => {
const contents = `{{ value | valueToTranslationKey: 'argument' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]);
});
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';
class TestServiceParser extends ServiceParser {
/*public getInstancePropertyName(): string {
return this._getInstancePropertyName();
}*/
}
describe('ServiceParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: TestServiceParser;
let parser: ServiceParser;
beforeEach(() => {
parser = new TestServiceParser();
parser = new ServiceParser();
});
/*it('should extract variable used for TranslateService', () => {
it('should extract strings when TranslateService is accessed directly via constructor parameter', () => {
const contents = `
@Component({ })
export class MyComponent {
public constructor(protected translateService: TranslateService) {
translateService.get('It works!');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['It works!']);
});
it('should support extracting binary expressions', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(
_serviceA: ServiceA,
public _serviceB: ServiceB,
protected _translateService: TranslateService
) { }
public constructor(protected _translateService: TranslateService) { }
public test() {
const message = 'The Message';
this._translateService.get(message || 'Fallback message');
}
`;
const name = parser.getInstancePropertyName();
expect(name).to.equal('_translateService');
});*/
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Fallback message']);
});
it('should extract strings in TranslateService\'s get() method', () => {
it('should support conditional operator', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
const message = 'The Message';
this._translateService.get(message ? message : 'Fallback message');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Fallback message']);
});
it("should extract strings in TranslateService's get() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -47,7 +64,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s instant() method', () => {
it("should extract strings in TranslateService's instant() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -60,7 +77,20 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
it("should extract strings in TranslateService's stream() method", () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it("should extract array of strings in TranslateService's get() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -73,7 +103,7 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract array of strings in TranslateService\'s instant() method', () => {
it("should extract array of strings in TranslateService's instant() method", () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -86,7 +116,33 @@ describe('ServiceParser', () => {
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant() methods of other services', () => {
it("should extract array of strings in TranslateService's stream() method", () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream(['Hello', 'World']);
}
`;
const key = parser.extract(contents, componentFilename).keys();
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should extract string arrays encapsulated in backticks', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.get([\`Hello\`, \`World\`]);
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant()/stream() methods of other services', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -97,6 +153,7 @@ describe('ServiceParser', () => {
public test() {
this._otherService.get('Hello World');
this._otherService.instant('Hi there');
this._otherService.stream('Hi there');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
@@ -151,4 +208,101 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['You are expected at {{time}}']);
});
it('should not crash when constructor parameter has no type', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService) { }
public test() {
this._translateService.instant('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should not extract variables', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected translateService: TranslateService) { }
public test() {
this.translateService.get(["yes", variable]).then(translations => {
console.log(translations[variable]);
});
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['yes']);
});
it('should extract strings from all classes in the file', () => {
const contents = `
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
export class Stuff {
thing: string;
translate: any;
constructor(thing: string) {
this.translate.get('Not me');
this.thing = thing;
}
}
@Injectable()
export class MyComponent {
constructor(public translate: TranslateService) {
this.translate.instant("Extract me!");
}
}
export class OtherClass {
constructor(thing: string, _translate: TranslateService) {
this._translate.get("Do not extract me");
}
}
@Injectable()
export class AuthService {
constructor(public translate: TranslateService) {
this.translate.instant("Hello!");
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Extract me!', 'Hello!']);
});
it('should extract strings when TranslateService is declared as a property', () => {
const contents = `
export class MyComponent {
protected translateService: TranslateService;
public constructor() {
this.translateService = new TranslateService();
}
public test() {
this.translateService.instant('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings passed to TranslateServices methods only', () => {
const contents = `
export class AppComponent implements OnInit {
constructor(protected config: Config, protected translateService: TranslateService) {}
public ngOnInit(): void {
this.localizeBackButton();
}
protected localizeBackButton(): void {
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.config.set('backButtonText', this.translateService.instant('Back'));
});
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Back']);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { expect } from 'chai';
import { TranslationCollection } from '../../src/utils/translation.collection';
describe('StringCollection', () => {
let collection: TranslationCollection;
beforeEach(() => {
@@ -64,12 +63,15 @@ describe('StringCollection', () => {
it('should merge with other collection', () => {
collection = collection.add('oldKey', 'oldVal');
const newCollection = new TranslationCollection({ newKey: 'newVal' });
expect(collection.union(newCollection).values).to.deep.equal({ oldKey: 'oldVal', newKey: 'newVal' });
expect(collection.union(newCollection).values).to.deep.equal({
oldKey: 'oldVal',
newKey: 'newVal'
});
});
it('should intersect with passed collection', () => {
collection = collection.addKeys(['red', 'green', 'blue']);
const newCollection = new TranslationCollection( { red: '', blue: '' });
const newCollection = new TranslationCollection({ red: '', blue: '' });
expect(collection.intersect(newCollection).values).to.deep.equal({ red: '', blue: '' });
});
@@ -79,10 +81,19 @@ describe('StringCollection', () => {
expect(collection.intersect(newCollection).values).to.deep.equal({ red: 'rød', blue: 'blå' });
});
it('should sort translations in alphabetical order', () => {
it('should sort keys alphabetically', () => {
collection = new TranslationCollection({ red: 'rød', green: 'grøn', blue: 'blå' });
collection = collection.sort();
expect(collection.keys()).deep.equal(['blue', 'green', 'red']);
});
it('should map values', () => {
collection = new TranslationCollection({ red: 'rød', green: 'grøn', blue: 'blå' });
collection = collection.map((key, val) => 'mapped value');
expect(collection.values).to.deep.equal({
red: 'mapped value',
green: 'mapped value',
blue: 'mapped value'
});
});
});

View File

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

8
tslint.commit.json Normal file
View File

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

View File

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