@douglasneuroinformatics/libui 4.4.1 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5B62SIBQ.js +60 -0
- package/dist/chunk-5B62SIBQ.js.map +1 -0
- package/dist/chunk-GT5NL2RJ.js +306 -0
- package/dist/chunk-GT5NL2RJ.js.map +1 -0
- package/dist/{chunk-LSGD4EQY.js → chunk-SGSRBDMT.js} +30 -24
- package/dist/{chunk-LSGD4EQY.js.map → chunk-SGSRBDMT.js.map} +1 -1
- package/dist/components.d.ts +13 -17
- package/dist/components.js +10 -4
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +3 -6
- package/dist/hooks.js +3 -2
- package/dist/i18n.d.ts +25 -28
- package/dist/i18n.js +4 -5
- package/dist/{types-DDyMlEuL.d.ts → types-CxoDu4Em.d.ts} +18 -6
- package/dist/utils.js +1 -0
- package/package.json +9 -15
- package/src/components/Accordion/Accordion.stories.tsx +1 -1
- package/src/components/ActionDropdown/ActionDropdown.stories.tsx +1 -1
- package/src/components/AlertDialog/AlertDialog.stories.tsx +1 -1
- package/src/components/ArrowToggle/ArrowToggle.stories.tsx +1 -1
- package/src/components/Avatar/Avatar.stories.tsx +1 -1
- package/src/components/Badge/Badge.stories.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -1
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/Card/Card.stories.tsx +1 -1
- package/src/components/Chart/Chart.stories.tsx +1 -1
- package/src/components/Checkbox/Checkbox.stories.tsx +2 -2
- package/src/components/ClientTable/ClientTable.stories.tsx +1 -1
- package/src/components/Collapsible/Collapsible.stories.tsx +1 -1
- package/src/components/Command/Command.stories.tsx +1 -1
- package/src/components/ContextMenu/ContextMenu.stories.tsx +1 -1
- package/src/components/CopyButton/CopyButton.stories.tsx +1 -1
- package/src/components/DataTable/DataTable.stories.tsx +1 -1
- package/src/components/DatePicker/DatePicker.stories.tsx +1 -1
- package/src/components/Dialog/Dialog.stories.tsx +1 -1
- package/src/components/Drawer/Drawer.stories.tsx +1 -1
- package/src/components/DropdownButton/DropdownButton.stories.tsx +1 -1
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1 -1
- package/src/components/ErrorFallback/ErrorFallback.stories.tsx +1 -1
- package/src/components/FileDropzone/FileDropzone.stories.tsx +1 -1
- package/src/components/Form/BooleanField/BooleanField.stories.tsx +1 -1
- package/src/components/Form/DateField/DateField.stories.tsx +1 -1
- package/src/components/Form/Form.stories.tsx +1 -1
- package/src/components/Form/Form.tsx +13 -6
- package/src/components/Form/NumberField/NumberField.stories.tsx +1 -1
- package/src/components/Form/SetField/SetField.stories.tsx +1 -1
- package/src/components/Form/StringField/StringField.stories.tsx +1 -1
- package/src/components/Heading/Heading.stories.tsx +1 -1
- package/src/components/HoverCard/HoverCard.stories.tsx +2 -2
- package/src/components/Input/Input.stories.tsx +1 -1
- package/src/components/Label/Label.stories.tsx +1 -1
- package/src/components/LanguageToggle/LanguageToggle.stories.tsx +22 -2
- package/src/components/LineGraph/LineGraph.stories.tsx +1 -1
- package/src/components/ListboxDropdown/ListboxDropdown.stories.tsx +1 -1
- package/src/components/MenuBar/MenuBar.stories.tsx +1 -1
- package/src/components/NotificationHub/NotificationHub.stories.tsx +1 -1
- package/src/components/OneTimePasswordInput/OneTimePasswordInput.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +1 -1
- package/src/components/Popover/Popover.stories.tsx +4 -4
- package/src/components/Progress/Progress.stories.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.stories.tsx +1 -1
- package/src/components/Resizable/Resizable.stories.tsx +1 -1
- package/src/components/ScrollArea/ScrollArea.stories.tsx +1 -1
- package/src/components/SearchBar/SearchBar.stories.tsx +1 -1
- package/src/components/Select/Select.stories.tsx +1 -1
- package/src/components/Separator/Separator.stories.tsx +3 -3
- package/src/components/Sheet/Sheet.stories.tsx +1 -1
- package/src/components/Slider/Slider.stories.tsx +1 -1
- package/src/components/Spinner/Spinner.stories.tsx +1 -1
- package/src/components/SpinnerIcon/SpinnerIcon.stories.tsx +1 -1
- package/src/components/StatisticCard/StatisticCard.stories.tsx +1 -1
- package/src/components/Switch/Switch.stories.tsx +1 -1
- package/src/components/Table/Table.stories.tsx +1 -1
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/TextArea/TextArea.stories.tsx +1 -1
- package/src/components/ThemeToggle/ThemeToggle.stories.tsx +1 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/hooks/useTranslation/useTranslation.ts +39 -24
- package/src/i18n/__tests__/translator.test.ts +91 -0
- package/src/i18n/index.ts +4 -1
- package/src/i18n/translator.ts +129 -0
- package/src/i18n/types.ts +23 -6
- package/src/testing/mocks.ts +0 -9
- package/src/testing/setup-tests.ts +3 -2
- package/dist/chunk-VFVO337W.js +0 -252
- package/dist/chunk-VFVO337W.js.map +0 -1
- package/src/i18n/internal.ts +0 -23
- package/src/i18n/store.ts +0 -69
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment } from 'react';
|
|
2
2
|
|
|
3
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
4
|
import { CircleHelpIcon } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
import { Button } from '../Button';
|
|
@@ -28,8 +28,8 @@ export const Form: Story = {
|
|
|
28
28
|
<Popover.Content className="w-80 p-4">
|
|
29
29
|
<div className="grid gap-4">
|
|
30
30
|
<div className="space-y-2">
|
|
31
|
-
<h4 className="font-medium
|
|
32
|
-
<p className="text-
|
|
31
|
+
<h4 className="leading-none font-medium">Dimensions</h4>
|
|
32
|
+
<p className="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
|
|
33
33
|
</div>
|
|
34
34
|
<div className="grid gap-2">
|
|
35
35
|
<div className="grid grid-cols-3 items-center gap-4">
|
|
@@ -65,7 +65,7 @@ export const Icon: Story = {
|
|
|
65
65
|
<CircleHelpIcon />
|
|
66
66
|
</Button>
|
|
67
67
|
</Popover.Trigger>
|
|
68
|
-
<Popover.Content className="w-min whitespace-nowrap
|
|
68
|
+
<Popover.Content className="w-min text-sm whitespace-nowrap">Hello World</Popover.Content>
|
|
69
69
|
</Fragment>
|
|
70
70
|
)
|
|
71
71
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import { Separator } from './Separator';
|
|
4
4
|
|
|
@@ -11,8 +11,8 @@ export const Default: Story = {
|
|
|
11
11
|
(Story) => (
|
|
12
12
|
<div>
|
|
13
13
|
<div className="space-y-1">
|
|
14
|
-
<h4 className="text-sm font-medium
|
|
15
|
-
<p className="text-
|
|
14
|
+
<h4 className="text-sm leading-none font-medium">Radix Primitives</h4>
|
|
15
|
+
<p className="text-muted-foreground text-sm">An open-source UI component library.</p>
|
|
16
16
|
</div>
|
|
17
17
|
<Story args={{ className: 'my-4' }} />
|
|
18
18
|
<div className="flex h-5 items-center space-x-4 text-sm">
|
|
@@ -1,30 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type {
|
|
4
|
+
TranslateFunction,
|
|
5
|
+
TranslationKey,
|
|
6
|
+
TranslationKeyForNamespace,
|
|
7
|
+
TranslationNamespace,
|
|
8
|
+
TranslatorType
|
|
9
|
+
} from '@/i18n';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import { getTranslation } from '@/i18n/internal';
|
|
11
|
+
// this is required since our storybook manager plugin cannot use vite aliases
|
|
12
|
+
import { i18n } from '../../i18n';
|
|
8
13
|
|
|
9
|
-
export function useTranslation<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const resolvedLanguage =
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export function useTranslation(): TranslatorType<TranslationKey>;
|
|
15
|
+
export function useTranslation<TNamespace extends TranslationNamespace>(
|
|
16
|
+
namespace: TNamespace
|
|
17
|
+
): TranslatorType<TranslationKeyForNamespace<TNamespace>>;
|
|
18
|
+
export function useTranslation(namespace?: TranslationNamespace): TranslatorType<string> {
|
|
19
|
+
const [resolvedLanguage, setResolvedLanguage] = useState(i18n.resolvedLanguage);
|
|
20
|
+
const { changeLanguage, t } = useMemo(() => {
|
|
21
|
+
const t: TranslateFunction<string> = (target, options) => {
|
|
22
|
+
if (typeof target === 'object') {
|
|
23
|
+
return i18n.t(target, options);
|
|
24
|
+
}
|
|
25
|
+
return i18n.t((namespace ? `${namespace}.${target}` : target) as TranslationKey, options);
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
changeLanguage: i18n.changeLanguage.bind(i18n),
|
|
29
|
+
t
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
i18n.addEventListener('languageChange', setResolvedLanguage);
|
|
35
|
+
return () => {
|
|
36
|
+
i18n.removeEventListener('languageChange', setResolvedLanguage);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
28
39
|
|
|
29
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
changeLanguage,
|
|
42
|
+
resolvedLanguage,
|
|
43
|
+
t
|
|
44
|
+
};
|
|
30
45
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Translator } from '../translator';
|
|
4
|
+
|
|
5
|
+
describe('Translator', () => {
|
|
6
|
+
const translator = new Translator();
|
|
7
|
+
|
|
8
|
+
it('should initially not be initialized', () => {
|
|
9
|
+
expect(translator.isInitialized).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should not allow accessing the changeLanguage method before initialization', () => {
|
|
13
|
+
expect(() => translator.changeLanguage('fr')).toThrow(
|
|
14
|
+
"Cannot access method 'changeLanguage' of Translator instance before initialization"
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should not allow accessing the resolvedLanguage method before initialization', () => {
|
|
19
|
+
expect(() => translator.resolvedLanguage).toThrow(
|
|
20
|
+
"Cannot access getter 'resolvedLanguage' of Translator instance before initialization"
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should allow initialization', () => {
|
|
25
|
+
translator.init({ defaultLanguage: 'en', translations: {} });
|
|
26
|
+
expect(translator.isInitialized).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not allow reinitialization', () => {
|
|
30
|
+
expect(() => translator.init({ defaultLanguage: 'en', translations: {} })).toThrow(
|
|
31
|
+
'Cannot reinitialize Translator'
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should have set the document language to en', () => {
|
|
36
|
+
expect(document.documentElement.getAttribute('lang')).toBe('en');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should allow accessing translations', () => {
|
|
40
|
+
expect(translator.t('libui.days.monday')).toBe('Monday');
|
|
41
|
+
expect(translator.t({ en: 'Yes', fr: 'Oui' })).toBe('Yes');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should allow changing the language', () => {
|
|
45
|
+
translator.changeLanguage('fr');
|
|
46
|
+
expect(translator.resolvedLanguage).toBe('fr');
|
|
47
|
+
expect(document.documentElement.getAttribute('lang')).toBe('fr');
|
|
48
|
+
expect(translator.t('libui.days.monday')).toBe('Lundi');
|
|
49
|
+
expect(translator.t({ en: 'Yes', fr: 'Oui' })).toBe('Oui');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return the language from the defaultLanguage, if the resolvedLanguage is unavailable', () => {
|
|
53
|
+
expect(translator.resolvedLanguage).toBe('fr');
|
|
54
|
+
expect(translator.t({ en: 'Yes' })).toBe('Yes');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return an empty string if no translation is available', () => {
|
|
58
|
+
vi.spyOn(console, 'error').mockImplementationOnce(() => undefined);
|
|
59
|
+
expect(translator.t({})).toBe('');
|
|
60
|
+
expect(console.error).toHaveBeenLastCalledWith("Failed to extract translation from object '{}'");
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should allow adding and removing event listeners', () => {
|
|
65
|
+
const handleLanguageChange1 = vi.fn();
|
|
66
|
+
const handleLanguageChange2 = vi.fn();
|
|
67
|
+
translator.addEventListener('languageChange', handleLanguageChange1);
|
|
68
|
+
translator.addEventListener('languageChange', handleLanguageChange2);
|
|
69
|
+
translator.changeLanguage('en');
|
|
70
|
+
expect(translator.removeEventListener('languageChange', handleLanguageChange1)).toBe(true);
|
|
71
|
+
translator.changeLanguage('fr');
|
|
72
|
+
expect(translator.removeEventListener('languageChange', handleLanguageChange2)).toBe(true);
|
|
73
|
+
expect(handleLanguageChange1).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(handleLanguageChange2).toHaveBeenCalledTimes(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should apply arguments', () => {
|
|
78
|
+
expect(translator.t({ en: 'Hello, {}' }, { args: ['World'] })).toBe('Hello, World');
|
|
79
|
+
expect(
|
|
80
|
+
translator.t(
|
|
81
|
+
{ en: 'Hello, {}', fr: 'Bonjour, {}' },
|
|
82
|
+
{
|
|
83
|
+
args: {
|
|
84
|
+
en: ['World'],
|
|
85
|
+
fr: ['tout le monde']
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
).toBe('Bonjour, tout le monde');
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/i18n/index.ts
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { format } from '@douglasneuroinformatics/libjs';
|
|
2
|
+
import { get } from 'lodash-es';
|
|
3
|
+
import type { SetOptional } from 'type-fest';
|
|
4
|
+
|
|
5
|
+
import libui from './translations/libui.json';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Language,
|
|
9
|
+
TranslateFormatArgs,
|
|
10
|
+
TranslateOptions,
|
|
11
|
+
TranslationKey,
|
|
12
|
+
Translations,
|
|
13
|
+
TranslatorType
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
type TranslatorEventMap = {
|
|
17
|
+
languageChange: (...args: [language: Language]) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type TranslatorConfig = {
|
|
21
|
+
defaultLanguage?: Language;
|
|
22
|
+
translations: SetOptional<Translations, 'libui'>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function InitializedOnly<T extends Translator, TArgs extends any[], TReturn>(
|
|
26
|
+
target: (this: T, ...args: TArgs) => TReturn,
|
|
27
|
+
context: ClassGetterDecoratorContext<T> | ClassMethodDecoratorContext<T> | ClassSetterDecoratorContext<T>
|
|
28
|
+
) {
|
|
29
|
+
const name = context.name.toString();
|
|
30
|
+
function replacementMethod(this: T, ...args: TArgs): TReturn {
|
|
31
|
+
if (!this.isInitialized) {
|
|
32
|
+
throw new Error(`Cannot access ${context.kind} '${name}' of Translator instance before initialization`);
|
|
33
|
+
}
|
|
34
|
+
return target.call(this, ...args);
|
|
35
|
+
}
|
|
36
|
+
return replacementMethod;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Translator implements TranslatorType<TranslationKey> {
|
|
40
|
+
#config: Required<TranslatorConfig>;
|
|
41
|
+
#eventHandlers: {
|
|
42
|
+
[K in keyof TranslatorEventMap]: Set<TranslatorEventMap[K]>;
|
|
43
|
+
};
|
|
44
|
+
#resolvedLanguage: Language;
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
// in the implementation, these should only be accessed in methods decorated with @InitializedOnly
|
|
48
|
+
this.#config = null!;
|
|
49
|
+
this.#eventHandlers = {
|
|
50
|
+
languageChange: new Set()
|
|
51
|
+
};
|
|
52
|
+
this.#resolvedLanguage = null!;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get isInitialized() {
|
|
56
|
+
return this.#config !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@InitializedOnly
|
|
60
|
+
get resolvedLanguage() {
|
|
61
|
+
return this.#resolvedLanguage;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addEventListener<TKey extends keyof TranslatorEventMap>(key: TKey, handler: TranslatorEventMap[TKey]) {
|
|
65
|
+
this.#eventHandlers[key].add(handler);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@InitializedOnly
|
|
69
|
+
changeLanguage(language: Language) {
|
|
70
|
+
this.#resolvedLanguage = language;
|
|
71
|
+
document.documentElement.lang = language;
|
|
72
|
+
this.emitEvent('languageChange', [language]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
init({ defaultLanguage, translations }: TranslatorConfig) {
|
|
76
|
+
if (this.isInitialized) {
|
|
77
|
+
throw new Error('Cannot reinitialize Translator');
|
|
78
|
+
}
|
|
79
|
+
this.#config = {
|
|
80
|
+
defaultLanguage: defaultLanguage ?? 'en',
|
|
81
|
+
translations: {
|
|
82
|
+
libui,
|
|
83
|
+
...translations
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
this.changeLanguage(this.#config.defaultLanguage);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
removeEventListener<TKey extends keyof TranslatorEventMap>(key: TKey, handler: TranslatorEventMap[TKey]) {
|
|
90
|
+
return this.#eventHandlers[key].delete(handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@InitializedOnly
|
|
94
|
+
t(target: TranslationKey | { [L in Language]?: string }, { args }: TranslateOptions = {}): string {
|
|
95
|
+
let obj: { [key: string]: string };
|
|
96
|
+
if (typeof target === 'string') {
|
|
97
|
+
obj = (get(this.#config.translations, target) ?? {}) as { [key: string]: string };
|
|
98
|
+
} else {
|
|
99
|
+
obj = target;
|
|
100
|
+
}
|
|
101
|
+
const value = obj[this.#resolvedLanguage] ?? obj[this.#config.defaultLanguage];
|
|
102
|
+
if (!value) {
|
|
103
|
+
console.error(`Failed to extract translation from object '${JSON.stringify(obj)}'`);
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
if (!args) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return format(value, ...this.getFormatArgs(args));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private emitEvent<TKey extends keyof TranslatorEventMap>(key: TKey, payload: Parameters<TranslatorEventMap[TKey]>) {
|
|
113
|
+
this.#eventHandlers[key].forEach((fn: (...args: any[]) => any) => {
|
|
114
|
+
fn(...payload);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getFormatArgs(args: TranslateFormatArgs) {
|
|
119
|
+
if (Array.isArray(args)) {
|
|
120
|
+
return args;
|
|
121
|
+
}
|
|
122
|
+
const result = args[this.#resolvedLanguage] ?? args[this.#config.defaultLanguage];
|
|
123
|
+
if (!result) {
|
|
124
|
+
console.error(`Failed to extract args from object '${JSON.stringify(args)}'`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/i18n/types.ts
CHANGED
|
@@ -41,11 +41,28 @@ export type ExtractTranslationKey<T extends { [key: string]: any }, Key = keyof
|
|
|
41
41
|
|
|
42
42
|
export type TranslationNamespace = Extract<keyof Translations, string>;
|
|
43
43
|
|
|
44
|
-
export type TranslationKey
|
|
45
|
-
? ExtractTranslationKey<Translations[TNamespace]>
|
|
46
|
-
: ExtractTranslationKey<Translations>;
|
|
44
|
+
export type TranslationKey = ExtractTranslationKey<Translations>;
|
|
47
45
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
export type TranslationKeyForNamespace<TNamespace extends TranslationNamespace> =
|
|
47
|
+
TranslationKey extends `${TNamespace}.${infer TKey}` ? TKey : never;
|
|
48
|
+
|
|
49
|
+
export type TranslateFormatArgs =
|
|
50
|
+
| Exclude<Primitive, symbol>[]
|
|
51
|
+
| {
|
|
52
|
+
[L in Language]?: Exclude<Primitive, symbol>[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type TranslateOptions = {
|
|
56
|
+
args?: TranslateFormatArgs;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface TranslateFunction<TKey extends string> {
|
|
60
|
+
(key: TKey, options?: TranslateOptions): string;
|
|
61
|
+
(translations: { [L in Language]?: string }, options?: TranslateOptions): string;
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
export type TranslatorType<TKey extends string> = {
|
|
65
|
+
changeLanguage: (language: Language) => void;
|
|
66
|
+
resolvedLanguage: Language;
|
|
67
|
+
t: TranslateFunction<TKey>;
|
|
68
|
+
};
|
package/src/testing/mocks.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
1
|
import { vi } from 'vitest';
|
|
3
2
|
|
|
4
3
|
import type { StorageName } from '@/hooks';
|
|
@@ -51,11 +50,3 @@ export const mockStorage = (name: StorageName): void => {
|
|
|
51
50
|
value: new StorageMock()
|
|
52
51
|
});
|
|
53
52
|
};
|
|
54
|
-
|
|
55
|
-
export const mockTranslationStore = () => {
|
|
56
|
-
vi.mock('@/hooks/useTranslation', () => ({
|
|
57
|
-
useTranslation: () => ({
|
|
58
|
-
t: () => faker.word.sample()
|
|
59
|
-
})
|
|
60
|
-
}));
|
|
61
|
-
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { cleanup } from '@testing-library/react';
|
|
2
2
|
import { afterEach, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { i18n } from '@/i18n';
|
|
5
5
|
|
|
6
6
|
import '@testing-library/jest-dom/vitest';
|
|
7
7
|
|
|
8
8
|
vi.mock('zustand');
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
i18n.init({ translations: {} });
|
|
10
11
|
|
|
11
12
|
// Since we're not using vitest globals, we need to explicitly call cleanup()
|
|
12
13
|
// for testing-library. See:
|