feat: introduce handleMissingMessage (#175)

* feat: introduce onMissingMessageHandler

* Update src/runtime/types/index.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* Update src/runtime/configs.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* Update src/runtime/types/index.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* Update src/runtime/stores/formatters.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* Update test/runtime/stores/formatters.test.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* Update test/runtime/stores/formatters.test.ts

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>

* rename to handleMissingKey and optionally use return as default value

* rename also in the defaultOptions

* test for optional default result from handleMissingKey

Co-authored-by: Christian Kaisermann <christian@kaisermann.me>
This commit is contained in:
Adriano Raiano 2022-04-05 16:38:45 +02:00 committed by Christian Kaisermann
parent 9467cfb78b
commit a8b5df0442
6 changed files with 172 additions and 62 deletions

View File

@ -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<string, Intl.NumberFormatOptions>
date: Record<string, Intl.DateTimeFormatOptions>
time: Record<string, Intl.DateTimeFormatOptions>
number: Record<string, Intl.NumberFormatOptions>;
date: Record<string, Intl.DateTimeFormatOptions>;
time: Record<string, Intl.DateTimeFormatOptions>;
}
```
@ -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
<div>
{$_.number(123456.789, { format: 'EUR' })}
</div>
<div>{$_.number(123456.789, { format: 'EUR' })}</div>
<!-- 123.456,79 € -->
```
@ -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).

View File

@ -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<string, any>;
@ -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) {

View File

@ -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.`,

View File

@ -72,12 +72,40 @@ export interface MessagesLoader {
(): Promise<any>;
}
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;
}

View File

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

View File

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