diff --git a/README.md b/README.md index 2e4b9ae..b8935f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

💋 FrenchKiss.js

[![Build Status](https://travis-ci.com/koala-interactive/frenchkiss.js.svg?branch=master)](https://travis-ci.com/koala-interactive/frenchkiss.js) -[![File size](https://img.shields.io/badge/GZIP%20size-1028%20B-brightgreen.svg)](./dist/umd/frenchkiss.js) +[![File size](https://img.shields.io/badge/GZIP%20size-1087%20B-brightgreen.svg)](./dist/umd/frenchkiss.js) ![](https://img.shields.io/badge/dependencies-none-brightgreen.svg) ![](https://img.shields.io/snyk/vulnerabilities/github/koala-interactive/frenchkiss.js.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) @@ -57,6 +57,7 @@ Or install using [npm](https://npmjs.org): - [frenchkiss.unset()](#frenchkiss.unsetlanguage-string) - [frenchkiss.fallback()](#frenchkissfallbacklanguage-string-string) - [frenchkiss.onMissingKey()](#frenchkissonMissingKeyfn-Function) +- [frenchkiss.onMissingVariable()](#frenchkissonMissingVariablefn-Function) - [SELECT expression](#select-expression) - [PLURAL expression](#plural-expression) - [Plural category](#plural-category) @@ -215,6 +216,31 @@ frenchkiss.t('missingkey'); // => 'An error happened (missingkey)' --- +### frenchkiss.onMissingVariable(fn: Function) + +It's possible to handle missing variables, sending errors to your monitoring server or handle it directly by returning something to replace with. + +```js +frenchkiss.set('en', { + hello: 'Hello {name} !', +}); +frenchkiss.locale('en'); + +frenchkiss.t('hello'); // => 'Hello !' + +frenchkiss.onMissingVariable((variable, key, language) => { + // Send error to your server + sendReport(`Missing the variable "${variable}" in ${language}->${key}.`); + + // Returns the text you want + return `[missing:${variable}]`; +}); + +frenchkiss.t('hello'); // => 'Hello [missing:name] !' +``` + +--- + ### SELECT expression If you need to display different text messages depending on the value of a variable, you need to translate all of those text messages... or you can handle this with a select ICU expression. diff --git a/package.json b/package.json index e86c982..040ae33 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@babel/preset-env": "^7.1.6", "@babel/register": "^7.0.0", "chai": "^4.2.0", + "chai-spies": "^1.0.0", "del-cli": "^1.1.0", "eslint": "^5.12.1", "eslint-config-prettier": "^4.0.0", diff --git a/src/compiler.js b/src/compiler.js index 1226789..25cf739 100644 --- a/src/compiler.js +++ b/src/compiler.js @@ -19,13 +19,22 @@ const escapeText = JSON.stringify; // (text) => '"' + text.replace(/(["\\])/g, ' /** * Helper to bind variable name to value. - * Default to empty string if not defined + * Default to onMissingVariable returns if not defined + * + * Mapping : + * - undefined -> '' + * - null -> '' + * - 0 -> 0 + * - 155 -> 155 + * - 'test' -> 'test' + * - not defined -> onMissingVariable(value, key, language) * * @param {String} text * @returns {String} */ const escapeVariable = text => - '(p["' + text + '"]||(p["' + text + '"]=="0"?0:""))'; + // prettier-ignore + '(p["' + text + '"]||(p["' + text + '"]=="0"?0:"' + text + '" in p?"":v("' + text + '",k,l)))'; /** * Compile the translation to executable optimized function @@ -48,8 +57,11 @@ export function compileCode(text) { } return new Function( - 'a', - 'f', + 'a', // params + 'f', // plural category function + 'k', // key + 'l', // language + 'v', // missingVariableHandler 'var p=a||{}' + (size ? ',m=f?{' + pluralCode + '}:{}' : '') + ';return ' + diff --git a/src/frenchkiss.js b/src/frenchkiss.js index 8521816..671bafd 100644 --- a/src/frenchkiss.js +++ b/src/frenchkiss.js @@ -18,6 +18,17 @@ let _fallback = ''; */ let missingKeyHandler = key => key; +/** + * Default function used in case of missing variable + * Returns the value you want + * + * @param {String} variable + * @param {String} key + * @param {String} language + * @returns {String} + */ +let missingVariableHandler = () => ''; + /** * Get back a translation and returns the optimized function * Store the function in the cache to re-use it @@ -54,17 +65,27 @@ export const t = (key, params, language) => { let fn, lang = language || _locale; + // Try to get the specified or locale if (lang) { fn = getCompiledCode(key, lang); + + if (fn) { + return fn(params, _plural[lang], key, lang, missingVariableHandler); + } } + lang = _fallback; + // Try to get the fallback language - if (!fn && _fallback) { - lang = _fallback; + if (lang) { fn = getCompiledCode(key, lang); + + if (fn) { + return fn(params, _plural[lang], key, lang, missingVariableHandler); + } } - return fn ? fn(params, _plural[lang]) : missingKeyHandler(key); + return missingKeyHandler(key); }; /** @@ -78,6 +99,17 @@ export const onMissingKey = fn => { missingKeyHandler = fn; }; +/** + * Set a function to handle missing variable to: + * - Returns the value you want + * - Report the poblem to your server + * + * @param {Function} fn + */ +export const onMissingVariable = fn => { + missingVariableHandler = fn; +}; + /** * Getter/setter for locale * @@ -171,6 +203,7 @@ export default { store, t, onMissingKey, + onMissingVariable, locale, fallback, set, diff --git a/test/index.js b/test/index.js index 657541b..2a13e97 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,9 @@ -import { expect } from 'chai'; +import chai, { expect } from 'chai'; +import spies from 'chai-spies'; import i18n from '../src/frenchkiss'; +chai.use(spies); + describe('locale', () => { it('should not bug if no locale', () => { expect(i18n.t('test')).to.equal('test'); @@ -237,6 +240,7 @@ describe('t', () => { describe('onMissingKey', () => { beforeEach(() => { i18n.locale('en'); + i18n.fallback('xyz'); }); afterEach(() => { @@ -247,6 +251,15 @@ describe('onMissingKey', () => { expect(i18n.t('bogus_key')).to.equal('bogus_key'); }); + it('is called with key', () => { + const fn = chai.spy(() => ''); + + i18n.onMissingKey(fn); + i18n.t('bogus_key'); + + expect(fn).to.have.been.called.with('bogus_key'); + }); + it('replace the key with something custom when not found', () => { i18n.onMissingKey(key => 'missing:' + key); expect(i18n.t('bogus_key')).to.equal('missing:bogus_key'); @@ -261,6 +274,42 @@ describe('onMissingKey', () => { }); }); +describe('onMissingVariable', () => { + beforeEach(() => { + i18n.locale('en'); + i18n.set('en', { + test: 'Test {value} !', + }); + }); + + afterEach(() => { + i18n.onMissingVariable(() => ''); + }); + + it('returns empty string if variable not found', () => { + expect(i18n.t('test')).to.equal('Test !'); + }); + + it('returns empty string if variable not found', () => { + i18n.onMissingVariable(value => `[${value}]`); + expect(i18n.t('test', { value: '' })).to.equal('Test !'); + }); + + it('call onMissingVariable with parameters', () => { + const fn = chai.spy(() => ''); + + i18n.onMissingVariable(fn); + i18n.t('test'); + + expect(fn).to.have.been.called.with('value', 'test', 'en'); + }); + + it('replace the variable with something custom when not found', () => { + i18n.onMissingVariable(value => `[${value}]`); + expect(i18n.t('test')).to.equal('Test [value] !'); + }); +}); + describe('set', () => { beforeEach(() => { i18n.locale('en');