mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 09:59:58 +01:00
feat: 🎸 add $json method to get raw dictionary values
✅ Closes: #109, #83
This commit is contained in:
parent
c02009e49a
commit
9dd1a19aef
@ -3,10 +3,11 @@
|
||||
<!-- code_chunk_output -->
|
||||
|
||||
- [Message syntax](#message-syntax)
|
||||
- [`$format` or `$_` or `$t`](#format-or-_-or-t)
|
||||
- [`$format`, `$_` or `$t`](#format-_-or-t)
|
||||
- [`$time(number: Date, options: MessageObject)`](#timenumber-date-options-messageobject)
|
||||
- [`$date(date: Date, options: MessageObject)`](#datedate-date-options-messageobject)
|
||||
- [`$number(number: number, options: MessageObject)`](#numbernumber-number-options-messageobject)
|
||||
- [`$json(messageId: string)`](#jsonmessageid-string)
|
||||
- [Formats](#formats)
|
||||
- [Accessing formatters directly](#accessing-formatters-directly)
|
||||
|
||||
@ -20,7 +21,7 @@ Under the hood, `formatjs` is used for localizing your messages. It allows `svel
|
||||
- [Runtime Environments](https://formatjs.io/docs/guides/runtime-requirements/)
|
||||
- [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/)
|
||||
|
||||
### `$format` or `$_` or `$t`
|
||||
### `$format`, `$_` or `$t`
|
||||
|
||||
`import { _, t, format } from 'svelte-i18n'`
|
||||
|
||||
@ -41,11 +42,11 @@ The formatter can be called with two different signatures:
|
||||
|
||||
```ts
|
||||
interface MessageObject {
|
||||
id?: string
|
||||
locale?: string
|
||||
format?: string
|
||||
default?: string
|
||||
values?: Record<string, string | number | Date>
|
||||
id?: string;
|
||||
locale?: string;
|
||||
format?: string;
|
||||
default?: string;
|
||||
values?: Record<string, string | number | Date>;
|
||||
}
|
||||
```
|
||||
|
||||
@ -56,6 +57,7 @@ interface MessageObject {
|
||||
- `values`: properties that should be interpolated in the message;
|
||||
|
||||
You can pass a `string` as the first parameter for a less verbose way of formatting a message. It is also possible to inject values into the translation like so:
|
||||
|
||||
```jsonc
|
||||
// en.json
|
||||
{
|
||||
@ -126,6 +128,30 @@ Formats a number with the specified locale and format. Please refer to the [#for
|
||||
<!-- 100.000.000 -->
|
||||
```
|
||||
|
||||
### `$json(messageId: string)`
|
||||
|
||||
`import { json } from 'svelte-i18n'`
|
||||
|
||||
Returns the raw JSON value of the specified `messageId` for the current locale. While [`$format`](#format-_-or-t) always returns a string, `$json` can be used to get an object relative to the current locale.
|
||||
|
||||
```html
|
||||
<ul>
|
||||
{#each $json('list.items') as item}
|
||||
<li>{item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
If you're using TypeScript, you can define the returned type as well:
|
||||
|
||||
```html
|
||||
<ul>
|
||||
{#each $json<Item[]>('list.items') as item}
|
||||
<li>{item.name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
### Formats
|
||||
|
||||
`svelte-i18n` comes with a default set of `number`, `time` and `date` formats:
|
||||
@ -163,24 +189,24 @@ import {
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
getMessageFormatter,
|
||||
} from 'svelte-i18n'
|
||||
} from 'svelte-i18n';
|
||||
```
|
||||
|
||||
By using these methods, it's possible to manipulate values in a more specific way that fits your needs. For example, it's possible to create a method which receives a `date` and returns its relevant date related parts:
|
||||
|
||||
```js
|
||||
import { getDateFormatter } from 'svelte-i18n'
|
||||
import { getDateFormatter } from 'svelte-i18n';
|
||||
|
||||
const getDateParts = date =>
|
||||
const getDateParts = (date) =>
|
||||
getDateFormatter()
|
||||
.formatToParts(date)
|
||||
.filter(({ type }) => type !== 'literal')
|
||||
.reduce((acc, { type, value }) => {
|
||||
acc[type] = value
|
||||
return acc
|
||||
}, {})
|
||||
acc[type] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
getDateParts(new Date(2020, 0, 1)) // { month: '1', day: '1', year: '2020' }
|
||||
getDateParts(new Date(2020, 0, 1)); // { month: '1', day: '1', year: '2020' }
|
||||
```
|
||||
|
||||
Check the [methods documentation](/docs/Methods.md#low-level-api) for more information.
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { getMessageFromDictionary } from '../stores/dictionary';
|
||||
import { getFallbackOf } from '../stores/locale';
|
||||
|
||||
export const lookupCache: Record<string, Record<string, string>> = {};
|
||||
export const lookupCache: {
|
||||
[locale: string]: {
|
||||
[messageId: string]: any;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const addToCache = (path: string, locale: string, message: string) => {
|
||||
if (!message) return message;
|
||||
@ -11,8 +15,8 @@ const addToCache = (path: string, locale: string, message: string) => {
|
||||
return message;
|
||||
};
|
||||
|
||||
const searchForMessage = (path: string, locale: string): string => {
|
||||
if (locale == null) return null;
|
||||
const searchForMessage = (path: string, locale: string): any => {
|
||||
if (locale == null) return undefined;
|
||||
|
||||
const message = getMessageFromDictionary(locale, path);
|
||||
|
||||
@ -32,5 +36,5 @@ export const lookup = (path: string, locale: string) => {
|
||||
return addToCache(path, locale, message);
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
};
|
||||
|
@ -39,6 +39,7 @@ export {
|
||||
$formatDate as date,
|
||||
$formatNumber as number,
|
||||
$formatTime as time,
|
||||
$json as json,
|
||||
} from './stores/formatters';
|
||||
|
||||
// low-level
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
TimeFormatter,
|
||||
DateFormatter,
|
||||
NumberFormatter,
|
||||
JSONGetter,
|
||||
} from '../types';
|
||||
import { lookup } from '../includes/lookup';
|
||||
import { hasLocaleQueue } from '../includes/loaderQueue';
|
||||
@ -54,23 +55,39 @@ const formatMessage: MessageFormatter = (id, options = {}) => {
|
||||
}
|
||||
|
||||
message = defaultValue || id;
|
||||
} else if (typeof message !== 'string') {
|
||||
console.error(
|
||||
`[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.`,
|
||||
);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!values) return message;
|
||||
if (!values) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return getMessageFormatter(message, locale).format(values) as string;
|
||||
};
|
||||
|
||||
const formatTime: TimeFormatter = (t, options) =>
|
||||
getTimeFormatter(options).format(t);
|
||||
const formatTime: TimeFormatter = (t, options) => {
|
||||
return getTimeFormatter(options).format(t);
|
||||
};
|
||||
|
||||
const formatDate: DateFormatter = (d, options) =>
|
||||
getDateFormatter(options).format(d);
|
||||
const formatDate: DateFormatter = (d, options) => {
|
||||
return getDateFormatter(options).format(d);
|
||||
};
|
||||
|
||||
const formatNumber: NumberFormatter = (n, options) =>
|
||||
getNumberFormatter(options).format(n);
|
||||
const formatNumber: NumberFormatter = (n, options) => {
|
||||
return getNumberFormatter(options).format(n);
|
||||
};
|
||||
|
||||
const getJSON: JSONGetter = <T>(id: string, locale = getCurrentLocale()) => {
|
||||
return lookup(id, locale) as T;
|
||||
};
|
||||
|
||||
export const $format = derived([$locale, $dictionary], () => formatMessage);
|
||||
export const $formatTime = derived([$locale], () => formatTime);
|
||||
export const $formatDate = derived([$locale], () => formatDate);
|
||||
export const $formatNumber = derived([$locale], () => formatNumber);
|
||||
export const $json = derived([$locale, $dictionary], () => getJSON);
|
||||
|
@ -49,6 +49,8 @@ export type NumberFormatter = (
|
||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
|
||||
) => string;
|
||||
|
||||
export type JSONGetter = <T extends any>(id: string, locale?: string) => T;
|
||||
|
||||
type IntlFormatterOptions<T> = T & {
|
||||
format?: string;
|
||||
locale?: string;
|
||||
|
@ -9,8 +9,8 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
test('returns null if no locale was passed', () => {
|
||||
expect(lookup('message.id', undefined)).toBeNull();
|
||||
expect(lookup('message.id', null)).toBeNull();
|
||||
expect(lookup('message.id', undefined)).toBeUndefined();
|
||||
expect(lookup('message.id', null)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('gets a shallow message of a locale dictionary', () => {
|
||||
@ -61,6 +61,7 @@ test('gets an array ', () => {
|
||||
test('caches found messages by locale', () => {
|
||||
addMessages('en', { field: 'name' });
|
||||
addMessages('pt', { field: 'nome' });
|
||||
|
||||
lookup('field', 'en-US');
|
||||
lookup('field', 'pt');
|
||||
|
||||
@ -73,8 +74,10 @@ test('caches found messages by locale', () => {
|
||||
test("doesn't cache falsy messages", () => {
|
||||
addMessages('en', { field: 'name' });
|
||||
addMessages('pt', { field: 'nome' });
|
||||
|
||||
lookup('field_2', 'en-US');
|
||||
lookup('field_2', 'pt');
|
||||
|
||||
expect(lookupCache).not.toMatchObject({
|
||||
'en-US': { field_2: 'name' },
|
||||
pt: { field_2: 'nome' },
|
||||
|
@ -1,31 +1,35 @@
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import {
|
||||
JSONGetter,
|
||||
MessageFormatter,
|
||||
TimeFormatter,
|
||||
DateFormatter,
|
||||
NumberFormatter,
|
||||
} from '../../../src/runtime/types/index';
|
||||
import {
|
||||
$format,
|
||||
$formatTime,
|
||||
$formatDate,
|
||||
$formatNumber,
|
||||
$json,
|
||||
} from '../../../src/runtime/stores/formatters';
|
||||
import { init } from '../../../src/runtime/configs';
|
||||
import { addMessages } from '../../../src/runtime/stores/dictionary';
|
||||
import { $locale } from '../../../src/runtime/stores/locale';
|
||||
import {
|
||||
MessageFormatter,
|
||||
TimeFormatter,
|
||||
DateFormatter,
|
||||
NumberFormatter,
|
||||
} from '../../../src/runtime/types';
|
||||
|
||||
let formatMessage: MessageFormatter;
|
||||
let formatTime: TimeFormatter;
|
||||
let formatDate: DateFormatter;
|
||||
let formatNumber: NumberFormatter;
|
||||
let getJSON: JSONGetter;
|
||||
|
||||
$locale.subscribe(() => {
|
||||
formatMessage = get($format);
|
||||
formatTime = get($formatTime);
|
||||
formatDate = get($formatDate);
|
||||
formatNumber = get($formatNumber);
|
||||
getJSON = get($json);
|
||||
});
|
||||
|
||||
addMessages('en', require('../../fixtures/en.json'));
|
||||
@ -37,89 +41,113 @@ addMessages('pt-PT', require('../../fixtures/pt-PT.json'));
|
||||
beforeEach(() => {
|
||||
init({ fallbackLocale: 'en' });
|
||||
});
|
||||
|
||||
test('formats a message by its id and the current locale', () => {
|
||||
expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name');
|
||||
});
|
||||
|
||||
test('formats a message by its id and the a passed locale', () => {
|
||||
expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome');
|
||||
});
|
||||
|
||||
test('formats a message with interpolated values', () => {
|
||||
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
|
||||
'You have no photos.',
|
||||
);
|
||||
expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe(
|
||||
'You have one photo.',
|
||||
);
|
||||
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
|
||||
'You have 21 photos.',
|
||||
);
|
||||
});
|
||||
|
||||
test('formats the default value with interpolated values', () => {
|
||||
expect(
|
||||
formatMessage({
|
||||
id: 'non-existent',
|
||||
default: '{food}',
|
||||
values: { food: 'potato' },
|
||||
}),
|
||||
).toBe('potato');
|
||||
});
|
||||
|
||||
test('formats the key with interpolated values', () => {
|
||||
expect(
|
||||
formatMessage({
|
||||
id: '{food}',
|
||||
values: { food: 'potato' },
|
||||
}),
|
||||
).toBe('potato');
|
||||
});
|
||||
|
||||
test('accepts a message id as first argument', () => {
|
||||
expect(formatMessage('form.field_1_name')).toBe('Name');
|
||||
});
|
||||
|
||||
test('accepts a message id as first argument and formatting options as second', () => {
|
||||
expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome');
|
||||
});
|
||||
|
||||
test('throws if no locale is set', () => {
|
||||
$locale.set(null);
|
||||
expect(() => formatMessage('form.field_1_name')).toThrow(
|
||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses a missing message default value', () => {
|
||||
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
|
||||
'Missing Default',
|
||||
);
|
||||
});
|
||||
|
||||
test('warn on missing messages', () => {
|
||||
const { warn } = global.console;
|
||||
|
||||
jest.spyOn(global.console, 'warn').mockImplementation();
|
||||
|
||||
formatMessage('missing');
|
||||
|
||||
expect(console.warn).toBeCalledWith(
|
||||
`[svelte-i18n] The message "missing" was not found in "en".`,
|
||||
);
|
||||
|
||||
global.console.warn = warn;
|
||||
});
|
||||
|
||||
describe('format utilities', () => {
|
||||
it('time', () => {
|
||||
expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM');
|
||||
describe('format message', () => {
|
||||
it('formats a message by its id and the current locale', () => {
|
||||
expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name');
|
||||
});
|
||||
it('date', () => {
|
||||
expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19');
|
||||
|
||||
it('formats a message by its id and the a passed locale', () => {
|
||||
expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe(
|
||||
'Nome',
|
||||
);
|
||||
});
|
||||
it('number', () => {
|
||||
expect(formatNumber(123123123)).toBe('123,123,123');
|
||||
|
||||
it('formats a message with interpolated values', () => {
|
||||
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
|
||||
'You have no photos.',
|
||||
);
|
||||
expect(formatMessage({ id: 'photos', values: { n: 1 } })).toBe(
|
||||
'You have one photo.',
|
||||
);
|
||||
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
|
||||
'You have 21 photos.',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats the default value with interpolated values', () => {
|
||||
expect(
|
||||
formatMessage({
|
||||
id: 'non-existent',
|
||||
default: '{food}',
|
||||
values: { food: 'potato' },
|
||||
}),
|
||||
).toBe('potato');
|
||||
});
|
||||
|
||||
it('formats the key with interpolated values', () => {
|
||||
expect(
|
||||
formatMessage({
|
||||
id: '{food}',
|
||||
values: { food: 'potato' },
|
||||
}),
|
||||
).toBe('potato');
|
||||
});
|
||||
|
||||
it('accepts a message id as first argument', () => {
|
||||
expect(formatMessage('form.field_1_name')).toBe('Name');
|
||||
});
|
||||
|
||||
it('accepts a message id as first argument and formatting options as second', () => {
|
||||
expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome');
|
||||
});
|
||||
|
||||
it('throws if no locale is set', () => {
|
||||
$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', () => {
|
||||
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
|
||||
'Missing Default',
|
||||
);
|
||||
});
|
||||
|
||||
it('errors out when value found is not string', () => {
|
||||
const { error } = global.console;
|
||||
|
||||
jest.spyOn(global.console, 'error').mockImplementation();
|
||||
|
||||
expect(typeof formatMessage('form')).toBe('object');
|
||||
expect(console.error).toBeCalledWith(
|
||||
`[svelte-i18n] Message with id "form" must be of type "string", found: "object". Gettin its value through the "$format" method is deprecated; use the "json" method instead.`,
|
||||
);
|
||||
|
||||
global.console.error = error;
|
||||
});
|
||||
|
||||
it('warn on missing messages', () => {
|
||||
const { warn } = global.console;
|
||||
|
||||
jest.spyOn(global.console, 'warn').mockImplementation();
|
||||
|
||||
formatMessage('missing');
|
||||
|
||||
expect(console.warn).toBeCalledWith(
|
||||
`[svelte-i18n] The message "missing" was not found in "en".`,
|
||||
);
|
||||
|
||||
global.console.warn = warn;
|
||||
});
|
||||
});
|
||||
|
||||
test('format time', () => {
|
||||
expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM');
|
||||
});
|
||||
|
||||
test('format date', () => {
|
||||
expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19');
|
||||
});
|
||||
|
||||
test('format number', () => {
|
||||
expect(formatNumber(123123123)).toBe('123,123,123');
|
||||
});
|
||||
|
||||
test('get raw JSON data from the current locale dictionary', () => {
|
||||
expect(getJSON('form')).toMatchObject({
|
||||
field_1_name: 'Name',
|
||||
field_2_name: 'Lastname',
|
||||
});
|
||||
expect(getJSON('non-existing')).toBeUndefined();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user