feat: 🎸 add $json method to get raw dictionary values

 Closes: #109, #83
This commit is contained in:
Christian Kaisermann 2020-11-23 10:50:58 -03:00
parent c02009e49a
commit 9dd1a19aef
7 changed files with 196 additions and 115 deletions

View File

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

View File

@ -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;
};

View File

@ -39,6 +39,7 @@ export {
$formatDate as date,
$formatNumber as number,
$formatTime as time,
$json as json,
} from './stores/formatters';
// low-level

View File

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

View File

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

View File

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

View File

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