@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.
Files changed (54) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/i18n.d.ts +36 -0
  4. package/dist/i18n.js +62 -0
  5. package/dist/index.d.ts +46 -0
  6. package/dist/index.js +50 -0
  7. package/dist/locales/ar.d.ts +112 -0
  8. package/dist/locales/ar.js +112 -0
  9. package/dist/locales/de.d.ts +111 -0
  10. package/dist/locales/de.js +111 -0
  11. package/dist/locales/en.d.ts +112 -0
  12. package/dist/locales/en.js +111 -0
  13. package/dist/locales/es.d.ts +111 -0
  14. package/dist/locales/es.js +111 -0
  15. package/dist/locales/fr.d.ts +111 -0
  16. package/dist/locales/fr.js +111 -0
  17. package/dist/locales/index.d.ts +1094 -0
  18. package/dist/locales/index.js +38 -0
  19. package/dist/locales/ja.d.ts +111 -0
  20. package/dist/locales/ja.js +111 -0
  21. package/dist/locales/ko.d.ts +111 -0
  22. package/dist/locales/ko.js +111 -0
  23. package/dist/locales/pt.d.ts +111 -0
  24. package/dist/locales/pt.js +111 -0
  25. package/dist/locales/ru.d.ts +111 -0
  26. package/dist/locales/ru.js +111 -0
  27. package/dist/locales/zh.d.ts +111 -0
  28. package/dist/locales/zh.js +111 -0
  29. package/dist/provider.d.ts +65 -0
  30. package/dist/provider.js +84 -0
  31. package/dist/utils/formatting.d.ts +45 -0
  32. package/dist/utils/formatting.js +86 -0
  33. package/dist/utils/index.d.ts +1 -0
  34. package/dist/utils/index.js +1 -0
  35. package/package.json +48 -0
  36. package/src/__tests__/i18n.test.ts +228 -0
  37. package/src/i18n.ts +98 -0
  38. package/src/index.ts +63 -0
  39. package/src/locales/ar.ts +113 -0
  40. package/src/locales/de.ts +112 -0
  41. package/src/locales/en.ts +113 -0
  42. package/src/locales/es.ts +112 -0
  43. package/src/locales/fr.ts +112 -0
  44. package/src/locales/index.ts +42 -0
  45. package/src/locales/ja.ts +112 -0
  46. package/src/locales/ko.ts +112 -0
  47. package/src/locales/pt.ts +112 -0
  48. package/src/locales/ru.ts +112 -0
  49. package/src/locales/zh.ts +112 -0
  50. package/src/provider.tsx +125 -0
  51. package/src/utils/formatting.ts +136 -0
  52. package/src/utils/index.ts +10 -0
  53. package/tsconfig.json +12 -0
  54. package/tsconfig.tsbuildinfo +1 -0
@@ -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';