mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 18:10:43 +01:00
Add package for deep access
This commit is contained in:
parent
a189f9e90b
commit
7e79fc4c66
5
package-lock.json
generated
5
package-lock.json
generated
@ -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",
|
||||
|
@ -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
123
src/formatter.ts
Normal 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
|
||||
}
|
@ -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))
|
||||
|
19
src/utils.ts
19
src/utils.ts
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user