├── .gitignore
├── .travis.yml
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── bin
└── i18nline.js
├── lib
├── call_helpers.js
├── commands.js
├── commands
│ ├── base_command.js
│ ├── check.js
│ ├── export.js
│ ├── help.js
│ ├── index.js
│ └── synch.js
├── errors.js
├── extensions
│ └── i18n_js.js
├── extractors
│ ├── i18n_js_extractor.js
│ ├── translate_call.js
│ └── translation_hash.js
├── i18n.js
├── i18nline.js
├── load-config.js
├── log.js
├── main.js
├── pluralize.js
├── processors
│ ├── base_processor.js
│ └── js_processor.js
├── template.js
└── utils.js
├── package-lock.json
├── package.json
└── test
├── call_helpers_test.js
├── commands
├── check_test.js
├── export_test.js
└── synch_test.js
├── extensions
└── i18n_js_test.js
├── extractors
├── i18n_js_extractor_test.js
├── translate_call_test.js
└── translation_hash_test.js
└── fixtures
├── .i18nignore
├── i18n_js
├── invalid.js
└── valid.js
└── skipme.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /tmp
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_script: 'npm install -g grunt-cli'
2 | language: node_js
3 | node_js:
4 | - "node"
5 | - "8"
6 | - "7"
7 | - "6"
8 | - "5"
9 | - "4"
10 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Mocha Tests",
11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
12 | "args": [
13 | "./test/**/*_test.js",
14 | "--recursive",
15 | "-u",
16 | "tdd",
17 | "--timeout",
18 | "999999",
19 | "--colors"
20 | ],
21 | "internalConsoleOptions": "openOnSessionStart"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2016 by Stijn de Witt & Jon Jensen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # i18nline
2 | ### Keep your translations in line
3 |
4 | [](https://npmjs.com/package/i18nline)
5 | [](https://github.com/download/i18nline/LICENSE)
6 | [](https://travis-ci.org/Download/i18nline)
7 | [](https://greenkeeper.io/)
8 | 
9 |
10 |
11 | ```
12 | ██╗ ███╗ ██╗██╗ ██╗███╗ ██╗███████╗
13 | ██║ ████╗ ██║██║ ██║████╗ ██║██╔════╝
14 | ██║18 ██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗
15 | ██║ ██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝
16 | ██║ ██║ ╚████║███████╗██║██║ ╚████║███████╗
17 | ╚═╝ ╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝
18 | KEEP YOUR TRANSLATIONS IN LINE
19 | ```
20 |
21 | No .js/yml translation files. Easy inline defaults. Optional keys. Easy
22 | pluralization. Wrappers for HTML-free translations.
23 |
24 | I18nline extends [i18n-js](https://github.com/fnando/i18n-js), so you can
25 | add it to an already-internationalized app that uses it.
26 |
27 | ## TL;DR
28 |
29 | i18nline lets you do stuff like this:
30 |
31 | ```javascript
32 | I18n.t("Ohai %{user}, my default translation is right here in the code. \
33 | Inferred keys, oh my!", {user: user.name});
34 | ```
35 |
36 | and this:
37 |
38 | ```javascript
39 | I18n.t("*Translators* won't see any markup!",
40 | {wrappers: ['$1']});
41 | ```
42 |
43 | Best of all, you don't need to maintain translation files anymore;
44 | i18nline will do it for you.
45 |
46 | ## What is this?
47 |
48 | This project is a fork of Jon Jensen's
49 | [i18nline-js](https://github.com/jenseng/i18nline-js)
50 | that attempts to simplify usage by adding:
51 | * Sensible defaults
52 | * Auto-configuration of plugins
53 | * Improved documentation
54 | * CLI `help` command shows man page for the CLI
55 | * CLI `index` command generates an *index.js* file you can import
56 | * CLI `synch` command synchs all internationalization files
57 | Basically Jon did all the hard work and this project is just adding lots
58 | of sugar to make it sweeter.
59 |
60 | ## Project setup
61 | `i18nline` preprocesses your source files, generating new source files and
62 | translation files based on what it finds. To setup a project you need to:
63 | * Install `i18nline` (see next section).
64 | * Create a `script` in *package.json* to run the command-line tool.
65 | * Import `I18n` and use `I18n.t()` to render internationalized text.
66 | * Create an empty file in the `out` folder (by default: `'src/i18n'`) named
67 | `'[locale].json'` for each locale you want to support.
68 | * Run `i18nline synch` to synch the translation files and index file.
69 | * `import` the index file into your project.
70 | * Call `I18n.changeLocale` to set the locale (which loads the right
71 | translation file on demand)
72 | * Call `I18n.on` to react to the `'change'` event (e.g. by re-rendering)
73 | * Get your translators to translate all the messages :)
74 |
75 | ## Installation
76 |
77 | ```sh
78 | npm install --save i18nline
79 | ```
80 |
81 | i18nline has a dependency on i18n-js, so it will be installed automatically.
82 |
83 | ## Create a `script` to run the command-line tool
84 | `i18nline` comes with a command-line tool. This tool is written in Javascript
85 | and can be executed by Node JS. All you need to do to be able to use it is
86 | expose it via a `script` in your *package.json* (recommended), or install
87 | `i18nline` globally using the `-g` flag for `npm install`. The recommended
88 | approach is via a `script` in *package.json* because this means you only need
89 | to install `i18nline` as a normal dependency of your project.
90 |
91 | Add a script with the command `i18nline synch` to *package.json*:
92 |
93 | ```json
94 | {
95 | "scripts": {
96 | "i18n": "i18nline synch"
97 | }
98 | }
99 | ```
100 |
101 | You can now invoke this command using `npm run`:
102 |
103 | ```sh
104 | $ npm run i18n
105 | ```
106 |
107 | Alternatively, you can expose the raw command:
108 |
109 | ```json
110 | {
111 | "scripts": {
112 | "i18nline": "i18nline"
113 | }
114 | }
115 | ```
116 |
117 | Then pass arguments via `npm run`:
118 |
119 | ```sh
120 | $ npm run i18nline -- synch
121 | ```
122 |
123 | The extra dashes here are used to tell `npm run` that all arguments following
124 | the dashes should be passed on to the script.
125 |
126 | ## Import `I18n` and use `I18n.t()` to render internationalized text.
127 |
128 | i18nline adds some extensions to the i18n-js runtime. If you require i18n-js
129 | via i18nline, these will be added automatically for you:
130 |
131 | ```js
132 | var I18n = require('i18nline');
133 | // Ready to rock!
134 | ```
135 |
136 | Alternatively, you can add i18n to your app any way you like and apply the
137 | extensions manually:
138 |
139 | ```js
140 | var I18n = // get it from somewhere... script tag or whatever
141 | // add the runtime extensions manually
142 | require('i18nline/lib/extensions/i18n_js')(I18n);
143 | ```
144 |
145 | Every file that needs to translate stuff needs to get access to the `I18n`
146 | object somehow. You can add a require call to every such file (recommended),
147 | use `I18n` from the global sope, use some Webpack loader to add the import
148 | or whatever. The choice is yours.
149 |
150 | Once `I18n` is available, you can use its `I18n.t()` function to render
151 | internationalized text:
152 |
153 | ```js
154 | console.info(I18n.t('This text will be internationalized'));
155 | ```
156 |
157 | > i18nline will preprocess your source, extracting all calls to `I18n.t`. For this
158 | reason, you should not rename the `I18n` object, or alias the method etc.
159 |
160 | ## Create an empty file for each locale
161 | Adding support for a locale is as simple as adding an empty file
162 | named `'[locale].json'` to the `out` folder and running `i18nline synch`.
163 | You still need to translate the text of course!
164 |
165 | > If you use Webpack with Hot Module Replacement (HMR) enabled, you can
166 | change the translations while your app is running and the changes will
167 | be picked up automatically.
168 |
169 | ## Run `i18nline synch` to synch the translation files
170 | Using the script you created before, run `i18nline synch`:
171 |
172 | ```sh
173 | $ npm run i18n
174 | ```
175 |
176 | This will create/synch a bunch of files in the `out` folder:
177 | * `default.json`: Contains the default translations extracted from the source code
178 | * `en.json`: Contains the messages for the default locale (assuming that is `'en'`)
179 | * `de.json`: Assuming you added an empty file `de.json`, it will be synched by `i18nline`.
180 | * `index.js`: Index file that you can import into your project.
181 |
182 | > The files `default.json` and `index.js` are regenerated every time, so don't change
183 | them as your changes will get lost. The translation files for the different locales
184 | are synched in a smart way, so any changes there will be respected.
185 |
186 | ## `import` the index file into your project.
187 | Since version 2, `i18nline` features an `index` command (which is also run
188 | as part of the `synch` command) that generates an index file containing the
189 | Javascript code needed to load the translation files into your project.
190 |
191 | The generated file uses dynamic `import()` statements to allow Webpack and
192 | other bundlers to perform code splitting, making sure that each translation
193 | file ends up in a separate Javascript bundle. Also, it adds support for
194 | Webpacks Hot Module Replacement.
195 |
196 | > You need to use a transpiler like [Babel](https://babeljs.io/) in
197 | combination with a bundler like [Webpack](https://webpack.js.org/) to
198 | take advantage of the code splitting and hot reloading features that the
199 | generated index file uses. If your tool chain does not support ES2015+ with
200 | dynamic `import()`, you cannot use the generated index file and need to load
201 | the translations yourself somehow. Just make sure you assign the loaded
202 | translations to `I18n.translations`.
203 |
204 | To import the index file, simply `require` or `import` it:
205 |
206 | *some file in the root of src/*
207 | ```js
208 | var I18n = require(`./i18n`);
209 | // or
210 | import I18n from './i18n';
211 | ```
212 |
213 | This will add a method `I18n.import` to the regular `I18n` object,
214 | with the code in it to load the translations for the given locale.
215 | If you inspect the contents of `index.js`, you will find that it
216 | contains a switch statement with similar code to load each file.
217 | That may seem redundant, but it is needed so that all the `import()`
218 | statements are statically analyzable, allowing the bundler to
219 | determine which files to include in the bundles it generates. You
220 | don't actually need to call the `I18n.import()` method yourself.
221 | It is called automatically when you use `I18n.changeLocale`
222 | (see next section).
223 |
224 | ## Call `I18n.changeLocale` to change the locale
225 | `i18nline` adds a method `changeLocale` to `I18n` that uses the
226 | method `I18n.import` (found in the generated index file) to load the
227 | translations for the locale when needed. So call `changeLocale` in
228 | your code to change the locale and the translation files will be
229 | loaded automatically when needed.
230 |
231 | ```js
232 | I18n.changeLocale('de');
233 | ```
234 |
235 | ## Call `I18n.on` to react to the `'change'` event
236 | `i18nline` uses [uevents](https://npmjs.com/package/uevents) to turn `I18n`
237 | into an event emitter. Whenever the locale or the translations for a
238 | locale have changed, `i18nline` emits a `'change'` event. You can add a
239 | listener for this event like so:
240 |
241 | ```js
242 | I18n.on('change', locale => {
243 | // the locale changed to the given locale, or the translations for the
244 | // given locale changed. React accordingly, e.g. by re-rendering
245 | });
246 | ```
247 |
248 | The docs for the [Node JS Events API](https://nodejs.org/api/events.html)
249 | explain how to remove listeners and perform other bookkeeping operations
250 | on event emitters.
251 |
252 | ## Features
253 |
254 | ### No more .js/.yml translation files
255 |
256 | Instead of maintaining .js/.yml files and doing stuff like this:
257 |
258 | ```javascript
259 | I18n.t('account_page_title');
260 | ```
261 |
262 | Forget the translation file and just do:
263 |
264 | ```javascript
265 | I18n.t('account_page_title', "My Account");
266 | ```
267 |
268 | Regular I18n options follow the (optional) default translation, so you can do
269 | the usual stuff (placeholders, etc.).
270 |
271 | #### Okay, but don't the translators need them?
272 |
273 | Sure, but *you* don't need to write them. Just run `i18nline export`
274 | to extract all default translations from your codebase and output them to
275 | `src/i18n/default.json` In addition, any translation files already present
276 | in the `out` folder are synched: any keys no longer present in the source
277 | are removed and any new keys are added. Finally this outputs an index file
278 | named `index.js` that you can `import` in your app.
279 |
280 | ### It's okay to lose your keys
281 |
282 | Why waste time coming up with keys that are less descriptive than the
283 | default translation? i18nline makes keys optional, so you can just do this:
284 |
285 | ```javascript
286 | I18n.t("My Account")
287 | ```
288 |
289 | i18nline will create a unique key based on the translation (e.g.
290 | `'my_account'`), so you don't have to.
291 | See [inferredKeyFormat](#inferredkeyformat) for more information.
292 |
293 | This can actually be a **good thing**, because when the `default`
294 | translation changes, the key changes, which means you know you need
295 | to get it retranslated (instead of letting a now-inaccurate
296 | translation hang out indefinitely).
297 |
298 | If you are changing the meaning of the default translation, e.g.
299 | by changing "Enter your username and password to log in" to
300 | "Enter your e-mail address and password to log in", you should make
301 | the change in the source code to force a re-translation for all
302 | languages. If you are just changing the wording of the message,
303 | e.g. by changing "Enter your username and password to log in" to
304 | "Enter your username and password to sign in", you can make the
305 | change in the translation file `en.js` instead, so other languages
306 | are not affected.
307 |
308 | > Never change the file `default.json`, it is intended to
309 | accurately reflect the text that was extracted from the program
310 | source and as such it is always regenerated and not synched.
311 |
312 | ### Wrappers
313 |
314 | Suppose you have something like this in your JavaScript:
315 |
316 | ```javascript
317 | var string = 'You can lead a new discussion or \
318 | join an existing one.';
319 | ```
320 |
321 | You might say "No, I'd use handlebars". Bear with me here, we're
322 | trying to make this easy for you *and* the translators :).
323 | For I18n, you might try something like this:
324 |
325 | ```javascript
326 | var string = I18n.t('You can %{lead} a new discussion or %{join} an \
327 | existing one.', {
328 | lead: '' + I18n.t('lead') + '',
329 | join: ' + 'I18n.t('join') + '')
330 | });
331 | ```
332 |
333 | This is not great, because:
334 |
335 | 1. There are three strings to translate.
336 | 2. When translating the verbs, the translator has no context for where it's
337 | being used... Is "lead" a verb or a noun?
338 | 3. Translators have their hands somewhat tied as far as what is inside the
339 | links and what is not.
340 |
341 | So you might try this instead:
342 |
343 | ```javascript
344 | var string = I18n.t('You can lead a new \
345 | discussion or join an existing one.', {
346 | leadUrl: "/new",
347 | joinUrl: "/search"
348 | });
349 | ```
350 |
351 | This isn't much better, because now you have HTML in your translations.
352 | If you want to add a class to the link, you have to go update all the
353 | translations. A translator could accidentally break your page (or worse,
354 | cross-site script it).
355 |
356 | So what do you do?
357 |
358 | i18nline lets you specify wrappers, so you can keep HTML out the translations,
359 | while still just having a single string needing translation:
360 |
361 | ```javascript
362 | var string = I18n.t('You can *lead* a new discussion or **join** an \
363 | existing one.', {
364 | wrappers: [
365 | '$1',
366 | '", raw_input: ""});
384 | => "If you type <input> you get "
385 | ```
386 |
387 | If any interpolated value or wrapper is HTML-safe, everything else will be HTML-
388 | escaped.
389 |
390 | ### Inline Pluralization Support
391 |
392 | Pluralization can be tricky, but i18n.js gives you some flexibility.
393 | i18nline brings this inline with a default translation object, e.g.
394 |
395 | ```javascript
396 | I18n.t({one: "There is one light!", other: "There are %{count} lights!"},
397 | {count: picard.visibleLights.length});
398 | ```
399 |
400 | Note that the `count` interpolation value needs to be explicitly set when doing
401 | pluralization.
402 |
403 | If you just want to pluralize a single word, there's a shortcut:
404 |
405 | ```javascript
406 | I18n.t("person", {count: users.length});
407 | ```
408 |
409 | This is equivalent to:
410 |
411 | ```javascript
412 | I18n.t({one: "1 person", other: "%{count} people"},
413 | {count: users.length});
414 | ```
415 |
416 | ## Configuration
417 |
418 | For most projects, no configuration should be needed.
419 | The default configuration should work without changes, unless:
420 |
421 | * You have source files in a directory that is in the default
422 | `ignoreDirectories`, or in the root of your project (not recommended)
423 | * You have source files that don't match the default `patterns`
424 | * You need the output to go some place other than the default
425 | `out` folder of `'src/i18n/'`
426 | * You have i18nline(r) `plugins` you want to configure that are
427 | not recognized by the auto-configuration feature
428 |
429 | If you find you need to change the configuration, you can configure
430 | i18nline through *package.json*, *i18nline.rc* or command line arguments.
431 |
432 | If multiple sources of configuration are present, they will be
433 | applied in this order, with the last option specified overwriting
434 | the previous settings:
435 |
436 | * Defaults
437 | * package.json
438 | * .i18nrc file
439 | * CLI arguments
440 |
441 | In your *package.json*, create a key named `"i18n"` and
442 | specify your project's global configuration settings there.
443 |
444 | *package.json*
445 | ```json
446 | {
447 | "name": "my-module",
448 | "version": "1.0.0",
449 |
450 | "i18n": {
451 | "settings": "go here"
452 | }
453 | }
454 | ```
455 |
456 | > If i18nline detects that your project is using
457 | [pkgcfg](https://npmjs.com/package/pkgcfg), it will load
458 | `package.json` using it, enabling all dynamic goodness.
459 |
460 | Or, if you prefer, you can create a `.i18nrc` options file in the root
461 | of your project.
462 |
463 | You can also pass some configuration options directly to the CLI.
464 |
465 | For your convenience, this is the default configuration that will
466 | be used if you supply no custom configuration:
467 |
468 | ```json
469 | {
470 | "basePath": ".",
471 | "ignoreDirectories": ["node_modules", "bower_components", ".git", "dist", "build"],
472 | "patterns": ["**/*.js", "**/*.jsx"],
473 | "ignorePatterns": [],
474 | "out": "src/i18n",
475 | "inferredKeyFormat": "underscored_crc32",
476 | "underscoredKeyLength": 50,
477 | "defaultLocale": "en",
478 | }
479 | ```
480 |
481 | ### Options
482 |
483 | #### basePath
484 | String. Defaults to `"."`.
485 | The base path (relative to the current directory). `out`,
486 | `directories`, `ignoreDirectories`, `patterns`, `ignorePatterns`
487 | and any ignore patterns coming from `.i18nignore` files are
488 | interpreted as being relative to `basePath`.
489 |
490 | #### directories
491 | Array of directories, or a String containing a comma separated
492 | list of directories. Defaults to `undefined`.
493 | Only files in these directories will be processed.
494 |
495 | > If no directories are specified, the i18nline CLI will try to
496 | auto-configure this setting with all directories in `basePath`
497 | that are not excluded by `ignoreDirectories`. This mostly works
498 | great, but if you have source files in the root of your project,
499 | they won't be found this way. Set `directories` to `"."` to force
500 | the processing to start at the root (not recommended as it may
501 | be very slow).
502 |
503 | #### ignoreDirectories
504 | Array of directories, or a String containing a comma separated
505 | list of directories. Defaults to `['node_modules', 'bower_components', '.git', 'dist']`.
506 | These directories will not be processed.
507 |
508 | #### patterns
509 | Array of pattern strings, or a String containing a comma separated
510 | list of pattern(s). Defaults to `["**/*.js", "**/*.jsx"]`. Only
511 | files matching these patterns will be processed.
512 |
513 | > Note that for your convenience, the defaults include .jsx files
514 |
515 | #### ignorePatterns
516 | Array of pattern strings, or a String containing a comma separated
517 | list of patterns. Defaults to `[]`. Files matching these patterns
518 | will be ignored, even if they match `patterns`.
519 |
520 | #### out
521 | String. Defaults to `'src/i18n'`.
522 | In case `out` ends with `'.json'`, the `export` command will export
523 | the default translations to this file and the `synch` command will
524 | just perform an export. Otherwise, `out` is interpreted as a folder
525 | to be used by the `synch` command to synch the translations and the
526 | file with the default translations will be named `default.json`.
527 |
528 | #### outputFile
529 | String. Alias for `out`. **deprecated**.
530 | In previous versions of `i18nline`, `outputFile` was used to
531 | indicate where to export the default translations. However,
532 | starting with version 2, `i18nline` now supports synching the
533 | entire translations folder, so `out` is preferred to be set
534 | to a folder, making the name `outputFile` confusing. As long as
535 | your outputFile is set to some path ending in `'.json'`, your
536 | old configuration will continue to work for all versions in the
537 | 2.x branch, but may stop working at version 3+. If you relied
538 | on the default, consider adopting the new default filename of
539 | `default.json` i.s.o. `en.json`, or explicitly set `out` to
540 | `'i18n/en.json'` (the old default, not recommended).
541 | When you set this option, a deprecation warning is logged.
542 |
543 | #### inferredKeyFormat
544 | String. Defaults to `"underscored_crc32"`.
545 | When no key was specified for a translation, `i18nline` will infer one
546 | from the default translation using the format specified here. Available
547 | formats are: `"underscored"` and `"underscored_crc32"`, where the second
548 | form uses a checksum over the whole message to ensure that changes in the
549 | message beyond the `underscoredKeyLength` limit will still result in the
550 | key changing.
551 |
552 | > If `inferredKeyFormat` is set to an unknown format, the unaltered default
553 | translation string is used as the key (not recommended).
554 |
555 | #### underscoredKeyLength
556 | Number. Defaults to `50`. The maximum length the inferred `underscored`
557 | key derived of a message will be. If the message is longer than this
558 | limit, changes in the message will only have an effect on the inferred
559 | key if `inferredKeyFormat` is set to `underscored_crc32`. In that
560 | case the checksum is appended to the underscored key (separated by an
561 | underscore), making the total max key length `underscoredKeyLength + 9`.
562 |
563 | ## Command Line Utility
564 |
565 | ### i18nline check
566 |
567 | Ensures that there are no problems with your translate calls (e.g. missing
568 | interpolation values, reusing a key for a different translation, etc.). **Go
569 | add this to your Jenkins/Travis tasks.**
570 |
571 | ### i18nline export
572 |
573 | Does an `i18nline check`, and then extracts all default translations from your
574 | codebase. If `out` ends with `'.json'`, it outputs the default translations to
575 | the configured file. Otherwise it assumes `out` is a folder and saves the default translations in this folder in a file named `default.json`.
576 |
577 | ### i18nline index
578 |
579 | Generates an index file named `index.js` that you can `import` into your project
580 | and that takes care of (hot re-)loading the individual translations when needed.
581 |
582 | ### i18nline synch
583 |
584 | Does an `i18nline check`, and then extracts all default translations from your
585 | codebase. It then runs `i18nline export` to export the default translations.
586 | If `out` ends with `'.json'` it prints a warning and stops. Otherwise it checks
587 | if a translation file for the default locale (normally `'en'`) is found. If not,
588 | it generates an empty file for it to be synched in the next step. Then, it reads
589 | all translation files present in the folder (expected to be named `'[locale].json'`,
590 | e.g. `'fr.json'`, `'de.json'`, etc.) and synchs them, removing keys that are no
591 | longer in use and adding new keys with their value set to the default translation
592 | for that key. Finally, it runs `i18nline index` to generate an index file that
593 | you can `import` into your project.
594 |
595 | Adding support for a new locale can be done by adding an empty file for that
596 | locale and running `i18nline synch` so it will populate the new file with all
597 | default translations.
598 |
599 | > The synch command works best when you use inferred keys with the
600 | `inferredKeyFormat` set to `"underscored_crc32"` (the default).
601 |
602 | ### i18nline help
603 |
604 | Prints this message:
605 |
606 | ```
607 | ██╗ ███╗ ██╗██╗ ██╗███╗ ██╗███████╗
608 | ██║ ████╗ ██║██║ ██║████╗ ██║██╔════╝
609 | ██║18 ██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗
610 | ██║ ██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝
611 | ██║ ██║ ╚████║███████╗██║██║ ╚████║███████╗
612 | ╚═╝ ╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝
613 | keep your translations in line
614 |
615 | Usage
616 |
617 | i18nline [options]
618 |
619 | Commands
620 |
621 | check Performs a dry-run with all checks, but does not write any files
622 | export Performs a check, then exports the default translation file
623 | index Generates an index file you can import in your program
624 | synch Synchronizes all generated files with the source code
625 | help Prints this help screen
626 |
627 | Options
628 |
629 | You can set/override all of i18nline's configuration options on the command line.
630 | SEE: https://github.com/download/i18nline#configuration
631 | In addition these extra options are available in the CLI:
632 |
633 | -o Alias for --out (SEE config docs)
634 | --only Only process a single file/directory/pattern
635 | --silent Don't log any messages
636 | -s Alias for --silent
637 |
638 | Examples
639 |
640 | $ i18nline check --only=src/some-file.js
641 | > Only check the given file for errors
642 |
643 | $ i18nline export --directory=src --patterns=**/*.js,**/*.jsx
644 | > Export all translations in `src` directory from .js and .jsx files
645 | > to default output file src/i18n/default.json
646 |
647 | $ i18nline export -o=translations
648 | > Export all translations in any directory but the ignored ones, from
649 | > .js and .jsx files to the given output file translations/default.json
650 |
651 | See what's happening
652 |
653 | i18nline uses ulog for it's logging. The default level is info. To change it:
654 | $ LOG=debug (or trace, log, info, warn, error)
655 | Now, i18nline will log any messages at or above the set level
656 | ```
657 |
658 | #### .i18nignore and more
659 |
660 | By default, the check and export commands will look for inline translations
661 | in any .js files. You can tell it to always skip certain files/directories/patterns
662 | by creating a .i18nignore file. The syntax is the same as
663 | [.gitignore](http://www.kernel.org/pub/software/scm/git/docs/gitignore.html),
664 | though it supports
665 | [a few extra things](https://github.com/jenseng/globby#compatibility-notes).
666 |
667 | If you only want to check a particular file/directory/pattern, you can set the
668 | `--only` option when you run the command, e.g.
669 |
670 | ```bash
671 | i18nline check --only=/app/**/user*
672 | ```
673 |
674 | ## Compatibility
675 |
676 | i18nline is compatible with i18n.js, i18nliner-js, i18nliner (ruby) etc so you can
677 | add it to an established (and already internationalized) app. Your existing
678 | translation calls, keys and translation files will still just work without modification.
679 |
680 | If you want to maximize the portability of your code across the I18n ecosystem, you
681 | should avoid including hard dependencies to any particular library in every file. One way
682 | to easily achieve that is to set `I18n` as a global. Another simple way is to make your
683 | own `i18n.js` file that just requires `'i18nline/i18n'`and sets it on `module.exports`. then
684 | you let all your modules require this file. If you ever want to change 'providers',
685 | you only need to change this file.
686 |
687 | ## Related Projects
688 |
689 | * [i18nliner (ruby)](https://github.com/jenseng/i18nliner)
690 | * [i18nliner-js](https://github.com/jenseng/i18nliner-js)
691 | * [i18nliner-handlebars](https://github.com/fivetanley/i18nliner-handlebars)
692 | * [react-i18nliner](https://github.com/jenseng/react-i18nliner)
693 | * [preact-i18nline](https://github.com/download/preact-i18nline)
694 |
695 | ## License
696 |
697 | Copyright (c) 2018 Stijn de Witt & Jon Jensen,
698 | released under the MIT license
699 |
--------------------------------------------------------------------------------
/bin/i18nline.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var minimist = require('minimist');
4 | var extend = require('extend');
5 | var argv = minimist(process.argv.slice(2));
6 |
7 | var log = require('../lib/log')('i18nline', argv.s || argv.silent)
8 |
9 | var command = argv._[0];
10 | delete argv._
11 | var I18nline = require('../lib/main').configure(argv);
12 | I18nline.Commands.run(command, I18nline.config);
13 |
--------------------------------------------------------------------------------
/lib/call_helpers.js:
--------------------------------------------------------------------------------
1 | var log = require('./log')('i18nline:call-helpers');
2 |
3 | var pluralize = require('./pluralize');
4 | var Utils = require('./utils');
5 | var I18nline = require('./i18nline');
6 | var getSlug = require('speakingurl');
7 | var crc32 = require('crc32');
8 |
9 | var CallHelpers = {
10 | ALLOWED_PLURALIZATION_KEYS: ["zero", "one", "few", "many", "other"],
11 | REQUIRED_PLURALIZATION_KEYS: ["one", "other"],
12 | UNSUPPORTED_EXPRESSION: [],
13 |
14 | normalizeKey: function(key) {
15 | return key;
16 | },
17 |
18 | normalizeDefault: function(defaultValue, translateOptions) {
19 | defaultValue = CallHelpers.inferPluralizationHash(defaultValue, translateOptions);
20 | return defaultValue;
21 | },
22 |
23 | inferPluralizationHash: function(defaultValue, translateOptions) {
24 | if (typeof defaultValue === 'string' && defaultValue.match(/^[\w-]+$/) && translateOptions && ("count" in translateOptions)) {
25 | return {one: "1 " + defaultValue, other: "%{count} " + pluralize(defaultValue)};
26 | }
27 | else {
28 | return defaultValue;
29 | }
30 | },
31 |
32 | isObject: function(object) {
33 | return typeof object === 'object' && object !== this.UNSUPPORTED_EXPRESSION;
34 | },
35 |
36 | validDefault: function(allowBlank) {
37 | var defaultValue = this.defaultValue;
38 | return allowBlank && (typeof defaultValue === 'undefined' || defaultValue === null) ||
39 | typeof defaultValue === 'string' ||
40 | this.isObject(defaultValue);
41 | },
42 |
43 | inferKey: function(defaultValue, translateOptions) {
44 | if (this.validDefault(defaultValue)) {
45 | defaultValue = this.normalizeDefault(defaultValue, translateOptions);
46 | if (typeof defaultValue === 'object')
47 | defaultValue = "" + defaultValue.other;
48 | return this.keyify(defaultValue);
49 | }
50 | },
51 |
52 | keyifyUnderscored: function(string) {
53 | var key = getSlug(string, {separator: '_', lang: false}).replace(/[-_]+/g, '_');
54 | return key.substring(0, I18nline.config.underscoredKeyLength);
55 | },
56 |
57 | keyifyUnderscoredCrc32: function(string) {
58 | var checksum = crc32(string.length + ":" + string).toString(16);
59 | return this.keyifyUnderscored(string) + "_" + checksum;
60 | },
61 |
62 | keyify: function(string) {
63 | switch (I18nline.config.inferredKeyFormat) {
64 | case 'underscored':
65 | return this.keyifyUnderscored(string);
66 | case 'underscored_crc32':
67 | return this.keyifyUnderscoredCrc32(string);
68 | default:
69 | return string;
70 | }
71 | },
72 |
73 | keyPattern: /^(\w+\.)+\w+$/,
74 |
75 | /**
76 | * Possible translate signatures:
77 | *
78 | * key [, options]
79 | * key, default_string [, options]
80 | * key, default_object, options
81 | * default_string [, options]
82 | * default_object, options
83 | **/
84 | isKeyProvided: function(keyOrDefault, defaultOrOptions, maybeOptions) {
85 | if (typeof keyOrDefault === 'object')
86 | return false;
87 | if (typeof defaultOrOptions === 'string')
88 | return true;
89 | if (maybeOptions)
90 | return true;
91 | if (typeof keyOrDefault === 'string' && keyOrDefault.match(CallHelpers.keyPattern))
92 | return true;
93 | return false;
94 | },
95 |
96 | isPluralizationHash: function(object) {
97 | var pKeys;
98 | return this.isObject(object) &&
99 | (pKeys = Utils.keys(object)) &&
100 | pKeys.length > 0 &&
101 | Utils.difference(pKeys, this.ALLOWED_PLURALIZATION_KEYS).length === 0;
102 | },
103 |
104 | inferArguments: function(args, meta) {
105 | if (args.length === 2 && typeof args[1] === 'object' && args[1].defaultValue)
106 | return args;
107 |
108 | var hasKey = this.isKeyProvided.apply(this, args);
109 | if (meta)
110 | meta.inferredKey = !hasKey;
111 | if (!hasKey)
112 | args.unshift(null);
113 |
114 | var defaultValue = null;
115 | var defaultOrOptions = args[1];
116 | if (args[2] || typeof defaultOrOptions === 'string' || this.isPluralizationHash(defaultOrOptions))
117 | defaultValue = args.splice(1, 1)[0];
118 | if (args.length === 1)
119 | args.push({});
120 | var options = args[1];
121 | if (defaultValue)
122 | options.defaultValue = defaultValue;
123 | if (!hasKey)
124 | args[0] = this.inferKey(defaultValue, options);
125 | return args;
126 | },
127 |
128 | applyWrappers: function(string, wrappers) {
129 | var i;
130 | var len;
131 | var keys;
132 | if (typeof wrappers === 'string')
133 | wrappers = [wrappers];
134 | if (wrappers instanceof Array) {
135 | for (i = wrappers.length; i; i--)
136 | string = this.applyWrapper(string, new Array(i + 1).join("*"), wrappers[i - 1]);
137 | }
138 | else {
139 | keys = Utils.keys(wrappers);
140 | keys.sort(function(a, b) { return b.length - a.length; }); // longest first
141 | for (i = 0, len = keys.length; i < len; i++)
142 | string = this.applyWrapper(string, keys[i], wrappers[keys[i]]);
143 | }
144 | return string;
145 | },
146 |
147 | applyWrapper: function(string, delimiter, wrapper) {
148 | var escapedDelimiter = Utils.regexpEscape(delimiter);
149 | var pattern = new RegExp(escapedDelimiter + "(.*?)" + escapedDelimiter, "g");
150 | return string.replace(pattern, wrapper);
151 | }
152 | };
153 |
154 | module.exports = CallHelpers;
155 |
156 | log.debug('Initialized ' + log.name);
157 |
--------------------------------------------------------------------------------
/lib/commands.js:
--------------------------------------------------------------------------------
1 | var log = require('./log')('i18nline:commands');
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var chalk = require('chalk');
6 | var w = chalk.white, gr = chalk.gray, grb = chalk.gray.bold, g=chalk.green, gb=chalk.green.bold;
7 |
8 | var Utils = require('./utils');
9 | var Check = require('./commands/check');
10 | var Export = require('./commands/export');
11 | var Index = require('./commands/index');
12 | var Synch = require('./commands/synch');
13 | var Help = require('./commands/help');
14 |
15 | function capitalize(string) {
16 | return typeof string === "string" && string ?
17 | string.slice(0, 1).toUpperCase() + string.slice(1) :
18 | string;
19 | }
20 |
21 | var Commands = {
22 | run: function(name, options) {
23 | name = name || 'help';
24 | options = options || {};
25 | log.log(log.name + ': ' + name, options);
26 | if (name != 'help' && !options.directories) {
27 | options.directories = autoConfigureDirectories(options);
28 | }
29 | var Command = this[capitalize(name)];
30 | if (Command) {
31 | try {
32 | log.info('');
33 | log.info(w(' ██╗ ███╗ ██╗██╗ ██╗███╗ ██╗███████╗ '));
34 | log.info(w(' ██║ ████╗ ██║██║ ██║████╗ ██║██╔════╝ '));
35 | log.info(w(' ██║') + gr('18') + w(' ██╔██╗ ██║██║ ██║██╔██╗ ██║█████╗ '));
36 | log.info(w(' ██║ ██║╚██╗██║██║ ██║██║╚██╗██║██╔══╝ '));
37 | log.info(w(' ██║ ██║ ╚████║███████╗██║██║ ╚████║███████╗ '));
38 | log.info(w(' ╚═╝ ╚═╝ ╚═══╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ '));
39 | log.info(grb(' keep your translations in line '));
40 | log.info('');
41 | return (new Command(options)).run();
42 | } catch (e) {
43 | log.error(log.name + ': ERROR: ' + name + ' failed', e);
44 | }
45 | } else {
46 | log.error(log.name + ": ERROR: unknown command " + name + "\n");
47 | }
48 | return false;
49 | },
50 |
51 | Check: Check,
52 | Export: Export,
53 | Synch: Synch,
54 | Help: Help,
55 | Index: Index,
56 | };
57 |
58 | function autoConfigureDirectories(options) {
59 | var base = path.resolve(process.cwd(), options.basePath);
60 | return fs.readdirSync(base).filter(function(file) {
61 | return (
62 | fs.statSync(path.resolve(base, file)).isDirectory() &&
63 | options.ignoreDirectories.indexOf(file) === -1
64 | )
65 | });
66 | }
67 |
68 | module.exports = Commands;
69 |
70 | log.debug('Initialized ' + log.name);
71 |
--------------------------------------------------------------------------------
/lib/commands/base_command.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:base');
2 | var chalk = require("chalk");
3 | var fs = require('fs');
4 | var extend = require('extend');
5 | var I18nline = require('../i18nline');
6 |
7 | function BaseCommand(options) {
8 | if (options.silent) log.level = log.NONE;
9 | options.out = options.o || options.out || options.outputFile;
10 | if (options.outputFile) {
11 | log.warn(chalk.yellow('i18nline: Option `outputFile` is deprecated. Prefer `out` instead.'));
12 | }
13 | options = extend({}, I18nline.config, options);
14 | options.patterns = typeof options.patterns == 'string' ? options.patterns.split(',') : options.patterns || [];
15 | options.ignorePatterns = typeof options.ignorePatterns == 'string' ? options.ignorePatterns.split(',') : options.ignorePatterns || [];
16 | options.directories = typeof options.directories == 'string' ? options.directories.split(',') : options.directories;
17 | this.options = options;
18 | }
19 |
20 | module.exports = BaseCommand;
21 |
22 | log.debug('Initialized ' + log.name);
23 |
--------------------------------------------------------------------------------
/lib/commands/check.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:check');
2 |
3 | var chalk = require("chalk");
4 | var r = chalk.red, g = chalk.green, gr = chalk.gray;
5 |
6 | var TranslationHash = require("../extractors/translation_hash");
7 | var BaseCommand = require("./base_command");
8 | var JsProcessor = require("../processors/js_processor");
9 |
10 |
11 | function sum(array, prop) {
12 | var total = 0;
13 | for (var i = 0, len = array.length; i < len; i++) {
14 | total += array[i][prop];
15 | }
16 | return total;
17 | }
18 |
19 | function Check(options) {
20 | if (options.silent) log.level = log.NONE;
21 | BaseCommand.call(this, options);
22 | this.errors = [];
23 | this.translations = new this.TranslationHash();
24 | this.setUpProcessors();
25 | }
26 |
27 | Check.prototype = Object.create(BaseCommand.prototype);
28 | Check.prototype.constructor = Check;
29 | Check.prototype.TranslationHash = TranslationHash;
30 |
31 | Check.prototype.setUpProcessors = function() {
32 | this.processors = [];
33 | for (var key in Check.processors) {
34 | var Processor = Check.processors[key];
35 | this.processors.push(
36 | new Processor(this.translations, {
37 | translations: this.translations,
38 | checkWrapper: this.checkWrapper.bind(this),
39 | only: this.options.only,
40 | patterns: this.options.patterns,
41 | ignorePatterns: this.options.ignorePatterns,
42 | directories: this.options.directories,
43 | ignoreDirectories: this.options.ignoreDirectories,
44 | })
45 | );
46 | }
47 | };
48 |
49 | Check.prototype.checkFiles = function() {
50 | for (var i = 0; i < this.processors.length; i++) {
51 | this.processors[i].checkFiles();
52 | }
53 | };
54 |
55 | Check.prototype.checkWrapper = function(file, checker) {
56 | try {
57 | var found = checker(file);
58 | if (found) {log.info(g("+" + found) + (found < 10 ? ' ' : ' ') + gr(file));}
59 | return found;
60 | } catch (e) {
61 | this.errors.push(e.message + "\n" + file);
62 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' ') + gr(file));
63 | return 0;
64 | }
65 | };
66 |
67 | Check.prototype.isSuccess = function() {
68 | return !this.errors.length;
69 | };
70 |
71 | Check.prototype.printSummary = function() {
72 | var processors = this.processors;
73 | var summary;
74 | var errors = this.errors;
75 | var errorsLen = errors.length;
76 | var i;
77 |
78 | var translationCount = sum(processors, 'translationCount');
79 | var fileCount = sum(processors, 'fileCount');
80 |
81 | for (i = 0; i < errorsLen; i++) {
82 | log.error("\nERR" + (i+1) + ")\n" + r(errors[i]));
83 | }
84 | summary = "\n" + fileCount + " files, " + translationCount + " strings, " + errorsLen + " failures\n";
85 | if (this.isSuccess()) {log.info(g(summary));}
86 | else {log.error(r(summary));}
87 | };
88 |
89 | Check.prototype.run = function() {
90 | var now = new Date();
91 | this.startTime = now.getTime();
92 |
93 | log[!this.sub && this.constructor === Check ? 'info' : 'debug'](
94 | 'Checking source files for errors\n' +
95 | gr('Tip: Add this task to your continuous build.\n')
96 | );
97 |
98 | this.checkFiles();
99 | this.printSummary();
100 | var elapsed = (new Date()).getTime() - this.startTime;
101 | log[!this.sub && this.constructor === Check ? 'info' : 'debug'](
102 | "\nCheck finished " + (this.isSuccess() ? "" : "with errors ") + "in " + (elapsed / 1000) + " seconds\n"
103 | );
104 | return this.isSuccess();
105 | };
106 |
107 | Check.processors = {JsProcessor: JsProcessor};
108 |
109 | module.exports = Check;
110 |
111 | log.debug('Initialized ' + log.name);
112 |
--------------------------------------------------------------------------------
/lib/commands/export.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:export');
2 | var path = require('path');
3 | var fs = require('fs');
4 | var mkdirp = require('mkdirp');
5 | var extend = require('extend');
6 | var chalk = require("chalk");
7 | var r = chalk.red, g = chalk.green, gr = chalk.gray;
8 |
9 | var I18nline = require('../i18nline');
10 | var Check = require('./check');
11 |
12 | var template = fs.readFileSync(path.resolve(__dirname, '../template.js')).toString();
13 |
14 | function Export(options) {
15 | if (options.silent) log.level = log.NONE;
16 | Check.call(this, options);
17 | }
18 |
19 | Export.prototype = Object.create(Check.prototype);
20 | Export.prototype.constructor = Export;
21 |
22 | Export.prototype.run = function() {
23 | var now = new Date();
24 | this.startTime = now.getTime();
25 |
26 | var locale = this.options.defaultLocale || I18nline.config.defaultLocale;
27 | var basePath = this.options.basePath || I18nline.config.basePath;
28 | this.out = path.resolve(basePath, this.options.out || I18nline.config.out);
29 | var outputFolder = path.extname(this.out) == '.json' ? path.dirname(this.out) : this.out;
30 |
31 | log[!this.sub && this.constructor === Export ? 'info' : 'debug'](
32 | 'Exporting default translations to ' + (this.out.endsWith('.json') ? this.out : path.join(this.out, 'default.json')) + '\n'
33 | );
34 |
35 | Check.prototype.run.call(this);
36 |
37 | if (this.isSuccess()) {
38 | var translations = {};
39 | translations[locale] = Object.keys(this.translations.translations).sort()
40 | .reduce(function(r,k){return ((r[k] = this.translations.translations[k]) && r) || r}.bind(this), {});
41 |
42 | try {
43 | mkdirp.sync(outputFolder);
44 | var def = path.extname(this.out) == '.json' ? this.out : path.resolve(outputFolder, 'default.json');
45 |
46 | this.oldDefaults = {};
47 | if (fs.existsSync(def)) {
48 | try {
49 | this.oldDefaults = JSON.parse(fs.readFileSync(def));
50 | } catch(e) {
51 | this.errors.push(e.message + "\n" + def);
52 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' '));
53 | }
54 | }
55 |
56 | if (this.isSuccess()) {
57 | var removed = Object.keys(this.oldDefaults[locale] || {})
58 | .filter(function(k){return !(k in translations[locale])}).length;
59 | var added = Object.keys(translations[locale])
60 | .filter(function(k){return !(k in (this.oldDefaults[locale] || {}))}.bind(this)).length;
61 | var status = '';
62 | if (added || removed) {
63 | try {
64 | fs.writeFileSync(def, JSON.stringify(translations, null, 2), {encoding:'utf8',flag:'w'});
65 | status += added > 0 ? (g('+' + added) + (added < 10 ? ' ' : ' ')) : ' ';
66 | status += removed > 0 ? (r('-' + removed) + (removed < 10 ? ' ' : ' ')) : ' ';
67 | } catch(e) {
68 | this.errors.push(e.message + "\n" + def);
69 | status = r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' ');
70 | }
71 | log.info(status + gr(def));
72 | }
73 | }
74 |
75 | for (var i=0,e; e=this.errors[i]; i++) {
76 | log.error('\nERR' + (i+1) + '\n' + e);
77 | }
78 | } catch(e) {
79 | this.errors.push(r(e.message + "\n" + def));
80 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' '));
81 | for (var i=0,e; e=this.errors[i]; i++) {
82 | log.error('\nERR' + (i+1) + '\n' + e);
83 | }
84 | }
85 | }
86 | var elapsed = (new Date()).getTime() - this.startTime;
87 | log[!this.sub && this.constructor === Export ? 'info' : 'debug'](
88 | "\nExport finished " + (this.isSuccess() ? "" : "with errors ") + "in " + (elapsed / 1000) + " seconds\n"
89 | );
90 | return this.isSuccess();
91 | };
92 |
93 | module.exports = Export;
94 |
95 | log.debug('Initialized ' + log.name);
96 |
--------------------------------------------------------------------------------
/lib/commands/help.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:help');
2 |
3 | var fs = require('fs');
4 | var mkdirp = require('mkdirp');
5 | var chalk = require('chalk');
6 | var wb = chalk.white.bold, gr = chalk.gray, grb = chalk.gray.bold, g=chalk.green, gb=chalk.green.bold;
7 |
8 | var BaseCommand = require('./base_command');
9 | var I18nline = require('../../lib/i18nline');
10 |
11 |
12 | function Help(options) {
13 | if (options.silent) log.level = log.NONE;
14 | BaseCommand.call(this, options);
15 | }
16 |
17 | Help.prototype = Object.create(BaseCommand.prototype);
18 | Help.prototype.constructor = Help;
19 |
20 | Help.prototype.run = function() {
21 | log.info(wb('Usage'));
22 | log.info('');
23 | log.info(gb('i18nline [options]'))
24 | log.info('');
25 | log.info(wb('Commands'))
26 | log.info('');
27 | log.info('check ' + gr('Performs a dry-run with all checks, but does not write any files'));
28 | log.info('export ' + gr('Performs a check, then exports the default translation file'));
29 | log.info('index ' + gr('Generates an index file you can import in your program'));
30 | log.info('synch ' + gr('Synchronizes all generated files with the source code'));
31 | log.info('help ' + gr('Prints this help screen'));
32 | log.info('');
33 | log.info(wb('Options'));
34 | log.info('');
35 | log.info(gr('You can set/override all of i18nline\'s configuration options on the command line.'));
36 | log.info(grb('SEE: ') + g('https://github.com/download/i18nline#configuration'));
37 | log.info(gr('In addition these extra options are available in the CLI:\n'));
38 | log.info('-o ' + gr('Alias for --out (SEE config docs)'));
39 | log.info('--only ' + gr('Only process a single file/directory/pattern'));
40 | log.info('--silent ' + gr('Don\'t log any messages'));
41 | log.info('-s ' + gr('Alias for --silent'));
42 | log.info('');
43 | log.info(wb('Examples'));
44 | log.info('');
45 | log.info(gr('$ ') + 'i18nline check --only=src/some-file.js');
46 | log.info(gr('> Only check the given file for errors'));
47 | log.info('');
48 | log.info(gr('$ ') + 'i18nline export --directory=src --patterns=**/*.js,**/*.jsx');
49 | log.info(gr('> Export all translations in `src` directory from .js and .jsx files'));
50 | log.info(gr('> to default output file src/i18n/default.json'));
51 | log.info('');
52 | log.info(gr('$ ') + 'i18nline export -o=translations');
53 | log.info(gr('> Export all translations in any directory but the ignored ones, from'));
54 | log.info(gr('> .js and .jsx files to the given output file translations/default.json'));
55 | log.info('');
56 | log.info(wb('See what\'s happening'));
57 | log.info('');
58 | log.info(gr('i18nline uses ') + g('ulog') + gr(' for it\'s logging. The default level is info. To change it:'));
59 | log.info(gr('$ ') + 'LOG=debug ' + gr(' (or trace, log, info, warn, error)'));
60 | log.info(gr('Now, i18nline will log any messages at or above the set level'));
61 | log.info('');
62 | return 0;
63 | };
64 |
65 | module.exports = Help;
66 |
67 | log.debug('Initialized ' + log.name);
68 |
--------------------------------------------------------------------------------
/lib/commands/index.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:index');
2 | var path = require('path');
3 | var fs = require('fs');
4 | var mkdirp = require('mkdirp');
5 | var chalk = require("chalk");
6 | var r = chalk.red, g = chalk.green, gr = chalk.grey;
7 |
8 | var I18nline = require('../i18nline');
9 | var Check = require('./check');
10 |
11 | var template = fs.readFileSync(path.resolve(__dirname, '../template.js')).toString();
12 |
13 | function Index(options) {
14 | if (options.silent) log.level = log.NONE;
15 | Check.call(this, options);
16 | }
17 |
18 | Index.prototype = Object.create(Check.prototype);
19 | Index.prototype.constructor = Index;
20 |
21 | Index.prototype.run = function() {
22 | var now = new Date();
23 | this.startTime = now.getTime();
24 |
25 | var basePath = this.options.basePath || I18nline.config.basePath;
26 | this.out = path.resolve(basePath, this.options.out || I18nline.config.out);
27 |
28 | log[!this.sub && this.constructor === Index ? 'info' : 'debug'](
29 | 'Generating index file\n' +
30 | gr('Import the generated file into your project\n')
31 | );
32 |
33 | mkdirp.sync(this.out);
34 | var supportedLocales = fs.readdirSync(this.out)
35 | .filter(function(f){return !fs.lstatSync(path.resolve(this.out, f)).isDirectory();}.bind(this))
36 | .filter(function(f){return path.basename(f).match(/^[a-z][a-z]_?([A-Z][A-Z])?\.json$/);})
37 | .map(function(f){return f.substring(0, f.length - 5);});
38 | var indexFile = path.resolve(this.out, 'index.js');
39 | var ignoredConfigKeys = ['basePath', 'directories', 'ignoreDirectories', 'patterns', 'ignorePatterns', 'autoTranslateTags', 'neverTranslateTags', 'out', 'inferredKeyFormat', 'underscoredKeyLength', 'locales'];
40 | var configuration = [].concat(
41 | 'I18n.supportedLocales = ' + JSON.stringify(supportedLocales).replace(/"/g, "'") + ';',
42 | Object.keys(I18nline.config)
43 | .filter(function(k){return ignoredConfigKeys.indexOf(k) === -1})
44 | .map(function(k){return 'I18n.' + k + ' = ' + JSON.stringify(I18nline.config[k]).replace(/"/g, "'") + ';'})
45 | );
46 | if (I18nline.config.locales) configuration = configuration.concat(
47 | Object.keys(I18nline.config.locales)
48 | .map(function(k){return 'I18n.locales.' + k + ' = ' + JSON.stringify(I18nline.config.locales[k]).replace(/"/g, "'") + ';'})
49 | )
50 | var imports = supportedLocales
51 | .map(function(l){return "case '" + l + "': return import(/* webpackChunkName: 'i18n." + l + "' */ './" + l + ".json');"});
52 | var reloads = supportedLocales
53 | .map(function(l){return "module.hot.accept('./" + l + ".json', I18n.reload('" + l + "'));";});
54 | var parts = template.split(/\/\*\[[A-Z_]?[A-Z0-9_]+\]\*\//);
55 | var script = parts[0] +
56 | configuration.join('\n') + parts[1] +
57 | imports.join('\n\t\t') + parts[2] +
58 | reloads.join('\n\t') + parts[3];
59 |
60 | // allow outside code to process the generated script
61 | // by assigning a function to indexFilehook
62 | if (this.indexFileHook) {
63 | script = this.indexFileHook(script)
64 | }
65 |
66 | try {
67 | fs.writeFileSync(indexFile, script, {encoding:'utf8',flag:'w'});
68 | log.info(g('index ') + gr(indexFile));
69 | } catch(e) {
70 | this.errors.push(e.message + "\n" + def);
71 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' ') + gr(indexFile));
72 | }
73 | if (this.constructor === Index) {
74 | for (var i=0,e; e=this.errors[i]; i++) {
75 | log.error('ERR' + (i+1) + '\n' + e);
76 | }
77 | }
78 | var elapsed = (new Date()).getTime() - this.startTime;
79 | log[!this.sub && this.constructor === Index ? 'info' : 'debug'](
80 | "\nIndex finished " + (this.isSuccess() ? "" : "with errors ") + "in " + (elapsed / 1000) + " seconds\n"
81 | );
82 | return this.isSuccess();
83 | };
84 |
85 | module.exports = Index;
86 |
87 | log.debug('Initialized ' + log.name);
88 |
--------------------------------------------------------------------------------
/lib/commands/synch.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:commands:synch');
2 | var path = require('path');
3 | var fs = require('fs');
4 | var mkdirp = require('mkdirp');
5 | var extend = require('extend');
6 | var chalk = require("chalk");
7 | var r = chalk.red, g = chalk.green, gr = chalk.gray, m = chalk.magenta;
8 |
9 | var I18nline = require('../i18nline');
10 | var Export = require('./export');
11 |
12 | var template = fs.readFileSync(path.resolve(__dirname, '../template.js')).toString();
13 |
14 | function Synch(options) {
15 | if (options.silent) log.level = log.NONE;
16 | Export.call(this, options);
17 | }
18 |
19 | Synch.prototype = Object.create(Export.prototype);
20 | Synch.prototype.constructor = Synch;
21 |
22 | Synch.prototype.run = function() {
23 | var now = new Date();
24 | this.startTime = now.getTime();
25 | log.debug(log.name + ': synch started at ' + now)
26 |
27 | var locale = this.options.defaultLocale || I18nline.config.defaultLocale;
28 | var basePath = this.options.basePath || I18nline.config.basePath;
29 | this.out = path.resolve(basePath, this.options.out || I18nline.config.out);
30 |
31 | try {
32 | if (path.extname(this.out) !== '.json') {
33 | log.info('Synching internationalization files in ' + this.out);
34 | log.info(gr("Create files here named '{locale}.json' (e.g. 'fr.json') to include them in the synching process.\n"));
35 | }
36 | else {
37 | log.warn(m('Unable to perform a synch. Option `out` is set to a file. Performing an export instead.'));
38 | log.warn(gr('Set `out` to a folder to enable synch. Current value: ') + this.out + '\n');
39 | }
40 |
41 | Export.prototype.run.call(this);
42 |
43 | if (this.isSuccess() && path.extname(this.out) !== '.json') {
44 | var translations = {};
45 | translations[locale] = Object.keys(this.translations.translations).sort()
46 | .reduce(function(r,k){return ((r[k] = this.translations.translations[k]) && r) || r}.bind(this), {});
47 |
48 | var defLoc = path.resolve(this.out, locale + '.json')
49 | if (! fs.existsSync(defLoc)) {
50 | try {
51 | var empty = {};
52 | empty[locale] = {};
53 | fs.writeFileSync(defLoc, JSON.stringify(empty, null, 2), {encoding:'utf8',flag:'w'});
54 | } catch(e) {
55 | this.errors.push(e.message + "\n" + defLoc);
56 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' '));
57 | }
58 | }
59 |
60 | var files = fs.readdirSync(this.out)
61 | .filter(function(f){return !fs.lstatSync(path.resolve(this.out, f)).isDirectory()}.bind(this))
62 | .filter(function(f){return path.basename(f).match(/^[a-z][a-z][_\-]?([A-Z][A-Z])?\.json$/)});
63 |
64 | var supportedLocales = [];
65 |
66 | if (this.isSuccess()) {
67 | for (var i=0,f; f = files[i]; i++) {
68 | var synchLocale = f.substring(0, f.length - 5);
69 | supportedLocales.push(synchLocale);
70 |
71 | log.debug('Synching translations for locale ' + synchLocale);
72 | var oldData = {};
73 | var file = path.resolve(this.out, f);
74 | try {
75 | oldData = fs.readFileSync(file);
76 | oldData = oldData && oldData.length && JSON.parse(oldData) || {};
77 | if (! (synchLocale in oldData)) oldData[synchLocale] = {};
78 | log.debug('Read translations for ' + synchLocale + ' from ' + f);
79 |
80 | log.debug('Compiling new translations for ' + synchLocale);
81 | var newData = {};
82 | newData[synchLocale] = {}
83 | Object.keys(translations[locale]).sort().forEach(k => (
84 | newData[synchLocale][k] = k in oldData[synchLocale]
85 | ? oldData[synchLocale][k]
86 | : translations[locale][k]
87 | ));
88 |
89 | log.debug('Writing translations for ' + synchLocale);
90 | var removed = Object.keys(oldData[synchLocale] || {})
91 | .filter(function(k){return !(k in newData[synchLocale])}).length;
92 | var added = Object.keys(newData[synchLocale])
93 | .filter(function(k){return !(k in (oldData[synchLocale] || {}))}).length;
94 | var status = '';
95 | var file = path.resolve(this.out, f);
96 | if (added || removed) {
97 | try {
98 | fs.writeFileSync(file, JSON.stringify(newData, null, 2), {encoding:'utf8',flag:'w'});
99 | status += added > 0 ? (g('+' + added) + (added < 10 ? ' ' : ' ')) : gr(' - ');
100 | status += removed > 0 ? (r('-' + removed) + (removed < 10 ? ' ' : ' ')) : gr(' - ');
101 | } catch(e) {
102 | this.errors.push(e.message + "\n" + file);
103 | status = (r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' '));
104 | }
105 | log.info(status + gr(path.resolve(this.out, f)));
106 | }
107 | } catch(e) {
108 | this.errors.push(e.message + "\n" + file);
109 | log.error(r("ERR" + this.errors.length) + (this.errors.length < 10 ? ' ' : ' '));
110 | }
111 | }
112 |
113 | var generateIndex = new I18nline.Commands.Index(this.options);
114 | generateIndex.sub = true; // ran as sub-command
115 | if (!generateIndex.run()) {
116 | this.errors.push.apply(this.errors, generateIndex.errors);
117 | }
118 |
119 | for (var i=0,e; e=this.errors[i]; i++) {
120 | log.error('ERR' + (i+1) + '\n' + e);
121 | }
122 | }
123 | }
124 | } catch(e) {
125 | this.errors.push(e.message + "\n");
126 | for (var i=0,e; e=this.errors[i]; i++) {
127 | log.error('ERR' + (i+1) + '\n' + e);
128 | }
129 | }
130 | var elapsed = (new Date()).getTime() - this.startTime;
131 | log[!this.sub && this.constructor === Synch ? 'info' : 'debug'](
132 | "\nSynch finished " + (this.isSuccess() ? "" : "with errors ") + "in " + (elapsed / 1000) + " seconds\n"
133 | );
134 | return this.isSuccess();
135 | };
136 |
137 | module.exports = Synch;
138 |
139 | log.debug('Initialized ' + log.name);
140 |
--------------------------------------------------------------------------------
/lib/errors.js:
--------------------------------------------------------------------------------
1 | var log = require('./log')('i18nline:errors');
2 |
3 | var CallHelpers = require('./call_helpers');
4 |
5 | function wordify(string) {
6 | return string.replace(/[A-Z]/g, function(s) {
7 | return " " + s.toLowerCase();
8 | }).trim();
9 | }
10 |
11 | var Errors = {
12 | register: function(name) {
13 | this[name] = function(line, details) {
14 | this.line = line;
15 | if (details) {
16 | var parts = [];
17 | var part;
18 | if (typeof details === "string" || !details.length) details = [details];
19 | for (var i = 0; i < details.length; i++) {
20 | part = details[i];
21 | part = part === CallHelpers.UNSUPPORTED_EXPRESSION ?
22 | "" :
23 | JSON.stringify(part);
24 | parts.push(part);
25 | }
26 | details = parts.join(', ');
27 | }
28 | this.name = name;
29 | this.message = wordify(name) + " on line " + line + (details ? ": " + details : "");
30 | };
31 | }
32 | };
33 |
34 | Errors.register('InvalidSignature');
35 | Errors.register('InvalidPluralizationKey');
36 | Errors.register('MissingPluralizationKey');
37 | Errors.register('InvalidPluralizationDefault');
38 | Errors.register('MissingInterpolationValue');
39 | Errors.register('MissingCountValue');
40 | Errors.register('InvalidOptionKey');
41 | Errors.register('KeyAsScope');
42 | Errors.register('KeyInUse');
43 |
44 | module.exports = Errors;
45 |
46 | log.debug('Initialized ' + log.name);
47 |
--------------------------------------------------------------------------------
/lib/extensions/i18n_js.js:
--------------------------------------------------------------------------------
1 | var log = require('../log')('i18nline:extensions:i18n');
2 | var EventEmitter = require('uevents');
3 | var CallHelpers = require('../call_helpers');
4 | var Utils = require('../utils');
5 |
6 | var extend = function(I18n) {
7 | function changed(locale) {
8 | return function() {
9 | I18n.emit('change', locale || I18n.locale);
10 | };
11 | }
12 |
13 | if (!I18n.on) EventEmitter(I18n);
14 |
15 | /**
16 | * Changes the current locale, loading the translations if needed.
17 | * If I18n has an emit method, emits a 'change' event with the given locale.
18 | * Returns a Promise that resolves with the locale when the change is completed.
19 | *
20 | * @param {string} locale
21 | * @return {PromiseLike