mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-09-28 23:24:44 +02:00
feat: 🎸 add $json method to get raw dictionary values
✅ Closes: #109, #83
This commit is contained in:
parent
c02009e49a
commit
52400b5c51
@ -3,10 +3,11 @@
|
|||||||
<!-- code_chunk_output -->
|
<!-- code_chunk_output -->
|
||||||
|
|
||||||
- [Message syntax](#message-syntax)
|
- [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)
|
- [`$time(number: Date, options: MessageObject)`](#timenumber-date-options-messageobject)
|
||||||
- [`$date(date: Date, options: MessageObject)`](#datedate-date-options-messageobject)
|
- [`$date(date: Date, options: MessageObject)`](#datedate-date-options-messageobject)
|
||||||
- [`$number(number: number, options: MessageObject)`](#numbernumber-number-options-messageobject)
|
- [`$number(number: number, options: MessageObject)`](#numbernumber-number-options-messageobject)
|
||||||
|
- [`$json(messageId: string)`](#jsonmessageid-string)
|
||||||
- [Formats](#formats)
|
- [Formats](#formats)
|
||||||
- [Accessing formatters directly](#accessing-formatters-directly)
|
- [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/)
|
- [Runtime Environments](https://formatjs.io/docs/guides/runtime-requirements/)
|
||||||
- [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/)
|
- [ICU Message Syntax](https://formatjs.io/docs/core-concepts/icu-syntax/)
|
||||||
|
|
||||||
### `$format` or `$_` or `$t`
|
### `$format`, `$_` or `$t`
|
||||||
|
|
||||||
`import { _, t, format } from 'svelte-i18n'`
|
`import { _, t, format } from 'svelte-i18n'`
|
||||||
|
|
||||||
@ -41,11 +42,11 @@ The formatter can be called with two different signatures:
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface MessageObject {
|
interface MessageObject {
|
||||||
id?: string
|
id?: string;
|
||||||
locale?: string
|
locale?: string;
|
||||||
format?: string
|
format?: string;
|
||||||
default?: string
|
default?: string;
|
||||||
values?: Record<string, string | number | Date>
|
values?: Record<string, string | number | Date>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ interface MessageObject {
|
|||||||
- `values`: properties that should be interpolated in the message;
|
- `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:
|
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
|
```jsonc
|
||||||
// en.json
|
// en.json
|
||||||
{
|
{
|
||||||
@ -126,6 +128,30 @@ Formats a number with the specified locale and format. Please refer to the [#for
|
|||||||
<!-- 100.000.000 -->
|
<!-- 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
|
### Formats
|
||||||
|
|
||||||
`svelte-i18n` comes with a default set of `number`, `time` and `date` formats:
|
`svelte-i18n` comes with a default set of `number`, `time` and `date` formats:
|
||||||
@ -163,24 +189,24 @@ import {
|
|||||||
getNumberFormatter,
|
getNumberFormatter,
|
||||||
getTimeFormatter,
|
getTimeFormatter,
|
||||||
getMessageFormatter,
|
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:
|
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
|
```js
|
||||||
import { getDateFormatter } from 'svelte-i18n'
|
import { getDateFormatter } from 'svelte-i18n';
|
||||||
|
|
||||||
const getDateParts = date =>
|
const getDateParts = (date) =>
|
||||||
getDateFormatter()
|
getDateFormatter()
|
||||||
.formatToParts(date)
|
.formatToParts(date)
|
||||||
.filter(({ type }) => type !== 'literal')
|
.filter(({ type }) => type !== 'literal')
|
||||||
.reduce((acc, { type, value }) => {
|
.reduce((acc, { type, value }) => {
|
||||||
acc[type] = value
|
acc[type] = value;
|
||||||
return acc
|
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.
|
Check the [methods documentation](/docs/Methods.md#low-level-api) for more information.
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { getMessageFromDictionary } from '../stores/dictionary';
|
import { getMessageFromDictionary } from '../stores/dictionary';
|
||||||
import { getFallbackOf } from '../stores/locale';
|
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) => {
|
const addToCache = (path: string, locale: string, message: string) => {
|
||||||
if (!message) return message;
|
if (!message) return message;
|
||||||
@ -11,8 +15,8 @@ const addToCache = (path: string, locale: string, message: string) => {
|
|||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchForMessage = (path: string, locale: string): string => {
|
const searchForMessage = (path: string, locale: string): any => {
|
||||||
if (locale == null) return null;
|
if (locale == null) return undefined;
|
||||||
|
|
||||||
const message = getMessageFromDictionary(locale, path);
|
const message = getMessageFromDictionary(locale, path);
|
||||||
|
|
||||||
@ -32,5 +36,5 @@ export const lookup = (path: string, locale: string) => {
|
|||||||
return addToCache(path, locale, message);
|
return addToCache(path, locale, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -39,6 +39,7 @@ export {
|
|||||||
$formatDate as date,
|
$formatDate as date,
|
||||||
$formatNumber as number,
|
$formatNumber as number,
|
||||||
$formatTime as time,
|
$formatTime as time,
|
||||||
|
$json as json,
|
||||||
} from './stores/formatters';
|
} from './stores/formatters';
|
||||||
|
|
||||||
// low-level
|
// low-level
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
TimeFormatter,
|
TimeFormatter,
|
||||||
DateFormatter,
|
DateFormatter,
|
||||||
NumberFormatter,
|
NumberFormatter,
|
||||||
|
JSONGetter,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { lookup } from '../includes/lookup';
|
import { lookup } from '../includes/lookup';
|
||||||
import { hasLocaleQueue } from '../includes/loaderQueue';
|
import { hasLocaleQueue } from '../includes/loaderQueue';
|
||||||
@ -54,23 +55,39 @@ const formatMessage: MessageFormatter = (id, options = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = defaultValue || id;
|
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;
|
return getMessageFormatter(message, locale).format(values) as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime: TimeFormatter = (t, options) =>
|
const formatTime: TimeFormatter = (t, options) => {
|
||||||
getTimeFormatter(options).format(t);
|
return getTimeFormatter(options).format(t);
|
||||||
|
};
|
||||||
|
|
||||||
const formatDate: DateFormatter = (d, options) =>
|
const formatDate: DateFormatter = (d, options) => {
|
||||||
getDateFormatter(options).format(d);
|
return getDateFormatter(options).format(d);
|
||||||
|
};
|
||||||
|
|
||||||
const formatNumber: NumberFormatter = (n, options) =>
|
const formatNumber: NumberFormatter = (n, options) => {
|
||||||
getNumberFormatter(options).format(n);
|
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 $format = derived([$locale, $dictionary], () => formatMessage);
|
||||||
export const $formatTime = derived([$locale], () => formatTime);
|
export const $formatTime = derived([$locale], () => formatTime);
|
||||||
export const $formatDate = derived([$locale], () => formatDate);
|
export const $formatDate = derived([$locale], () => formatDate);
|
||||||
export const $formatNumber = derived([$locale], () => formatNumber);
|
export const $formatNumber = derived([$locale], () => formatNumber);
|
||||||
|
export const $json = derived([$locale, $dictionary], () => getJSON);
|
||||||
|
@ -49,6 +49,8 @@ export type NumberFormatter = (
|
|||||||
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
|
options?: IntlFormatterOptions<Intl.NumberFormatOptions>,
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
export type JSONGetter = <T extends any>(id: string, locale?: string) => T;
|
||||||
|
|
||||||
type IntlFormatterOptions<T> = T & {
|
type IntlFormatterOptions<T> = T & {
|
||||||
format?: string;
|
format?: string;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
@ -9,8 +9,8 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('returns null if no locale was passed', () => {
|
test('returns null if no locale was passed', () => {
|
||||||
expect(lookup('message.id', undefined)).toBeNull();
|
expect(lookup('message.id', undefined)).toBeUndefined();
|
||||||
expect(lookup('message.id', null)).toBeNull();
|
expect(lookup('message.id', null)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gets a shallow message of a locale dictionary', () => {
|
test('gets a shallow message of a locale dictionary', () => {
|
||||||
@ -61,6 +61,7 @@ test('gets an array ', () => {
|
|||||||
test('caches found messages by locale', () => {
|
test('caches found messages by locale', () => {
|
||||||
addMessages('en', { field: 'name' });
|
addMessages('en', { field: 'name' });
|
||||||
addMessages('pt', { field: 'nome' });
|
addMessages('pt', { field: 'nome' });
|
||||||
|
|
||||||
lookup('field', 'en-US');
|
lookup('field', 'en-US');
|
||||||
lookup('field', 'pt');
|
lookup('field', 'pt');
|
||||||
|
|
||||||
@ -73,8 +74,10 @@ test('caches found messages by locale', () => {
|
|||||||
test("doesn't cache falsy messages", () => {
|
test("doesn't cache falsy messages", () => {
|
||||||
addMessages('en', { field: 'name' });
|
addMessages('en', { field: 'name' });
|
||||||
addMessages('pt', { field: 'nome' });
|
addMessages('pt', { field: 'nome' });
|
||||||
|
|
||||||
lookup('field_2', 'en-US');
|
lookup('field_2', 'en-US');
|
||||||
lookup('field_2', 'pt');
|
lookup('field_2', 'pt');
|
||||||
|
|
||||||
expect(lookupCache).not.toMatchObject({
|
expect(lookupCache).not.toMatchObject({
|
||||||
'en-US': { field_2: 'name' },
|
'en-US': { field_2: 'name' },
|
||||||
pt: { field_2: 'nome' },
|
pt: { field_2: 'nome' },
|
||||||
|
@ -1,31 +1,35 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
JSONGetter,
|
||||||
|
MessageFormatter,
|
||||||
|
TimeFormatter,
|
||||||
|
DateFormatter,
|
||||||
|
NumberFormatter,
|
||||||
|
} from '../../../src/runtime/types/index';
|
||||||
import {
|
import {
|
||||||
$format,
|
$format,
|
||||||
$formatTime,
|
$formatTime,
|
||||||
$formatDate,
|
$formatDate,
|
||||||
$formatNumber,
|
$formatNumber,
|
||||||
|
$json,
|
||||||
} from '../../../src/runtime/stores/formatters';
|
} from '../../../src/runtime/stores/formatters';
|
||||||
import { init } from '../../../src/runtime/configs';
|
import { init } from '../../../src/runtime/configs';
|
||||||
import { addMessages } from '../../../src/runtime/stores/dictionary';
|
import { addMessages } from '../../../src/runtime/stores/dictionary';
|
||||||
import { $locale } from '../../../src/runtime/stores/locale';
|
import { $locale } from '../../../src/runtime/stores/locale';
|
||||||
import {
|
|
||||||
MessageFormatter,
|
|
||||||
TimeFormatter,
|
|
||||||
DateFormatter,
|
|
||||||
NumberFormatter,
|
|
||||||
} from '../../../src/runtime/types';
|
|
||||||
|
|
||||||
let formatMessage: MessageFormatter;
|
let formatMessage: MessageFormatter;
|
||||||
let formatTime: TimeFormatter;
|
let formatTime: TimeFormatter;
|
||||||
let formatDate: DateFormatter;
|
let formatDate: DateFormatter;
|
||||||
let formatNumber: NumberFormatter;
|
let formatNumber: NumberFormatter;
|
||||||
|
let getJSON: JSONGetter;
|
||||||
|
|
||||||
$locale.subscribe(() => {
|
$locale.subscribe(() => {
|
||||||
formatMessage = get($format);
|
formatMessage = get($format);
|
||||||
formatTime = get($formatTime);
|
formatTime = get($formatTime);
|
||||||
formatDate = get($formatDate);
|
formatDate = get($formatDate);
|
||||||
formatNumber = get($formatNumber);
|
formatNumber = get($formatNumber);
|
||||||
|
getJSON = get($json);
|
||||||
});
|
});
|
||||||
|
|
||||||
addMessages('en', require('../../fixtures/en.json'));
|
addMessages('en', require('../../fixtures/en.json'));
|
||||||
@ -37,16 +41,18 @@ addMessages('pt-PT', require('../../fixtures/pt-PT.json'));
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
init({ fallbackLocale: 'en' });
|
init({ fallbackLocale: 'en' });
|
||||||
});
|
});
|
||||||
|
describe('format message', () => {
|
||||||
test('formats a message by its id and the current locale', () => {
|
it('formats a message by its id and the current locale', () => {
|
||||||
expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name');
|
expect(formatMessage({ id: 'form.field_1_name' })).toBe('Name');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats a message by its id and the a passed locale', () => {
|
it('formats a message by its id and the a passed locale', () => {
|
||||||
expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe('Nome');
|
expect(formatMessage({ id: 'form.field_1_name', locale: 'pt' })).toBe(
|
||||||
});
|
'Nome',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('formats a message with interpolated values', () => {
|
it('formats a message with interpolated values', () => {
|
||||||
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
|
expect(formatMessage({ id: 'photos', values: { n: 0 } })).toBe(
|
||||||
'You have no photos.',
|
'You have no photos.',
|
||||||
);
|
);
|
||||||
@ -56,9 +62,9 @@ test('formats a message with interpolated values', () => {
|
|||||||
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
|
expect(formatMessage({ id: 'photos', values: { n: 21 } })).toBe(
|
||||||
'You have 21 photos.',
|
'You have 21 photos.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats the default value with interpolated values', () => {
|
it('formats the default value with interpolated values', () => {
|
||||||
expect(
|
expect(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
id: 'non-existent',
|
id: 'non-existent',
|
||||||
@ -66,39 +72,52 @@ test('formats the default value with interpolated values', () => {
|
|||||||
values: { food: 'potato' },
|
values: { food: 'potato' },
|
||||||
}),
|
}),
|
||||||
).toBe('potato');
|
).toBe('potato');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formats the key with interpolated values', () => {
|
it('formats the key with interpolated values', () => {
|
||||||
expect(
|
expect(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
id: '{food}',
|
id: '{food}',
|
||||||
values: { food: 'potato' },
|
values: { food: 'potato' },
|
||||||
}),
|
}),
|
||||||
).toBe('potato');
|
).toBe('potato');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts a message id as first argument', () => {
|
it('accepts a message id as first argument', () => {
|
||||||
expect(formatMessage('form.field_1_name')).toBe('Name');
|
expect(formatMessage('form.field_1_name')).toBe('Name');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('accepts a message id as first argument and formatting options as second', () => {
|
it('accepts a message id as first argument and formatting options as second', () => {
|
||||||
expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome');
|
expect(formatMessage('form.field_1_name', { locale: 'pt' })).toBe('Nome');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throws if no locale is set', () => {
|
it('throws if no locale is set', () => {
|
||||||
$locale.set(null);
|
$locale.set(null);
|
||||||
expect(() => formatMessage('form.field_1_name')).toThrow(
|
expect(() => formatMessage('form.field_1_name')).toThrow(
|
||||||
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
|
'[svelte-i18n] Cannot format a message without first setting the initial locale.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('uses a missing message default value', () => {
|
it('uses a missing message default value', () => {
|
||||||
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
|
expect(formatMessage('missing', { default: 'Missing Default' })).toBe(
|
||||||
'Missing Default',
|
'Missing Default',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('warn on missing messages', () => {
|
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;
|
const { warn } = global.console;
|
||||||
|
|
||||||
jest.spyOn(global.console, 'warn').mockImplementation();
|
jest.spyOn(global.console, 'warn').mockImplementation();
|
||||||
@ -110,16 +129,25 @@ test('warn on missing messages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
global.console.warn = warn;
|
global.console.warn = warn;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('format utilities', () => {
|
test('format time', () => {
|
||||||
it('time', () => {
|
|
||||||
expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM');
|
expect(formatTime(new Date(2019, 0, 1, 20, 37))).toBe('8:37 PM');
|
||||||
});
|
});
|
||||||
it('date', () => {
|
|
||||||
expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19');
|
test('format date', () => {
|
||||||
});
|
expect(formatDate(new Date(2019, 0, 1, 20, 37))).toBe('1/1/19');
|
||||||
it('number', () => {
|
});
|
||||||
expect(formatNumber(123123123)).toBe('123,123,123');
|
|
||||||
});
|
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