feat: 🎸 add preloadLocale method

This commit is contained in:
Christian Kaisermann 2019-11-19 23:38:01 -03:00
parent 0c01a8b186
commit 0a0e4b3bab
5 changed files with 104 additions and 84 deletions

View File

@ -2,7 +2,15 @@ import { writable, derived } from 'svelte/store'
import resolvePath from 'object-resolve-path'
import memoize from 'micro-memoize'
import { capital, title, upper, lower, getClientLocale, getGenericLocaleFrom } from './utils'
import {
capital,
title,
upper,
lower,
getClientLocale,
getGenericLocaleFrom,
getGenericLocalesFrom,
} from './utils'
import { MessageObject, Formatter } from './types'
import {
getMessageFormatter,
@ -12,41 +20,24 @@ import {
} from './formatters'
let currentLocale: string
let currentDictionary: Record<string, any>
let currentDictionary: Record<string, Record<string, any>>
const hasLocale = (locale: string) => locale in currentDictionary
function getAvailableLocale(locale: string): { locale: string; loader?: () => Promise<any> } {
if (currentDictionary[locale]) {
if (typeof currentDictionary[locale] === 'function') {
return { locale, loader: currentDictionary[locale] }
}
return { locale }
}
locale = getGenericLocaleFrom(locale)
if (locale != null) {
return getAvailableLocale(locale)
}
return { locale: null }
function getAvailableLocale(locale: string): string | null {
if (locale in currentDictionary || locale == null) return locale
return getAvailableLocale(getGenericLocaleFrom(locale))
}
const lookupMessage = memoize((path: string, locale: string): string => {
if (path in currentDictionary[locale]) {
return currentDictionary[locale][path]
if (locale == null) return null
if (hasLocale(locale)) {
if (path in currentDictionary[locale]) return currentDictionary[locale][path]
const message = resolvePath(currentDictionary[locale], path)
if (message) return message
}
const message = resolvePath(currentDictionary[locale], path)
if (message == null) {
const genericLocale = getGenericLocaleFrom(locale)
if (genericLocale != null && hasLocale(genericLocale)) {
return lookupMessage(path, genericLocale)
}
return null
}
return message
return lookupMessage(path, getGenericLocaleFrom(locale))
})
const formatMessage: Formatter = (id, options = {}) => {
@ -60,18 +51,14 @@ const formatMessage: Formatter = (id, options = {}) => {
if (!message) {
console.warn(
`[svelte-i18n] The message "${id}" was not found in "${locale
.split('-')
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
.reverse()
.join('", "')}".`,
`[svelte-i18n] The message "${id}" was not found in "${getGenericLocalesFrom(locale).join(
'", "',
)}".`,
)
if (defaultValue != null) return defaultValue
return id
return defaultValue || id
}
if (!values) return message
return getMessageFormatter(message, locale).format(values)
}
@ -83,33 +70,62 @@ formatMessage.title = (id, options) => title(formatMessage(id, options))
formatMessage.upper = (id, options) => upper(formatMessage(id, options))
formatMessage.lower = (id, options) => lower(formatMessage(id, options))
const $dictionary = writable({})
$dictionary.subscribe((newDictionary: any) => (currentDictionary = newDictionary))
const $dictionary = writable<Record<string, Record<string, any>>>({})
$dictionary.subscribe(newDictionary => (currentDictionary = newDictionary))
const loadLocale = (localeToLoad: string) => {
return Promise.all(
getGenericLocalesFrom(localeToLoad)
.map(localeItem => {
const loader = currentDictionary[localeItem]
if (loader == null && localeItem !== localeToLoad) {
console.warn(
`[svelte-i18n] No dictionary or loader were found for the locale "${localeItem}". It's the fallback locale of "${localeToLoad}."`,
)
return
}
if (typeof loader !== 'function') return
return loader().then((dict: any) => [localeItem, dict.default || dict])
})
.filter(Boolean),
)
.then(updates => {
if (updates.length > 0) {
// update dictionary only once
$dictionary.update(d => {
updates.forEach(([localeItem, localeDict]) => {
d[localeItem] = localeDict
})
return d
})
}
return updates
})
.catch((e: Error) => {
throw e
})
}
const $locale = writable(null)
const localeSet = $locale.set
$locale.set = (newLocale: string): void | Promise<void> => {
const { locale, loader } = getAvailableLocale(newLocale)
if (typeof loader === 'function') {
return loader()
.then((dict: any) => {
currentDictionary[locale] = dict.default || dict
if (locale) return localeSet(locale)
})
.catch((e: Error) => {
throw e
})
const locale = getAvailableLocale(newLocale)
if (locale) {
if (typeof currentDictionary[locale] === 'function') {
// load all locales related to the passed locale
// i.e en-GB loads en, but en doesn't load en-GB
return loadLocale(locale).then(() => localeSet(newLocale))
}
return localeSet(newLocale)
}
if (locale) return localeSet(locale)
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
}
$locale.update = (fn: (locale: string) => void | Promise<void>) => localeSet(fn(currentLocale))
$locale.subscribe((newLocale: string) => (currentLocale = newLocale))
const format = derived([$locale, $dictionary], () => formatMessage)
const locales = derived([$dictionary], ([$dictionary]) => Object.keys($dictionary))
const $format = derived([$locale, $dictionary], () => formatMessage)
const $locales = derived([$dictionary], ([$dictionary]) => Object.keys($dictionary))
// defineMessages allow us to define and extract dynamic message ids
const defineMessages = (i: Record<string, MessageObject>) => i
@ -118,9 +134,10 @@ export { customFormats, addCustomFormats } from './formatters'
export {
$locale as locale,
$dictionary as dictionary,
locales,
$format as _,
$format as format,
$locales,
getClientLocale,
defineMessages,
format as _,
format,
loadLocale as preloadLocale,
}

View File

@ -1,12 +1,15 @@
export const capital = (str: string) =>
str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
export const title = (str: string) =>
str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
export const capital = (str: string) => str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
export const title = (str: string) => str.replace(/(^|\s)\S/g, l => l.toLocaleUpperCase())
export const upper = (str: string) => str.toLocaleUpperCase()
export const lower = (str: string) => str.toLocaleLowerCase()
export function getGenericLocaleFrom(locale: string) {
return locale.length > 2 ? locale.split('-').shift() : null
const index = locale.lastIndexOf('-')
return index > 0 ? locale.slice(0, index) : null
}
export function getGenericLocalesFrom(locale: string) {
return locale.split('-').map((_, i, arr) => arr.slice(0, i + 1).join('-'))
}
export const getClientLocale = ({

View File

@ -6,6 +6,7 @@ import {
getClientLocale,
addCustomFormats,
customFormats,
preloadLocale,
} from '../../src/client'
global.Intl = require('intl')
@ -14,9 +15,11 @@ let _: Formatter
let currentLocale: string
const dict = {
pt: require('../fixtures/pt.json'),
en: require('../fixtures/en.json'),
'en-GB': require('../fixtures/en-GB.json'),
'en-GB': () => import('../fixtures/en-GB.json'),
pt: () => import('../fixtures/pt.json'),
'pt-BR': () => import('../fixtures/pt-BR.json'),
'pt-PT': () => import('../fixtures/pt-PT.json'),
}
format.subscribe(formatFn => {
@ -26,22 +29,15 @@ dictionary.set(dict)
locale.subscribe((l: string) => {
currentLocale = l
})
locale.set('pt')
locale.set('en')
describe('locale', () => {
it('should change locale', () => {
locale.set('pt')
expect(currentLocale).toBe('pt')
locale.set('en')
expect(currentLocale).toBe('en')
})
it('should fallback to existing locale', () => {
locale.set('pt-BR')
expect(currentLocale).toBe('pt')
locale.set('en-US')
expect(currentLocale).toBe('en')
expect(currentLocale).toBe('en-US')
})
it("should throw an error if locale doesn't exist", () => {
@ -59,12 +55,16 @@ describe('dictionary', () => {
await locale.set('es')
expect(currentLocale).toBe('es')
})
it('load a locale and its derived locales if dictionary is a loader', async () => {
const loaded = await preloadLocale('pt-PT')
expect(loaded[0][0]).toEqual('pt')
expect(loaded[1][0]).toEqual('pt-PT')
})
})
describe('formatting', () => {
it('should translate to current locale', () => {
locale.set('pt')
expect(_('switch.lang')).toBe('Trocar idioma')
locale.set('en')
expect(_('switch.lang')).toBe('Switch language')
})
@ -79,21 +79,22 @@ describe('formatting', () => {
expect(_('batatinha.quente', { default: 'Hot Potato' })).toBe('Hot Potato')
})
it('should fallback to generic locale XX if id not found in XX-YY', async () => {
await locale.set('en-GB')
expect(_('sneakers', { locale: 'en-GB' })).toBe('trainers')
})
it('should fallback to generic locale XX if id not found in XX-YY', () => {
locale.set('en-GB')
expect(_('sneakers')).toBe('trainers')
expect(_('switch.lang')).toBe('Switch language')
})
it('should accept single object with id prop as the message path', () => {
locale.set('pt')
expect(_({ id: 'switch.lang' })).toBe('Trocar idioma')
locale.set('en')
expect(_({ id: 'switch.lang' })).toBe('Switch language')
})
it('should translate to passed locale', () => {
expect(_({ id: 'switch.lang', locale: 'pt' })).toBe('Trocar idioma')
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
})
@ -101,13 +102,6 @@ describe('formatting', () => {
locale.set('en')
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe('Hello Chris, how are you?')
})
it('should interpolate message with variables according to passed locale', () => {
locale.set('en')
expect(_('greeting.message', { values: { name: 'Chris' }, locale: 'pt' })).toBe(
'Olá Chris, como vai?',
)
})
})
describe('utilities', () => {

3
test/fixtures/pt-BR.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"french_bread": "pão francês"
}

3
test/fixtures/pt-PT.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"french_bread": "cacetinhos"
}