Add package for deep access

This commit is contained in:
Christian Kaisermann 2018-07-30 19:57:05 -03:00
parent a189f9e90b
commit 7e79fc4c66
6 changed files with 264 additions and 46 deletions

5
package-lock.json generated
View File

@ -7326,6 +7326,11 @@
"integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
"dev": true
},
"object-resolve-path": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-resolve-path/-/object-resolve-path-1.1.1.tgz",
"integrity": "sha1-p/j5Poogr4DkQhe6fbVDFtnRIjI="
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",

View File

@ -111,6 +111,7 @@
"validate-commit-msg": "^2.12.2"
},
"dependencies": {
"deepmerge": "^2.1.1"
"deepmerge": "^2.1.1",
"object-resolve-path": "^1.1.1"
}
}

123
src/formatter.ts Normal file
View File

@ -0,0 +1,123 @@
/**
* Adapted from 'https://github.com/kazupon/vue-i18n/blob/dev/src/format.js'
* Copyright (c) 2016 kazuya kawaguchi
**/
import { isObject, warn } from './utils'
const RE_TOKEN_LIST_VALUE: RegExp = /^(\d)+/
const RE_TOKEN_NAMED_VALUE: RegExp = /^(\w)+/
type Token = {
type: 'text' | 'named' | 'list' | 'unknown'
value: string
}
export default class Formatter {
_caches: { [key: string]: Array<Token> }
constructor() {
this._caches = Object.create(null)
}
interpolate(message: string, values: any): Array<any> {
if (!values) {
return [message]
}
let tokens: Array<Token> = this._caches[message]
if (!tokens) {
tokens = parse(message)
this._caches[message] = tokens
}
return compile(tokens, values)
}
}
/** Parse a identification string into cached Tokens */
export function parse(format: string): Array<Token> {
const tokens: Array<Token> = []
let position: number = 0
let currentText: string = ''
while (position < format.length) {
let char: string = format[position++]
/** If found any character that's not a '{' (does not include '\{'), assume text */
if (char !== '{' || (position > 0 && char[position - 1] === '\\')) {
currentText += char
} else {
/** Beginning of a interpolation */
if (currentText.length) {
tokens.push({ type: 'text', value: currentText })
}
/** Reset the current text string because we're dealing interpolation entry */
currentText = ''
/** Key name */
let namedKey: string = ''
char = format[position++]
while (char !== '}') {
namedKey += char
char = format[position++]
}
const type = RE_TOKEN_LIST_VALUE.test(namedKey)
? 'list'
: RE_TOKEN_NAMED_VALUE.test(namedKey)
? 'named'
: 'unknown'
tokens.push({ value: namedKey, type })
}
}
/** If there's any text left, push it to the tokens list */
if (currentText) {
tokens.push({ type: 'text', value: currentText })
}
return tokens
}
export function compile(tokens: Array<Token>, values: { [id: string]: any }): Array<any> {
const compiled: Array<any> = []
let index: number = 0
const mode: string = Array.isArray(values) ? 'list' : isObject(values) ? 'named' : 'unknown'
if (mode === 'unknown') {
return compiled
}
while (index < tokens.length) {
const token: Token = tokens[index++]
switch (token.type) {
case 'text':
compiled.push(token.value)
break
case 'list':
compiled.push(values[parseInt(token.value, 10)])
break
case 'named':
if (mode === 'named') {
compiled.push(values[token.value])
} else {
if (process.env.NODE_ENV !== 'production') {
warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
}
}
break
case 'unknown':
if (process.env.NODE_ENV !== 'production') {
warn(`Detect 'unknown' type of token!`)
}
break
}
}
return compiled
}

View File

@ -1,15 +1,21 @@
import { InterpolationObj, Sveltei18n, SvelteStore, LocaleDictionary, Locales } from './interfaces'
import { capitalize, titlelize, upper, lower, getNestedProp } from './utils'
import deepmerge from 'deepmerge'
import resolvePath from 'object-resolve-path'
import { InterpolationObj, Sveltei18n, SvelteStore, LocaleDictionary, Locales } from './interfaces'
import { capitalize, titlelize, upper, lower } from './utils'
import Formatter from './formatter'
export default function(store: SvelteStore, localesList: Array<Locales>) {
const formatter = new Formatter()
const locales: Locales = deepmerge.all(localesList)
store.locale = (locale: string) => store.fire('locale', locale)
store.setLocale = (locale: string) => store.fire('locale', locale)
store.on('locale', function(locale: string) {
const localeDict: LocaleDictionary = locales[locale]
const _ = <Sveltei18n>function(id, values) {
return getNestedProp(localeDict, id) || id
let message = resolvePath(localeDict, id) || id
message = formatter.interpolate(message, values).join('')
return message
}
_.capitalize = (id, values) => capitalize(_(id, values))

View File

@ -3,16 +3,13 @@ export const titlelize = (str: string) => str.replace(/(^|\s)\S/g, l => l.toUppe
export const upper = (str: string) => str.toLocaleUpperCase()
export const lower = (str: string) => str.toLocaleLowerCase()
export const getNestedProp = (obj: { [prop: string]: any }, path: string) => {
try {
return path
.replace('[', '.')
.replace(']', '')
.split('.')
.reduce(function(o, property) {
return o[property]
}, obj)
} catch (err) {
return undefined
export const isObject = (obj: any) => obj !== null && typeof obj === 'object'
export function warn(msg: string, err?: Error): void {
if (typeof console !== 'undefined') {
console.warn(`[svelte-i18n] ${msg}`)
if (err) {
console.warn(err.stack)
}
}
}

View File

@ -1,21 +1,27 @@
import i18n from '../src/svelte-i18n'
import { Store } from 'svelte/store.umd'
import { capitalize, titlelize, upper, lower, getNestedProp } from '../src/utils'
import { capitalize, titlelize, upper, lower, isObject } from '../src/utils'
const store = new Store()
const locales = {
'pt-br': {
test: 'teste',
phrase: 'Adoro banana',
phrase: 'adoro banana',
phrases: ['Frase 1', 'Frase 2'],
interpolation: {
key: 'Olá, {0}! Como está {1}?',
named: 'Olá, {name}! Como está {time}?'
},
wow: {
much: {
deep: {
list: [, 'muito profundo']
}
}
},
obj: {
a: 'a'
}
},
po: {
test: 'prøve',
phrase: 'Jeg elsker banan',
phrases: ['sætning 1', 'sætning 2']
}
}
@ -33,32 +39,112 @@ i18n(store, [
/**
* Dummy test
*/
describe('utils', () => {
it('works', () => {
expect(getNestedProp(store['pt-br'], 'phrases[3]')).toBe(undefined)
describe('Utilities', () => {
it('should check if a variable is an object', () => {
expect(isObject({})).toBe(true)
expect(isObject(1)).toBe(false)
})
})
describe('Dummy test', () => {
it('works if true is truthy', () => {
expect(store.get().locale).toBeFalsy()
expect(store.get()._).toBeFalsy()
describe('Localization', () => {
it('should start with a clean store', () => {
const { _, locale } = store.get()
expect(locale).toBeFalsy()
expect(_).toBeFalsy()
})
it('should change the locale after a "locale" store event', () => {
store.fire('locale', 'en')
expect(store.get().locale).toBe('en')
store.locale('pt-br')
expect(store.get().locale).toBe('pt-br')
expect(store.get()._).toBeInstanceOf(Function)
expect(store.get()._('non-existent')).toBe('non-existent')
expect(store.get()._('test')).toBe(locales['pt-br'].test)
expect(store.get()._('obj.b')).toBe('b')
store.fire('locale', 'po')
expect(store.get()._('test')).not.toBe(locales['pt-br'].test)
expect(store.get()._('test')).toBe(locales.po.test)
expect(store.get()._('phrases[1]')).toBe(locales.po.phrases[1])
expect(store.get()._('phrases[2]')).toBe('phrases[2]')
expect(store.get()._.capitalize('phrase')).toBe(capitalize(locales.po.phrase))
expect(store.get()._.titlelize('phrase')).toBe(titlelize(locales.po.phrase))
expect(store.get()._.upper('phrase')).toBe(upper(locales.po.phrase))
expect(store.get()._.lower('phrase')).toBe(lower(locales.po.phrase))
const { locale, _ } = store.get()
expect(locale).toBe('en')
expect(_).toBeInstanceOf(Function)
})
it('should have a .setLocale() method', () => {
expect(store.setLocale).toBeInstanceOf(Function)
store.setLocale('pt-br')
const { locale } = store.get()
expect(locale).toBe('pt-br')
})
it('should return the message id when no message identified by it was found', () => {
store.setLocale('pt-br')
const { locale, _ } = store.get()
expect(_('non.existent')).toBe('non.existent')
})
it('should get a message by its id', () => {
const { _ } = store.get()
expect(_('test')).toBe(locales['pt-br'].test)
})
it('should get a deep nested message by its string path', () => {
store.setLocale('pt-br')
const { locale, _ } = store.get()
expect(_('obj.b')).toBe('b')
})
it('should get a message within an array by its index', () => {
store.setLocale('pt-br')
const { locale, _ } = store.get()
expect(_('phrases[1]')).toBe(locales['pt-br'].phrases[1])
/** Not found */
expect(_('phrases[2]')).toBe('phrases[2]')
})
it('should interpolate with {numeric} placeholders', () => {
store.setLocale('pt-br')
const { locale, _ } = store.get()
expect(_('interpolation.key', ['Chris', 'o dia'])).toBe('Olá, Chris! Como está o dia?')
})
it('should interpolate with {named} placeholders', () => {
store.setLocale('pt-br')
const { locale, _ } = store.get()
expect(
_('interpolation.named', {
name: 'Chris',
time: 'o dia'
})
).toBe('Olá, Chris! Como está o dia?')
})
})
describe('Localization utilities', () => {
it('should capitalize a translated message', () => {
store.setLocale('pt-br')
const { _ } = store.get()
expect(_.capitalize('phrase')).toBe('Adoro banana')
})
it('should titlelize a translated message', () => {
store.setLocale('pt-br')
const { _ } = store.get()
expect(_.titlelize('phrase')).toBe('Adoro Banana')
})
it('should lowercase a translated message', () => {
store.setLocale('pt-br')
const { _ } = store.get()
expect(_.lower('phrase')).toBe('adoro banana')
})
it('should uppercase a translated message', () => {
store.setLocale('pt-br')
const { _ } = store.get()
expect(_.upper('phrase')).toBe('ADORO BANANA')
})
})