fix: 🐛 support more specific fallback locale (i.e en-GB vs en)

 Closes: #137
This commit is contained in:
Christian Kaisermann 2021-03-27 13:16:28 -03:00
parent d082d0e25d
commit 5db1dbc3a4
8 changed files with 105 additions and 101 deletions

View File

@ -4,7 +4,7 @@ import {
$dictionary, $dictionary,
addMessages, addMessages,
} from '../stores/dictionary'; } from '../stores/dictionary';
import { getRelatedLocalesOf } from '../stores/locale'; import { getPossibleLocales } from '../stores/locale';
type Queue = Set<MessagesLoader>; type Queue = Set<MessagesLoader>;
const queue: Record<string, Queue> = {}; const queue: Record<string, Queue> = {};
@ -32,8 +32,7 @@ function getLocaleQueue(locale: string) {
} }
function getLocalesQueues(locale: string) { function getLocalesQueues(locale: string) {
return getRelatedLocalesOf(locale) return getPossibleLocales(locale)
.reverse()
.map<[string, MessagesLoader[]]>((localeItem) => { .map<[string, MessagesLoader[]]>((localeItem) => {
const localeQueue = getLocaleQueue(localeItem); const localeQueue = getLocaleQueue(localeItem);
@ -43,9 +42,9 @@ function getLocalesQueues(locale: string) {
} }
export function hasLocaleQueue(locale: string) { export function hasLocaleQueue(locale: string) {
return getRelatedLocalesOf(locale) return getPossibleLocales(locale).some(
.reverse() (localeQueue) => getLocaleQueue(localeQueue)?.size,
.some((localeQueue) => getLocaleQueue(localeQueue)?.size); );
} }
function loadLocaleQueue(locale: string, localeQueue: MessagesLoader[]) { function loadLocaleQueue(locale: string, localeQueue: MessagesLoader[]) {

View File

@ -1,5 +1,5 @@
import { getMessageFromDictionary } from '../stores/dictionary'; import { getMessageFromDictionary } from '../stores/dictionary';
import { getFallbackOf } from '../stores/locale'; import { getPossibleLocales } from '../stores/locale';
export const lookupCache: { export const lookupCache: {
[locale: string]: { [locale: string]: {
@ -15,25 +15,25 @@ const addToCache = (path: string, locale: string, message: string) => {
return message; return message;
}; };
const searchForMessage = (path: string, locale: string): any => { export const lookup = (path: string, refLocale: string) => {
if (locale == null) return undefined; if (refLocale == null) return undefined;
const message = getMessageFromDictionary(locale, path); if (refLocale in lookupCache && path in lookupCache[refLocale]) {
return lookupCache[refLocale][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];
} }
const message = searchForMessage(path, locale); const locales = getPossibleLocales(refLocale);
if (message) { for (let i = 0; i < locales.length; i++) {
return addToCache(path, locale, message); 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; return undefined;

View File

@ -2,9 +2,10 @@ import { writable, derived } from 'svelte/store';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
import type { LocaleDictionary, LocalesDictionary } from '../types/index'; import type { LocaleDictionary, LocalesDictionary } from '../types/index';
import { getFallbackOf } from './locale'; import { getPossibleLocales } from './locale';
import { delve } from '../../shared/delve'; import { delve } from '../../shared/delve';
import { lookupCache } from '../includes/lookup'; import { lookupCache } from '../includes/lookup';
import { locales } from '..';
let dictionary: LocalesDictionary; let dictionary: LocalesDictionary;
const $dictionary = writable<LocalesDictionary>({}); const $dictionary = writable<LocalesDictionary>({});
@ -33,10 +34,20 @@ export function getMessageFromDictionary(locale: string, id: string) {
return match; return match;
} }
export function getClosestAvailableLocale(locale: string): string | null { export function getClosestAvailableLocale(refLocale: string): string | null {
if (locale == null || hasLocaleDictionary(locale)) return locale; 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[]) { export function addMessages(locale: string, ...partials: LocaleDictionary[]) {

View File

@ -18,7 +18,7 @@ import {
} from '../includes/formatters'; } from '../includes/formatters';
import { getOptions } from '../configs'; import { getOptions } from '../configs';
import { $dictionary } from './dictionary'; import { $dictionary } from './dictionary';
import { getCurrentLocale, getRelatedLocalesOf, $locale } from './locale'; import { getCurrentLocale, getPossibleLocales, $locale } from './locale';
const formatMessage: MessageFormatter = (id, options = {}) => { const formatMessage: MessageFormatter = (id, options = {}) => {
if (typeof id === 'object') { if (typeof id === 'object') {
@ -44,7 +44,7 @@ const formatMessage: MessageFormatter = (id, options = {}) => {
if (getOptions().warnOnMissingMessages) { if (getOptions().warnOnMissingMessages) {
// istanbul ignore next // istanbul ignore next
console.warn( console.warn(
`[svelte-i18n] The message "${id}" was not found in "${getRelatedLocalesOf( `[svelte-i18n] The message "${id}" was not found in "${getPossibleLocales(
locale, locale,
).join('", "')}".${ ).join('", "')}".${
hasLocaleQueue(getCurrentLocale()) hasLocaleQueue(getCurrentLocale())

View File

@ -8,41 +8,33 @@ import { $isLoading } from './loading';
let current: string; let current: string;
const $locale = writable(null); 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; return localeB.indexOf(localeA) === 0 && localeA !== localeB;
} }
export function isRelatedLocale(localeA: string, localeB: string) { export function isRelatedLocale(localeA: string, localeB: string) {
return ( return (
localeA === localeB || localeA === localeB ||
isFallbackLocaleOf(localeA, localeB) || isFallbackLocale(localeA, localeB) ||
isFallbackLocaleOf(localeB, localeA) isFallbackLocale(localeB, localeA)
); );
} }
export function getFallbackOf(locale: string) { function getSubLocales(refLocale: string) {
const index = locale.lastIndexOf('-'); return refLocale
.split('-')
if (index > 0) return locale.slice(0, index); .map((_, i, arr) => arr.slice(0, i + 1).join('-'))
.reverse();
const { fallbackLocale } = getOptions();
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
return fallbackLocale;
}
return null;
} }
export function getRelatedLocalesOf(locale: string): string[] { export function getPossibleLocales(
const locales = locale refLocale: string,
.split('-') fallbackLocale = getOptions().fallbackLocale,
.map((_, i, arr) => arr.slice(0, i + 1).join('-')); ): string[] {
const locales = getSubLocales(refLocale);
const { fallbackLocale } = getOptions(); if (fallbackLocale) {
return [...new Set([...locales, ...getSubLocales(fallbackLocale)])];
if (fallbackLocale && !isRelatedLocale(locale, fallbackLocale)) {
return locales.concat(getRelatedLocalesOf(fallbackLocale));
} }
return locales; return locales;

View File

@ -1,3 +1,4 @@
import { init } from '../../../src/runtime/configs';
import { lookup, lookupCache } from '../../../src/runtime/includes/lookup'; import { lookup, lookupCache } from '../../../src/runtime/includes/lookup';
import { import {
$dictionary, $dictionary,
@ -91,3 +92,11 @@ test('clears a locale lookup cache when new messages are added', () => {
addMessages('en', { field: 'name2' }); addMessages('en', { field: 'name2' });
expect(lookup('field', 'en')).toBe('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');
});

View File

@ -68,7 +68,7 @@ test('gets the closest available locale', () => {
test("returns null if there's no closest locale available", () => { test("returns null if there's no closest locale available", () => {
addMessages('pt', { field_1: 'name' }); addMessages('pt', { field_1: 'name' });
expect(getClosestAvailableLocale('it-IT')).toBeNull(); expect(getClosestAvailableLocale('it-IT')).toBeUndefined();
}); });
test('lists all locales in the dictionary', () => { test('lists all locales in the dictionary', () => {

View File

@ -2,9 +2,8 @@ import { get } from 'svelte/store';
import { lookup } from '../../../src/runtime/includes/lookup'; import { lookup } from '../../../src/runtime/includes/lookup';
import { import {
isFallbackLocaleOf, isFallbackLocale,
getFallbackOf, getPossibleLocales,
getRelatedLocalesOf,
getCurrentLocale, getCurrentLocale,
$locale, $locale,
isRelatedLocale, isRelatedLocale,
@ -24,9 +23,9 @@ test('sets and gets the fallback locale', () => {
}); });
test('checks if a locale is a fallback locale of another locale', () => { test('checks if a locale is a fallback locale of another locale', () => {
expect(isFallbackLocaleOf('en', 'en-US')).toBe(true); expect(isFallbackLocale('en', 'en-US')).toBe(true);
expect(isFallbackLocaleOf('en', 'en')).toBe(false); expect(isFallbackLocale('en', 'en')).toBe(false);
expect(isFallbackLocaleOf('it', 'en-US')).toBe(false); expect(isFallbackLocale('it', 'en-US')).toBe(false);
}); });
test('checks if a locale is a related locale of another locale', () => { 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); expect(isRelatedLocale('en-US', 'it')).toBe(false);
}); });
test('gets the next fallback locale of a locale', () => { test('gets all possible locales from a reference locale', () => {
expect(getFallbackOf('az-Cyrl-AZ')).toBe('az-Cyrl'); expect(getPossibleLocales('en-US')).toEqual(['en-US', 'en']);
expect(getFallbackOf('en-US')).toBe('en'); expect(getPossibleLocales('az-Cyrl-AZ')).toEqual([
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',
'az-Cyrl-AZ', 'az-Cyrl-AZ',
'az-Cyrl',
'az',
]); ]);
}); });
test('gets all fallback locales of a locale including the global fallback locale', () => { test('gets all fallback locales of a locale including the global fallback locale', () => {
init({ fallbackLocale: 'pt' }); init({ fallbackLocale: 'pt' });
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']); expect(getPossibleLocales('en-US')).toEqual(['en-US', 'en', 'pt']);
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt']); expect(getPossibleLocales('az-Cyrl-AZ')).toEqual([
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
'az',
'az-Cyrl',
'az-Cyrl-AZ', 'az-Cyrl-AZ',
'az-Cyrl',
'az',
'pt', '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', () => { test('gets all fallback locales of a locale including the global fallback locale and its fallbacks', () => {
init({ fallbackLocale: 'pt-BR' }); expect(getPossibleLocales('en-US', 'pt-BR')).toEqual([
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']); 'en-US',
expect(getRelatedLocalesOf('en-US')).toEqual(['en', 'en-US', 'pt', 'pt-BR']); 'en',
expect(getRelatedLocalesOf('az-Cyrl-AZ')).toEqual([
'az',
'az-Cyrl',
'az-Cyrl-AZ',
'pt',
'pt-BR', '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", () => { test("don't list fallback locale twice", () => {
init({ fallbackLocale: 'pt-BR' }); expect(getPossibleLocales('pt-BR', 'pt-BR')).toEqual(['pt-BR', 'pt']);
expect(getRelatedLocalesOf('pt-BR')).toEqual(['pt', 'pt-BR']); expect(getPossibleLocales('pt', 'pt-BR')).toEqual(['pt', 'pt-BR']);
expect(getRelatedLocalesOf('pt')).toEqual(['pt']);
}); });
test('gets the current locale', () => { test('gets the current locale', () => {