Skip to content

Commit

Permalink
Merge pull request #16 from koala-interactive/features/nested-keys
Browse files Browse the repository at this point in the history
✨  Introduce support for nested keys
  • Loading branch information
vthibault authored Apr 18, 2019
2 parents 144bdf7 + 32dbd0f commit 8a0547a
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .babelrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
presets: ['@babel/env'],
presets: [['@babel/env', { loose: true }]],
env: {
esm: {
presets: [['@babel/env', { modules: false }]],
Expand Down
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h1>💋 FrenchKiss.js</h1>

[![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-1076%20B-brightgreen.svg)](./dist/umd/frenchkiss.js)
[![File size](https://img.shields.io/badge/GZIP%20size-1.2%20kB-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)
Expand Down Expand Up @@ -62,6 +62,7 @@ Or install using [npm](https://npmjs.org):
- [frenchkiss.fallback()](#frenchkissfallbacklanguage-string-string)
- [frenchkiss.onMissingKey()](#frenchkissonMissingKeyfn-Function)
- [frenchkiss.onMissingVariable()](#frenchkissonMissingVariablefn-Function)
- [Nested keys](#nested-keys)
- [SELECT expression](#select-expression)
- [PLURAL expression](#plural-expression)
- [Plural category](#plural-category)
Expand All @@ -80,13 +81,17 @@ frenchkiss.locale('en');
// Add translations in each languages
frenchkiss.set('en', {
hello: 'Hello {name} !',
goodbye: 'Bye !',
fruits: {
apple: 'apples'
},
// and other sentences...
});

frenchkiss.t('hello', {
name: 'John',
}); // => 'Hello John !'

frenchkiss.t('fruits.apple'); // => 'apples'
```

---
Expand Down Expand Up @@ -123,7 +128,7 @@ Here is what you should know about it :
- ✅ It supports `PLURAL`.
- ✅ It supports `SELECT`.
- ✅ It supports nested `PLURAL`, `SELECT` and `variables`.
- It does not support nested keys _(to keep it fast)_.
- It supports nested keys (using dots in keys).
- ❌ It does not support date, number, currency formatting (maybe check for [Intl.NumberFormat](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/NumberFormat) and [Intl.DateTimeFormat](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/DateTimeFormat)).

```js
Expand Down Expand Up @@ -245,6 +250,54 @@ frenchkiss.t('hello'); // => 'Hello [missing:name] !'

---

### Nested keys

Under the hood, frenchkiss allows you to handle nested keys, by using `'.'` inside key names.

```js
frenchkiss.set('en', {
fruits: {
apple: 'An apple',
banana: 'A banana'
},
vegetables: {
carrot: 'A carrot',
daikon: 'A daikon'
}
});

frenchkiss.t('fruits.apple') // => 'An apple'
```

Accessing an object directly will result on the `onMissingKey` method to be called:

```js
frenchkiss.set('en', {
fruits: {
apple: 'An apple',
banana: 'A banana'
}
});

frenchkiss.onMissingKey(key => `[notfound:${key}]`);
frenchkiss.t('fruits'); // => '[notfound:fruits]'
```

In case of duplicate names on key and objects, the result will always prioritize the key value.

```js
frenchkiss.set('en', {
'fruits.apple': '(linear) apple'
fruits: {
apple: '(nested) apple'
}
});

frenchkiss.t('fruits.apple'); // => '(linear) apple'
```

---

### 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.
Expand Down
8 changes: 2 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ declare module 'frenchkiss' {
) => string;

interface StoreData {
[key: string]: string | number;
[key: string]: string | number | StoreData;
}

interface CacheData {
Expand All @@ -21,16 +21,12 @@ declare module 'frenchkiss' {
) => string;
}

interface StoreItems {
[key: string]: StoreData;
}

interface CacheItems {
[key: string]: CacheData;
}

export const cache: CacheItems;
export const store: StoreItems;
export const store: StoreData;
export function t(key: string, params?: object, language?: string): string;
export function onMissingKey(
missingKeyHandler: (key: string) => string
Expand Down
86 changes: 79 additions & 7 deletions src/frenchkiss.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,51 @@ let missingKeyHandler = key => key;
let missingVariableHandler = () => '';

/**
* Get back a translation and returns the optimized function
* Store the function in the cache to re-use it
* Get compiled code from cache or ask to generate it
*
* @param {String} key
* @param {String} language
* @returns {Function|null}
*/
const getCompiledCode = (key, language) =>
(cache[language] && cache[language][key]) ||
(store[language] &&
typeof store[language][key] === 'string' &&
(cache[language][key] = compileCode(store[language][key])));
generateCompiledCode(key, language);

/**
* Get back the translation key, compile the code and
* store it into the cache
*
* @param {String} key
* @param {String} language
* @returns {Function|null}
*/
const generateCompiledCode = (key, language) => {
let value = store[language];

if (value) {
// Direct assignment before resolving nested keys
if (typeof value[key] === 'string') {
value = value[key];
} else {
const keys = key.split('.');
const count = keys.length;

for (let i = 0; i < count; ++i) {
if (!value.hasOwnProperty(keys[i])) {
return null;
}

value = value[keys[i]];
}
}

if (typeof value === 'string') {
return (cache[language][key] = compileCode(value));
}
}

return null;
};

/**
* Get back translation and interpolate values stored in 'params' parameter
Expand Down Expand Up @@ -164,13 +197,52 @@ export const extend = (language, table) => {
cache[language] = {};
}

extendStoreRecursive(store[language], table, cache[language], '');
};

/**
* Helper to extends store recursively
*
* @param {Object} store
* @param {Object} table
* @param {Object} cache
* @param {String} prefix
* @internal
*/
const extendStoreRecursive = (store, table, cache, prefix) => {
const keys = Object.keys(table);
const count = keys.length;

for (let i = 0; i < count; ++i) {
const key = keys[i];
store[language][key] = table[key];
delete cache[language][key];
const cacheKey = prefix ? prefix + '.' + key : key;

if (typeof table[key] === 'object') {
if (typeof store[key] !== 'object') {
store[key] = {};
}

extendStoreRecursive(store[key], table[key], cache, cacheKey);
} else {
delete cache[cacheKey];

// If store is an object, we need to erase all cached functions matching /key\..*/
if (typeof store[key] === 'object') {
const cacheMatch = cacheKey + '.';
const cacheLength = cacheMatch.length;
const keys = Object.keys(cache);
const count = keys.length;

for (let i = 0; i < count; ++i) {
const key = keys[i];
if (key.substr(0, cacheLength) === cacheMatch) {
delete cache[key];
}
}
}

store[key] = table[key];
}
}
};

Expand Down
Loading

0 comments on commit 8a0547a

Please sign in to comment.