mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -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/core": "4.6.1",
|
||||||
"@material-ui/icons": "4.5.1",
|
"@material-ui/icons": "4.5.1",
|
||||||
"@material-ui/styles": "4.6.0",
|
"@material-ui/styles": "4.6.0",
|
||||||
|
"@nebula.js/locale": "0.1.0-alpha.25",
|
||||||
"@nebula.js/supernova": "0.1.0-alpha.25",
|
"@nebula.js/supernova": "0.1.0-alpha.25",
|
||||||
"@nebula.js/theme": "0.1.0-alpha.25",
|
"@nebula.js/theme": "0.1.0-alpha.25",
|
||||||
"@nebula.js/ui": "0.1.0-alpha.25",
|
"@nebula.js/ui": "0.1.0-alpha.25",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint no-underscore-dangle:0 */
|
/* eslint no-underscore-dangle:0 */
|
||||||
import 'regenerator-runtime/runtime'; // Polyfill for using async/await
|
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 appThemeFn from './app-theme';
|
||||||
|
|
||||||
import { createAppSelectionAPI } from './selections';
|
import { createAppSelectionAPI } from './selections';
|
||||||
@@ -79,12 +79,12 @@ const mergeConfigs = (base, c) => ({
|
|||||||
|
|
||||||
function nuked(configuration = {}, prev = {}) {
|
function nuked(configuration = {}, prev = {}) {
|
||||||
const logger = loggerFn(configuration.log);
|
const logger = loggerFn(configuration.log);
|
||||||
const locale = localeFn(configuration.locale);
|
const locale = appLocaleFn(configuration.locale);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
env: {
|
env: {
|
||||||
Promise,
|
Promise,
|
||||||
translator: locale.translator(),
|
translator: locale.translator,
|
||||||
nucleus, // eslint-disable-line no-use-before-define
|
nucleus, // eslint-disable-line no-use-before-define
|
||||||
},
|
},
|
||||||
load: configuration.load,
|
load: configuration.load,
|
||||||
@@ -125,7 +125,7 @@ function nuked(configuration = {}, prev = {}) {
|
|||||||
|
|
||||||
const root = App({
|
const root = App({
|
||||||
app,
|
app,
|
||||||
translator: locale.translator(),
|
translator: locale.translator,
|
||||||
direction: configuration.direction,
|
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/nucleus': path.resolve(process.cwd(), 'apis/nucleus/src'),
|
||||||
'@nebula.js/supernova': path.resolve(process.cwd(), 'apis/supernova/src'),
|
'@nebula.js/supernova': path.resolve(process.cwd(), 'apis/supernova/src'),
|
||||||
'@nebula.js/theme': path.resolve(process.cwd(), 'apis/theme/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({
|
babel({
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
include: ['/**/apis/nucleus/**', '/**/apis/supernova/**', '/**/apis/theme/**', '/**/packages/ui/**'],
|
include: [
|
||||||
|
'/**/apis/locale/**',
|
||||||
|
'/**/apis/nucleus/**',
|
||||||
|
'/**/apis/supernova/**',
|
||||||
|
'/**/apis/theme/**',
|
||||||
|
'/**/packages/ui/**',
|
||||||
|
],
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
'@babel/preset-env',
|
'@babel/preset-env',
|
||||||
|
|||||||
Reference in New Issue
Block a user