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');