Skip to content

Commit

Permalink
Introduce a child process - namespaced instance (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
omrilotan authored Sep 18, 2017
1 parent 4843732 commit 6afae75
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 14 deletions.
74 changes: 68 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const MISSING = typeof Symbol === 'function' ? Symbol() : '_missing';
/**
* @class I18n
* @classdesc an object capable of translating keys and interpolate using given data object
* @param {Object} options.translations JSON compliant object
* @param {String} [options.$scope] Root string to be use for looking for translation keys
* @param {Function} [options.missing] Method to call when key is not found
* @param {Object<Object>} options.translations JSON compliant object
* @param {Object<String>} [options.$scope] Root string to be use for looking for translation keys
* @param {Object<Function>} [options.missing] Method to call when key is not found
*/
module.exports = class I18n {
class I18n {
constructor({translations, $scope, missing} = {translations: {}, $scope: undefined, missing: undefined}) {
this[TRANSLATIONS] = freeze(jsonclone(translations));
this[MISSING] = missing || (() => {});
Expand Down Expand Up @@ -57,7 +57,7 @@ module.exports = class I18n {

/**
* Add translation object(s)
* @param {[type]} translations [description]
* @param {Object(s)} translations [description]
* @return {self}
*/
add(...args) {
Expand Down Expand Up @@ -116,6 +116,15 @@ module.exports = class I18n {
return done ? result : this.find(...alternatives);
}

/**
* Spawns a scoped child
* @param {String} scope Namespace
* @return {I18nChild} I18nChild instance
*/
spawn(scope) {
return new I18nChild(this, scope);
}

/**
* Make sure you only have one instance of I18n in your global scope
* @return {I18n} the same instance every time
Expand All @@ -126,4 +135,57 @@ module.exports = class I18n {
static get singleton() {
return _global.i18n = _global.i18n || new I18n();
}
};
}

/**
* @class I18nChild
* @extends I18n
* @classdesc A child with the same capabilities and access but which translation keys may be namespcaed
* @param {String} [$scope]
*/
class I18nChild extends I18n {
constructor(parent, $scope) {
super();
const scopeChain = [];

parent.$scope && scopeChain.push(parent.$scope);
$scope && scopeChain.push($scope);

this.$scope = scopeChain.join('.') || undefined;
this.parent = parent;
}

/**
* translations
* @return {Object} parent's translations object
*/
get translations() {
return this.parent.translations;
}

/**
* Passes the translations to the parent's store under the namespace
* @param {...Object} args Translation objects
*/
add(...args) {
if (this.$scope) {
this.parent.add(...args.map((arg) => {
const base = {};

this.$scope.split('.').reduce((base, item, index, array) => {
base[item] = index === array.length - 1 ? arg : {};

return base[item];
}, base);

return base;
}))
} else {
this.parent.add(...args);
}

return this;
}
}

module.exports = I18n;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fiverr/i18n",
"version": "1.2.0",
"version": "1.3.0",
"description": "Translation helper",
"author": "Fiverr dev team",
"license": "MIT",
Expand Down
29 changes: 22 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const i18n = new I18n({translations});
const i18n = new I18n({
translations: {...},
missing: key => logMissingKeyEvent({key: `missing_translation.${key.replace(/\W/g, '_')}`}),
$scope: 'my_app.page_name'
$scope: 'my_app.en'
});
```

Expand All @@ -52,7 +52,7 @@ const i18n = new I18n({
my: { string: 'a dynamic %{thing} in a static string' }
}
});
i18n.translate('my.string', {thing: 'value'}); // a dynamic value in a static string
i18n.t('my.string', {thing: 'value'}); // a dynamic value in a static string
```

### One/other
Expand All @@ -65,9 +65,9 @@ const i18n = new I18n({
}
}
});
i18n.translate('it_will_take_me_days', {count: 1}); // It'll take me one day
i18n.translate('it_will_take_me_days', {count: 3}); // It'll take me 3 days
i18n.translate('it_will_take_me_days', {count: 'a lot of'}); // It'll take me a lot of days
i18n.t('it_will_take_me_days', {count: 1}); // It'll take me one day
i18n.t('it_will_take_me_days', {count: 3}); // It'll take me 3 days
i18n.t('it_will_take_me_days', {count: 'a lot of'}); // It'll take me a lot of days
```

### Instance with a scope
Expand All @@ -85,7 +85,7 @@ const i18n = new I18n({
$scope: 'users.get'
});
// Use:
i18n.translate('title', {username: 'Arthur'}); // Arthur's page
i18n.t('title', {username: 'Arthur'}); // Arthur's page

// Single use scope (passed in with data)
const i18n = new I18n({
Expand All @@ -94,7 +94,22 @@ const i18n = new I18n({
}
});
// Use:
i18n.translate('title', {username: 'Arthur', $scope: 'users.get'}); // Arthur's page
i18n.t('title', {username: 'Arthur', $scope: 'users.get'}); // Arthur's page
```

### Scoped child instance
This is a good option for shorthand in enclosed parts of the application.

The translation store is shared so the parent can find the keys if it prefixes the namespace, and the child doesn't need to.
```javascript
const usersI18n = i18n.spawn('users.get');

// Add translations under the scope
usersI18n.add({introduction: 'Hi, my name is %{username}'});

// Use translations
usersI18n.t('introduction', {username: 'Martin'}); // Hi, my name is Martin
i18n.t('users.get.introduction', {username: 'Martin'}); // Hi, my name is Martin
```

### Singleton
Expand Down
47 changes: 47 additions & 0 deletions tests/child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const {expect} = require('chai');

const I18n = require('../');
const translations = require('./translations-stub.json');

describe('child instances', () => {
const i18n = new I18n({translations});
const child = i18n.spawn('controller_name.action_name');

it('Can spawn a child with no scope', () => {
const orphan = i18n.spawn();
expect(orphan.t('root.user.name')).to.equal('Martin');
});

it('Child finds namespaced translations', () => {
expect(child.t('i.am.in.scope')).to.equal('I am in scope');
});

it('Parent does not find namespaced translations', () => {
expect(i18n.t('i.am.in.scope')).to.equal('scope');
});

it('Child finds top level translations', () => {
expect(child.t('root.user.name')).to.equal('Martin');
});

it('Child adds translations to the parent\'s store under a namespace', () => {
child.add({nonsense: {words: 'Non sense words'}});
child.add({introduction: 'Hi, my name is %{username}'});

expect(child.t('introduction', {username: 'Martin'})).to.equal('Hi, my name is Martin');
expect(i18n.t('controller_name.action_name.introduction', {username: 'Martin'})).to.equal('Hi, my name is Martin');
});

it('Child\'s $scope is an approachable attribute', () => {
child.$scope = 'another_controller_name.action_name';
expect(child.t('i.am.in.scope')).to.equal('I am in a different scope');
});

it('Child\'s scope in nested under parent\'s scope (when applicable)', () => {
const i18n = new I18n({translations, $scope: 'en'});
const child = i18n.spawn('page');

expect(i18n.t('title')).to.equal('My App');
expect(child.t('title')).to.equal('My Page');
});
});
6 changes: 6 additions & 0 deletions tests/translations-stub.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,11 @@
}
}
}
},
"en": {
"title": "My App",
"page": {
"title": "My Page"
}
}
}

0 comments on commit 6afae75

Please sign in to comment.