diff --git a/docs/Methods.md b/docs/Methods.md index b866eee..ab7b4df 100644 --- a/docs/Methods.md +++ b/docs/Methods.md @@ -26,27 +26,41 @@ Method responsible for configuring some of the library behaviours such as the gl ```ts interface InitOptions { - // the global fallback locale - fallbackLocale: string - // the app initial locale - initialLocale?: string - // custom time/date/number formats - formats?: Formats - // loading delay interval - loadingDelay?: number + /** The global fallback locale **/ + fallbackLocale: string; + /** The app initial locale **/ + initialLocale?: string | null; + /** Custom time/date/number formats **/ + formats?: Formats; + /** Loading delay interval **/ + loadingDelay?: number; + /** + * @deprecated Use `handleMissingMessage` instead. + * */ + warnOnMissingMessages?: boolean; + /** + * Optional method that is executed whenever a message is missing. + * It may return a string to use as the fallback. + */ + handleMissingMessage?: MissingKeyHandler; + /** + * Whether to treat HTML/XML tags as string literal instead of parsing them as tag token. + * When this is false we only allow simple tags without any attributes + * */ + ignoreTag: boolean; } ``` **Example**: ```js -import { init } from 'svelte-i18n' +import { init } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: 'pt-br', -}) +}); ``` ##### Custom formats @@ -55,9 +69,9 @@ It's possible to define custom format styles via the `formats` property if you w ```ts interface Formats { - number: Record - date: Record - time: Record + number: Record; + date: Record; + time: Record; } ``` @@ -66,7 +80,7 @@ Please refer to the [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs **Example**: ```js -import { init } from 'svelte-i18n' +import { init } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary @@ -76,13 +90,11 @@ init({ EUR: { style: 'currency', currency: 'EUR' }, }, }, -}) +}); ``` ```html -
- {$_.number(123456.789, { format: 'EUR' })} -
+
{$_.number(123456.789, { format: 'EUR' })}
``` @@ -96,13 +108,13 @@ Utility method to help getting a initial locale based on a pattern of the curren **Example**: ```js -import { init, getLocaleFromHostname } from 'svelte-i18n' +import { init, getLocaleFromHostname } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: getLocaleFromHostname(/^(.*?)\./), -}) +}); ``` #### `getLocaleFromPathname` @@ -116,13 +128,13 @@ Utility method to help getting a initial locale based on a pattern of the curren **Example**: ```js -import { init, getLocaleFromPathname } from 'svelte-i18n' +import { init, getLocaleFromPathname } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: getLocaleFromPathname(/^\/(.*?)\//), -}) +}); ``` #### `getLocaleFromNavigator` @@ -136,13 +148,13 @@ Utility method to help getting a initial locale based on the browser's `navigato **Example**: ```js -import { init, getLocaleFromNavigator } from 'svelte-i18n' +import { init, getLocaleFromNavigator } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: getLocaleFromNavigator(), -}) +}); ``` #### `getLocaleFromQueryString` @@ -154,13 +166,13 @@ init({ Utility method to help getting a initial locale based on a query string value. ```js -import { init, getLocaleFromQueryString } from 'svelte-i18n' +import { init, getLocaleFromQueryString } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: getLocaleFromQueryString('lang'), -}) +}); ``` #### `getLocaleFromHash` @@ -174,13 +186,13 @@ Utility method to help getting a initial locale based on a hash `{key}={value}` **Example**: ```js -import { init, getLocaleFromHash } from 'svelte-i18n' +import { init, getLocaleFromHash } from 'svelte-i18n'; init({ // fallback to en if current locale is not in the dictionary fallbackLocale: 'en', initialLocale: getLocaleFromHash('lang'), -}) +}); ``` #### `addMessages` @@ -224,10 +236,10 @@ Registers an async message `loader` for the specified `locale`. The loader queue **Example**: ```js -import { register } from 'svelte-i18n' +import { register } from 'svelte-i18n'; -register('en', () => import('./_locales/en.json')) -register('pt', () => import('./_locales/pt.json')) +register('en', () => import('./_locales/en.json')); +register('pt', () => import('./_locales/pt.json')); ``` See [how to asynchronously load dictionaries](/svelte-i18n/blob/master/docs#22-asynchronous). diff --git a/src/runtime/configs.ts b/src/runtime/configs.ts index 5501714..d7cef18 100644 --- a/src/runtime/configs.ts +++ b/src/runtime/configs.ts @@ -1,5 +1,10 @@ -import type { ConfigureOptions, ConfigureOptionsInit } from './types'; -import { $locale } from './stores/locale'; +import type { + ConfigureOptions, + ConfigureOptionsInit, + MissingKeyHandlerInput, +} from './types'; +import { $locale, getCurrentLocale, getPossibleLocales } from './stores/locale'; +import { hasLocaleQueue } from './includes/loaderQueue'; interface Formats { number: Record; @@ -38,11 +43,28 @@ export const defaultFormats: Formats = { }, }; +/** + * Default missing key handler used in case "warnOnMissingMessages" is set to true. + */ +function defaultMissingKeyHandler({ locale, id }: MissingKeyHandlerInput) { + // istanbul ignore next + console.warn( + `[svelte-i18n] The message "${id}" was not found in "${getPossibleLocales( + locale, + ).join('", "')}".${ + hasLocaleQueue(getCurrentLocale()) + ? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.` + : '' + }`, + ); +} + export const defaultOptions: ConfigureOptions = { fallbackLocale: null as any, loadingDelay: 200, formats: defaultFormats, warnOnMissingMessages: true, + handleMissingMessage: undefined, ignoreTag: true, }; @@ -57,6 +79,18 @@ export function init(opts: ConfigureOptionsInit) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialLocale = opts.initialLocale || opts.fallbackLocale; + if (rest.warnOnMissingMessages) { + delete rest.warnOnMissingMessages; + + if (rest.handleMissingMessage == null) { + rest.handleMissingMessage = defaultMissingKeyHandler; + } else { + console.warn( + '[svelte-i18n] The "warnOnMissingMessages" option is deprecated. Please use the "handleMissingMessage" option instead.', + ); + } + } + Object.assign(options, rest, { initialLocale }); if (formats) { diff --git a/src/runtime/stores/formatters.ts b/src/runtime/stores/formatters.ts index 18326c5..989f35b 100644 --- a/src/runtime/stores/formatters.ts +++ b/src/runtime/stores/formatters.ts @@ -9,7 +9,6 @@ import type { JSONGetter, } from '../types'; import { lookup } from '../includes/lookup'; -import { hasLocaleQueue } from '../includes/loaderQueue'; import { getMessageFormatter, getTimeFormatter, @@ -18,7 +17,7 @@ import { } from '../includes/formatters'; import { getOptions } from '../configs'; import { $dictionary } from './dictionary'; -import { getCurrentLocale, getPossibleLocales, $locale } from './locale'; +import { getCurrentLocale, $locale } from './locale'; const formatMessage: MessageFormatter = (id, options = {}) => { let messageObj = options as MessageObject; @@ -43,20 +42,10 @@ const formatMessage: MessageFormatter = (id, options = {}) => { let message = lookup(id, locale); if (!message) { - if (getOptions().warnOnMissingMessages) { - // istanbul ignore next - console.warn( - `[svelte-i18n] The message "${id}" was not found in "${getPossibleLocales( - locale, - ).join('", "')}".${ - hasLocaleQueue(getCurrentLocale()) - ? `\n\nNote: there are at least one loader still registered to this locale that wasn't executed.` - : '' - }`, - ); - } - - message = defaultValue ?? id; + message = + getOptions().handleMissingMessage?.({ locale, id, defaultValue }) ?? + defaultValue ?? + id; } else if (typeof message !== 'string') { console.warn( `[svelte-i18n] Message with id "${id}" must be of type "string", found: "${typeof message}". Gettin its value through the "$format" method is deprecated; use the "json" method instead.`, diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index 16ee4b3..d579a27 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -72,12 +72,40 @@ export interface MessagesLoader { (): Promise; } +export type MissingKeyHandlerInput = { + locale: string; + id: string; + defaultValue: string | undefined; +}; + +export type MissingKeyHandlerOutput = string | void | undefined; + +export type MissingKeyHandler = ( + input: MissingKeyHandlerInput, +) => MissingKeyHandlerOutput; + export interface ConfigureOptions { + /** The global fallback locale * */ fallbackLocale: string; + /** The app initial locale * */ initialLocale?: string | null; + /** Custom time/date/number formats * */ formats: Formats; + /** Loading delay interval * */ loadingDelay: number; - warnOnMissingMessages: boolean; + /** + * @deprecated Use `handleMissingMessage` instead. + * */ + warnOnMissingMessages?: boolean; + /** + * Optional method that is executed whenever a message is missing. + * It may return a string to use as the fallback. + */ + handleMissingMessage?: MissingKeyHandler; + /** + * Whether to treat HTML/XML tags as string literal instead of parsing them as tag token. + * When this is false we only allow simple tags without any attributes + * */ ignoreTag: boolean; } diff --git a/test/runtime/configs.test.ts b/test/runtime/configs.test.ts index e182ad4..07a6484 100644 --- a/test/runtime/configs.test.ts +++ b/test/runtime/configs.test.ts @@ -1,20 +1,18 @@ /* eslint-disable node/global-require */ import { get } from 'svelte/store'; -import { - init, - getOptions, - defaultOptions, - defaultFormats, -} from '../../src/runtime/configs'; +import { init, getOptions, defaultFormats } from '../../src/runtime/configs'; import { $locale } from '../../src/runtime/stores/locale'; +const warnSpy = jest.spyOn(global.console, 'warn').mockImplementation(); + beforeEach(() => { - init(defaultOptions as any); + warnSpy.mockReset(); }); test('inits the fallback locale', () => { expect(getOptions().fallbackLocale).toBeNull(); + init({ fallbackLocale: 'en', }); @@ -45,3 +43,20 @@ test('sets the minimum delay to set the loading store value', () => { init({ fallbackLocale: 'en', loadingDelay: 300 }); expect(getOptions().loadingDelay).toBe(300); }); + +test('defines default missing key handler if "warnOnMissingMessages" is "true"', () => { + init({ fallbackLocale: 'en', warnOnMissingMessages: true }); + expect(typeof getOptions().handleMissingMessage).toBe('function'); +}); + +test('warns about using deprecated "warnOnMissingMessages" alongside "handleMissingMessage"', () => { + init({ + fallbackLocale: 'en', + warnOnMissingMessages: true, + handleMissingMessage() {}, + }); + + expect(warnSpy).toHaveBeenCalledWith( + '[svelte-i18n] The "warnOnMissingMessages" option is deprecated. Please use the "handleMissingMessage" option instead.', + ); +}); diff --git a/test/runtime/stores/formatters.test.ts b/test/runtime/stores/formatters.test.ts index f812e39..7882c5d 100644 --- a/test/runtime/stores/formatters.test.ts +++ b/test/runtime/stores/formatters.test.ts @@ -7,6 +7,7 @@ import type { TimeFormatter, DateFormatter, NumberFormatter, + MissingKeyHandler, } from '../../../src/runtime/types/index'; import { $format, @@ -39,12 +40,10 @@ addMessages('pt', require('../../fixtures/pt.json')); addMessages('pt-BR', require('../../fixtures/pt-BR.json')); addMessages('pt-PT', require('../../fixtures/pt-PT.json')); -beforeEach(() => { - init({ fallbackLocale: 'en' }); -}); - describe('format message', () => { it('formats a message by its id and the current locale', () => { + init({ fallbackLocale: 'en' }); + expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name'); }); @@ -55,6 +54,8 @@ describe('format message', () => { }); it('formats a message with interpolated values', () => { + init({ fallbackLocale: 'en' }); + expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe( 'You have no photos.', ); @@ -67,6 +68,8 @@ describe('format message', () => { }); it('formats the default value with interpolated values', () => { + init({ fallbackLocale: 'en' }); + expect( formatMessage({ id: 'non-existent', @@ -77,6 +80,8 @@ describe('format message', () => { }); it('formats the key with interpolated values', () => { + init({ fallbackLocale: 'en' }); + expect( formatMessage({ id: '{food}', @@ -86,27 +91,38 @@ describe('format message', () => { }); it('accepts a message id as first argument', () => { + init({ fallbackLocale: 'en' }); + expect(formatMessage('form.field_1_name')).toBe('Name'); }); it('accepts a message id as first argument and formatting options as second', () => { + init({ fallbackLocale: 'en' }); + expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome'); }); it('throws if no locale is set', () => { + init({ fallbackLocale: 'en' }); + $locale.set(null); + expect(() => formatMessage('form.field_1_name')).toThrow( '[svelte-i18n] Cannot format a message without first setting the initial locale.', ); }); it('uses a missing message default value', () => { + init({ fallbackLocale: 'en' }); + expect(formatMessage('missing', { default: 'Missing Default' })).toBe( 'Missing Default', ); }); it('errors out when value found is not string', () => { + init({ fallbackLocale: 'en' }); + const spy = jest.spyOn(global.console, 'warn').mockImplementation(); expect(typeof formatMessage('form')).toBe('object'); @@ -117,7 +133,12 @@ describe('format message', () => { spy.mockRestore(); }); - it('warn on missing messages', () => { + it('warn on missing messages if "warnOnMissingMessages" is true', () => { + init({ + fallbackLocale: 'en', + warnOnMissingMessages: true, + }); + const spy = jest.spyOn(global.console, 'warn').mockImplementation(); formatMessage('missing'); @@ -129,7 +150,18 @@ describe('format message', () => { spy.mockRestore(); }); + it('uses result of handleMissingMessage handler', () => { + init({ + fallbackLocale: 'en', + handleMissingMessage: () => 'from handler', + }); + + expect(formatMessage('should-default')).toBe('from handler'); + }); + it('does not throw with invalid syntax', () => { + init({ fallbackLocale: 'en' }); + $locale.set('en'); const spy = jest.spyOn(global.console, 'warn').mockImplementation();