Add locale support

This commit is contained in:
Misode
2020-05-29 03:38:41 +02:00
parent a8fabd555b
commit a994464730
15 changed files with 132 additions and 43 deletions
+1
View File
@@ -12,6 +12,7 @@
"author": "Misode",
"license": "MIT",
"dependencies": {
"copy-webpack-plugin": "^6.0.1",
"ts-loader": "^7.0.4",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
+46 -6
View File
@@ -1,4 +1,5 @@
import { INode } from "./nodes/AbstractNode"
import { Path, PathElement } from "./model/Path"
export interface Registry<T> {
register(id: string, value: T): void
@@ -6,14 +7,14 @@ export interface Registry<T> {
}
class SchemaRegistry implements Registry<INode<any>> {
private registery: { [id: string]: INode<any> } = {}
private registry: { [id: string]: INode<any> } = {}
register(id: string, node: INode<any>) {
this.registery[id] = node
this.registry[id] = node
}
get(id: string) {
const node = this.registery[id]
const node = this.registry[id]
if (node === undefined) {
console.error(`Tried to access schema "${id}, but that doesn't exit.`)
}
@@ -22,14 +23,14 @@ class SchemaRegistry implements Registry<INode<any>> {
}
class CollectionRegistry implements Registry<string[]> {
private registery: { [id: string]: string[] } = {}
private registry: { [id: string]: string[] } = {}
register(id: string, list: string[]) {
this.registery[id] = list
this.registry[id] = list
}
get(id: string) {
const list = this.registery[id]
const list = this.registry[id]
if (list === undefined) {
console.warn(`Tried to access collection "${id}", but that doesn't exist`)
}
@@ -37,5 +38,44 @@ class CollectionRegistry implements Registry<string[]> {
}
}
export interface Locale {
[key: string]: string
}
class LocaleRegistry implements Registry<Locale> {
private registry: { [id: string]: Locale } = {}
language: string = ''
register(id: string, locale: Locale): void {
this.registry[id] = locale
}
get(id: string) {
const locale = this.registry[id]
if (locale === undefined) {
console.warn(`Tried to access locale "${id}", but that doesn't exist`)
}
return locale ?? []
}
getLocale(key: string) {
return this.get(this.language)[key]
}
}
export const SCHEMAS = new SchemaRegistry()
export const COLLECTIONS = new CollectionRegistry()
export const LOCALES = new LocaleRegistry()
export const locale = (key: string | Path) => {
if (typeof key === 'string') {
return LOCALES.getLocale(key) ?? key
}
let path = key.getArray().filter(e => (typeof e === 'string'))
while (path.length > 0) {
const locale = LOCALES.getLocale(path.join('.'))
if (locale !== undefined) return locale
path.shift()
}
return key.last()
}
+9 -1
View File
@@ -3,6 +3,7 @@ import { TreeView } from '../view/TreeView'
import { SourceView } from '../view/SourceView'
import { ConditionSchema } from '../minecraft/schemas/Condition'
import { SandboxSchema } from './Sandbox'
import { LOCALES, locale } from '../Registries'
const predicateModel = new DataModel(ConditionSchema)
const sandboxModel = new DataModel(SandboxSchema)
@@ -29,4 +30,11 @@ document.getElementById('header')?.append(modelSelector)
new TreeView(model, document!.getElementById('view')!)
new SourceView(model, document!.getElementById('source')!)
model.invalidate()
fetch('../build/locales/en.json')
.then(r => r.json())
.then(l => {
LOCALES.register('en', l)
LOCALES.language = 'en'
model.invalidate()
})
+13
View File
@@ -0,0 +1,13 @@
{
"add": "Add",
"chance": "Chance",
"condition": "Condition",
"condition.random_chance": "Random Chance",
"false": "False",
"remove": "Remove",
"true": "True",
"type": "Type",
"overworld": "Overworld",
"the_nether": "The Nether",
"the_end": "The End"
}
+9 -8
View File
@@ -2,6 +2,7 @@ import { AbstractNode, NodeMods, RenderOptions, StateNode } from '../../nodes/Ab
import { Path } from '../../model/Path'
import { DataModel } from '../../model/DataModel'
import { TreeView } from '../../view/TreeView'
import { locale } from '../../Registries'
export type IRange = number
| { min?: number, max?: number, type?: 'uniform' }
@@ -57,15 +58,15 @@ export class RangeNode extends AbstractNode<IRange> implements StateNode<IRange>
input = `<input value=${value === undefined ? '' : value}>`
} else if (value.type === 'binomial') {
curType = 'binomial'
input = `<label>n</label>
input = `<label>${locale('range.n')}</label>
<input value=${value.n === undefined ? '' : value.n}>
<label>p</label>
<label>${locale('range.p')}</label>
<input value=${value.p === undefined ? '' : value.p}>`
} else {
curType = 'range'
input = `<label>Min</label>
input = `<label>${locale('range.min')}</label>
<input value=${value.min === undefined ? '' : value.min}>
<label>Max</label>
<label>${locale('range.max')}</label>
<input value=${value.max === undefined ? '' : value.max}>`
}
const id = view.register(el => {
@@ -78,11 +79,11 @@ export class RangeNode extends AbstractNode<IRange> implements StateNode<IRange>
evt.stopPropagation()
})
})
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<select data-id="${id}">
<option value="exact">Exact</option>
<option value="range">Range</option>
<option value="binomial">Binomial</option>
<option value="exact">${locale('range.exact')}</option>
<option value="range">${locale('range.range')}</option>
<option value="binomial">${locale('range.binomial')}</option>
</select>
${input}`
}
+5 -2
View File
@@ -2,6 +2,7 @@ import { NodeMods, RenderOptions } from '../../nodes/AbstractNode'
import { EnumNode } from '../../nodes/EnumNode'
import { Path } from '../../model/Path'
import { TreeView, getId } from '../../view/TreeView'
import { locale } from '../../Registries'
export interface ResourceNodeMods extends NodeMods<string> {
additional?: boolean
@@ -30,9 +31,11 @@ export class ResourceNode extends EnumNode {
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
if (this.additional) {
const id = `datalist-${getId()}`
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input list=${id} value="${value ?? ''}">
<datalist id=${id}>${this.options.map(o => `<option value="${o}">`).join('')}</datalist>`
<datalist id=${id}>${this.options.map(o =>
`<option value="${o}">${locale(path.push(o))}</option>`
).join('')}</datalist>`
} else {
return super.renderRaw(path, value, view, options)
}
+5 -1
View File
@@ -1,6 +1,6 @@
import { DataModel } from "./DataModel"
type PathElement = (string | number)
export type PathElement = (string | number)
export class Path implements Iterable<PathElement> {
private arr: PathElement[]
@@ -27,6 +27,10 @@ export class Path implements Iterable<PathElement> {
return new Path([...this.arr], this.model)
}
getArray(): PathElement[] {
return this.arr
}
withModel(model: DataModel): Path {
return new Path([...this.arr], model)
}
+6 -3
View File
@@ -1,6 +1,7 @@
import { AbstractNode, NodeMods, RenderOptions } from "./AbstractNode";
import { Path } from "../model/Path";
import { TreeView } from "../view/TreeView";
import { locale } from "../Registries";
export class BooleanNode extends AbstractNode<boolean> {
@@ -17,9 +18,11 @@ export class BooleanNode extends AbstractNode<boolean> {
const trueButton = view.registerClick(el => {
view.model.set(path, !this.force() && value === true ? undefined : true)
})
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
<button${value === false ? ' style="font-weight: bold"' : ' '} data-id="${falseButton}">False</button>
<button${value === true ? ' style="font-weight: bold"' : ' '} data-id="${trueButton}">True</button>`
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<button${value === false ? ' style="font-weight: bold"' : ' '}
data-id="${falseButton}">${locale('false')}</button>
<button${value === true ? ' style="font-weight: bold"' : ' '}
data-id="${trueButton}">${locale('true')}</button>`
}
getClassName() {
+5 -2
View File
@@ -2,6 +2,7 @@ import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { locale } from '../Registries'
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
protected options: string[]
@@ -25,9 +26,11 @@ export class EnumNode extends AbstractNode<string> implements StateNode<string>
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
const id = view.register(el => (el as HTMLInputElement).value = value)
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<select data-id=${id}>
${this.options.map(o => `<option value="${o}">${o}</option>`).join('')}
${this.options.map(o =>
`<option value="${o}">${locale(path.push(o))}</option>`
).join('')}
</select>`
}
+4 -3
View File
@@ -3,6 +3,7 @@ import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { IObject } from './ObjectNode'
import { locale } from '../Registries'
export class ListNode extends AbstractNode<IObject[]> {
protected children: INode<any>
@@ -31,8 +32,8 @@ export class ListNode extends AbstractNode<IObject[]> {
const button = view.registerClick(el => {
view.model.set(path, [...value, this.children.default()])
})
return `<label>${path.last()}:</label>
<button data-id="${button}">Add</button>
return `<label>${locale(path)}:</label>
<button data-id="${button}">${locale('add')}</button>
<div class="list-fields">
${value.map((obj, index) => {
return this.renderEntry(path.push(index), obj, view)
@@ -44,7 +45,7 @@ export class ListNode extends AbstractNode<IObject[]> {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div class="list-entry"><button data-id="${button}">Remove</button>
return `<div class="list-entry"><button data-id="${button}">${locale('remove')}</button>
${this.children.render(path, value, view, {hideLabel: true})}
</div>`
}
+4 -3
View File
@@ -2,6 +2,7 @@ import { AbstractNode, NodeMods, INode, StateNode } from './AbstractNode'
import { TreeView } from '../view/TreeView'
import { Path } from '../model/Path'
import { IObject } from './ObjectNode'
import { locale } from '../Registries'
export type IMap = {
[name: string]: IObject
@@ -34,9 +35,9 @@ export class MapNode extends AbstractNode<IMap> {
const key = this.keys.getState(el.parentElement!)
view.model.set(path.push(key), this.values.default())
})
return `<label>${path.last()}:</label>
return `<label>${locale(path)}:</label>
${this.keys.renderRaw(path, '', view, {hideLabel: true, syncModel: false})}
<button data-id="${button}">Add</button>
<button data-id="${button}">${locale('add')}</button>
<div class="map-fields">
${Object.keys(value).map(key => {
return this.renderEntry(path.push(key), value[key], view)
@@ -48,7 +49,7 @@ export class MapNode extends AbstractNode<IMap> {
const button = view.registerClick(el => {
view.model.set(path, undefined)
})
return `<div class="map-entry"><button data-id="${button}">Remove</button>
return `<div class="map-entry"><button data-id="${button}">${locale('remove')}</button>
${this.values.render(path, value, view)}
</div>`
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode
import { Path } from '../model/Path'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export interface NumberNodeMods extends NodeMods<number> {
integer?: boolean
@@ -36,7 +37,7 @@ export class NumberNode extends AbstractNode<number> implements StateNode<number
}
renderRaw(path: Path, value: number, view: TreeView, options?: RenderOptions) {
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input value="${value ?? ''}">`
}
+11 -11
View File
@@ -1,6 +1,7 @@
import { NodeMods, INode, NodeChildren, AbstractNode, RenderOptions } from './AbstractNode'
import { Path } from '../model/Path'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export const Switch = Symbol('switch')
export const Case = Symbol('case')
@@ -52,32 +53,31 @@ export class ObjectNode extends AbstractNode<IObject> {
}
renderRaw(path: Path, value: IObject, view: TreeView, options?: RenderOptions) {
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
const activeFields = {...this.fields, ...activeCase}
if (options?.hideLabel) {
value = value ?? {}
return this.renderFields(path, value, view, activeFields)
return this.renderFields(path, value, view)
} else if (this.collapse || options?.collapse) {
if (value === undefined) {
const id = view.registerClick(() => view.model.set(path, this.default()))
return `<label class="collapse closed" data-id="${id}">${path.last()}</label>`
return `<label class="collapse closed" data-id="${id}">${locale(path)}</label>`
} else {
const id = view.registerClick(() => view.model.set(path, undefined))
return `<label class="collapse open" data-id="${id}">${path.last()}</label>
return `<label class="collapse open" data-id="${id}">${locale(path)}</label>
<div class="object-fields">
${this.renderFields(path, value, view, activeFields)}
${this.renderFields(path, value, view)}
</div>`
}
} else {
value = value ?? {}
return `<label>${path.last()}</label>
return `<label>${locale(path)}</label>
<div class="object-fields">
${this.renderFields(path, value, view, activeFields)}
${this.renderFields(path, value, view)}
</div>`
}
}
renderFields(path: Path, value: IObject, view: TreeView, activeFields: NodeChildren) {
renderFields(path: Path, value: IObject, view: TreeView) {
value = value ?? {}
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
const activeFields = {...this.fields, ...activeCase}
return Object.keys(activeFields).map(f => {
return activeFields[f].render(path.push(f), value[f], view)
}).join('')
+2 -1
View File
@@ -2,6 +2,7 @@ import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode
import { Path } from '../model/Path'
import { DataModel } from '../model/DataModel'
import { TreeView } from '../view/TreeView'
import { locale } from '../Registries'
export interface StringNodeMods extends NodeMods<string> {
allowEmpty?: boolean
@@ -25,7 +26,7 @@ export class StringNode extends AbstractNode<string> implements StateNode<string
}
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
return `${options?.hideLabel ? `` : `<label>${path.last()}</label>`}
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
<input value="${value ?? ''}">`
}
+10 -1
View File
@@ -1,3 +1,5 @@
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
entry: './src/app/app.ts',
output: {
@@ -11,5 +13,12 @@ module.exports = {
rules: [
{ test: /\.ts$/, loader: 'ts-loader' }
]
}
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'src/locales', to: 'build/locales'}
]
})
]
}