fix: 🐛 fallback behaviour and simplify API contact points

This commit is contained in:
Christian Kaisermann 2019-11-23 23:38:15 -03:00
parent ea2ac47d14
commit 6e0df2fb25
9 changed files with 83 additions and 96 deletions

View File

@ -1,10 +1,11 @@
<script context="module"> <script context="module">
import { isLoading, waitInitialLocale } from 'svelte-i18n' import { isLoading, setInitialLocale } from 'svelte-i18n'
export async function preload() { export async function preload() {
return waitInitialLocale({ return setInitialLocale({
default: 'en-US', fallback: 'en-US',
navigator: true, // navigator: true,
search: 'lang'
}) })
} }
</script> </script>

View File

@ -4,11 +4,9 @@ import {
$dictionary, $dictionary,
addMessages, addMessages,
} from '../stores/dictionary' } from '../stores/dictionary'
import { getCurrentLocale } from '../stores/locale' import { getCurrentLocale, getFallbacksOf } from '../stores/locale'
import { $isLoading } from '../stores/loading' import { $isLoading } from '../stores/loading'
import { getAllFallbackLocales } from './utils'
type Queue = Set<MessagesLoader> type Queue = Set<MessagesLoader>
const loaderQueue: Record<string, Queue> = {} const loaderQueue: Record<string, Queue> = {}
@ -25,7 +23,7 @@ function getLocaleQueue(locale: string) {
} }
function getLocalesQueues(locale: string) { function getLocalesQueues(locale: string) {
return getAllFallbackLocales(locale) return getFallbacksOf(locale)
.reverse() .reverse()
.map<[string, MessagesLoader[]]>(localeItem => { .map<[string, MessagesLoader[]]>(localeItem => {
const queue = getLocaleQueue(localeItem) const queue = getLocaleQueue(localeItem)
@ -35,7 +33,7 @@ function getLocalesQueues(locale: string) {
} }
export function hasLocaleQueue(locale: string) { export function hasLocaleQueue(locale: string) {
return getAllFallbackLocales(locale) return getFallbacksOf(locale)
.reverse() .reverse()
.some(getLocaleQueue) .some(getLocaleQueue)
} }

View File

@ -2,8 +2,7 @@
import resolvePath from 'object-resolve-path' import resolvePath from 'object-resolve-path'
import { hasLocaleDictionary } from '../stores/dictionary' import { hasLocaleDictionary } from '../stores/dictionary'
import { getFallbackOf } from '../stores/locale'
import { getFallbackLocale } from './utils'
const lookupCache: Record<string, Record<string, string>> = {} const lookupCache: Record<string, Record<string, string>> = {}
@ -34,6 +33,6 @@ export const lookupMessage = (
return addToCache( return addToCache(
path, path,
locale, locale,
lookupMessage(dictionary, path, getFallbackLocale(locale)) lookupMessage(dictionary, path, getFallbackOf(locale))
) )
} }

View File

@ -16,15 +16,6 @@ export function lower(str: string) {
return str.toLocaleLowerCase() return str.toLocaleLowerCase()
} }
export function getFallbackLocale(locale: string) {
const index = locale.lastIndexOf('-')
return index > 0 ? locale.slice(0, index) : null
}
export function getAllFallbackLocales(locale: string) {
return locale.split('-').map((_, i, arr) => arr.slice(0, i + 1).join('-'))
}
const getFromURL = (urlPart: string, key: string) => { const getFromURL = (urlPart: string, key: string) => {
const keyVal = urlPart const keyVal = urlPart
.substr(1) .substr(1)
@ -49,12 +40,11 @@ export const getClientLocale = ({
pathname, pathname,
hostname, hostname,
default: defaultLocale, default: defaultLocale,
fallback = defaultLocale,
}: GetClientLocaleOptions) => { }: GetClientLocaleOptions) => {
let locale let locale
if (typeof window === 'undefined') { if (typeof window === 'undefined') return fallback
return defaultLocale
}
if (hostname) { if (hostname) {
locale = getFirstMatch(window.location.hostname, hostname) locale = getFirstMatch(window.location.hostname, hostname)
@ -88,5 +78,5 @@ export const getClientLocale = ({
if (locale) return locale if (locale) return locale
} }
return defaultLocale return fallback
} }

View File

@ -1,22 +1,18 @@
import merge from 'deepmerge' import merge from 'deepmerge'
import { GetClientLocaleOptions, MessageObject } from './types' import { MessageObject } from './types'
import { getClientLocale } from './includes/utils'
import { $locale } from './stores/locale'
// defineMessages allow us to define and extract dynamic message ids // defineMessages allow us to define and extract dynamic message ids
export function defineMessages(i: Record<string, MessageObject>) { export function defineMessages(i: Record<string, MessageObject>) {
return i return i
} }
export function waitInitialLocale(options: GetClientLocaleOptions | string) { export {
if (typeof options === 'string') { $locale as locale,
return $locale.set(options) setInitialLocale,
} // @deprecated
return $locale.set(getClientLocale(options)) setInitialLocale as waitInitialLocale,
} } from './stores/locale'
export { $locale as locale, loadLocale as preloadLocale } from './stores/locale'
export { export {
$dictionary as dictionary, $dictionary as dictionary,
$locales as locales, $locales as locales,
@ -26,7 +22,7 @@ export { $isLoading as isLoading } from './stores/loading'
export { $format as format, $format as _, $format as t } from './stores/format' export { $format as format, $format as _, $format as t } from './stores/format'
// utilities // utilities
export { getClientLocale, merge } export { merge }
export { customFormats, addCustomFormats } from './includes/formats' export { customFormats, addCustomFormats } from './includes/formats'
export { export {
flushQueue as waitLocale, flushQueue as waitLocale,

View File

@ -3,22 +3,25 @@ import { writable, derived } from 'svelte/store'
import { LocaleDictionary } from '../types/index' import { LocaleDictionary } from '../types/index'
import { getFallbackOf } from './locale'
let dictionary: LocaleDictionary let dictionary: LocaleDictionary
const $dictionary = writable<LocaleDictionary>({}) const $dictionary = writable<LocaleDictionary>({})
$dictionary.subscribe(newDictionary => {
dictionary = newDictionary
})
function getDictionary() { export function getDictionary() {
return dictionary return dictionary
} }
function hasLocaleDictionary(locale: string) { export function hasLocaleDictionary(locale: string) {
return locale in dictionary return locale in dictionary
} }
function addMessages(locale: string, ...partials: LocaleDictionary[]) { export function getAvailableLocale(locale: string): string | null {
if (locale in dictionary || locale == null) return locale
return getAvailableLocale(getFallbackOf(locale))
}
export function addMessages(locale: string, ...partials: LocaleDictionary[]) {
$dictionary.update(d => { $dictionary.update(d => {
dictionary[locale] = merge.all([dictionary[locale] || {}].concat(partials)) dictionary[locale] = merge.all([dictionary[locale] || {}].concat(partials))
return d return d
@ -29,10 +32,6 @@ const $locales = derived([$dictionary], ([$dictionary]) =>
Object.keys($dictionary) Object.keys($dictionary)
) )
export { $dictionary.subscribe(newDictionary => (dictionary = newDictionary))
$dictionary,
$locales, export { $dictionary, $locales }
getDictionary,
hasLocaleDictionary,
addMessages,
}

View File

@ -3,13 +3,7 @@ import { derived } from 'svelte/store'
import { Formatter, MessageObject } from '../types' import { Formatter, MessageObject } from '../types'
import { lookupMessage } from '../includes/lookup' import { lookupMessage } from '../includes/lookup'
import { hasLocaleQueue } from '../includes/loaderQueue' import { hasLocaleQueue } from '../includes/loaderQueue'
import { import { capital, upper, lower, title } from '../includes/utils'
getAllFallbackLocales,
capital,
upper,
lower,
title,
} from '../includes/utils'
import { import {
getMessageFormatter, getMessageFormatter,
getTimeFormatter, getTimeFormatter,
@ -18,7 +12,7 @@ import {
} from '../includes/formats' } from '../includes/formats'
import { getDictionary, $dictionary } from './dictionary' import { getDictionary, $dictionary } from './dictionary'
import { getCurrentLocale, $locale } from './locale' import { getCurrentLocale, getFallbacksOf, $locale } from './locale'
const formatMessage: Formatter = (id, options = {}) => { const formatMessage: Formatter = (id, options = {}) => {
if (typeof id === 'object') { if (typeof id === 'object') {
@ -38,7 +32,7 @@ const formatMessage: Formatter = (id, options = {}) => {
if (!message) { if (!message) {
console.warn( console.warn(
`[svelte-i18n] The message "${id}" was not found in "${getAllFallbackLocales( `[svelte-i18n] The message "${id}" was not found in "${getFallbacksOf(
locale locale
).join('", "')}". ${ ).join('", "')}". ${
hasLocaleQueue(getCurrentLocale()) hasLocaleQueue(getCurrentLocale())

View File

@ -1,30 +1,54 @@
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { getFallbackLocale, getAllFallbackLocales } from '../includes/utils'
import { flushQueue, hasLocaleQueue } from '../includes/loaderQueue' import { flushQueue, hasLocaleQueue } from '../includes/loaderQueue'
import { getClientLocale } from '../includes/utils'
import { GetClientLocaleOptions } from '../types'
import { getDictionary } from './dictionary' import { getAvailableLocale } from './dictionary'
let fallback: string = null
let current: string let current: string
const $locale = writable(null) const $locale = writable(null)
export function getFallbackLocale() {
return fallback
}
export function setfallbackLocale(locale: string) {
fallback = locale
}
export function isFallbackLocaleOf(localeA: string, localeB: string) {
return localeB.indexOf(localeA) === 0
}
export function getFallbackOf(locale: string) {
const index = locale.lastIndexOf('-')
if (index > 0) return locale.slice(0, index)
if (fallback && !isFallbackLocaleOf(locale, fallback)) return fallback
return null
}
export function getFallbacksOf(locale: string): string[] {
const locales = locale
.split('-')
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
if (fallback != null && !isFallbackLocaleOf(locale, fallback)) {
return locales.concat(getFallbacksOf(fallback))
}
return locales
}
function getCurrentLocale() { function getCurrentLocale() {
return current return current
} }
function getAvailableLocale(locale: string): string | null { export function setInitialLocale(options: GetClientLocaleOptions) {
if (locale in getDictionary() || locale == null) return locale if (typeof options.fallback === 'string') {
return getAvailableLocale(getFallbackLocale(locale)) setfallbackLocale(options.fallback)
} }
return $locale.set(getClientLocale(options))
function loadLocale(localeToLoad: string) {
return Promise.all(
getAllFallbackLocales(localeToLoad).map(localeItem =>
flushQueue(localeItem)
.then(() => [localeItem, { err: undefined }])
.catch(e => [localeItem, { err: e }])
)
)
} }
$locale.subscribe((newLocale: string) => { $locale.subscribe((newLocale: string) => {
@ -37,17 +61,13 @@ $locale.subscribe((newLocale: string) => {
const localeSet = $locale.set const localeSet = $locale.set
$locale.set = (newLocale: string): void | Promise<void> => { $locale.set = (newLocale: string): void | Promise<void> => {
if (getAvailableLocale(newLocale)) { if (getAvailableLocale(newLocale) && hasLocaleQueue(newLocale)) {
if (hasLocaleQueue(newLocale)) { return flushQueue(newLocale).then(() => localeSet(newLocale))
return flushQueue(newLocale).then(() => localeSet(newLocale))
}
return localeSet(newLocale)
} }
return localeSet(newLocale)
throw Error(`[svelte-i18n] Locale "${newLocale}" not found.`)
} }
$locale.update = (fn: (locale: string) => void | Promise<void>) => $locale.update = (fn: (locale: string) => void | Promise<void>) =>
localeSet(fn(current)) localeSet(fn(current))
export { $locale, loadLocale, flushQueue, getCurrentLocale } export { $locale, flushQueue, getCurrentLocale }

View File

@ -4,13 +4,12 @@ import {
dictionary, dictionary,
locale, locale,
format, format,
getClientLocale,
addCustomFormats, addCustomFormats,
customFormats, customFormats,
preloadLocale,
register, register,
waitLocale, waitLocale,
} from '../../src/client' } from '../../src/client'
import { getClientLocale } from '../../src/client/includes/utils'
global.Intl = require('intl') global.Intl = require('intl')
@ -42,19 +41,9 @@ describe('locale', () => {
await locale.set('en-US') await locale.set('en-US')
expect(currentLocale).toBe('en-US') expect(currentLocale).toBe('en-US')
}) })
it("should throw an error if locale doesn't exist", () => {
expect(() => locale.set('FOO')).toThrow()
})
}) })
describe('dictionary', () => { describe('dictionary', () => {
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')
})
it('load a partial dictionary and merge it with the existing one', async () => { it('load a partial dictionary and merge it with the existing one', async () => {
await locale.set('en') await locale.set('en')
register('en', () => import('../fixtures/partials/en.json')) register('en', () => import('../fixtures/partials/en.json'))
@ -96,7 +85,8 @@ describe('formatting', () => {
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', async () => {
await waitLocale('pt-BR')
expect(_('switch.lang', { locale: 'pt' })).toBe('Trocar idioma') expect(_('switch.lang', { locale: 'pt' })).toBe('Trocar idioma')
}) })