mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 09:48:18 -05:00
feat: add locale module (#193)
This commit is contained in:
26
apis/locale/package.json
Normal file
26
apis/locale/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
80
apis/locale/src/__tests__/translator.spec.js
Normal file
80
apis/locale/src/__tests__/translator.spec.js
Normal 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
12
apis/locale/src/index.js
Normal 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,
|
||||
};
|
||||
}
|
||||
60
apis/locale/src/translator.js
Normal file
60
apis/locale/src/translator.js
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
21
apis/nucleus/src/locale/app-locale.js
Normal file
21
apis/nucleus/src/locale/app-locale.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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'),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user