mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 18:10:43 +01:00
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:
parent
9467cfb78b
commit
a8b5df0442
@ -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).
|
||||
|
@ -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) {
|
||||
|
@ -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.`,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user