diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa041f..04cf507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [3.5.0-alpha.0](https://github.com/kaisermann/svelte-i18n/compare/v3.4.0...v3.5.0-alpha.0) (2022-11-19) + + +### Features + +* support using an array of locales for setting current locale ([280c5b4](https://github.com/kaisermann/svelte-i18n/commit/280c5b43fbc0a786ab8ee9e388db49d30af1a009)) + + + ## [3.4.1](https://github.com/kaisermann/svelte-i18n/compare/v3.4.0...v3.4.1) (2022-11-19) diff --git a/package.json b/package.json index a8118c0..080a7a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-i18n", - "version": "3.4.1", + "version": "3.5.0-alpha.0", "main": "dist/runtime.cjs.js", "module": "dist/runtime.esm.js", "types": "dist/runtime.d.ts", diff --git a/src/runtime/includes/loaderQueue.ts b/src/runtime/includes/loaderQueue.ts index bc6d9d3..dff8c43 100644 --- a/src/runtime/includes/loaderQueue.ts +++ b/src/runtime/includes/loaderQueue.ts @@ -101,6 +101,7 @@ export function registerLocaleLoader(locale: string, loader: MessagesLoader) { // istanbul ignore if if (getLocaleQueue(locale).has(loader)) return; + // Add an empty dictionary for this locale if it doesn't exist already if (!hasLocaleDictionary(locale)) { $dictionary.update((d) => { d[locale] = {}; diff --git a/src/runtime/stores/dictionary.ts b/src/runtime/stores/dictionary.ts index a4d9b56..010470d 100644 --- a/src/runtime/stores/dictionary.ts +++ b/src/runtime/stores/dictionary.ts @@ -35,7 +35,7 @@ export function getMessageFromDictionary(locale: string, id: string) { } export function getClosestAvailableLocale( - refLocale: string | null | undefined, + refLocale: string | string[] | null | undefined, ): string | undefined { if (refLocale == null) return undefined; diff --git a/src/runtime/stores/formatters.ts b/src/runtime/stores/formatters.ts index 551eb7a..de10c3e 100644 --- a/src/runtime/stores/formatters.ts +++ b/src/runtime/stores/formatters.ts @@ -34,6 +34,8 @@ const formatMessage: MessageFormatter = (id, options = {}) => { default: defaultValue, } = messageObj; + locale; // + if (locale == null) { throw new Error( '[svelte-i18n] Cannot format a message without first setting the initial locale.', diff --git a/src/runtime/stores/locale.ts b/src/runtime/stores/locale.ts index d843aa7..27e5126 100644 --- a/src/runtime/stores/locale.ts +++ b/src/runtime/stores/locale.ts @@ -5,8 +5,9 @@ import { getOptions } from '../configs'; import { getClosestAvailableLocale } from './dictionary'; import { $isLoading } from './loading'; -let current: string | null | undefined; -const internalLocale = writable(null); +type LocaleStoreValue = string | null | undefined; +let current: LocaleStoreValue; +const internalLocale = writable(null); function getSubLocales(refLocale: string) { return refLocale @@ -16,23 +17,25 @@ function getSubLocales(refLocale: string) { } export function getPossibleLocales( - refLocale: string, + referenceLocales: string | string[], fallbackLocale = getOptions().fallbackLocale, ): string[] { - const locales = getSubLocales(refLocale); + const allSubLocales = Array.isArray(referenceLocales) + ? referenceLocales.flatMap((locale) => getSubLocales(locale)) + : getSubLocales(referenceLocales); if (fallbackLocale) { - return [...new Set([...locales, ...getSubLocales(fallbackLocale)])]; + return [...new Set([...allSubLocales, ...getSubLocales(fallbackLocale)])]; } - return locales; + return allSubLocales; } export function getCurrentLocale() { return current ?? undefined; } -internalLocale.subscribe((newLocale: string | null | undefined) => { +internalLocale.subscribe((newLocale: LocaleStoreValue) => { current = newLocale ?? undefined; if (typeof window !== 'undefined' && newLocale != null) { @@ -40,46 +43,69 @@ internalLocale.subscribe((newLocale: string | null | undefined) => { } }); -const set = (newLocale: string | null | undefined): void | Promise => { +/** + * Sets the current locale and loads any pending messages + * for the specified locale. + * + * If an array of locales is passed, the first locale available + * in the dictionary will be used. + * + * Note: for a locale to be available, it must have been loaded + * or registered via (`addMessages` or `register`). + */ +const set = ( + newLocale: string | string[] | null | undefined, +): Promise => { + const availableLocale = Array.isArray(newLocale) + ? /** + * if an array was passed, get the closest available locale + * i.e if the dictionary has 'en', 'de' and 'es' and the user requests + * 'it' and 'es', 'es' will be used + */ + getClosestAvailableLocale(newLocale) + : newLocale; + + if (!hasLocaleQueue(availableLocale)) { + internalLocale.set(availableLocale); + + return Promise.resolve(); + } + + const { loadingDelay } = getOptions(); + + let loadingTimer: number; + + // if there's no current locale, we don't wait to set isLoading to true + // because it would break pages when loading the initial locale if ( - newLocale && - getClosestAvailableLocale(newLocale) && - hasLocaleQueue(newLocale) + typeof window !== 'undefined' && + getCurrentLocale() != null && + loadingDelay ) { - const { loadingDelay } = getOptions(); - - let loadingTimer: number; - - // if there's no current locale, we don't wait to set isLoading to true - // because it would break pages when loading the initial locale - if ( - typeof window !== 'undefined' && - getCurrentLocale() != null && - loadingDelay - ) { - loadingTimer = window.setTimeout( - () => $isLoading.set(true), - loadingDelay, - ); - } else { + loadingTimer = window.setTimeout(() => { $isLoading.set(true); - } - - return flush(newLocale as string) - .then(() => { - internalLocale.set(newLocale); - }) - .finally(() => { - clearTimeout(loadingTimer); - $isLoading.set(false); - }); + }, loadingDelay); + } else { + $isLoading.set(true); } - return internalLocale.set(newLocale); + return flush(newLocale as string) + .then(() => { + internalLocale.set(availableLocale); + }) + .finally(() => { + clearTimeout(loadingTimer); + $isLoading.set(false); + }); }; const $locale = { - ...internalLocale, + subscribe: internalLocale.subscribe, + update: ( + fn: (value: LocaleStoreValue) => string | string[] | null | undefined, + ) => { + return set(fn(getCurrentLocale())); + }, set, }; diff --git a/test/runtime/stores/dictionary.test.ts b/test/runtime/stores/dictionary.test.ts index 20f90d7..093884f 100644 --- a/test/runtime/stores/dictionary.test.ts +++ b/test/runtime/stores/dictionary.test.ts @@ -61,16 +61,6 @@ test('checks if a locale dictionary exists', () => { expect(hasLocaleDictionary('pt')).toBe(true); }); -test('gets the closest available locale', () => { - addMessages('pt', { field_1: 'name' }); - expect(getClosestAvailableLocale('pt-BR')).toBe('pt'); -}); - -test("returns null if there's no closest locale available", () => { - addMessages('pt', { field_1: 'name' }); - expect(getClosestAvailableLocale('it-IT')).toBeUndefined(); -}); - test('lists all locales in the dictionary', () => { addMessages('en', {}); addMessages('pt', {}); @@ -137,3 +127,23 @@ describe('getting messages', () => { expect(getMessageFromDictionary('en', 'foo.potato')).toBeUndefined(); }); }); + +describe('getClosestAvailableLocale', () => { + it('gets the closest available locale', () => { + addMessages('pt', { field_1: 'name' }); + expect(getClosestAvailableLocale('pt-BR')).toBe('pt'); + }); + + it("returns undefined if there's no closest locale available", () => { + addMessages('pt', { field_1: 'name' }); + expect(getClosestAvailableLocale('it-IT')).toBeUndefined(); + }); + + it('gets the closest available locale of a list of locales', () => { + addMessages('pt', { field_1: 'name' }); + addMessages('es', { field_1: 'name' }); + + expect(getClosestAvailableLocale(['pt-BR'])).toBe('pt'); + expect(getClosestAvailableLocale(['es'])).toBe('es'); + }); +}); diff --git a/test/runtime/stores/locale.test.ts b/test/runtime/stores/locale.test.ts index 996243f..17df686 100644 --- a/test/runtime/stores/locale.test.ts +++ b/test/runtime/stores/locale.test.ts @@ -9,10 +9,12 @@ import { import { getOptions, init } from '../../../src/runtime/configs'; import { register, isLoading } from '../../../src/runtime'; import { hasLocaleQueue } from '../../../src/runtime/includes/loaderQueue'; +import { $dictionary } from '../../../src/runtime/stores/dictionary'; beforeEach(() => { init({ fallbackLocale: undefined as any }); $locale.set(undefined); + $dictionary.set({}); }); test('sets and gets the fallback locale', () => { @@ -20,61 +22,6 @@ test('sets and gets the fallback locale', () => { expect(getOptions().fallbackLocale).toBe('en'); }); -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(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', () => { - 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", () => { - expect(getPossibleLocales('pt-BR', 'pt-BR')).toEqual(['pt-BR', 'pt']); - expect(getPossibleLocales('pt', 'pt-BR')).toEqual(['pt', 'pt-BR']); -}); - test('gets the current locale', () => { expect(getCurrentLocale()).toBeUndefined(); $locale.set('es-ES'); @@ -143,3 +90,108 @@ test("if a locale is set, don't ignore the loading delay", async () => { expect(get(isLoading)).toBe(false); }); + +describe('array input', () => { + it('sets the locale to the first available locale in the array', async () => { + init({ fallbackLocale: 'en' }); + + register('en', () => Promise.resolve({ foo: 'Foo' })); + register('de', () => Promise.resolve({ foo: 'Foo' })); + register('es', () => Promise.resolve({ foo: 'Foo' })); + + expect(get($locale)).toBe('en'); + + await $locale.set(['it', 'es']); + + expect(get($locale)).toBe('es'); + }); + + it('sets the locale to the first closest available locale in the array', async () => { + init({ fallbackLocale: 'en' }); + + register('en', () => Promise.resolve({ foo: 'Foo' })); + register('es', () => Promise.resolve({ foo: 'Foo' })); + + expect(get($locale)).toBe('en'); + + await $locale.set(['it', 'es-AR']); + + expect(get($locale)).toBe('es'); + }); + + it('sets the locale to undefined if no locale passed is available', async () => { + init({ fallbackLocale: 'en' }); + + expect(get($locale)).toBe('en'); + + await $locale.set(['it', 'es']); + + expect(get($locale)).toBeUndefined(); + }); +}); + +describe('getPossibleLocales', () => { + it('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', + ]); + }); + + it('gets all possible locales from a list of reference locales', () => { + expect(getPossibleLocales(['en-US', 'es-AR'])).toEqual([ + 'en-US', + 'en', + 'es-AR', + 'es', + ]); + }); + + it('gets all fallback locales of a locale including the global fallback locale', () => { + init({ fallbackLocale: 'pt' }); + expect(getPossibleLocales('en-US')).toEqual(['en-US', 'en', 'pt']); + expect(getPossibleLocales('az-Cyrl-AZ')).toEqual([ + 'az-Cyrl-AZ', + 'az-Cyrl', + 'az', + 'pt', + ]); + }); + + it('remove duplicate fallback locales', () => { + expect(getPossibleLocales('en-AU', 'en-GB')).toEqual([ + 'en-AU', + 'en', + 'en-GB', + ]); + }); + + it('gets all fallback locales of a locale including the global fallback locale and its fallbacks', () => { + 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', + ]); + }); + + it("don't list fallback locale twice", () => { + expect(getPossibleLocales('pt-BR', 'pt-BR')).toEqual(['pt-BR', 'pt']); + expect(getPossibleLocales('pt', 'pt-BR')).toEqual(['pt', 'pt-BR']); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 2c808d4..247dba2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "strictNullChecks": true, "noImplicitAny": true, - "sourceMap": false, + "sourceMap": true, "module": "es2020", "moduleResolution": "node", "skipLibCheck": true,