@object-ui/i18n 0.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/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/dist/i18n.d.ts +36 -0
- package/dist/i18n.js +62 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/locales/ar.d.ts +112 -0
- package/dist/locales/ar.js +112 -0
- package/dist/locales/de.d.ts +111 -0
- package/dist/locales/de.js +111 -0
- package/dist/locales/en.d.ts +112 -0
- package/dist/locales/en.js +111 -0
- package/dist/locales/es.d.ts +111 -0
- package/dist/locales/es.js +111 -0
- package/dist/locales/fr.d.ts +111 -0
- package/dist/locales/fr.js +111 -0
- package/dist/locales/index.d.ts +1094 -0
- package/dist/locales/index.js +38 -0
- package/dist/locales/ja.d.ts +111 -0
- package/dist/locales/ja.js +111 -0
- package/dist/locales/ko.d.ts +111 -0
- package/dist/locales/ko.js +111 -0
- package/dist/locales/pt.d.ts +111 -0
- package/dist/locales/pt.js +111 -0
- package/dist/locales/ru.d.ts +111 -0
- package/dist/locales/ru.js +111 -0
- package/dist/locales/zh.d.ts +111 -0
- package/dist/locales/zh.js +111 -0
- package/dist/provider.d.ts +65 -0
- package/dist/provider.js +84 -0
- package/dist/utils/formatting.d.ts +45 -0
- package/dist/utils/formatting.js +86 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +48 -0
- package/src/__tests__/i18n.test.ts +228 -0
- package/src/i18n.ts +98 -0
- package/src/index.ts +63 -0
- package/src/locales/ar.ts +113 -0
- package/src/locales/de.ts +112 -0
- package/src/locales/en.ts +113 -0
- package/src/locales/es.ts +112 -0
- package/src/locales/fr.ts +112 -0
- package/src/locales/index.ts +42 -0
- package/src/locales/ja.ts +112 -0
- package/src/locales/ko.ts +112 -0
- package/src/locales/pt.ts +112 -0
- package/src/locales/ru.ts +112 -0
- package/src/locales/zh.ts +112 -0
- package/src/provider.tsx +125 -0
- package/src/utils/formatting.ts +136 -0
- package/src/utils/index.ts +10 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/i18n - React integration
|
|
3
|
+
*
|
|
4
|
+
* Provides I18nProvider and useObjectTranslation hook for React components.
|
|
5
|
+
*/
|
|
6
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { I18nextProvider, useTranslation } from 'react-i18next';
|
|
8
|
+
import { createI18n, getDirection } from './i18n';
|
|
9
|
+
const ObjectI18nContext = createContext(null);
|
|
10
|
+
/**
|
|
11
|
+
* I18nProvider - Wraps your app with i18n support
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <I18nProvider config={{ defaultLanguage: 'zh' }}>
|
|
16
|
+
* <App />
|
|
17
|
+
* </I18nProvider>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function I18nProvider({ config, instance: externalInstance, children }) {
|
|
21
|
+
const i18nInstance = useMemo(() => externalInstance || createI18n(config), [externalInstance]);
|
|
22
|
+
const [language, setLanguage] = useState(i18nInstance.language || 'en');
|
|
23
|
+
const direction = getDirection(language);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const handleLanguageChanged = (lng) => {
|
|
26
|
+
setLanguage(lng);
|
|
27
|
+
// Update document direction for RTL support
|
|
28
|
+
if (typeof document !== 'undefined') {
|
|
29
|
+
document.documentElement.dir = getDirection(lng);
|
|
30
|
+
document.documentElement.lang = lng;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
i18nInstance.on('languageChanged', handleLanguageChanged);
|
|
34
|
+
return () => {
|
|
35
|
+
i18nInstance.off('languageChanged', handleLanguageChanged);
|
|
36
|
+
};
|
|
37
|
+
}, [i18nInstance]);
|
|
38
|
+
const contextValue = useMemo(() => ({
|
|
39
|
+
language,
|
|
40
|
+
changeLanguage: async (lang) => {
|
|
41
|
+
await i18nInstance.changeLanguage(lang);
|
|
42
|
+
},
|
|
43
|
+
direction,
|
|
44
|
+
i18n: i18nInstance,
|
|
45
|
+
}), [language, direction, i18nInstance]);
|
|
46
|
+
return React.createElement(ObjectI18nContext.Provider, { value: contextValue }, React.createElement(I18nextProvider, { i18n: i18nInstance }, children));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Hook to access Object UI i18n context
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* function MyComponent() {
|
|
54
|
+
* const { t, language, changeLanguage, direction } = useObjectTranslation();
|
|
55
|
+
* return <div dir={direction}>{t('common.save')}</div>;
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function useObjectTranslation(ns) {
|
|
60
|
+
const context = useContext(ObjectI18nContext);
|
|
61
|
+
const { t, i18n } = useTranslation(ns);
|
|
62
|
+
return {
|
|
63
|
+
/** Translation function */
|
|
64
|
+
t,
|
|
65
|
+
/** Current language code */
|
|
66
|
+
language: context?.language || i18n.language || 'en',
|
|
67
|
+
/** Change the active language */
|
|
68
|
+
changeLanguage: context?.changeLanguage || (async (lang) => { await i18n.changeLanguage(lang); }),
|
|
69
|
+
/** Current text direction */
|
|
70
|
+
direction: context?.direction || 'ltr',
|
|
71
|
+
/** The underlying i18next instance */
|
|
72
|
+
i18n,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hook to access the i18n context directly
|
|
77
|
+
*/
|
|
78
|
+
export function useI18nContext() {
|
|
79
|
+
const context = useContext(ObjectI18nContext);
|
|
80
|
+
if (!context) {
|
|
81
|
+
throw new Error('useI18nContext must be used within an I18nProvider');
|
|
82
|
+
}
|
|
83
|
+
return context;
|
|
84
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/i18n - Date and currency formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Uses the native Intl API for locale-aware formatting.
|
|
5
|
+
*/
|
|
6
|
+
export interface DateFormatOptions {
|
|
7
|
+
locale?: string;
|
|
8
|
+
style?: 'short' | 'medium' | 'long' | 'full';
|
|
9
|
+
dateStyle?: Intl.DateTimeFormatOptions['dateStyle'];
|
|
10
|
+
timeStyle?: Intl.DateTimeFormatOptions['timeStyle'];
|
|
11
|
+
}
|
|
12
|
+
export interface CurrencyFormatOptions {
|
|
13
|
+
locale?: string;
|
|
14
|
+
currency?: string;
|
|
15
|
+
style?: 'currency' | 'decimal' | 'percent';
|
|
16
|
+
minimumFractionDigits?: number;
|
|
17
|
+
maximumFractionDigits?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface NumberFormatOptions {
|
|
20
|
+
locale?: string;
|
|
21
|
+
style?: 'decimal' | 'percent' | 'unit';
|
|
22
|
+
minimumFractionDigits?: number;
|
|
23
|
+
maximumFractionDigits?: number;
|
|
24
|
+
notation?: 'standard' | 'scientific' | 'engineering' | 'compact';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Format a date according to locale conventions
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatDate(date: Date | string | number, options?: DateFormatOptions): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format a date and time according to locale conventions
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatDateTime(date: Date | string | number, options?: DateFormatOptions): string;
|
|
34
|
+
/**
|
|
35
|
+
* Format a relative time (e.g., "2 days ago", "in 3 hours")
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatRelativeTime(date: Date | string | number, locale?: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Format a currency value according to locale conventions
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatCurrency(value: number, options?: CurrencyFormatOptions): string;
|
|
42
|
+
/**
|
|
43
|
+
* Format a number according to locale conventions
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatNumber(value: number, options?: NumberFormatOptions): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/i18n - Date and currency formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Uses the native Intl API for locale-aware formatting.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Format a date according to locale conventions
|
|
8
|
+
*/
|
|
9
|
+
export function formatDate(date, options = {}) {
|
|
10
|
+
const { locale = 'en', style = 'medium' } = options;
|
|
11
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
12
|
+
if (isNaN(d.getTime())) {
|
|
13
|
+
return String(date);
|
|
14
|
+
}
|
|
15
|
+
const styleMap = {
|
|
16
|
+
short: { year: '2-digit', month: 'numeric', day: 'numeric' },
|
|
17
|
+
medium: { year: 'numeric', month: 'short', day: 'numeric' },
|
|
18
|
+
long: { year: 'numeric', month: 'long', day: 'numeric' },
|
|
19
|
+
full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
|
|
20
|
+
};
|
|
21
|
+
const formatOptions = options.dateStyle
|
|
22
|
+
? { dateStyle: options.dateStyle, timeStyle: options.timeStyle }
|
|
23
|
+
: styleMap[style] || styleMap.medium;
|
|
24
|
+
return new Intl.DateTimeFormat(locale, formatOptions).format(d);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Format a date and time according to locale conventions
|
|
28
|
+
*/
|
|
29
|
+
export function formatDateTime(date, options = {}) {
|
|
30
|
+
const { locale = 'en', style = 'medium' } = options;
|
|
31
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
32
|
+
if (isNaN(d.getTime())) {
|
|
33
|
+
return String(date);
|
|
34
|
+
}
|
|
35
|
+
const styleMap = {
|
|
36
|
+
short: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
|
37
|
+
medium: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
|
38
|
+
long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
|
39
|
+
full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' },
|
|
40
|
+
};
|
|
41
|
+
return new Intl.DateTimeFormat(locale, styleMap[style] || styleMap.medium).format(d);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format a relative time (e.g., "2 days ago", "in 3 hours")
|
|
45
|
+
*/
|
|
46
|
+
export function formatRelativeTime(date, locale = 'en') {
|
|
47
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const diffMs = d.getTime() - now.getTime();
|
|
50
|
+
const diffSec = Math.round(diffMs / 1000);
|
|
51
|
+
const diffMin = Math.round(diffSec / 60);
|
|
52
|
+
const diffHour = Math.round(diffMin / 60);
|
|
53
|
+
const diffDay = Math.round(diffHour / 24);
|
|
54
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
55
|
+
if (Math.abs(diffSec) < 60)
|
|
56
|
+
return rtf.format(diffSec, 'second');
|
|
57
|
+
if (Math.abs(diffMin) < 60)
|
|
58
|
+
return rtf.format(diffMin, 'minute');
|
|
59
|
+
if (Math.abs(diffHour) < 24)
|
|
60
|
+
return rtf.format(diffHour, 'hour');
|
|
61
|
+
if (Math.abs(diffDay) < 30)
|
|
62
|
+
return rtf.format(diffDay, 'day');
|
|
63
|
+
const diffMonth = Math.round(diffDay / 30);
|
|
64
|
+
if (Math.abs(diffMonth) < 12)
|
|
65
|
+
return rtf.format(diffMonth, 'month');
|
|
66
|
+
return rtf.format(Math.round(diffDay / 365), 'year');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Format a currency value according to locale conventions
|
|
70
|
+
*/
|
|
71
|
+
export function formatCurrency(value, options = {}) {
|
|
72
|
+
const { locale = 'en', currency = 'USD', minimumFractionDigits, maximumFractionDigits } = options;
|
|
73
|
+
return new Intl.NumberFormat(locale, {
|
|
74
|
+
style: 'currency',
|
|
75
|
+
currency,
|
|
76
|
+
minimumFractionDigits,
|
|
77
|
+
maximumFractionDigits,
|
|
78
|
+
}).format(value);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Format a number according to locale conventions
|
|
82
|
+
*/
|
|
83
|
+
export function formatNumber(value, options = {}) {
|
|
84
|
+
const { locale = 'en', ...rest } = options;
|
|
85
|
+
return new Intl.NumberFormat(locale, rest).format(value);
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, type DateFormatOptions, type CurrencyFormatOptions, type NumberFormatOptions, } from './formatting';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { formatDate, formatDateTime, formatRelativeTime, formatCurrency, formatNumber, } from './formatting';
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@object-ui/i18n",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Internationalization (i18n) support for Object UI with 10+ language packs, RTL layout, and date/currency formatting.",
|
|
7
|
+
"homepage": "https://www.objectui.org",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
11
|
+
"directory": "packages/i18n"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/objectstack-ai/objectui/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./locales/*": {
|
|
25
|
+
"types": "./dist/locales/*.d.ts",
|
|
26
|
+
"import": "./dist/locales/*.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"i18next": "^25.8.4",
|
|
31
|
+
"react-i18next": "^16.5.4"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.1.8",
|
|
38
|
+
"react": "^19.1.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"type-check": "tsc --noEmit",
|
|
46
|
+
"lint": "eslint ."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createI18n,
|
|
4
|
+
getDirection,
|
|
5
|
+
getAvailableLanguages,
|
|
6
|
+
builtInLocales,
|
|
7
|
+
isRTL,
|
|
8
|
+
RTL_LANGUAGES,
|
|
9
|
+
formatDate,
|
|
10
|
+
formatDateTime,
|
|
11
|
+
formatCurrency,
|
|
12
|
+
formatNumber,
|
|
13
|
+
} from '../index';
|
|
14
|
+
|
|
15
|
+
describe('@object-ui/i18n', () => {
|
|
16
|
+
describe('createI18n', () => {
|
|
17
|
+
it('creates an i18next instance with default config', () => {
|
|
18
|
+
const i18n = createI18n({ detectBrowserLanguage: false });
|
|
19
|
+
expect(i18n).toBeDefined();
|
|
20
|
+
expect(i18n.language).toBe('en');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('creates an instance with specified default language', () => {
|
|
24
|
+
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
25
|
+
expect(i18n.language).toBe('zh');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('loads all built-in locales', () => {
|
|
29
|
+
const i18n = createI18n({ detectBrowserLanguage: false });
|
|
30
|
+
const langs = getAvailableLanguages(i18n);
|
|
31
|
+
expect(langs).toContain('en');
|
|
32
|
+
expect(langs).toContain('zh');
|
|
33
|
+
expect(langs).toContain('ja');
|
|
34
|
+
expect(langs).toContain('ko');
|
|
35
|
+
expect(langs).toContain('de');
|
|
36
|
+
expect(langs).toContain('fr');
|
|
37
|
+
expect(langs).toContain('es');
|
|
38
|
+
expect(langs).toContain('pt');
|
|
39
|
+
expect(langs).toContain('ru');
|
|
40
|
+
expect(langs).toContain('ar');
|
|
41
|
+
expect(langs.length).toBeGreaterThanOrEqual(10);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('translates common keys in English', () => {
|
|
45
|
+
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
46
|
+
expect(i18n.t('common.save')).toBe('Save');
|
|
47
|
+
expect(i18n.t('common.cancel')).toBe('Cancel');
|
|
48
|
+
expect(i18n.t('common.delete')).toBe('Delete');
|
|
49
|
+
expect(i18n.t('common.loading')).toBe('Loading...');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('translates common keys in Chinese', () => {
|
|
53
|
+
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
54
|
+
expect(i18n.t('common.save')).toBe('保存');
|
|
55
|
+
expect(i18n.t('common.cancel')).toBe('取消');
|
|
56
|
+
expect(i18n.t('common.delete')).toBe('删除');
|
|
57
|
+
expect(i18n.t('common.loading')).toBe('加载中...');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('translates common keys in Japanese', () => {
|
|
61
|
+
const i18n = createI18n({ defaultLanguage: 'ja', detectBrowserLanguage: false });
|
|
62
|
+
expect(i18n.t('common.save')).toBe('保存');
|
|
63
|
+
expect(i18n.t('common.cancel')).toBe('キャンセル');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('translates common keys in Korean', () => {
|
|
67
|
+
const i18n = createI18n({ defaultLanguage: 'ko', detectBrowserLanguage: false });
|
|
68
|
+
expect(i18n.t('common.save')).toBe('저장');
|
|
69
|
+
expect(i18n.t('common.cancel')).toBe('취소');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('supports interpolation in validation messages', () => {
|
|
73
|
+
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
74
|
+
expect(i18n.t('validation.required', { field: 'Name' })).toBe('Name is required');
|
|
75
|
+
expect(i18n.t('validation.minLength', { field: 'Password', min: 8 })).toBe(
|
|
76
|
+
'Password must be at least 8 characters',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('supports interpolation in Chinese', () => {
|
|
81
|
+
const i18n = createI18n({ defaultLanguage: 'zh', detectBrowserLanguage: false });
|
|
82
|
+
expect(i18n.t('validation.required', { field: '姓名' })).toBe('姓名不能为空');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('falls back to English for unknown language', () => {
|
|
86
|
+
const i18n = createI18n({ defaultLanguage: 'xx', fallbackLanguage: 'en', detectBrowserLanguage: false });
|
|
87
|
+
expect(i18n.t('common.save')).toBe('Save');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('merges custom resources with built-in locales', () => {
|
|
91
|
+
const i18n = createI18n({
|
|
92
|
+
defaultLanguage: 'en',
|
|
93
|
+
detectBrowserLanguage: false,
|
|
94
|
+
resources: {
|
|
95
|
+
en: { custom: { greeting: 'Hello!' } },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
expect(i18n.t('custom.greeting')).toBe('Hello!');
|
|
99
|
+
// Built-in translations still work
|
|
100
|
+
expect(i18n.t('common.save')).toBe('Save');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('changes language dynamically', async () => {
|
|
104
|
+
const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
|
|
105
|
+
expect(i18n.t('common.save')).toBe('Save');
|
|
106
|
+
|
|
107
|
+
await i18n.changeLanguage('zh');
|
|
108
|
+
expect(i18n.language).toBe('zh');
|
|
109
|
+
expect(i18n.t('common.save')).toBe('保存');
|
|
110
|
+
|
|
111
|
+
await i18n.changeLanguage('ja');
|
|
112
|
+
expect(i18n.language).toBe('ja');
|
|
113
|
+
expect(i18n.t('common.save')).toBe('保存');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('getDirection', () => {
|
|
118
|
+
it('returns ltr for English', () => {
|
|
119
|
+
expect(getDirection('en')).toBe('ltr');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns ltr for Chinese', () => {
|
|
123
|
+
expect(getDirection('zh')).toBe('ltr');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns rtl for Arabic', () => {
|
|
127
|
+
expect(getDirection('ar')).toBe('rtl');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns ltr for unknown languages', () => {
|
|
131
|
+
expect(getDirection('xx')).toBe('ltr');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('isRTL', () => {
|
|
136
|
+
it('returns true for Arabic', () => {
|
|
137
|
+
expect(isRTL('ar')).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns false for English', () => {
|
|
141
|
+
expect(isRTL('en')).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns false for Chinese', () => {
|
|
145
|
+
expect(isRTL('zh')).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('RTL_LANGUAGES', () => {
|
|
150
|
+
it('includes Arabic', () => {
|
|
151
|
+
expect(RTL_LANGUAGES).toContain('ar');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('includes Hebrew', () => {
|
|
155
|
+
expect(RTL_LANGUAGES).toContain('he');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('builtInLocales', () => {
|
|
160
|
+
it('has 10 built-in language packs', () => {
|
|
161
|
+
expect(Object.keys(builtInLocales).length).toBe(10);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('all locales have the same top-level keys', () => {
|
|
165
|
+
const enKeys = Object.keys(builtInLocales.en).sort();
|
|
166
|
+
for (const [lang, locale] of Object.entries(builtInLocales)) {
|
|
167
|
+
const keys = Object.keys(locale).sort();
|
|
168
|
+
expect(keys).toEqual(enKeys);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('all locales have common section keys matching English', () => {
|
|
173
|
+
const enCommonKeys = Object.keys(builtInLocales.en.common).sort();
|
|
174
|
+
for (const [lang, locale] of Object.entries(builtInLocales)) {
|
|
175
|
+
const keys = Object.keys(locale.common).sort();
|
|
176
|
+
expect(keys).toEqual(enCommonKeys);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('formatDate', () => {
|
|
182
|
+
it('formats a date with default options', () => {
|
|
183
|
+
const result = formatDate(new Date(2026, 0, 15), { locale: 'en' });
|
|
184
|
+
expect(result).toContain('Jan');
|
|
185
|
+
expect(result).toContain('15');
|
|
186
|
+
expect(result).toContain('2026');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('formats a date with short style', () => {
|
|
190
|
+
const result = formatDate(new Date(2026, 0, 15), { locale: 'en', style: 'short' });
|
|
191
|
+
expect(result).toContain('15');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns string for invalid dates', () => {
|
|
195
|
+
const result = formatDate('invalid-date');
|
|
196
|
+
expect(result).toBe('invalid-date');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('formatCurrency', () => {
|
|
201
|
+
it('formats USD by default', () => {
|
|
202
|
+
const result = formatCurrency(1234.56, { locale: 'en' });
|
|
203
|
+
expect(result).toContain('1,234.56');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('formats EUR', () => {
|
|
207
|
+
const result = formatCurrency(1234.56, { locale: 'de', currency: 'EUR' });
|
|
208
|
+
expect(result).toContain('1.234,56');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('formats CNY', () => {
|
|
212
|
+
const result = formatCurrency(1234.56, { locale: 'zh', currency: 'CNY' });
|
|
213
|
+
expect(result).toContain('1,234.56');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('formatNumber', () => {
|
|
218
|
+
it('formats a number with default locale', () => {
|
|
219
|
+
const result = formatNumber(1234567.89, { locale: 'en' });
|
|
220
|
+
expect(result).toContain('1,234,567.89');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('formats with compact notation', () => {
|
|
224
|
+
const result = formatNumber(1234567, { locale: 'en', notation: 'compact' });
|
|
225
|
+
expect(result).toContain('M'); // e.g. 1.2M
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/i18n - Core i18n configuration and initialization
|
|
3
|
+
*
|
|
4
|
+
* Wraps i18next with Object UI defaults and built-in locale support.
|
|
5
|
+
*/
|
|
6
|
+
import i18next, { type i18n as I18nInstance } from 'i18next';
|
|
7
|
+
import { builtInLocales, isRTL } from './locales/index';
|
|
8
|
+
import type { TranslationKeys } from './locales/en';
|
|
9
|
+
|
|
10
|
+
export interface I18nConfig {
|
|
11
|
+
/** Default language (default: 'en') */
|
|
12
|
+
defaultLanguage?: string;
|
|
13
|
+
/** Fallback language (default: 'en') */
|
|
14
|
+
fallbackLanguage?: string;
|
|
15
|
+
/** Additional translation resources to merge with built-in locales */
|
|
16
|
+
resources?: Record<string, Record<string, unknown>>;
|
|
17
|
+
/** Whether to detect browser language automatically (default: true) */
|
|
18
|
+
detectBrowserLanguage?: boolean;
|
|
19
|
+
/** i18next interpolation options */
|
|
20
|
+
interpolation?: {
|
|
21
|
+
escapeValue?: boolean;
|
|
22
|
+
prefix?: string;
|
|
23
|
+
suffix?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create and initialize an i18next instance with Object UI defaults
|
|
29
|
+
*/
|
|
30
|
+
export function createI18n(config: I18nConfig = {}): I18nInstance {
|
|
31
|
+
const {
|
|
32
|
+
defaultLanguage = 'en',
|
|
33
|
+
fallbackLanguage = 'en',
|
|
34
|
+
resources = {},
|
|
35
|
+
detectBrowserLanguage = true,
|
|
36
|
+
interpolation,
|
|
37
|
+
} = config;
|
|
38
|
+
|
|
39
|
+
// Merge built-in locales with user-provided resources
|
|
40
|
+
const mergedResources: Record<string, { translation: Record<string, unknown> }> = {};
|
|
41
|
+
|
|
42
|
+
for (const [lang, translations] of Object.entries(builtInLocales)) {
|
|
43
|
+
mergedResources[lang] = {
|
|
44
|
+
translation: {
|
|
45
|
+
...translations,
|
|
46
|
+
...(resources[lang] || {}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add any additional languages from resources not in built-in locales
|
|
52
|
+
for (const [lang, translations] of Object.entries(resources)) {
|
|
53
|
+
if (!mergedResources[lang]) {
|
|
54
|
+
mergedResources[lang] = { translation: translations as Record<string, unknown> };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Detect browser language if enabled
|
|
59
|
+
let lng = defaultLanguage;
|
|
60
|
+
if (detectBrowserLanguage && typeof navigator !== 'undefined') {
|
|
61
|
+
const browserLang = navigator.language?.split('-')[0];
|
|
62
|
+
if (browserLang && mergedResources[browserLang]) {
|
|
63
|
+
lng = browserLang;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const instance = i18next.createInstance();
|
|
68
|
+
|
|
69
|
+
instance.init({
|
|
70
|
+
lng,
|
|
71
|
+
fallbackLng: fallbackLanguage,
|
|
72
|
+
resources: mergedResources,
|
|
73
|
+
interpolation: {
|
|
74
|
+
escapeValue: false, // React already escapes
|
|
75
|
+
...interpolation,
|
|
76
|
+
},
|
|
77
|
+
returnNull: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return instance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the text direction for the current language
|
|
85
|
+
*/
|
|
86
|
+
export function getDirection(lang: string): 'ltr' | 'rtl' {
|
|
87
|
+
return isRTL(lang) ? 'rtl' : 'ltr';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get available languages from an i18n instance
|
|
92
|
+
*/
|
|
93
|
+
export function getAvailableLanguages(instance: I18nInstance): string[] {
|
|
94
|
+
const resources = instance.options.resources;
|
|
95
|
+
return resources ? Object.keys(resources) : [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type { TranslationKeys };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/i18n
|
|
3
|
+
*
|
|
4
|
+
* Internationalization (i18n) support for Object UI.
|
|
5
|
+
* Provides 10+ built-in language packs, RTL support, and date/currency formatting.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { I18nProvider, useObjectTranslation } from '@object-ui/i18n';
|
|
10
|
+
*
|
|
11
|
+
* function App() {
|
|
12
|
+
* return (
|
|
13
|
+
* <I18nProvider config={{ defaultLanguage: 'zh' }}>
|
|
14
|
+
* <MyComponent />
|
|
15
|
+
* </I18nProvider>
|
|
16
|
+
* );
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* function MyComponent() {
|
|
20
|
+
* const { t, language, changeLanguage, direction } = useObjectTranslation();
|
|
21
|
+
* return (
|
|
22
|
+
* <div dir={direction}>
|
|
23
|
+
* <h1>{t('common.save')}</h1>
|
|
24
|
+
* <button onClick={() => changeLanguage('en')}>English</button>
|
|
25
|
+
* <button onClick={() => changeLanguage('zh')}>中文</button>
|
|
26
|
+
* </div>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @packageDocumentation
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// Core i18n setup
|
|
35
|
+
export { createI18n, getDirection, getAvailableLanguages, type I18nConfig, type TranslationKeys } from './i18n';
|
|
36
|
+
|
|
37
|
+
// React integration
|
|
38
|
+
export { I18nProvider, useObjectTranslation, useI18nContext, type I18nProviderProps } from './provider';
|
|
39
|
+
|
|
40
|
+
// Locale packs
|
|
41
|
+
export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index';
|
|
42
|
+
export { default as en } from './locales/en';
|
|
43
|
+
export { default as zh } from './locales/zh';
|
|
44
|
+
export { default as ja } from './locales/ja';
|
|
45
|
+
export { default as ko } from './locales/ko';
|
|
46
|
+
export { default as de } from './locales/de';
|
|
47
|
+
export { default as fr } from './locales/fr';
|
|
48
|
+
export { default as es } from './locales/es';
|
|
49
|
+
export { default as pt } from './locales/pt';
|
|
50
|
+
export { default as ru } from './locales/ru';
|
|
51
|
+
export { default as ar } from './locales/ar';
|
|
52
|
+
|
|
53
|
+
// Formatting utilities
|
|
54
|
+
export {
|
|
55
|
+
formatDate,
|
|
56
|
+
formatDateTime,
|
|
57
|
+
formatRelativeTime,
|
|
58
|
+
formatCurrency,
|
|
59
|
+
formatNumber,
|
|
60
|
+
type DateFormatOptions,
|
|
61
|
+
type CurrencyFormatOptions,
|
|
62
|
+
type NumberFormatOptions,
|
|
63
|
+
} from './utils/index';
|