mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-09-28 15:14:45 +02: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 resolvePath from 'object-resolve-path'
|
||||||
import memoize from 'micro-memoize'
|
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 { MessageObject, Formatter } from './types'
|
||||||
import {
|
import {
|
||||||
getMessageFormatter,
|
getMessageFormatter,
|
||||||
@ -12,41 +20,24 @@ import {
|
|||||||
} from './formatters'
|
} from './formatters'
|
||||||
|
|
||||||
let currentLocale: string
|
let currentLocale: string
|
||||||
let currentDictionary: Record<string, any>
|
let currentDictionary: Record<string, Record<string, any>>
|
||||||
|
|
||||||
const hasLocale = (locale: string) => locale in currentDictionary
|
const hasLocale = (locale: string) => locale in currentDictionary
|
||||||
|
|
||||||
function getAvailableLocale(locale: string): { locale: string; loader?: () => Promise<any> } {
|
function getAvailableLocale(locale: string): string | null {
|
||||||
if (currentDictionary[locale]) {
|
if (locale in currentDictionary || locale == null) return locale
|
||||||
if (typeof currentDictionary[locale] === 'function') {
|
return getAvailableLocale(getGenericLocaleFrom(locale))
|
||||||
return { locale, loader: currentDictionary[locale] }
|
|
||||||
}
|
|
||||||
return { locale }
|
|
||||||
}
|
|
||||||
|
|
||||||
locale = getGenericLocaleFrom(locale)
|
|
||||||
if (locale != null) {
|
|
||||||
return getAvailableLocale(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { locale: null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookupMessage = memoize((path: string, locale: string): string => {
|
const lookupMessage = memoize((path: string, locale: string): string => {
|
||||||
if (path in currentDictionary[locale]) {
|
if (locale == null) return null
|
||||||
return currentDictionary[locale][path]
|
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)
|
return lookupMessage(path, getGenericLocaleFrom(locale))
|
||||||
if (message == null) {
|
|
||||||
const genericLocale = getGenericLocaleFrom(locale)
|
|
||||||
if (genericLocale != null && hasLocale(genericLocale)) {
|
|
||||||
return lookupMessage(path, genericLocale)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatMessage: Formatter = (id, options = {}) => {
|
const formatMessage: Formatter = (id, options = {}) => {
|
||||||
@ -60,18 +51,14 @@ const formatMessage: Formatter = (id, options = {}) => {
|
|||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[svelte-i18n] The message "${id}" was not found in "${locale
|
`[svelte-i18n] The message "${id}" was not found in "${getGenericLocalesFrom(locale).join(
|
||||||
.split('-')
|
'", "',
|
||||||
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
)}".`,
|
||||||
.reverse()
|
|
||||||
.join('", "')}".`,
|
|
||||||
)
|
)
|
||||||
if (defaultValue != null) return defaultValue
|
return defaultValue || id
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values) return message
|
if (!values) return message
|
||||||
|
|
||||||
return getMessageFormatter(message, locale).format(values)
|
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.upper = (id, options) => upper(formatMessage(id, options))
|
||||||
formatMessage.lower = (id, options) => lower(formatMessage(id, options))
|
formatMessage.lower = (id, options) => lower(formatMessage(id, options))
|
||||||
|
|
||||||
const $dictionary = writable({})
|
const $dictionary = writable<Record<string, Record<string, any>>>({})
|
||||||
$dictionary.subscribe((newDictionary: any) => (currentDictionary = newDictionary))
|
$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 $locale = writable(null)
|
||||||
const localeSet = $locale.set
|
const localeSet = $locale.set
|
||||||
$locale.set = (newLocale: string): void | Promise<void> => {
|
$locale.set = (newLocale: string): void | Promise<void> => {
|
||||||
const { locale, loader } = getAvailableLocale(newLocale)
|
const locale = getAvailableLocale(newLocale)
|
||||||
if (typeof loader === 'function') {
|
if (locale) {
|
||||||
return loader()
|
if (typeof currentDictionary[locale] === 'function') {
|
||||||
.then((dict: any) => {
|
// load all locales related to the passed locale
|
||||||
currentDictionary[locale] = dict.default || dict
|
// i.e en-GB loads en, but en doesn't load en-GB
|
||||||
if (locale) return localeSet(locale)
|
return loadLocale(locale).then(() => localeSet(newLocale))
|
||||||
})
|
}
|
||||||
.catch((e: Error) => {
|
return localeSet(newLocale)
|
||||||
throw e
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locale) return localeSet(locale)
|
|
||||||
|
|
||||||
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
|
||||||
}
|
}
|
||||||
$locale.update = (fn: (locale: string) => void | Promise<void>) => localeSet(fn(currentLocale))
|
$locale.update = (fn: (locale: string) => void | Promise<void>) => localeSet(fn(currentLocale))
|
||||||
$locale.subscribe((newLocale: string) => (currentLocale = newLocale))
|
$locale.subscribe((newLocale: string) => (currentLocale = newLocale))
|
||||||
|
|
||||||
const format = derived([$locale, $dictionary], () => formatMessage)
|
const $format = derived([$locale, $dictionary], () => formatMessage)
|
||||||
const locales = derived([$dictionary], ([$dictionary]) => Object.keys($dictionary))
|
const $locales = derived([$dictionary], ([$dictionary]) => Object.keys($dictionary))
|
||||||
|
|
||||||
// defineMessages allow us to define and extract dynamic message ids
|
// defineMessages allow us to define and extract dynamic message ids
|
||||||
const defineMessages = (i: Record<string, MessageObject>) => i
|
const defineMessages = (i: Record<string, MessageObject>) => i
|
||||||
@ -118,9 +134,10 @@ export { customFormats, addCustomFormats } from './formatters'
|
|||||||
export {
|
export {
|
||||||
$locale as locale,
|
$locale as locale,
|
||||||
$dictionary as dictionary,
|
$dictionary as dictionary,
|
||||||
locales,
|
$format as _,
|
||||||
|
$format as format,
|
||||||
|
$locales,
|
||||||
getClientLocale,
|
getClientLocale,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
format as _,
|
loadLocale as preloadLocale,
|
||||||
format,
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
export const capital = (str: string) =>
|
export const capital = (str: string) => str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
||||||
str.replace(/(^|\s)\S/, l => l.toLocaleUpperCase())
|
export const title = (str: string) => str.replace(/(^|\s)\S/g, 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 upper = (str: string) => str.toLocaleUpperCase()
|
||||||
export const lower = (str: string) => str.toLocaleLowerCase()
|
export const lower = (str: string) => str.toLocaleLowerCase()
|
||||||
|
|
||||||
export function getGenericLocaleFrom(locale: string) {
|
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 = ({
|
export const getClientLocale = ({
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
getClientLocale,
|
getClientLocale,
|
||||||
addCustomFormats,
|
addCustomFormats,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
preloadLocale,
|
||||||
} from '../../src/client'
|
} from '../../src/client'
|
||||||
|
|
||||||
global.Intl = require('intl')
|
global.Intl = require('intl')
|
||||||
@ -14,9 +15,11 @@ let _: Formatter
|
|||||||
let currentLocale: string
|
let currentLocale: string
|
||||||
|
|
||||||
const dict = {
|
const dict = {
|
||||||
pt: require('../fixtures/pt.json'),
|
|
||||||
en: require('../fixtures/en.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 => {
|
format.subscribe(formatFn => {
|
||||||
@ -26,22 +29,15 @@ dictionary.set(dict)
|
|||||||
locale.subscribe((l: string) => {
|
locale.subscribe((l: string) => {
|
||||||
currentLocale = l
|
currentLocale = l
|
||||||
})
|
})
|
||||||
locale.set('pt')
|
locale.set('en')
|
||||||
|
|
||||||
describe('locale', () => {
|
describe('locale', () => {
|
||||||
it('should change locale', () => {
|
it('should change locale', () => {
|
||||||
locale.set('pt')
|
|
||||||
expect(currentLocale).toBe('pt')
|
|
||||||
locale.set('en')
|
locale.set('en')
|
||||||
expect(currentLocale).toBe('en')
|
expect(currentLocale).toBe('en')
|
||||||
})
|
|
||||||
|
|
||||||
it('should fallback to existing locale', () => {
|
|
||||||
locale.set('pt-BR')
|
|
||||||
expect(currentLocale).toBe('pt')
|
|
||||||
|
|
||||||
locale.set('en-US')
|
locale.set('en-US')
|
||||||
expect(currentLocale).toBe('en')
|
expect(currentLocale).toBe('en-US')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should throw an error if locale doesn't exist", () => {
|
it("should throw an error if locale doesn't exist", () => {
|
||||||
@ -59,12 +55,16 @@ describe('dictionary', () => {
|
|||||||
await locale.set('es')
|
await locale.set('es')
|
||||||
expect(currentLocale).toBe('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', () => {
|
describe('formatting', () => {
|
||||||
it('should translate to current locale', () => {
|
it('should translate to current locale', () => {
|
||||||
locale.set('pt')
|
|
||||||
expect(_('switch.lang')).toBe('Trocar idioma')
|
|
||||||
locale.set('en')
|
locale.set('en')
|
||||||
expect(_('switch.lang')).toBe('Switch language')
|
expect(_('switch.lang')).toBe('Switch language')
|
||||||
})
|
})
|
||||||
@ -79,21 +79,22 @@ describe('formatting', () => {
|
|||||||
expect(_('batatinha.quente', { default: 'Hot Potato' })).toBe('Hot Potato')
|
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', () => {
|
it('should fallback to generic locale XX if id not found in XX-YY', () => {
|
||||||
locale.set('en-GB')
|
locale.set('en-GB')
|
||||||
expect(_('sneakers')).toBe('trainers')
|
|
||||||
expect(_('switch.lang')).toBe('Switch language')
|
expect(_('switch.lang')).toBe('Switch language')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should accept single object with id prop as the message path', () => {
|
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')
|
locale.set('en')
|
||||||
expect(_({ id: 'switch.lang' })).toBe('Switch language')
|
expect(_({ id: 'switch.lang' })).toBe('Switch language')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should translate to passed locale', () => {
|
it('should translate to passed locale', () => {
|
||||||
expect(_({ id: 'switch.lang', locale: 'pt' })).toBe('Trocar idioma')
|
|
||||||
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
|
expect(_('switch.lang', { locale: 'en' })).toBe('Switch language')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -101,13 +102,6 @@ describe('formatting', () => {
|
|||||||
locale.set('en')
|
locale.set('en')
|
||||||
expect(_('greeting.message', { values: { name: 'Chris' } })).toBe('Hello Chris, how are you?')
|
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', () => {
|
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