mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 09:59:58 +01:00
feat: 🎸 add preloadLocale method
This commit is contained in:
parent
0c01a8b186
commit
0a0e4b3bab
@ -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,
|
||||
}
|
||||
|
@ -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 = ({
|
||||
|
@ -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
3
test/fixtures/pt-BR.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"french_bread": "pão francês"
|
||||
}
|
3
test/fixtures/pt-PT.json
vendored
Normal file
3
test/fixtures/pt-PT.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"french_bread": "cacetinhos"
|
||||
}
|
Loading…
Reference in New Issue
Block a user