i18 can be dynamically loaded from json file

This commit is contained in:
Michal Szczepanski 2020-06-29 11:20:49 +02:00
parent a854bb0eb3
commit 8614375f9c
8 changed files with 263 additions and 101 deletions

View File

@ -10,12 +10,12 @@ npm run build
#### Features #### Features
* jsx syntax * jsx syntax
* i18n * i18n
* event -> command * event -> command
* controller -> view injection
#### TODO #### TODO
merge with this repo merge with this repo
* [ ] history * [ ] history
* [ ] model -> controller to view injection
add example add example
* [ ] http * [ ] http

View File

@ -4,6 +4,5 @@
["@babel/plugin-transform-react-jsx", { ["@babel/plugin-transform-react-jsx", {
"pragma": "huh.eh" "pragma": "huh.eh"
}], }],
["@babel/plugin-proposal-class-properties"]
] ]
} }

View File

@ -0,0 +1,16 @@
{
"data": {
"test.btn": "Click me!",
"language.label": "Language:",
"h1.hello": "Hello",
"alert.hi": {
"txt": "hi {{click.count}}",
"var": [
{
"key": "click.count",
"model": "alert.button"
}
]
}
}
}

View File

@ -0,0 +1,16 @@
{
"data": {
"test.btn": "Kliknij!",
"language.label": "Język:",
"h1.hello": "Witaj",
"alert.hi": {
"txt": "Cześć {{click.count}}",
"var": [
{
"key": "click.count",
"model": "alert.button"
}
]
}
}
}

View File

@ -1,55 +1,87 @@
import huh, {Random, I18, HFacade, HEvent} from '@szczepano/huh' import huh, {Random, I18, HFacade, HEvent, HCtrl, HInjection, HModel} from '@szczepano/huh'
class Hello { // I18n
I18.registerLanguage('en', {
path:'assets/i18/en.x'
});
I18.registerLanguage('pl', {
path:'assets/i18/pl.x'
});
// View
class HelloView {
render() { render() {
return <h1 h-i18="h1.hello">Hello</h1> return <h1 h-i18="h1.hello">Hello</h1>
} }
} }
const AlertHiCmd = () => { class AlertButtonView {
alert(I18.getKey('alert.hi')); render() {
return <button h-i18="test.btn" h-ctrl="alert.button">Click me!</button>
}
}
class LanguageButton {
constructor(language) {
this.language = language
}
render() {
return <button h-ctrl="language.button">{this.language}</button>
}
} }
const start = () => { const start = () => {
const title = new Hello()
const arr = ['w','o','r','l','d'] const arr = ['w','o','r','l','d']
const components = [] const components = [];
console.log(Random.intRange(0, 10))
arr.forEach((el) => { arr.forEach((el) => {
components.push(<span>{el}</span>) components.push(<span>{el}</span>)
}) })
I18.registerLanguage('en', { const langs = []
data: { new Array('en', 'pl').forEach((l, i) => {
'alert.hi': 'hi', langs.push(new LanguageButton(l));
}
})
I18.registerLanguage('pl', {
data: {
'test.btn':'Kliknij!',
'language.label': 'Język:',
'h1.hello': 'Witaj',
'alert.hi': 'Cześć',
}
})
const handleLanguageClick = (e) => {
I18.switchLanguage(e.target.innerText);
}
const btn = <button h-i18="test.btn" onclick={() => dispatchEvent(new HEvent('alert.hi'))}>Click me!</button>
const langs = ['en', 'pl']
langs.forEach((l, i) => {
langs[i] = <button onclick={handleLanguageClick}>{l}</button>
}) })
return (<div> return (<div>
<div> <div>
<label h-i18="language.label">Language:</label>{langs} <label h-i18="language.label">Language:</label>
{langs}
</div> </div>
{title} {new HelloView()}
{components} {components}
<br /> <br />
{btn} {new AlertButtonView()}
{new AlertButtonView()}
</div>) </div>)
} }
document.getElementById('main').appendChild(start())
// model
class AlertButtonModel extends HModel {
constructor () {
super({
'click.count': 0,
})
}
}
// controller
HCtrl.add('language.button', null, [
new HInjection('click', (e) => {
I18.switchLanguage(e.target.innerText);
}),
]);
HCtrl.add('alert.button', new AlertButtonModel(), [
new HInjection('click', () => {
dispatchEvent(new HEvent('alert.hi'));
}),
]);
// command
const AlertHiCmd = () => {
const model = HCtrl.model('alert.button');
const count = model.get('click.count');
model.set('click.count', count + 1);
alert(I18.getKey('alert.hi', model));
}
HFacade.register('alert.hi', AlertHiCmd); HFacade.register('alert.hi', AlertHiCmd);
// hack to assign data to all components I18.loadLanguage('en');
I18.switchLanguage('en'); document.getElementById('main').appendChild(start())

View File

@ -5,18 +5,18 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "parcel index.html" "dev": "parcel index.html assets/**/*",
"build": "parcel build index.html assets/**/*"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.3", "@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-transform-react-jsx": "^7.10.3", "@babel/plugin-transform-react-jsx": "^7.10.3",
"@babel/preset-env": "^7.10.3", "@babel/preset-env": "^7.10.3",
"parcel-bundler": "^1.12.4" "parcel-bundler": "^1.12.4"
}, },
"dependencies": { "dependencies": {
"@szczepano/huh": "0.0.4" "@szczepano/huh": "0.0.3"
} }
} }

221
huh.js
View File

@ -2,9 +2,11 @@
export default class huh { export default class huh {
static eh(tag, data, ...children) { static eh(tag, data, ...children) {
let el = document.createElement(tag); let el = document.createElement(tag);
if(data && 'h-i18' in data) { if(data) {
I18.register(data['h-i18'], el); if('h-i18' in data) I18.register(data['h-i18'], el);
if('h-ctrl' in data) HCtrl.register(data['h-ctrl'], el);
} }
for(let arg in data) { for(let arg in data) {
if(typeof data[arg] == "function") { if(typeof data[arg] == "function") {
el[arg] = data[arg]; el[arg] = data[arg];
@ -78,46 +80,6 @@ export const HTTP = (o) => {
}); });
} }
// Model
let _minstance = null
export class Model {
constructor() {
if(_minstance) {
throw new Error('Model is Singleton')
} else {
this._data = {}
this._pub = Pub.instance()
_minstance = this
}
}
set(key, value) {
this._data[key] = value
this._pub.send(new Sub(`model.${key}`, {type:'set', value}))
}
get(key) {
return this._data[key]
}
update(key, value) {
if(!(key in this._data)) throw new Error(`Invalid key ${key}`)
this._data[key] = value
this._pub.send(new Sub(`model.${key}`, {type:'update', value}))
}
del(key) {
delete this._data[key]
this._pub.send(new Sub(`model.${key}`, {type:'del', value}))
}
static instance() {
if (!_minstance) _minstance = new Model()
return _minstance
}
}
export class Random { export class Random {
static intRange(min, max) { static intRange(min, max) {
min = Math.ceil(min); min = Math.ceil(min);
@ -147,7 +109,7 @@ export class Random {
// Event -> Command pattern // Event -> Command pattern
export class HEvent extends Event{ export class HEvent extends Event {
constructor(type, data) { constructor(type, data) {
super(type); super(type);
this.data = data; this.data = data;
@ -185,11 +147,91 @@ export class HFacade {
_hlisteners[name] = new CmdWrapper(cmd, once, chain); _hlisteners[name] = new CmdWrapper(cmd, once, chain);
addEventListener(name, _hlisteners[name].handler); addEventListener(name, _hlisteners[name].handler);
} }
static initialize(data) {
data.forEach((el) => {
_hlisteners[el.name] = new CmdWrapper(el.cmd, el.once, el.chain);
addEventListener(el.name, _hlisteners[el.name].handler);
})
}
} }
// View -> Model -> Controller
export class HModel {
constructor(predefined) {
this._data = Object.assign({}, predefined);
}
set(key, value) {
this._data[key] = value
}
get(key) {
return this._data[key]
}
update(key, value) {
if(!(key in this._data)) throw new Error(`Invalid key ${key}`)
this._data[key] = value
}
del(key) {
delete this._data[key]
}
}
export class HInjection {
constructor(name, fn) {
this.name = name;
this.fn = fn;
}
}
const _ctrls = {}
class CtrlWrapper {
constructor(name, model, inject) {
this.name = name;
this.model = model;
this.inject = inject;
this.register = (view) => {
this.inject.forEach((el) => {
view.addEventListener(el.name, el.fn);
});
}
}
}
export class HCtrl {
static add(name, model, inject) {
const w = new CtrlWrapper(name, model, inject);
_ctrls[name] = w;
}
static register(name, view) {
_ctrls[name].register(view);
}
static view(name) {
if(!(name in _ctrls)) {
throw Error(`View named : "${name}" not found.`)
}
return _ctrls[name].view;
}
static model(name) {
if(!(name in _ctrls)) {
throw Error(`View named : "${name}" not found.`)
}
return _ctrls[name].model
}
}
// i18n
const _i18comp = {}; const _i18comp = {};
const _i18langs = {}; const _i18langs = {};
let _currentLanguage = null; let _currentLanguage = 'en';
export class I18 { export class I18 {
@ -209,40 +251,97 @@ export class I18 {
throw Error(`Language not found ${lang}`); throw Error(`Language not found ${lang}`);
} }
_currentLanguage = lang; _currentLanguage = lang;
const data = _i18langs[lang].data; const ldata = _i18langs[lang];
if(ldata.data) {
I18.translate(ldata);
} else if (ldata.path) {
I18.getData(lang, ldata.path, I18.translate);
} else {
throw Error(`Invalid language definition for language ${lang}`);
}
}
static loadLanguage(lang) {
const ldata = _i18langs[lang];
return I18.getData(lang, ldata.path);
}
static getData(lang, path, handler) {
return HTTP({
url: path
}).then((resp) => {
const data = JSON.parse(resp.data);
_i18langs[lang].data = data.data;
if(handler) handler(data);
});
}
static translate(lang) {
const data = lang.data;
for(let key in _i18comp) { for(let key in _i18comp) {
if(!(key in data)) { if(!(key in data)) {
console.warn(`Missing translation for language "${lang}" key: "${key}" setting current value`); console.warn(`Missing translation for language "${lang}" key: "${key}" setting current value`);
_i18langs[lang].data[key] = _i18comp[key].innerText;
} else {
I18.t(_i18comp[key], data[key]);
} }
I18.t(_i18comp[key], data[key]);
} }
} }
static register(key, el) { static register(key, el) {
if(key in _i18comp) { if(!(key in _i18comp)) {
throw Error(`Duplicate key ${key}`); _i18comp[key] = [];
} }
_i18comp[key] = el; _i18comp[key].push(el);
} }
static t(el, data) { static t(elarr, data) {
if(typeof data == "string" || typeof data == "number") { if(!data) return;
el.innerText = data; elarr.forEach((el) => {
if(typeof data == "string" || typeof data == "number") {
el.innerText = data;
} else {
let txt = data.txt;
data.var.forEach((el) => {
const model = _ctrls[el.model].model
txt = txt.replace(`{{${el.key}}}`, model.get(el.key))
});
el.innerText = txt;
}
});
}
static getKey(key, model) {
const ldata = _i18langs[_currentLanguage];
if (ldata.data) {
const t = _i18langs[_currentLanguage].data[key];
return I18.tkey(t, model);
} else { } else {
throw Error(`TODO implement dynamic data type ${data}`); throw Error(`Language not loaded ${_currentLanguage}`);
} }
} }
static getKey(key) { static getLKey(lang, key, model) {
return _i18langs[_currentLanguage].data[key];
}
static getLKey(lang, key) {
if(!(lang in _i18langs)) { if(!(lang in _i18langs)) {
throw Error(`Language not found ${lang}`); throw Error(`Language not found ${lang}`);
} }
return _i18langs[lang].data[key]; const ldata = _i18langs[lang];
if (ldata.data) {
const t = _i18langs[lang].data[key];
return I18.tkey(t, model);
} else {
throw Error(`Language not loaded ${lang}`);
}
}
static tkey(t, model) {
if(typeof t == "string" || typeof t == "number") {
return t;
} else {
let txt = t.txt;
t.var.forEach((el) => {
txt = txt.replace(`{{${el.key}}}`, model.get(el.key))
});
return txt;
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@szczepano/huh", "name": "@szczepano/huh",
"version": "0.0.4", "version": "0.0.5",
"description": "huh", "description": "huh",
"main": "huh.js", "main": "huh.js",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",