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,
addMessages,
} from '../stores/dictionary';
import { getRelatedLocalesOf } from '../stores/locale';
import { getPossibleLocales } from '../stores/locale';
type Queue = Set<MessagesLoader>;
const queue: Record<string, Queue> = {};
@ -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[]) {

View File

@ -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;

View File

@ -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<LocalesDictionary>({});
@ -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[]) {

View File

@ -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())

View File

@ -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;

View File

@ -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');
});

View File

@ -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', () => {

View File

@ -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', () => {