diff --git a/src/runtime/includes/loaderQueue.ts b/src/runtime/includes/loaderQueue.ts index 58c8158..6b01161 100644 --- a/src/runtime/includes/loaderQueue.ts +++ b/src/runtime/includes/loaderQueue.ts @@ -4,7 +4,7 @@ import { $dictionary, addMessages, } from '../stores/dictionary'; -import { getRelatedLocalesOf } from '../stores/locale'; +import { getPossibleLocales } from '../stores/locale'; type Queue = Set; const queue: Record = {}; @@ -32,8 +32,7 @@ function getLocaleQueue(locale: string) { } function getLocalesQueues(locale: string) { - return getRelatedLocalesOf(locale) - .reverse() + return getPossibleLocales(locale) .map<[string, MessagesLoader[]]>((localeItem) => { const localeQueue = getLocaleQueue(localeItem); @@ -43,9 +42,9 @@ function getLocalesQueues(locale: string) { } export function hasLocaleQueue(locale: string) { - return getRelatedLocalesOf(locale) - .reverse() - .some((localeQueue) => getLocaleQueue(localeQueue)?.size); + return getPossibleLocales(locale).some( + (localeQueue) => getLocaleQueue(localeQueue)?.size, + ); } function loadLocaleQueue(locale: string, localeQueue: MessagesLoader[]) { diff --git a/src/runtime/includes/lookup.ts b/src/runtime/includes/lookup.ts index d0d282d..921fe7c 100644 --- a/src/runtime/includes/lookup.ts +++ b/src/runtime/includes/lookup.ts @@ -1,5 +1,5 @@ import { getMessageFromDictionary } from '../stores/dictionary'; -import { getFallbackOf } from '../stores/locale'; +import { getPossibleLocales } from '../stores/locale'; export const lookupCache: { [locale: string]: { @@ -15,25 +15,25 @@ const addToCache = (path: string, locale: string, message: string) => { return message; }; -const searchForMessage = (path: string, locale: string): any => { - if (locale == null) return undefined; +export const lookup = (path: string, refLocale: string) => { + if (refLocale == null) return undefined; - const message = getMessageFromDictionary(locale, path); - - if (message) return message; - - return searchForMessage(path, getFallbackOf(locale)); -}; - -export const lookup = (path: string, locale: string) => { - if (locale in lookupCache && path in lookupCache[locale]) { - return lookupCache[locale][path]; + if (refLocale in lookupCache && path in lookupCache[refLocale]) { + return lookupCache[refLocale][path]; } - const message = searchForMessage(path, locale); + const locales = getPossibleLocales(refLocale); - if (message) { - return addToCache(path, locale, message); + for (let i = 0; i < locales.length; i++) { + const locale = locales[i]; + const message = getMessageFromDictionary(locale, path); + + if (message) { + // Used the requested locale as the cache key + // Ex: { en: { title: "Title" }} + // lookup('title', 'en-GB') should cache with 'en-GB' instead of 'en' + return addToCache(path, refLocale, message); + } } return undefined; diff --git a/src/runtime/stores/dictionary.ts b/src/runtime/stores/dictionary.ts index 9df6672..bce55ea 100644 --- a/src/runtime/stores/dictionary.ts +++ b/src/runtime/stores/dictionary.ts @@ -2,9 +2,10 @@ import { writable, derived } from 'svelte/store'; import deepmerge from 'deepmerge'; import type { LocaleDictionary, LocalesDictionary } from '../types/index'; -import { getFallbackOf } from './locale'; +import { getPossibleLocales } from './locale'; import { delve } from '../../shared/delve'; import { lookupCache } from '../includes/lookup'; +import { locales } from '..'; let dictionary: LocalesDictionary; const $dictionary = writable({}); @@ -33,10 +34,20 @@ export function getMessageFromDictionary(locale: string, id: string) { return match; } -export function getClosestAvailableLocale(locale: string): string | null { - if (locale == null || hasLocaleDictionary(locale)) return locale; +export function getClosestAvailableLocale(refLocale: string): string | null { + if (refLocale == null) return undefined; - return getClosestAvailableLocale(getFallbackOf(locale)); + const relatedLocales = getPossibleLocales(refLocale); + + for (let i = 0; i < relatedLocales.length; i++) { + const locale = relatedLocales[i]; + + if (hasLocaleDictionary(locale)) { + return locale; + } + } + + return undefined; } export function addMessages(locale: string, ...partials: LocaleDictionary[]) { diff --git a/src/runtime/stores/formatters.ts b/src/runtime/stores/formatters.ts index 55957f2..3a0eb1b 100644 --- a/src/runtime/stores/formatters.ts +++ b/src/runtime/stores/formatters.ts @@ -18,7 +18,7 @@ import { } from '../includes/formatters'; import { getOptions } from '../configs'; import { $dictionary } from './dictionary'; -import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale'; +import { getCurrentLocale, getPossibleLocales, $locale } from './locale'; const formatMessage: MessageFormatter = (id, options = {}) => { if (typeof id === 'object') { @@ -44,7 +44,7 @@ const formatMessage: MessageFormatter = (id, options = {}) => { if (getOptions().warnOnMissingMessages) { // istanbul ignore next console.warn( - `[svelte-i18n] The message "${id}" was not found in "${getRelatedLocalesOf( + `[svelte-i18n] The message "${id}" was not found in "${getPossibleLocales( locale, ).join('", "')}".${ hasLocaleQueue(getCurrentLocale()) diff --git a/src/runtime/stores/locale.ts b/src/runtime/stores/locale.ts index d879cc1..7888736 100644 --- a/src/runtime/stores/locale.ts +++ b/src/runtime/stores/locale.ts @@ -8,41 +8,33 @@ import { $isLoading } from './loading'; let current: string; const $locale = writable(null); -export function isFallbackLocaleOf(localeA: string, localeB: string) { +export function isFallbackLocale(localeA: string, localeB: string) { return localeB.indexOf(localeA) === 0 && localeA !== localeB; } export function isRelatedLocale(localeA: string, localeB: string) { return ( localeA === localeB || - isFallbackLocaleOf(localeA, localeB) || - isFallbackLocaleOf(localeB, localeA) + isFallbackLocale(localeA, localeB) || + isFallbackLocale(localeB, localeA) ); } -export function getFallbackOf(locale: string) { - const index = locale.lastIndexOf('-'); - - if (index > 0) return locale.slice(0, index); - - const { fallbackLocale } = getOptions(); - - if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) { - return fallbackLocale; - } - - return null; +function getSubLocales(refLocale: string) { + return refLocale + .split('-') + .map((_, i, arr) => arr.slice(0, i + 1).join('-')) + .reverse(); } -export function getRelatedLocalesOf(locale: string): string[] { - const locales = locale - .split('-') - .map((_, i, arr) => arr.slice(0, i + 1).join('-')); +export function getPossibleLocales( + refLocale: string, + fallbackLocale = getOptions().fallbackLocale, +): string[] { + const locales = getSubLocales(refLocale); - const { fallbackLocale } = getOptions(); - - if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) { - return locales.concat(getRelatedLocalesOf(fallbackLocale)); + if (fallbackLocale) { + return [...new Set([...locales, ...getSubLocales(fallbackLocale)])]; } return locales; diff --git a/test/runtime/includes/lookup.test.ts b/test/runtime/includes/lookup.test.ts index f989f2b..e4cf6b7 100644 --- a/test/runtime/includes/lookup.test.ts +++ b/test/runtime/includes/lookup.test.ts @@ -1,3 +1,4 @@ +import { init } from '../../../src/runtime/configs'; import { lookup, lookupCache } from '../../../src/runtime/includes/lookup'; import { $dictionary, @@ -91,3 +92,11 @@ test('clears a locale lookup cache when new messages are added', () => { addMessages('en', { field: 'name2' }); expect(lookup('field', 'en')).toBe('name2'); }); + +test('fallback to fallback locale', () => { + init({ fallbackLocale: 'en-GB', initialLocale: 'en-AU' }); + + addMessages('en-GB', { field: 'name' }); + + expect(lookup('field', 'en-AU')).toBe('name'); +}); diff --git a/test/runtime/stores/dictionary.test.ts b/test/runtime/stores/dictionary.test.ts index ea7fa2d..20f90d7 100644 --- a/test/runtime/stores/dictionary.test.ts +++ b/test/runtime/stores/dictionary.test.ts @@ -68,7 +68,7 @@ test('gets the closest available locale', () => { test("returns null if there's no closest locale available", () => { addMessages('pt', { field_1: 'name' }); - expect(getClosestAvailableLocale('it-IT')).toBeNull(); + expect(getClosestAvailableLocale('it-IT')).toBeUndefined(); }); test('lists all locales in the dictionary', () => { diff --git a/test/runtime/stores/locale.test.ts b/test/runtime/stores/locale.test.ts index 72e2511..2b973e9 100644 --- a/test/runtime/stores/locale.test.ts +++ b/test/runtime/stores/locale.test.ts @@ -2,9 +2,8 @@ import { get } from 'svelte/store'; import { lookup } from '../../../src/runtime/includes/lookup'; import { - isFallbackLocaleOf, - getFallbackOf, - getRelatedLocalesOf, + isFallbackLocale, + getPossibleLocales, getCurrentLocale, $locale, isRelatedLocale, @@ -24,9 +23,9 @@ test('sets and gets the fallback locale', () => { }); test('checks if a locale is a fallback locale of another locale', () => { - expect(isFallbackLocaleOf('en', 'en-US')).toBe(true); - expect(isFallbackLocaleOf('en', 'en')).toBe(false); - expect(isFallbackLocaleOf('it', 'en-US')).toBe(false); + expect(isFallbackLocale('en', 'en-US')).toBe(true); + expect(isFallbackLocale('en', 'en')).toBe(false); + expect(isFallbackLocale('it', 'en-US')).toBe(false); }); test('checks if a locale is a related locale of another locale', () => { @@ -37,65 +36,59 @@ test('checks if a locale is a related locale of another locale', () => { expect(isRelatedLocale('en-US', 'it')).toBe(false); }); -test('gets the next fallback locale of a locale', () => { - expect(getFallbackOf('az-Cyrl-AZ')).toBe('az-Cyrl'); - expect(getFallbackOf('en-US')).toBe('en'); - expect(getFallbackOf('en')).toBeNull(); -}); - -test('gets the global fallback locale if set', () => { - init({ fallbackLocale: 'en' }); - expect(getFallbackOf('it')).toBe('en'); -}); - -test('should not get the global fallback as the fallback of itself', () => { - init({ fallbackLocale: 'en' }); - expect(getFallbackOf('en')).toBeNull(); -}); - -test('if global fallback locale has a fallback, it should return it', () => { - init({ fallbackLocale: 'en-US' }); - expect(getFallbackOf('en-US')).toBe('en'); -}); - -test('gets all fallback locales of a locale', () => { - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US']); - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US']); - expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([ - 'az', - 'az-Cyrl', +test('gets all possible locales from a reference locale', () => { + expect(getPossibleLocales('en-US')).toEqual(['en-US', 'en']); + expect(getPossibleLocales('az-Cyrl-AZ')).toEqual([ 'az-Cyrl-AZ', + 'az-Cyrl', + 'az', ]); }); test('gets all fallback locales of a locale including the global fallback locale', () => { init({ fallbackLocale: 'pt' }); - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']); - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']); - expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([ - 'az', - 'az-Cyrl', + expect(getPossibleLocales('en-US')).toEqual(['en-US', 'en', 'pt']); + expect(getPossibleLocales('az-Cyrl-AZ')).toEqual([ 'az-Cyrl-AZ', + 'az-Cyrl', + 'az', 'pt', ]); }); + +test('remove duplicate fallback locales', () => { + expect(getPossibleLocales('en-AU', 'en-GB')).toEqual([ + 'en-AU', + 'en', + 'en-GB', + ]); +}); + test('gets all fallback locales of a locale including the global fallback locale and its fallbacks', () => { - init({ fallbackLocale: 'pt-BR' }); - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']); - expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']); - expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([ - 'az', - 'az-Cyrl', - 'az-Cyrl-AZ', - 'pt', + expect(getPossibleLocales('en-US', 'pt-BR')).toEqual([ + 'en-US', + 'en', 'pt-BR', + 'pt', + ]); + expect(getPossibleLocales('en-US', 'pt-BR')).toEqual([ + 'en-US', + 'en', + 'pt-BR', + 'pt', + ]); + expect(getPossibleLocales('az-Cyrl-AZ', 'pt-BR')).toEqual([ + 'az-Cyrl-AZ', + 'az-Cyrl', + 'az', + 'pt-BR', + 'pt', ]); }); test("don't list fallback locale twice", () => { - init({ fallbackLocale: 'pt-BR' }); - expect(getRelatedLocalesOf('pt-BR')).toEqual(['pt', 'pt-BR']); - expect(getRelatedLocalesOf('pt')).toEqual(['pt']); + expect(getPossibleLocales('pt-BR', 'pt-BR')).toEqual(['pt-BR', 'pt']); + expect(getPossibleLocales('pt', 'pt-BR')).toEqual(['pt', 'pt-BR']); }); test('gets the current locale', () => {