mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-06-30 18:54:49 +02: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=",
|
"integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
|
||||||
"dev": true
|
"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": {
|
"object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
||||||
|
|
|
@ -111,6 +111,7 @@
|
||||||
"validate-commit-msg": "^2.12.2"
|
"validate-commit-msg": "^2.12.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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 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>) {
|
export default function(store: SvelteStore, localesList: Array<Locales>) {
|
||||||
|
const formatter = new Formatter()
|
||||||
const locales: Locales = deepmerge.all(localesList)
|
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) {
|
store.on('locale', function(locale: string) {
|
||||||
const localeDict: LocaleDictionary = locales[locale]
|
const localeDict: LocaleDictionary = locales[locale]
|
||||||
const _ = <Sveltei18n>function(id, values) {
|
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))
|
_.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 upper = (str: string) => str.toLocaleUpperCase()
|
||||||
export const lower = (str: string) => str.toLocaleLowerCase()
|
export const lower = (str: string) => str.toLocaleLowerCase()
|
||||||
|
|
||||||
export const getNestedProp = (obj: { [prop: string]: any }, path: string) => {
|
export const isObject = (obj: any) => obj !== null && typeof obj === 'object'
|
||||||
try {
|
|
||||||
return path
|
export function warn(msg: string, err?: Error): void {
|
||||||
.replace('[', '.')
|
if (typeof console !== 'undefined') {
|
||||||
.replace(']', '')
|
console.warn(`[svelte-i18n] ${msg}`)
|
||||||
.split('.')
|
if (err) {
|
||||||
.reduce(function(o, property) {
|
console.warn(err.stack)
|
||||||
return o[property]
|
}
|
||||||
}, obj)
|
|
||||||
} catch (err) {
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,27 @@
|
||||||
import i18n from '../src/svelte-i18n'
|
import i18n from '../src/svelte-i18n'
|
||||||
import { Store } from 'svelte/store.umd'
|
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 store = new Store()
|
||||||
const locales = {
|
const locales = {
|
||||||
'pt-br': {
|
'pt-br': {
|
||||||
test: 'teste',
|
test: 'teste',
|
||||||
phrase: 'Adoro banana',
|
phrase: 'adoro banana',
|
||||||
phrases: ['Frase 1', 'Frase 2'],
|
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: {
|
obj: {
|
||||||
a: 'a'
|
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
|
* Dummy test
|
||||||
*/
|
*/
|
||||||
describe('utils', () => {
|
describe('Utilities', () => {
|
||||||
it('works', () => {
|
it('should check if a variable is an object', () => {
|
||||||
expect(getNestedProp(store['pt-br'], 'phrases[3]')).toBe(undefined)
|
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')
|
store.fire('locale', 'en')
|
||||||
expect(store.get().locale).toBe('en')
|
const { locale, _ } = store.get()
|
||||||
store.locale('pt-br')
|
|
||||||
expect(store.get().locale).toBe('pt-br')
|
expect(locale).toBe('en')
|
||||||
expect(store.get()._).toBeInstanceOf(Function)
|
expect(_).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')
|
it('should have a .setLocale() method', () => {
|
||||||
store.fire('locale', 'po')
|
expect(store.setLocale).toBeInstanceOf(Function)
|
||||||
expect(store.get()._('test')).not.toBe(locales['pt-br'].test)
|
|
||||||
expect(store.get()._('test')).toBe(locales.po.test)
|
store.setLocale('pt-br')
|
||||||
expect(store.get()._('phrases[1]')).toBe(locales.po.phrases[1])
|
const { locale } = store.get()
|
||||||
expect(store.get()._('phrases[2]')).toBe('phrases[2]')
|
|
||||||
expect(store.get()._.capitalize('phrase')).toBe(capitalize(locales.po.phrase))
|
expect(locale).toBe('pt-br')
|
||||||
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))
|
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