Keep your translations in line
Pull request Compare This branch is 40 commits ahead, 7 commits behind jenseng:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.vscode
bin
lib
test
.gitignore
.travis.yml
LICENSE
README.md
package-lock.json
package.json

README.md

i18nline

Keep your translations in line

npm license travis greenkeeper mind BLOWN

  ██╗   ███╗   ██╗██╗     ██╗███╗   ██╗███████╗
  ██║   ████╗  ██║██║     ██║████╗  ██║██╔════╝
  ██║18 ██╔██╗ ██║██║     ██║██╔██╗ ██║█████╗
  ██║   ██║╚██╗██║██║     ██║██║╚██╗██║██╔══╝
  ██║   ██║ ╚████║███████╗██║██║ ╚████║███████╗
  ╚═╝   ╚═╝  ╚═══╝╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝
         KEEP YOUR TRANSLATIONS IN LINE

No .js/yml translation files. Easy inline defaults. Optional keys. Easy pluralization. Wrappers for HTML-free translations.

I18nline extends i18n-js, so you can add it to an already-internationalized app that uses it.

TL;DR

i18nline lets you do stuff like this:

I18n.t("Ohai %{user}, my default translation is right here in the code. \
  Inferred keys, oh my!", {user: user.name});

and this:

I18n.t("*Translators* won't see any markup!",
  {wrappers: ['<a href="/translators">$1</em>']});

Best of all, you don't need to maintain translation files anymore; i18nline will do it for you.

What is this?

This project is a fork of Jon Jensen's i18nline-js that attempts to simplify usage by adding:

  • Sensible defaults
  • Auto-configuration of plugins
  • Improved documentation
  • CLI help command shows man page for the CLI
  • CLI index command generates an index.js file you can import
  • CLI synch command synchs all internationalization files Basically Jon did all the hard work and this project is just adding lots of sugar to make it sweeter.

Project setup

i18nline preprocesses your source files, generating new source files and translation files based on what it finds. To setup a project you need to:

  • Install i18nline (see next section).
  • Create a script in package.json to run the command-line tool.
  • Import I18n and use I18n.t() to render internationalized text.
  • Create an empty file in the out folder (by default: 'src/i18n') named '[locale].json' for each locale you want to support.
  • Run i18nline synch to synch the translation files and index file.
  • import the index file into your project.
  • Call I18n.changeLocale to set the locale (which loads the right translation file on demand)
  • Call I18n.on to react to the 'change' event (e.g. by re-rendering)
  • Get your translators to translate all the messages :)

Installation

npm install --save i18nline

i18nline has a dependency on i18n-js, so it will be installed automatically.

Create a script to run the command-line tool

i18nline comes with a command-line tool. This tool is written in Javascript and can be executed by Node JS. All you need to do to be able to use it is expose it via a script in your package.json (recommended), or install i18nline globally using the -g flag for npm install. The recommended approach is via a script in package.json because this means you only need to install i18nline as a normal dependency of your project.

Add a script with the command i18nline synch to package.json:

{
  "scripts": {
    "i18n": "i18nline synch"
  }
}

You can now invoke this command using npm run:

$ npm run i18n

Alternatively, you can expose the raw command:

{
  "scripts": {
    "i18nline": "i18nline"
  }
}

Then pass arguments via npm run:

$ npm run i18nline -- synch

The extra dashes here are used to tell npm run that all arguments following the dashes should be passed on to the script.

Import I18n and use I18n.t() to render internationalized text.

i18nline adds some extensions to the i18n-js runtime. If you require i18n-js via i18nline, these will be added automatically for you:

var I18n = require('i18nline');
// Ready to rock!

Alternatively, you can add i18n to your app any way you like and apply the extensions manually:

var I18n = // get it from somewhere... script tag or whatever
// add the runtime extensions manually
require('i18nline/lib/extensions/i18n_js')(I18n);

Every file that needs to translate stuff needs to get access to the I18n object somehow. You can add a require call to every such file (recommended), use I18n from the global sope, use some Webpack loader to add the import or whatever. The choice is yours.

Once I18n is available, you can use its I18n.t() function to render internationalized text:

console.info(I18n.t('This text will be internationalized'));

i18nline will preprocess your source, extracting all calls to I18n.t. For this reason, you should not rename the I18n object, or alias the method etc.

Create an empty file for each locale

Adding support for a locale is as simple as adding an empty file named '[locale].json' to the out folder and running i18nline synch. You still need to translate the text of course!

If you use Webpack with Hot Module Replacement (HMR) enabled, you can change the translations while your app is running and the changes will be picked up automatically.

Run i18nline synch to synch the translation files

Using the script you created before, run i18nline synch:

$ npm run i18n

This will create/synch a bunch of files in the out folder:

  • default.json: Contains the default translations extracted from the source code
  • en.json: Contains the messages for the default locale (assuming that is 'en')
  • de.json: Assuming you added an empty file de.json, it will be synched by i18nline.
  • index.js: Index file that you can import into your project.

The files default.json and index.js are regenerated every time, so don't change them as your changes will get lost. The translation files for the different locales are synched in a smart way, so any changes there will be respected.

import the index file into your project.

Since version 2, i18nline features an index command (which is also run as part of the synch command) that generates an index file containing the Javascript code needed to load the translation files into your project.

The generated file uses dynamic import() statements to allow Webpack and other bundlers to perform code splitting, making sure that each translation file ends up in a separate Javascript bundle. Also, it adds support for Webpacks Hot Module Replacement.

You need to use a transpiler like Babel in combination with a bundler like Webpack to take advantage of the code splitting and hot reloading features that the generated index file uses. If your tool chain does not support ES2015+ with dynamic import(), you cannot use the generated index file and need to load the translations yourself somehow. Just make sure you assign the loaded translations to I18n.translations.

To import the index file, simply require or import it:

some file in the root of src/

var I18n = require(`./i18n`); 
// or
import I18n from './i18n';

This will add a method I18n.import to the regular I18n object, with the code in it to load the translations for the given locale. If you inspect the contents of index.js, you will find that it contains a switch statement with similar code to load each file. That may seem redundant, but it is needed so that all the import() statements are statically analyzable, allowing the bundler to determine which files to include in the bundles it generates. You don't actually need to call the I18n.import() method yourself. It is called automatically when you use I18n.changeLocale (see next section).

Call I18n.changeLocale to change the locale

i18nline adds a method changeLocale to I18n that uses the method I18n.import (found in the generated index file) to load the translations for the locale when needed. So call changeLocale in your code to change the locale and the translation files will be loaded automatically when needed.

I18n.changeLocale('de');

Call I18n.on to react to the 'change' event

i18nline uses uevents to turn I18n into an event emitter. Whenever the locale or the translations for a locale have changed, i18nline emits a 'change' event. You can add a listener for this event like so:

I18n.on('change', locale => {
  // the locale changed to the given locale, or the translations for the
  // given locale changed. React accordingly, e.g. by re-rendering
});

The docs for the Node JS Events API explain how to remove listeners and perform other bookkeeping operations on event emitters.

Features

No more .js/.yml translation files

Instead of maintaining .js/.yml files and doing stuff like this:

I18n.t('account_page_title');

Forget the translation file and just do:

I18n.t('account_page_title', "My Account");

Regular I18n options follow the (optional) default translation, so you can do the usual stuff (placeholders, etc.).

Okay, but don't the translators need them?

Sure, but you don't need to write them. Just run i18nline export to extract all default translations from your codebase and output them to src/i18n/default.json In addition, any translation files already present in the out folder are synched: any keys no longer present in the source are removed and any new keys are added. Finally this outputs an index file named index.js that you can import in your app.

It's okay to lose your keys

Why waste time coming up with keys that are less descriptive than the default translation? i18nline makes keys optional, so you can just do this:

I18n.t("My Account")

i18nline will create a unique key based on the translation (e.g. 'my_account'), so you don't have to. See inferredKeyFormat for more information.

This can actually be a good thing, because when the default translation changes, the key changes, which means you know you need to get it retranslated (instead of letting a now-inaccurate translation hang out indefinitely).

If you are changing the meaning of the default translation, e.g. by changing "Enter your username and password to log in" to "Enter your e-mail address and password to log in", you should make the change in the source code to force a re-translation for all languages. If you are just changing the wording of the message, e.g. by changing "Enter your username and password to log in" to "Enter your username and password to sign in", you can make the change in the translation file en.js instead, so other languages are not affected.

Never change the file default.json, it is intended to accurately reflect the text that was extracted from the program source and as such it is always regenerated and not synched.

Wrappers

Suppose you have something like this in your JavaScript:

var string = 'You can <a href="/new">lead</a> a new discussion or \
  <a href="/search">join</a> an existing one.';

You might say "No, I'd use handlebars". Bear with me here, we're trying to make this easy for you and the translators :). For I18n, you might try something like this:

var string = I18n.t('You can %{lead} a new discussion or %{join} an \
  existing one.', {
    lead: '<a href="/new">' + I18n.t('lead') + '</a>',
    join: '<a href="/search"> + 'I18n.t('join') + '</a>')
  });

This is not great, because:

  1. There are three strings to translate.
  2. When translating the verbs, the translator has no context for where it's being used... Is "lead" a verb or a noun?
  3. Translators have their hands somewhat tied as far as what is inside the links and what is not.

So you might try this instead:

var string = I18n.t('You can <a href="%{leadUrl}">lead</a> a new \
  discussion or <a href="%{joinUrl}">join</a> an existing one.', {
    leadUrl: "/new",
    joinUrl: "/search"
  });

This isn't much better, because now you have HTML in your translations. If you want to add a class to the link, you have to go update all the translations. A translator could accidentally break your page (or worse, cross-site script it).

So what do you do?

i18nline lets you specify wrappers, so you can keep HTML out the translations, while still just having a single string needing translation:

var string = I18n.t('You can *lead* a new discussion or **join** an \
  existing one.', {
    wrappers: [
      '<a href="/new">$1</a>',
      '<a href="/search>$1</a>'
    ]
  });

Default delimiters are increasing numbers of asterisks, but you can specify any string as a delimiter by using a object rather than an array.

HTML Safety

i18nline ensures translations, interpolated values, and wrappers all play nicely (and safely) when it comes to HTML escaping. Wrappers are assumed to be HTML-safe, so everything else that is unsafe will get automatically escaped. If you are using i18n.js, you can hint that an interpolation value is already HTML-safe via %h{...}, e.g.

I18n.t("If you type %{input} you get %h{raw_input}", {input: "<input>", raw_input: "<input>"});
=> "If you type &lt;input&gt; you get <input>"

If any interpolated value or wrapper is HTML-safe, everything else will be HTML- escaped.

Inline Pluralization Support

Pluralization can be tricky, but i18n.js gives you some flexibility. i18nline brings this inline with a default translation object, e.g.

I18n.t({one: "There is one light!", other: "There are %{count} lights!"},
  {count: picard.visibleLights.length});

Note that the count interpolation value needs to be explicitly set when doing pluralization.

If you just want to pluralize a single word, there's a shortcut:

I18n.t("person", {count: users.length});

This is equivalent to:

I18n.t({one: "1 person", other: "%{count} people"},
  {count: users.length});

Configuration

For most projects, no configuration should be needed. The default configuration should work without changes, unless:

  • You have source files in a directory that is in the default ignoreDirectories, or in the root of your project (not recommended)
  • You have source files that don't match the default patterns
  • You need the output to go some place other than the default out folder of 'src/i18n/'
  • You have i18nline(r) plugins you want to configure that are not recognized by the auto-configuration feature

If you find you need to change the configuration, you can configure i18nline through package.json, i18nline.rc or command line arguments.

If multiple sources of configuration are present, they will be applied in this order, with the last option specified overwriting the previous settings:

  • Defaults
  • package.json
  • .i18nrc file
  • CLI arguments

In your package.json, create a key named "i18n" and specify your project's global configuration settings there.

package.json

{
  "name": "my-module",
  "version": "1.0.0",
  
  "i18n": {
    "settings": "go here"
  }
}

If i18nline detects that your project is using pkgcfg, it will load package.json using it, enabling all dynamic goodness.

Or, if you prefer, you can create a .i18nrc options file in the root of your project.

You can also pass some configuration options directly to the CLI.

For your convenience, this is the default configuration that will be used if you supply no custom configuration:

{
  "basePath": ".",
  "ignoreDirectories": ["node_modules", "bower_components", ".git", "dist", "build"],
  "patterns": ["**/*.js", "**/*.jsx"],
  "ignorePatterns": [],
  "out": "src/i18n",
  "inferredKeyFormat": "underscored_crc32",
  "underscoredKeyLength": 50,
  "defaultLocale": "en",
}

Options

basePath

String. Defaults to ".". The base path (relative to the current directory). out, directories, ignoreDirectories, patterns, ignorePatterns and any ignore patterns coming from .i18nignore files are interpreted as being relative to basePath.

directories

Array of directories, or a String containing a comma separated list of directories. Defaults to undefined. Only files in these directories will be processed.

If no directories are specified, the i18nline CLI will try to auto-configure this setting with all directories in basePath that are not excluded by ignoreDirectories. This mostly works great, but if you have source files in the root of your project, they won't be found this way. Set directories to "." to force the processing to start at the root (not recommended as it may be very slow).

ignoreDirectories

Array of directories, or a String containing a comma separated list of directories. Defaults to ['node_modules', 'bower_components', '.git', 'dist']. These directories will not be processed.

patterns

Array of pattern strings, or a String containing a comma separated list of pattern(s). Defaults to ["**/*.js", "**/*.jsx"]. Only files matching these patterns will be processed.

Note that for your convenience, the defaults include .jsx files

ignorePatterns

Array of pattern strings, or a String containing a comma separated list of patterns. Defaults to []. Files matching these patterns will be ignored, even if they match patterns.

out

String. Defaults to 'src/i18n'. In case out ends with '.json', the export command will export the default translations to this file and the synch command will just perform an export. Otherwise, out is interpreted as a folder to be used by the synch command to synch the translations and the file with the default translations will be named default.json.

outputFile

String. Alias for out. deprecated. In previous versions of i18nline, outputFile was used to indicate where to export the default translations. However, starting with version 2, i18nline now supports synching the entire translations folder, so out is preferred to be set to a folder, making the name outputFile confusing. As long as your outputFile is set to some path ending in '.json', your old configuration will continue to work for all versions in the 2.x branch, but may stop working at version 3+. If you relied on the default, consider adopting the new default filename of default.json i.s.o. en.json, or explicitly set out to 'i18n/en.json' (the old default, not recommended). When you set this option, a deprecation warning is logged.

inferredKeyFormat

String. Defaults to "underscored_crc32". When no key was specified for a translation, i18nline will infer one from the default translation using the format specified here. Available formats are: "underscored" and "underscored_crc32", where the second form uses a checksum over the whole message to ensure that changes in the message beyond the underscoredKeyLength limit will still result in the key changing.

If inferredKeyFormat is set to an unknown format, the unaltered default translation string is used as the key (not recommended).

underscoredKeyLength

Number. Defaults to 50. The maximum length the inferred underscored key derived of a message will be. If the message is longer than this limit, changes in the message will only have an effect on the inferred key if inferredKeyFormat is set to underscored_crc32. In that case the checksum is appended to the underscored key (separated by an underscore), making the total max key length underscoredKeyLength + 9.

Command Line Utility

i18nline check

Ensures that there are no problems with your translate calls (e.g. missing interpolation values, reusing a key for a different translation, etc.). Go add this to your Jenkins/Travis tasks.

i18nline export

Does an i18nline check, and then extracts all default translations from your codebase. If out ends with '.json', it outputs the default translations to the configured file. Otherwise it assumes out is a folder and saves the default translations in this folder in a file named default.json.

i18nline index

Generates an index file named index.js that you can import into your project and that takes care of (hot re-)loading the individual translations when needed.

i18nline synch

Does an i18nline check, and then extracts all default translations from your codebase. It then runs i18nline export to export the default translations. If out ends with '.json' it prints a warning and stops. Otherwise it checks if a translation file for the default locale (normally 'en') is found. If not, it generates an empty file for it to be synched in the next step. Then, it reads all translation files present in the folder (expected to be named '[locale].json', e.g. 'fr.json', 'de.json', etc.) and synchs them, removing keys that are no longer in use and adding new keys with their value set to the default translation for that key. Finally, it runs i18nline index to generate an index file that you can import into your project.

Adding support for a new locale can be done by adding an empty file for that locale and running i18nline synch so it will populate the new file with all default translations.

The synch command works best when you use inferred keys with the inferredKeyFormat set to "underscored_crc32" (the default).

i18nline help

Prints this message:

  ██╗   ███╗   ██╗██╗     ██╗███╗   ██╗███████╗
  ██║   ████╗  ██║██║     ██║████╗  ██║██╔════╝
  ██║18 ██╔██╗ ██║██║     ██║██╔██╗ ██║█████╗
  ██║   ██║╚██╗██║██║     ██║██║╚██╗██║██╔══╝
  ██║   ██║ ╚████║███████╗██║██║ ╚████║███████╗
  ╚═╝   ╚═╝  ╚═══╝╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝
         keep your translations in line

Usage

i18nline <command> [options]

Commands

check       Performs a dry-run with all checks, but does not write any files
export      Performs a check, then exports the default translation file
index       Generates an index file you can import in your program
synch       Synchronizes all generated files with the source code
help        Prints this help screen

Options

You can set/override all of i18nline's configuration options on the command line.
SEE: https://github.com/download/i18nline#configuration
In addition these extra options are available in the CLI:

-o          Alias for --out (SEE config docs)
--only      Only process a single file/directory/pattern
--silent    Don't log any messages
-s          Alias for --silent

Examples

$ i18nline check --only=src/some-file.js
> Only check the given file for errors

$ i18nline export --directory=src --patterns=**/*.js,**/*.jsx
> Export all translations in `src` directory from .js and .jsx files
> to default output file src/i18n/default.json

$ i18nline export -o=translations
> Export all translations in any directory but the ignored ones, from
> .js and .jsx files to the given output file translations/default.json

See what's happening

i18nline uses ulog for it's logging. The default level is info. To change it:
$ LOG=debug   (or trace, log, info, warn, error)
Now, i18nline will log any messages at or above the set level

.i18nignore and more

By default, the check and export commands will look for inline translations in any .js files. You can tell it to always skip certain files/directories/patterns by creating a .i18nignore file. The syntax is the same as .gitignore, though it supports a few extra things.

If you only want to check a particular file/directory/pattern, you can set the --only option when you run the command, e.g.

i18nline check --only=/app/**/user*

Compatibility

i18nline is compatible with i18n.js, i18nliner-js, i18nliner (ruby) etc so you can add it to an established (and already internationalized) app. Your existing translation calls, keys and translation files will still just work without modification.

If you want to maximize the portability of your code across the I18n ecosystem, you should avoid including hard dependencies to any particular library in every file. One way to easily achieve that is to set I18n as a global. Another simple way is to make your own i18n.js file that just requires 'i18nline/i18n'and sets it on module.exports. then you let all your modules require this file. If you ever want to change 'providers', you only need to change this file.

Related Projects

License

Copyright (c) 2018 Stijn de Witt & Jon Jensen, released under the MIT license