feat: add locale module (#193)

This commit is contained in:
Miralem Drek
2019-11-27 16:58:44 +01:00
committed by GitHub
parent a0af5278ef
commit ac49af32ad
11 changed files with 212 additions and 95 deletions

26
apis/locale/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@nebula.js/locale",
"version": "0.1.0-alpha.25",
"description": "",
"license": "MIT",
"author": "QlikTech International AB",
"keywords": [],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/qlik-oss/nebula.js.git"
},
"main": "dist/locale.js",
"module": "dist/locale.esm.js",
"files": [
"dist"
],
"scripts": {
"build": "cross-env NODE_ENV=production rollup --config ../../rollup.config.js",
"build:dev": "rollup --config ../../rollup.config.js",
"build:watch": "rollup --config ../../rollup.config.js -w",
"prepublishOnly": "rm -rf dist && yarn run build"
}
}

View File

@@ -0,0 +1,80 @@
import translatorFn from '../translator';
describe('translator', () => {
beforeEach(() => {});
it('should prefer en-US by default', () => {
const t = translatorFn({
fallback: 'b',
});
t.add({
id: 'x',
locale: {
'en-US': 'us',
},
});
expect(t.get('x')).to.equal('us');
});
it('should prefer initial locale', () => {
const t = translatorFn({ initial: 'sv-SE' });
t.add({
id: 'x',
locale: {
'sv-SE': 'sv',
},
});
expect(t.get('x')).to.equal('sv');
});
it('should fallback to en-US by default', () => {
const t = translatorFn({ initial: 'sv-SE' });
t.add({
id: 'x',
locale: {
'en-US': 'us',
},
});
expect(t.get('x')).to.equal('us');
});
it('should fallback to sv-SE', () => {
const t = translatorFn({
fallback: 'sv-SE',
});
t.add({
id: 'x',
locale: {
a: 'AA',
'sv-SE': 'sv',
},
});
expect(t.get('x')).to.equal('sv');
});
it('should return string id when not registered', () => {
const t = translatorFn();
expect(t.get('x')).to.equal('x');
});
it('should format strings with args', () => {
const t = translatorFn();
t.add({
id: 'x',
locale: {
'en-US': 'hello {0} {1}',
},
});
expect(t.get('x', ['a', 'b'])).to.equal('hello a b');
});
});

12
apis/locale/src/index.js Normal file
View File

@@ -0,0 +1,12 @@
import translator from './translator';
export default function locale({ initial = 'en-US', fallback = 'en-US' }) {
const t = translator({
initial,
fallback,
});
return {
translator: t,
};
}

View File

@@ -0,0 +1,60 @@
const format = (message = '', args = []) => {
const arr = typeof args === 'string' || typeof args === 'number' ? [args] : args;
return message.replace(/\{(\d+)\}/g, (match, number) => (typeof arr[number] !== 'undefined' ? arr[number] : match));
};
export default function translator({ initial = 'en-US', fallback = 'en-US' } = {}) {
const dictionaries = {};
const currentLocale = initial;
/**
* @interface Translator
*/
const api = {
/**
* Register a string in multiple locales
* @param {object} item
* @param {string} item.id
* @param {object<string,string>} item.locale
* @example
* translator.add({
* id: 'company.hello_user',
* locale: {
* 'en-US': 'Hello {0}',
* 'sv-SE': 'Hej {0}
* }
* });
* translator.get('company.hello_user', ['John']); // Hello John
*/
add: item => {
// TODO - disallow override?
const { id, locale } = item;
Object.keys(locale).forEach(lang => {
if (!dictionaries[lang]) {
dictionaries[lang] = {};
}
dictionaries[lang][id] = locale[lang];
});
},
/**
* Translate string for current locale
* @param {string} str - Id of the registered string
* @param {string[]} [args] - Values passed down for string interpolation
*/
get(str, args) {
let v;
if (dictionaries[currentLocale] && typeof dictionaries[currentLocale][str] !== 'undefined') {
v = dictionaries[currentLocale][str];
} else if (dictionaries[fallback] && typeof dictionaries[fallback][str] !== 'undefined') {
v = dictionaries[fallback][str];
} else {
v = str;
}
return typeof args !== 'undefined' ? format(v, args) : v;
},
};
return api;
}

View File

@@ -35,6 +35,7 @@
"@material-ui/core": "4.6.1",
"@material-ui/icons": "4.5.1",
"@material-ui/styles": "4.6.0",
"@nebula.js/locale": "0.1.0-alpha.25",
"@nebula.js/supernova": "0.1.0-alpha.25",
"@nebula.js/theme": "0.1.0-alpha.25",
"@nebula.js/ui": "0.1.0-alpha.25",

View File

@@ -1,7 +1,7 @@
/* eslint no-underscore-dangle:0 */
import 'regenerator-runtime/runtime'; // Polyfill for using async/await
import localeFn from './locale';
import appLocaleFn from './locale/app-locale';
import appThemeFn from './app-theme';
import { createAppSelectionAPI } from './selections';
@@ -79,12 +79,12 @@ const mergeConfigs = (base, c) => ({
function nuked(configuration = {}, prev = {}) {
const logger = loggerFn(configuration.log);
const locale = localeFn(configuration.locale);
const locale = appLocaleFn(configuration.locale);
const config = {
env: {
Promise,
translator: locale.translator(),
translator: locale.translator,
nucleus, // eslint-disable-line no-use-before-define
},
load: configuration.load,
@@ -125,7 +125,7 @@ function nuked(configuration = {}, prev = {}) {
const root = App({
app,
translator: locale.translator(),
translator: locale.translator,
direction: configuration.direction,
});

View File

@@ -0,0 +1,21 @@
import localeFn from '@nebula.js/locale';
import en from './translations/en-US';
export default function appLocaleFn({ language }) {
const l = localeFn({
initial: language,
});
Object.keys(en).forEach(id => {
l.translator.add({
id,
locale: {
'en-US': en[id],
},
});
});
return {
translator: l.translator,
};
}

View File

@@ -1,35 +0,0 @@
import translatorFn from './translator';
import enUs from './translations/en-US';
const DEFAULT_LOCALE = 'en-US';
const SUPPORTED_LANGUAGES = [
{
info: {
short: 'en',
long: 'en-US',
label: 'English',
translatedLabel: 'Common.English',
},
data: enUs,
},
];
export default function locale({ language } = {}) {
const translator = translatorFn();
const api = {
locale(lang) {
translator.setLanguage(translator.getLongCode(lang));
return api;
},
translator: () => translator.api,
};
SUPPORTED_LANGUAGES.forEach(d => translator.addLanguage(d.info, d.data));
api.locale(language || DEFAULT_LOCALE);
return api;
}

View File

@@ -1,55 +0,0 @@
const DEFAULT_LANGUAGE = 'en-US';
const format = (message = '', args = []) => {
const arr = typeof args === 'string' || typeof args === 'number' ? [args] : args;
return message.replace(/\{(\d+)\}/g, (match, number) => (typeof arr[number] !== 'undefined' ? arr[number] : match));
};
const getLongLanguageCode = (lang, list) => {
const code =
list.filter(item => item.long.toLowerCase() === lang.toLowerCase())[0] ||
list.filter(item => item.short.toLowerCase() === lang.toLowerCase())[0];
if (!code) {
console.warn(`Language '${lang}' not supported, falling back to ${DEFAULT_LANGUAGE}`);
return DEFAULT_LANGUAGE;
}
return code.long;
};
export default function translator() {
const dictionaries = {};
const languageList = [];
let currentLocale = null;
const api = {
locale: () => currentLocale,
append(data, loc) {
const lang = getLongLanguageCode(loc || currentLocale, languageList);
if (!dictionaries[lang]) {
dictionaries[lang] = data;
} else {
Object.assign(dictionaries[lang], data);
}
},
get(str, args) {
const v = typeof dictionaries[currentLocale][str] !== 'undefined' ? dictionaries[currentLocale][str] : str;
return typeof args !== 'undefined' ? format(v, args) : v;
},
};
return {
setLanguage: code => {
currentLocale = code;
// TODO - emit change?
},
addLanguage: (descr, data) => {
languageList.push(descr);
api.append(data, descr.long);
},
getLongCode: lang => getLongLanguageCode(lang, languageList),
api,
};
}

View File

@@ -32,6 +32,7 @@ const cfg = ({ srcDir, distDir, dev = false }) => {
'@nebula.js/nucleus': path.resolve(process.cwd(), 'apis/nucleus/src'),
'@nebula.js/supernova': path.resolve(process.cwd(), 'apis/supernova/src'),
'@nebula.js/theme': path.resolve(process.cwd(), 'apis/theme/src'),
'@nebula.js/locale': path.resolve(process.cwd(), 'apis/locale/src'),
}
: {}),
},

View File

@@ -129,7 +129,13 @@ const config = isEsm => {
}),
babel({
babelrc: false,
include: ['/**/apis/nucleus/**', '/**/apis/supernova/**', '/**/apis/theme/**', '/**/packages/ui/**'],
include: [
'/**/apis/locale/**',
'/**/apis/nucleus/**',
'/**/apis/supernova/**',
'/**/apis/theme/**',
'/**/packages/ui/**',
],
presets: [
[
'@babel/preset-env',