Skip to content

Commit

Permalink
Merge branch 'release/0.10.0' into npm
Browse files Browse the repository at this point in the history
  • Loading branch information
mashpie committed May 24, 2020
2 parents 80251c2 + 36e858e commit 3486c89
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 33 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github: "mashpie"
tidelift: "npm/i18n"
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Stores language files in json files compatible to [webtranslateit](http://webtra
Adds new strings on-the-fly when first used in your app.
No extra parsing needed.

[![Travis][travis-image]][travis-url]
[![Travis][travis-image]][travis-url]
[![Test Coverage][coveralls-image]][coveralls-url]
[![NPM version][npm-image]][npm-url]
![npm](https://img.shields.io/npm/dw/i18n)
Expand Down Expand Up @@ -192,6 +192,11 @@ i18n.configure({
console.log('error', msg);
},

// used to alter the behaviour of missing keys
missingKeyFn: function (locale, value) {
return value;
},

// object or [obj1, obj2] to bind the i18n api and current locale to - defaults to null
register: global,

Expand All @@ -205,7 +210,13 @@ i18n.configure({
// Downcase locale when passed on queryParam; e.g. lang=en-US becomes
// en-us. When set to false, the queryParam value will be used as passed;
// e.g. lang=en-US remains en-US.
preserveLegacyCase: true
preserveLegacyCase: true,

// set the language catalog statically
// also overrides locales
staticCatalog: {
de: { /* require('de.json') */ },
}
});
```

Expand Down Expand Up @@ -247,6 +258,28 @@ i18n.setLocale('de');
__('Hello'); // --> Hallo`
```

#### Some words on `staticCatalog` option

Instead of letting i18n load translations from a given directory you may pass translations as static js object right on configuration. This supports any method that returns a `key:value` translation object (`{ Hello: 'Hallo', Cat: 'Katze' }`). So you might even mix native **json** with **js** modules and parsed **yaml** files, like so:

```js
// DEMO: quickly add yaml support
const yaml = require('js-yaml');
const fs = require('fs');

// configure and load translations from different locations
i18n.configure({
staticCatalog: {
de: require('../../locales/de-as-json.json'),
en: require('../../locales/en-as-module.js'),
fr: yaml.safeLoad(fs.readFileSync('../../locales/fr-as-yaml.yml', 'utf8'));
},
defaultLocale: 'de'
})
```

**NOTE:** Enabling `staticCatalog` disables all other fs realated options such as `updateFiles`, `autoReload` and `syncFiles`

### i18n.init()

When used as middleware in frameworks like express to setup the current environment for each loop. In contrast to configure the `i18n.init()` should be called within each request-response-cycle.
Expand Down Expand Up @@ -346,7 +379,7 @@ __n('%s cat', 3) // --> 3 Katzen
// long syntax works fine in combination with `updateFiles`
// --> writes '%s cat' to `one` and '%s cats' to `other` plurals
// "one" (singular) & "other" (plural) just covers the basic Germanic Rule#1 correctly.
// "one" (singular) & "other" (plural) just covers the basic Germanic Rule#1 correctly.
__n("%s cat", "%s cats", 1); // 1 Katze
__n("%s cat", "%s cats", 3); // 3 Katzen
Expand Down Expand Up @@ -661,6 +694,7 @@ which puts

You may also use [mustache](http://mustache.github.io/) syntax for your message strings. To pass named parameters to your message, just provide an object as the last parameter. You can still pass unnamed parameters by adding additional arguments.


```js
var greeting = __('Hello {{name}}, how are you today?', { name: 'Marcus' });
```
Expand All @@ -679,6 +713,24 @@ var greeting = __( __('Hello {{name}}, how was your %s?', { name: 'Marcus' }), _

which both put *Hello Marcus, how was your weekend.*

#### how about markup?

Including markup in translation and/or variables is considered to be bad practice, as it leads to side effects (translators need to understand it, might break it, inject malformed markup or worse). But well, mustache supports unescaped markup out-of-the-box (*Quote from https://mustache.github.io/mustache.5.html*):

> All variables are HTML escaped by default. If you want to return unescaped HTML, use the triple mustache: {{{name}}}.

So this will work

```js
var greeting = __('Hello {{{name}}}, how are you today?', { name: '<u>Marcus</u>' });
```

as expected:

```html
Hello <u>Marcus</u>, how are you today
```

### basic plural support

two different plural forms are supported as response to `count`:
Expand Down
34 changes: 29 additions & 5 deletions i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ module.exports = (function() {
queryParameter,
register,
updateFiles,
syncFiles;
syncFiles,
missingKeyFn;

// public exports
var i18n = {};
Expand Down Expand Up @@ -146,15 +147,29 @@ module.exports = (function() {
preserveLegacyCase = (typeof opt.preserveLegacyCase === 'undefined') ?
true : opt.preserveLegacyCase;

// setting custom missing key function
missingKeyFn = (typeof opt.missingKeyFn === 'function') ? opt.missingKeyFn : missingKey;

// when missing locales we try to guess that from directory
opt.locales = opt.locales || guessLocales(directory);
opt.locales = opt.staticCatalog ? Object.keys(opt.staticCatalog) : opt.locales || guessLocales(directory);

// some options should be disabled when using staticCatalog
if(opt.staticCatalog){
updateFiles = false;
autoReload = false;
syncFiles = false;
}

// implicitly read all locales
if (Array.isArray(opt.locales)) {

opt.locales.forEach(function(l) {
read(l);
});
if (opt.staticCatalog) {
locales = opt.staticCatalog;
} else {
opt.locales.forEach(function(l) {
read(l);
});
}

// auto reload locale files when changed
if (autoReload) {
Expand Down Expand Up @@ -1060,12 +1075,14 @@ module.exports = (function() {
// invoke the search again, but allow branching
// this time (because here the mutator is being invoked)
// otherwise, just change the value directly.
value = missingKeyFn(locale, value);
return (reTraverse) ? localeMutator(locale, singular, true)(value) : accessor(value);
};

} else {
// No object notation, just return a mutator that performs array lookup and changes the value.
return function(value) {
value = missingKeyFn(locale, value);
locales[locale][singular] = value;
return value;
};
Expand Down Expand Up @@ -1187,6 +1204,13 @@ module.exports = (function() {
logErrorFn(msg);
}

/**
* Missing key function
*/
function missingKey(locale, value) {
return value;
}

return i18n;

}());
3 changes: 2 additions & 1 deletion locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
},
"ordered arguments": "%1$s then %2$s",
"ordered arguments with numbers": "%1$s then %2$d then %3$.2f",
"Hallo Marcus, wie war dein %s?": "Hallo Marcus, wie war dein %s?"
"Hallo Marcus, wie war dein %s?": "Hallo Marcus, wie war dein %s?",
"Hello {{{name}}}": "Hallo {{{name}}}"
}
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,6 @@
"repeated argument": "%1$s, %1$s, %1$s",
". is first character": "Dot is first character",
"last character is .": "last character is Dot",
"few sentences. with .": "few sentences with Dot"
"few sentences. with .": "few sentences with Dot",
"Hello {{{name}}}": "Hello {{{name}}}"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "i18n",
"description": "lightweight translation module with dynamic json storage",
"version": "0.9.1",
"version": "0.10.0",
"homepage": "http://github.com/mashpie/i18n-node",
"repository": {
"type": "git",
Expand Down
4 changes: 4 additions & 0 deletions test/i18n.api.global.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ describe('Module API', function() {
it('should return en translations as expected, using mustached messages', function() {
i18n.setLocale('en');
should.equal(__('Hello {{name}}', { name: 'Marcus' }), 'Hello Marcus');
should.equal(__('Hello {{{name}}}', { name: '<u>Marcus</u>' }), 'Hello <u>Marcus</u>');
should.equal(__('Hello {{name}}', { name: '<u>Marcus</u>' }), 'Hello &lt;u&gt;Marcus&lt;&#x2F;u&gt;');
should.equal(__('Hello {{name}}, how was your %s?', __('weekend'), { name: 'Marcus' }), 'Hello Marcus, how was your weekend?');
});

Expand All @@ -91,6 +93,8 @@ describe('Module API', function() {

// named only
should.equal(__('Hello {{name}}', { name: 'Marcus' }), 'Hallo Marcus');
should.equal(__('Hello {{{name}}}', { name: '<u>Marcus</u>' }), 'Hallo <u>Marcus</u>');
should.equal(__('Hello {{name}}', { name: '<u>Marcus</u>' }), 'Hallo &lt;u&gt;Marcus&lt;&#x2F;u&gt;');

// named + sprintf
should.equal(__('Hello {{name}}, how was your %s?', __('weekend'), { name: 'Marcus' }), 'Hallo Marcus, wie war dein Wochenende?');
Expand Down
32 changes: 32 additions & 0 deletions test/i18n.configureStaticCatalog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
var i18n = require('../i18n'),
should = require("should");

describe('staticCatalog configuration', function() {

it('should take locales from static catalog if set', function(done) {
i18n.configure({
staticCatalog: {
'en': {}
}
});

var expected = ['en'].sort();
should.deepEqual(i18n.getLocales(), expected);

done();
});

it('should use static locale definitions from static catalog if set', function(done) {
i18n.configure({
staticCatalog: {
'en': {}
}
});

var expected = new Object();
should.deepEqual(i18n.getCatalog('en'), expected);

done();
});

});
75 changes: 53 additions & 22 deletions test/i18n.missingPhrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,74 @@ describe('Missing Phrases', function () {
headers: {}
};

beforeEach(function() {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: req,
updateFiles: false,
syncFiles: false
});
});

describe('i18nTranslate', function () {
it('should return the key if the translation does not exist', function(done) {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: req,
updateFiles: false,
syncFiles: false
});

i18n.setLocale(req, 'en').should.equal('en');
req.__('DoesNotExist').should.equal('DoesNotExist');
done();
});
});
});

describe('Global Module API', function () {
beforeEach(function() {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: global,
updateFiles: false,
syncFiles: false
it('should return a custom key if a missing key function is provided', function(done) {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: req,
updateFiles: false,
syncFiles: false,
missingKeyFn: function(locale, value) {
return 'DoesReallyNotExist';
}
});

i18n.setLocale(req, 'en').should.equal('en');
req.__n('DoesNotExist.sss.xxx').should.equal('DoesReallyNotExist');
done();
});
});
});

describe('Global Module API', function () {
describe('i18nTranslate', function () {
it('should return the key if the translation does not exist', function() {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: global,
updateFiles: false,
syncFiles: false
});

i18n.setLocale('en');
should.equal(__('DoesNotExist'), 'DoesNotExist');
});

it('should return a custom key if a missing key function is provided', function() {
i18n.configure({
locales: ['en', 'de', 'en-GB'],
defaultLocale: 'en',
directory: './locales',
register: global,
updateFiles: false,
syncFiles: false,
missingKeyFn: function(locale, value) {
return 'DoesReallyNotExist';
}
});

i18n.setLocale('en');
should.equal(__('DoesNotExist'), 'DoesReallyNotExist');
});
});
});
});

0 comments on commit 3486c89

Please sign in to comment.