Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement formatting helpers #2

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# i18nano [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/eolme/i18nano/blob/master/LICENSE) [![BundlePhobia](https://img.shields.io/bundlephobia/minzip/i18nano)](https://bundlephobia.com/package/i18nano) [![BundlePhobia](https://img.shields.io/bundlephobia/min/i18nano)](https://bundlephobia.com/package/i18nano) [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/eolme/i18nano/blob/master/tests)
# i18nano [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/eolme/i18nano/blob/master/LICENSE) [![BundlePhobia](https://img.shields.io/bundlephobia/minzip/i18nano)](https://bundlephobia.com/package/i18nano) [![BundlePhobia](https://img.shields.io/bundlephobia/min/i18nano)](https://bundlephobia.com/package/i18nano) [![Coverage](https://img.shields.io/badge/coverage-99.37%25-brightgreen)](https://github.com/eolme/i18nano/blob/master/tests)

> Internationalization for the react is done simply.

Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const config = {
collectCoverage: true,
collectCoverageFrom: [
'**/src/react.{js,ts}',
'**/src/format.{js,ts}',
'!**/node_modules/**'
],
coverageProvider: 'v8',
Expand Down
1 change: 0 additions & 1 deletion src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ export type FC<P = {}> = _FC<PropsWithoutChildren<P>>;
// eslint-disable-next-line @typescript-eslint/ban-types
export type FCC<P = {}> = _FC<PropsWithChildren<P>>;

// TODO: maybe incompatible
export type { _ComponentType as ComponentType };
80 changes: 80 additions & 0 deletions src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { FCC } from './compat.js';

import * as React from 'react';

import { useTranslationChange } from './react.js';

type IntlConstructor<T> = new (lang: string, options: Record<never, never>) => T;

type IntlFormat =
Intl.NumberFormat |
Intl.DateTimeFormat |
Intl.RelativeTimeFormat |
Intl.ListFormat |
Intl.PluralRules;

type IntlFormatOptions =
Intl.NumberFormatOptions |
Intl.DateTimeFormatOptions |
Intl.RelativeTimeFormatOptions |
Intl.ListFormatOptions |
Intl.PluralRulesOptions;

const createCache = () => new Map<IntlFormatOptions, IntlFormat>();

const FormatContext = React.createContext({
lang: '',
cache: createCache()
});

const useFormat = <T extends IntlFormat>(constructor: IntlConstructor<T>, options: IntlFormatOptions) => {
const context = React.useContext(FormatContext);

let cached = context.cache.get(options);

if (typeof cached === 'undefined') {
cached = new constructor(context.lang, options);
context.cache.set(options, cached);
}

return cached as T;
};

export const useFormatNumber = (options: Intl.NumberFormatOptions) => {
return useFormat(Intl.NumberFormat, options);
};

export const useFormatDate = (options: Intl.DateTimeFormatOptions) => {
return useFormat(Intl.DateTimeFormat, options);
};

export const useFormatRelative = (options: Intl.RelativeTimeFormatOptions) => {
return useFormat(Intl.RelativeTimeFormat, options);
};

export const useFormatList = (options: Intl.ListFormatOptions) => {
return useFormat(Intl.ListFormat, options);
};

export const useFormatPlural = (options: Intl.PluralRulesOptions) => {
return useFormat(Intl.PluralRules, options);
};

export const FormatProvider: FCC = ({ children }) => {
const translation = useTranslationChange();

const context = React.useMemo(() => ({
lang: translation.lang,
cache: createCache()
}), [translation.lang]);

const FormatContextProps = {
value: context
} as const;

return React.createElement(
FormatContext.Provider,
FormatContextProps,
children
);
};
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export {
withTranslationChange
} from './react.js';

export {
FormatProvider,

useFormatDate,
useFormatList,
useFormatNumber,
useFormatRelative
} from './format.js';

export type {
TranslationValues,
TranslationChange,
Expand Down
8 changes: 3 additions & 5 deletions src/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
TranslationValues
} from './types.js';

import React from 'react';
import * as React from 'react';

import { suspend, preload as suspendPreload } from 'suspend-react';

Expand Down Expand Up @@ -39,13 +39,11 @@ const {
unstable_startTransition = invoke,
startTransition = unstable_startTransition,

unstable_useOpaqueIdentifier = useInstanceId,
useId = unstable_useOpaqueIdentifier
useId = useInstanceId
} = React as unknown as {
unstable_startTransition: typeof invoke;
startTransition: typeof invoke;

unstable_useOpaqueIdentifier: typeof useInstanceId;
useId: typeof useInstanceId;
};

Expand Down Expand Up @@ -209,7 +207,7 @@ export const withTranslationChange = <P>(Component: ComponentType<P & Translatio
*
* @see {@link Translation}
*/
// @ts-expect-error DefinitelyTyped issue
// @ts-expect-error React 17 incompatible type
export const TranslationRender: FC<TranslationProps> = ({
path,
values = null
Expand Down
6 changes: 3 additions & 3 deletions tests/change.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import renderer from 'react-test-renderer';

import { waitForSuspense } from './suspense.js';
Expand Down Expand Up @@ -28,7 +28,7 @@ describe('change', () => {
translations: TRANSLATIONS
},

// @ts-expect-error DefinitelyTyped issue
// @ts-expect-error React 17 incompatible type
React.createElement(() => {
const translation = Module.useTranslationChange();

Expand Down Expand Up @@ -90,7 +90,7 @@ describe('change', () => {
translations: TRANSLATIONS
},

// @ts-expect-error DefinitelyTyped issue
// @ts-expect-error React 17 incompatible type
React.createElement(() => {
const translation = Module.useTranslationChange();

Expand Down
2 changes: 1 addition & 1 deletion tests/components.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import renderer from 'react-test-renderer';

import { waitForSuspense } from './suspense.js';
Expand Down
100 changes: 100 additions & 0 deletions tests/format.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as React from 'react';
import renderer from 'react-test-renderer';

import {
DATE,
DEFAULT_PROPS,
LIST,
Module,
ModuleFormat,
NUMBER,
PLURAL,
RELATIVE,
TRANSLATIONS_KEYS
} from './shared.js';

describe('format', () => {
it('formats correctly', async () => {
expect.assertions(3);

let change: (lang: string) => void;

const component = renderer.create(
React.createElement(
Module.TranslationProvider,
DEFAULT_PROPS,

// @ts-expect-error React 17 incompatible type
React.createElement(() => {
const translation = Module.useTranslationChange();

change = translation.change;

return translation.lang;
}),
React.createElement(
ModuleFormat.FormatProvider,
null,

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatDate(DATE.options.datetime).format(DATE.value)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatDate(DATE.options.date).format(DATE.value)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatDate(DATE.options.time).format(DATE.value)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatNumber(NUMBER.options).format(NUMBER.value)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatRelative(RELATIVE.options).format(RELATIVE.value, RELATIVE.unit)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatList(LIST.options).format(LIST.value)),

// @ts-expect-error React 17 incompatible type
React.createElement(() => ModuleFormat.useFormatPlural(PLURAL.options).select(PLURAL.value))
)
)
);

expect(component.toJSON()).toMatchObject([
TRANSLATIONS_KEYS[0],
DATE[TRANSLATIONS_KEYS[0]].datetime,
DATE[TRANSLATIONS_KEYS[0]].date,
DATE[TRANSLATIONS_KEYS[0]].time,
NUMBER[TRANSLATIONS_KEYS[0]],
RELATIVE[TRANSLATIONS_KEYS[0]],
LIST[TRANSLATIONS_KEYS[0]],
PLURAL[TRANSLATIONS_KEYS[0]]
]);

await renderer.act(() => change(TRANSLATIONS_KEYS[1]));

expect(component.toJSON()).toMatchObject([
TRANSLATIONS_KEYS[1],
DATE[TRANSLATIONS_KEYS[1]].datetime,
DATE[TRANSLATIONS_KEYS[1]].date,
DATE[TRANSLATIONS_KEYS[1]].time,
NUMBER[TRANSLATIONS_KEYS[1]],
RELATIVE[TRANSLATIONS_KEYS[1]],
LIST[TRANSLATIONS_KEYS[1]],
PLURAL[TRANSLATIONS_KEYS[1]]
]);

await renderer.act(() => change(TRANSLATIONS_KEYS[0]));

expect(component.toJSON()).toMatchObject([
TRANSLATIONS_KEYS[0],
DATE[TRANSLATIONS_KEYS[0]].datetime,
DATE[TRANSLATIONS_KEYS[0]].date,
DATE[TRANSLATIONS_KEYS[0]].time,
NUMBER[TRANSLATIONS_KEYS[0]],
RELATIVE[TRANSLATIONS_KEYS[0]],
LIST[TRANSLATIONS_KEYS[0]],
PLURAL[TRANSLATIONS_KEYS[0]]
]);
});
});
2 changes: 1 addition & 1 deletion tests/hoc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import renderer from 'react-test-renderer';

import {
Expand Down
2 changes: 1 addition & 1 deletion tests/provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import renderer from 'react-test-renderer';

import { fn } from 'jest-mock';
Expand Down
81 changes: 80 additions & 1 deletion tests/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const TRANSLATIONS = {
})
};

export const TRANSLATIONS_KEYS = Object.keys(TRANSLATIONS);
export const TRANSLATIONS_KEYS = Object.keys(TRANSLATIONS) as ['ru', 'it'];

export const VALUES = {
value: '0',
Expand All @@ -22,6 +22,81 @@ export const VALUES = {
]
};

export const DATE = {
value: new Date(2022, 4, 7, 4, 20, 13, 37),
options: {
date: {
day: 'numeric',
month: 'numeric',
year: '2-digit'
},
time: {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
},
datetime: {
day: 'numeric',
month: 'numeric',
year: 'numeric',

hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
},
ru: {
date: '07.05.22',
time: '04:20:13',
datetime: '07.05.2022, 04:20:13'
},
it: {
date: '7/5/22',
time: '04:20:13',
datetime: '7/5/2022, 04:20:13'
}
};

export const NUMBER = {
value: 1_337_420.42,
options: {
style: 'currency',
currency: 'USD',
signDisplay: 'always',
minimumFractionDigits: 4,
maximumFractionDigits: 4
},
ru: '+1\u00A0337\u00A0420,4200\u00A0$',
it: '+1.337.420,4200\u00A0USD'
};

export const RELATIVE = {
value: 0.5,
unit: 'days',
options: {
style: 'long',
numeric: 'always'
},
ru: 'через 0,5 дня',
it: 'tra 0,5 giorni'
};

export const LIST = {
value: [1, 2, 3].map(String),
options: {
style: 'long'
},
ru: '1, 2 и 3',
it: '1, 2 e 3'
};

export const PLURAL = {
value: 3,
options: {},
ru: 'few',
it: 'other'
};

export const DEFAULT_PROPS = {
language: TRANSLATIONS_KEYS[0],
translations: TRANSLATIONS
Expand All @@ -33,4 +108,8 @@ export const NOOP = () => {
// Noop
};

// eslint-disable-next-line no-restricted-syntax
export * as Module from '../src';

// eslint-disable-next-line no-restricted-syntax
export * as ModuleFormat from '../src/format';
Loading