Scan your code, extract translation keys/values, and merge them into i18n resource files.
Turns your code
i18n._('Loading...');
i18n._('Backslashes in single quote: \' \\ \'');
i18n._('This is \
a multiline \
string');
i18n.t('car',{context:'blue',count:1});// output: 'One blue car'
i18n.t('car',{context:'blue',count:2});// output: '2 blue cars'
<Transi18nKey="some.key">Default text</Trans>
into resource files
{
"Loading...":"Wird geladen...",// uses existing translation
"Backslashes in single quote: ' \\ '":"__NOT_TRANSLATED__",// returns a custom string
"This is a multiline string":"this is a multiline string",// returns the key as the default value
"car":"car",
"car_blue":"One blue car",
"car_blue_plural":"{{count}} blue cars",
"some":{
"key":"Default text"
}
}
There is a major breaking change since v1.0, and the API interface and options are not compatible with v0.x.
CheckoutMigration Guidewhile upgrading from earlier versions.
- Fully compatible withi18next- a full-featured i18n javascript library for translating your webapplication.
- Supportreact-i18nextfor parsing theTranscomponent
- SupportKey Based Fallbackto write your code without the need to maintain i18n keys. This feature is available sincei18next@^2.1.0
- A standalone parser API
- A transform stream that works with both Gulp and Grunt task runner.
- Support custom transform and flush functions.
npm install --save-dev i18next-scanner
or
npm install -g i18next-scanner
$ i18next-scanner
Usage: i18next-scanner [options]<file...>
Options:
-V, --version output the version number
--config<config>Path to the config file (default: i18next-scanner.config.js)
--output<path>Path to the output directory (default:.)
-h, --help output usage information
Examples:
$ i18next-scanner --config i18next-scanner.config.js --output /path/to/output'src/**/*.{js,jsx}'
$ i18next-scanner --config i18next-scanner.config.js'src/**/*.{js,jsx}'
$ i18next-scanner'/path/to/src/app.js''/path/to/assets/index.html'
Globbing patterns are supported for specifying file paths:
*
matches any number of characters, but not/
?
matches a single character, but not/
**
matches any number of characters, including/
,as long as it's the only thing in a path part{}
allows for a comma-separated list of "or" expressions!
at the beginning of a pattern will negate the match
Note: Globbing patterns should be wrapped in single quotes.
constfs=require('fs');
constchalk=require('chalk');
module.exports={
input:[
'app/**/*.{js,jsx}',
// Use! to filter out files or directories
'!app/**/*.spec.{js,jsx}',
'!app/i18n/**',
'!**/node_modules/**',
],
output:'./',
options:{
debug:true,
func:{
list:['i18next.t','i18n.t'],
extensions:['.js','.jsx']
},
trans:{
component:'Trans',
i18nKey:'i18nKey',
defaultsKey:'defaults',
extensions:['.js','.jsx'],
fallbackKey:function(ns,value){
returnvalue;
},
// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes:true,// Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor:['br','strong','i','p'],// Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.
// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn:{
ecmaVersion:2020,
sourceType:'module',// defaults to 'module'
}
},
lngs:['en','de'],
ns:[
'locale',
'resource'
],
defaultLng:'en',
defaultNs:'resource',
defaultValue:'__STRING_NOT_TRANSLATED__',
resource:{
loadPath:'i18n/{{lng}}/{{ns}}.json',
savePath:'i18n/{{lng}}/{{ns}}.json',
jsonIndent:2,
lineEnding:'\n'
},
nsSeparator:false,// namespace separator
keySeparator:false,// key separator
interpolation:{
prefix:'{{',
suffix:'}}'
},
metadata:{},
allowDynamicKeys:false,
},
transform:functioncustomTransform(file,enc,done){
"use strict";
constparser=this.parser;
constcontent=fs.readFileSync(file.path,enc);
letcount=0;
parser.parseFuncFromString(content,{list:['i18next._','i18next.__']},(key,options)=>{
parser.set(key,Object.assign({},options,{
nsSeparator:false,
keySeparator:false
}));
++count;
});
if(count>0){
console.log(`i18next-scanner: count=${chalk.cyan(count)},file=${chalk.yellow(JSON.stringify(file.relative))}`);
}
done();
}
};
constfs=require('fs');
constParser=require('i18next-scanner').Parser;
constcustomHandler=function(key){
parser.set(key,'__TRANSLATION__');
};
constparser=newParser();
letcontent='';
// Parse Translation Function
// i18next.t('key');
content=fs.readFileSync('/path/to/app.js','utf-8');
parser
.parseFuncFromString(content,customHandler)// pass a custom handler
.parseFuncFromString(content,{list:['i18next.t']})// override `func.list`
.parseFuncFromString(content,{list:['i18next.t']},customHandler)
.parseFuncFromString(content);// use default options and handler
// Parse Trans component
content=fs.readFileSync('/path/to/app.jsx','utf-8');
parser
.parseTransFromString(content,customHandler)// pass a custom handler
.parseTransFromString(content,{component:'Trans',i18nKey:'i18nKey',defaultsKey:'defaults'})
.parseTransFromString(content,{fallbackKey:true})// Uses defaultValue as the fallback key when the i18nKey attribute is missing
.parseTransFromString(content);// use default options and handler
// Parse HTML Attribute
// <div data-i18n= "key" ></div>
content=fs.readFileSync('/path/to/index.html','utf-8');
parser
.parseAttrFromString(content,customHandler)// pass a custom handler
.parseAttrFromString(content,{list:['data-i18n']})// override `attr.list`
.parseAttrFromString(content,{list:['data-i18n']},customHandler)
.parseAttrFromString(content);// using default options and handler
console.log(parser.get());
console.log(parser.get({sort:true}));
console.log(parser.get('translation:key',{lng:'en'}));
The main entry function ofi18next-scanneris a transform stream. You can usevinyl-fsto create a readable stream, pipe the stream throughi18next-scannerto transform your code into an i18n resource object, and write to a destination folder.
Here is a simple example showing how that works:
constscanner=require('i18next-scanner');
constvfs=require('vinyl-fs');
constsort=require('gulp-sort');
constoptions={
// See options at https://github.com/i18next/i18next-scanner#options
};
vfs.src(['/path/to/src'])
.pipe(sort())// Sort files in stream by path
.pipe(scanner(options))
.pipe(vfs.dest('/path/to/dest'));
Alternatively, you can get a transform stream by calling createStream() as show below:
vfs.src(['/path/to/src'])
.pipe(sort())// Sort files in stream by path
.pipe(scanner.createStream(options))
.pipe(vfs.dest('/path/to/dest'));
Now you are ready to set up a minimal configuration, and get started with Gulp. For example:
constgulp=require('gulp');
constsort=require('gulp-sort');
constscanner=require('i18next-scanner');
gulp.task('i18next',function(){
returngulp.src(['src/**/*.{js,html}'])
.pipe(sort())// Sort files in stream by path
.pipe(scanner({
lngs:['en','de'],// supported languages
resource:{
// the source path is relative to current working directory
loadPath:'assets/i18n/{{lng}}/{{ns}}.json',
// the destination path is relative to your `gulp.dest()` path
savePath:'i18n/{{lng}}/{{ns}}.json'
}
}))
.pipe(gulp.dest('assets'));
});
Once you've finished the installation, add this line to your project's Gruntfile:
grunt.loadNpmTasks('i18next-scanner');
In your project's Gruntfile, add a section namedi18next
to the data object passed intogrunt.initConfig()
,like so:
grunt.initConfig({
i18next:{
dev:{
src:'src/**/*.{js,html}',
dest:'assets',
options:{
lngs:['en','de'],
resource:{
loadPath:'assets/i18n/{{lng}}/{{ns}}.json',
savePath:'i18n/{{lng}}/{{ns}}.json'
}
}
}
}
});
There are two ways to use i18next-scanner:
constParser=require('i18next-scanner').Parser;
constparser=newParser(options);
constcode="i18next.t('key');...";
parser.parseFuncFromString(code);
constjsx='<Trans i18nKey= "some.key" >Default text</Trans>';
parser.parseTransFromString(jsx);
consthtml='<div data-i18n= "key" ></div>';
parser.parseAttrFromString(html);
parser.get();
Parse translation key from JS function
parser.parseFuncFromString(content)
parser.parseFuncFromString(content,{list:['_t']});
parser.parseFuncFromString(content,function(key,options){
options.defaultValue=key;// use key as the value
parser.set(key,options);
});
parser.parseFuncFromString(content,{list:['_t']},function(key,options){
parser.set(key,options);// use defaultValue
});
Parse translation key from theTrans component
parser.parseTransFromString(content);
parser.parseTransFromString(context,{component:'Trans',i18nKey:'i18nKey'});
// Uses defaultValue as the fallback key when the i18nKey attribute is missing
parser.parseTransFromString(content,{fallbackKey:true});
parser.parseTransFromString(content,{
fallbackKey:function(ns,value){
// Returns a hash value as the fallback key
returnsha1(value);
}
});
parser.parseTransFromString(content,function(key,options){
options.defaultValue=key;// use key as the value
parser.set(key,options);
});
Parse translation key from HTML attribute
parser.parseAttrFromString(content)
parser.parseAttrFromString(content,{list:['data-i18n']});
parser.parseAttrFromString(content,function(key){
constdefaultValue=key;// use key as the value
parser.set(key,defaultValue);
});
parser.parseAttrFromString(content,{list:['data-i18n']},function(key){
parser.set(key);// use defaultValue
});
Get the value of a translation key or the whole i18n resource store
// Returns the whole i18n resource store
parser.get();
// Returns the resource store with the top-level keys sorted by alphabetical order
parser.get({sort:true});
// Returns a value in fallback language (@see options.fallbackLng) with namespace and key
parser.get('ns:key');
// Returns a value with namespace, key, and lng
parser.get('ns:key',{lng:'en'});
Set a translation key with an optional defaultValue to i18n resource store
// Set a translation key
parser.set(key);
// Set a translation key with default value
parser.set(key,defaultValue);
// Set a translation key with default value using options
parser.set(key,{
defaultValue:defaultValue
});
constscanner=require('i18next-scanner');
scanner.createStream(options,customTransform/* optional */,customFlush/* optional */);
The optionalcustomTransform
function is provided as the 2nd argument for the transform stream API. It must have the following signature:function (file, encoding, done) {}
.A minimal implementation should call thedone()
function to indicate that the transformation is done, even if that transformation means discarding the file.
For example:
constscanner=require('i18next-scanner');
constvfs=require('vinyl-fs');
constcustomTransform=function_transform(file,enc,done){
constparser=this.parser;
constcontent=fs.readFileSync(file.path,enc);
// add your code
done();
};
vfs.src(['/path/to/src'])
.pipe(scanner(options,customTransform))
.pipe(vfs.dest('path/to/dest'));
To parse a translation key, callparser.set(key, defaultValue)
to assign the key with an optionaldefaultValue
.
For example:
constcustomTransform=function_transform(file,enc,done){
constparser=this.parser;
constcontent=fs.readFileSync(file.path,enc);
parser.parseFuncFromString(content,{list:['i18n.t']},function(key){
constdefaultValue='__L10N__';
parser.set(key,defaultValue);
});
done();
};
Alternatively, you may callparser.set(defaultKey, value)
to assign the value with a default key. ThedefaultKey
should be unique string and can never benull
,undefined
,or empty.
For example:
consthash=require('sha1');
constcustomTransform=function_transform(file,enc,done){
constparser=this.parser;
constcontent=fs.readFileSync(file.path,enc);
parser.parseFuncFromString(content,{list:['i18n._']},function(key){
constvalue=key;
constdefaultKey=hash(value);
parser.set(defaultKey,value);
});
done();
};
The optionalcustomFlush
function is provided as the last argument for the transform stream API, it is called just prior to the stream ending. You can implement yourcustomFlush
function to override the defaultflush
function. When everything's done, call thedone()
function to indicate the stream is finished.
For example:
constscanner=require('i18next-scanner');
constvfs=require('vinyl-fs');
constcustomFlush=function_flush(done){
constparser=this.parser;
constresStore=parser.getResourceStore();
// loop over the resStore
Object.keys(resStore).forEach(function(lng){
constnamespaces=resStore[lng];
Object.keys(namespaces).forEach(function(ns){
constobj=namespaces[ns];
// add your code
});
});
done();
};
vfs.src(['/path/to/src'])
.pipe(scanner(options,customTransform,customFlush))
.pipe(vfs.dest('/path/to/dest'));
Below are the configuration options with their default values:
{
compatibilityJSON:'v3',// One of: 'v1', 'v2', 'v3', 'v4
debug:false,
removeUnusedKeys:false,
sort:false,
attr:{
list:['data-i18n'],
extensions:['.html','.htm'],
},
func:{
list:['i18next.t','i18n.t'],
extensions:['.js','.jsx'],
},
trans:{
component:'Trans',
i18nKey:'i18nKey',
defaultsKey:'defaults',
extensions:['.js','.jsx'],
fallbackKey:false,
// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes:true,// Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor:['br','strong','i','p'],// Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.
// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn:{
ecmaVersion:2020,
sourceType:'module',// defaults to 'module'
},
},
lngs:['en'],
ns:['translation'],
defaultLng:'en',
defaultNs:'translation',
defaultValue:'',
resource:{
loadPath:'i18n/{{lng}}/{{ns}}.json',
savePath:'i18n/{{lng}}/{{ns}}.json',
jsonIndent:2,
lineEnding:'\n',
},
nsSeparator:':',
keySeparator:'.',
pluralSeparator:'_',
contextSeparator:'_',
contextDefaultValues:[],
interpolation:{
prefix:'{{',
suffix:'}}',
},
metadata:{},
allowDynamicKeys:false,
}
Type:String
Default:'v3'
ThecompatibilityJSON
version to use for plural suffixes.
Seehttps://www.i18next.com/misc/json-formatfor details.
Type:Boolean
Default:false
Set totrue
to turn on debug output.
Type:Boolean
Default:false
Set totrue
to remove unused translation keys from i18n resource files.
Type:Boolean
Default:false
Set totrue
if you want to sort translation keys in ascending order.
Type:Object
orfalse
If anObject
is supplied, you can either specify a list of attributes and extensions, or override the default.
{// Default
attr:{
list:['data-i18n'],
extensions:['.html','.htm']
}
}
You can set attr tofalse
to disable parsing attribute as below:
{
attr:false
}
Type:Object
orfalse
If anObject
is supplied, you can either specify a list of translation functions and extensions, or override the default.
{// Default
func:{
list:['i18next.t','i18n.t'],
extensions:['.js','.jsx']
}
}
You can set func tofalse
to disable parsing translation function as below:
{
func:false
}
Type:Object
orfalse
If anObject
is supplied, you can specify a list of extensions, or override the default.
{// Default
trans:{
component:'Trans',
i18nKey:'i18nKey',
defaultsKey:'defaults',
extensions:['.js','.jsx'],
fallbackKey:false,
// https://react.i18next.com/latest/trans-component#usage-with-simple-html-elements-like-less-than-br-greater-than-and-others-v10.4.0
supportBasicHtmlNodes:true,// Enables keeping the name of simple nodes (e.g. <br/>) in translations instead of indexed keys.
keepBasicHtmlNodesFor:['br','strong','i','p'],// Which nodes are allowed to be kept in translations during defaultValue generation of <Trans>.
// https://github.com/acornjs/acorn/tree/master/acorn#interface
acorn:{
ecmaVersion:2020,
sourceType:'module',// defaults to 'module'
},
}
}
You can set trans tofalse
to disable parsing Trans component as below:
{
trans:false
}
The fallbackKey can either be a boolean value, or a function like so:
fallbackKey:function(ns,value){
// Returns a hash value as the fallback key
returnsha1(value);
}
You can pass RexExp totrans.component
in case you want to match multiple things:
component:/Trans$/
Type:Array
Default:['en']
An array of supported languages.
Type:String
orArray
Default:['translation']
A namespace string or an array of namespaces.
Type:String
Default:'en'
The default language used for checking default values.
Type:String
Default:'translation'
The default namespace used if not passed to translation function.
Type:String
orFunction
Default:''
The default value used if not passed toparser.set
.
Provides the default value with a string:
{
defaultValue:'__NOT_TRANSLATED__'
}
Provides the default value as a callback function:
{
// @param {string} lng The language currently used.
// @param {string} ns The namespace currently used.
// @param {string} key The translation key.
// @return {string} Returns a default value for the translation key.
defaultValue:function(lng,ns,key){
if(lng==='en'){
// Return key as the default value for English language
returnkey;
}
// Return the string '__NOT_TRANSLATED__' for other languages
return'__NOT_TRANSLATED__';
}
}
Type:Object
Resource options:
{// Default
resource:{
// The path where resources get loaded from. Relative to current working directory.
loadPath:'i18n/{{lng}}/{{ns}}.json',
// The path to store resources. Relative to the path specified by `gulp.dest(path)`.
savePath:'i18n/{{lng}}/{{ns}}.json',
// Specify the number of space characters to use as white space to insert into the output JSON string for readability purpose.
jsonIndent:2,
// Normalize line endings to '\r\n', '\r', '\n', or 'auto' for the current operating system. Defaults to '\n'.
// Aliases: 'CRLF', 'CR', 'LF', 'crlf', 'cr', 'lf'
lineEnding:'\n'
}
}
loadPath
andsavePath
can be both be defined asFunction
with parameterslng
andns
{// Default
resource:{
// The path where resources get loaded from. Relative to current working directory.
loadPath:function(lng,ns){
return'i18n/'+lng+'/'+ns+'.json';
},
// The path to store resources. Relative to the path specified by `gulp.dest(path)`.
savePath:function(lng,ns){
return'i18n/'+lng+'/'+ns+'.json';
},
// Specify the number of space characters to use as white space to insert into the output JSON string for readability purpose.
jsonIndent:2,
// Normalize line endings to '\r\n', '\r', '\n', or 'auto' for the current operating system. Defaults to '\n'.
// Aliases: 'CRLF', 'CR', 'LF', 'crlf', 'cr', 'lf'
lineEnding:'\n'
}
}
Type:String
orfalse
Default:'.'
Key separator used in translation keys.
Set tofalse
to disable key separator if you prefer having keys as the fallback for translation (e.g. gettext). This feature is supported by[email protected].Also seeKey based fallbackathttps://www.i18next.com/principles/fallback#key-fallback.
Type:String
orfalse
Default:':'
Namespace separator used in translation keys.
Set tofalse
to disable namespace separator if you prefer having keys as the fallback for translation (e.g. gettext). This feature is supported by[email protected].Also seeKey based fallbackathttps://www.i18next.com/principles/fallback#key-fallback.
Type:Boolean
orFunction
Default:true
Whether to add context form key.
context:function(lng,ns,key,options){
returntrue;
}
Type:Boolean
Default:true
Whether to add a fallback key as well as the context form key.
Type:String
Default:'_'
The character to split context from key.
Type:Array
Default:[]
A list of default context values, used when the scanner encounters dynamic value as acontext
.
For a list of['male', 'female']
the scanner will generate an entry for each value.
Type:Boolean
orFunction
Default:true
Whether to add plural form key.
plural:function(lng,ns,key,options){
returntrue;
}
Type:Boolean
Default:true
Whether to add a fallback key as well as the plural form key.
Type:String
Default:'_'
The character to split plural from key.
Type:Object
interpolation options
{// Default
interpolation:{
// The prefix for variables
prefix:'{{',
// The suffix for variables
suffix:'}}'
}
}
Type:Object
Default:{}
This can be used to pass any additional information regarding the string.
Type:Boolean
Default:false
This can be used to allow dynamic keys e.g.friend${DynamicValue}
Example Usage:
transform: function customTransform(file, enc, done) {
'use strict';
const parser = this.parser;
const contexts = {
compact: ['compact'],
max: ['Max'],
};
const keys = {
difficulty: { list: ['Normal', 'Hard'] },
minMax: { list: ['Min', 'Max'] },
};
const content = fs.readFileSync(file.path, enc);
parser.parseFuncFromString(content, { list: ['i18next.t', 'i18n.t'] }, (key, options) => {
// Add context based on metadata
if (options.metadata?.context) {
delete options.context;
const context = contexts[options.metadata?.context];
parser.set(key, options);
for (let i = 0; i < context?.length; i++) {
parser.set(`${key}${parser.options.contextSeparator}${context[i]}`, options);
}
}
// Add keys based on metadata (dynamic or otherwise)
if (options.metadata?.keys) {
const list = keys[options.metadata?.keys].list;
for (let i = 0; i < list?.length; i++) {
parser.set(`${key}${list[i]}`, options);
}
}
// Add all other non-metadata related keys
if (!options.metadata) {
parser.set(key, options);
}
});
done();
CheckoutIntegration Guideto learn how to integrate withReact,Gettext Style I18n,andHandlebars.
MIT