@object-ui/i18n 3.1.5 → 3.3.1

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 (56) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +21 -1
  3. package/dist/i18n.js +10 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/locales/ar.d.ts +260 -1
  7. package/dist/locales/ar.js +260 -1
  8. package/dist/locales/de.d.ts +260 -1
  9. package/dist/locales/de.js +269 -1
  10. package/dist/locales/en.d.ts +279 -0
  11. package/dist/locales/en.js +289 -0
  12. package/dist/locales/es.d.ts +260 -1
  13. package/dist/locales/es.js +260 -1
  14. package/dist/locales/fr.d.ts +260 -1
  15. package/dist/locales/fr.js +269 -1
  16. package/dist/locales/index.d.ts +2661 -9
  17. package/dist/locales/ja.d.ts +271 -1
  18. package/dist/locales/ja.js +271 -1
  19. package/dist/locales/ko.d.ts +260 -1
  20. package/dist/locales/ko.js +260 -1
  21. package/dist/locales/pt.d.ts +260 -1
  22. package/dist/locales/pt.js +260 -1
  23. package/dist/locales/ru.d.ts +271 -1
  24. package/dist/locales/ru.js +280 -1
  25. package/dist/locales/zh.d.ts +280 -1
  26. package/dist/locales/zh.js +290 -1
  27. package/dist/useObjectLabel.d.ts +46 -0
  28. package/dist/useObjectLabel.js +79 -12
  29. package/dist/useSafeTranslation.d.ts +5 -0
  30. package/dist/useSafeTranslation.js +46 -0
  31. package/package.json +31 -8
  32. package/.turbo/turbo-build.log +0 -4
  33. package/src/__tests__/i18n.test.ts +0 -228
  34. package/src/__tests__/provider.test.tsx +0 -117
  35. package/src/__tests__/spec-formatters.test.ts +0 -208
  36. package/src/__tests__/useObjectLabel.test.tsx +0 -131
  37. package/src/i18n.ts +0 -98
  38. package/src/index.ts +0 -78
  39. package/src/locales/ar.ts +0 -541
  40. package/src/locales/de.ts +0 -545
  41. package/src/locales/en.ts +0 -625
  42. package/src/locales/es.ts +0 -540
  43. package/src/locales/fr.ts +0 -545
  44. package/src/locales/index.ts +0 -42
  45. package/src/locales/ja.ts +0 -540
  46. package/src/locales/ko.ts +0 -540
  47. package/src/locales/pt.ts +0 -540
  48. package/src/locales/ru.ts +0 -540
  49. package/src/locales/zh.ts +0 -624
  50. package/src/provider.tsx +0 -177
  51. package/src/useObjectLabel.ts +0 -122
  52. package/src/utils/formatting.ts +0 -136
  53. package/src/utils/index.ts +0 -21
  54. package/src/utils/spec-formatters.ts +0 -220
  55. package/tsconfig.json +0 -12
  56. package/tsconfig.tsbuildinfo +0 -1
@@ -1,208 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- resolvePlural,
4
- formatDateSpec,
5
- formatNumberSpec,
6
- applyLocaleConfig,
7
- type SpecPluralRule,
8
- } from '../utils/spec-formatters';
9
-
10
- // ============================================================================
11
- // PluralRuleSchema Consumer Tests
12
- // ============================================================================
13
-
14
- describe('resolvePlural', () => {
15
- const rule: SpecPluralRule = {
16
- key: 'items.count',
17
- zero: 'No items',
18
- one: '{count} item',
19
- other: '{count} items',
20
- };
21
-
22
- it('should resolve zero form', () => {
23
- // English 'other' is used for 0 by Intl.PluralRules, but 'zero' is explicitly checked
24
- // In English, 0 maps to "other" category, but we fall back to rule.other
25
- const result = resolvePlural(rule, 0, 'en');
26
- expect(result).toBe('0 items');
27
- });
28
-
29
- it('should resolve one form', () => {
30
- expect(resolvePlural(rule, 1, 'en')).toBe('1 item');
31
- });
32
-
33
- it('should resolve other form for plural', () => {
34
- expect(resolvePlural(rule, 5, 'en')).toBe('5 items');
35
- });
36
-
37
- it('should fallback to other when form is not defined', () => {
38
- const simpleRule: SpecPluralRule = {
39
- key: 'items',
40
- other: '{count} things',
41
- };
42
- expect(resolvePlural(simpleRule, 3, 'en')).toBe('3 things');
43
- });
44
-
45
- it('should work with different locales', () => {
46
- const ruleWithTwo: SpecPluralRule = {
47
- key: 'items',
48
- one: '{count} element',
49
- two: '{count} éléments (dual)',
50
- other: '{count} éléments',
51
- };
52
- // Arabic has a 'two' plural category
53
- expect(resolvePlural(ruleWithTwo, 2, 'ar')).toBe('2 éléments (dual)');
54
- });
55
- });
56
-
57
- // ============================================================================
58
- // DateFormatSchema Consumer Tests
59
- // ============================================================================
60
-
61
- describe('formatDateSpec', () => {
62
- it('should format date with dateStyle', () => {
63
- const date = new Date('2026-02-11T12:00:00Z');
64
- const result = formatDateSpec(date, { dateStyle: 'short' }, 'en-US');
65
- expect(result).toBeTruthy();
66
- expect(typeof result).toBe('string');
67
- });
68
-
69
- it('should return string for invalid date', () => {
70
- expect(formatDateSpec('invalid-date', { dateStyle: 'medium' })).toBe('invalid-date');
71
- });
72
-
73
- it('should handle timeZone', () => {
74
- const date = new Date('2026-02-11T12:00:00Z');
75
- const result = formatDateSpec(date, {
76
- dateStyle: 'medium',
77
- timeZone: 'America/New_York',
78
- }, 'en-US');
79
- expect(result).toBeTruthy();
80
- });
81
-
82
- it('should handle hour12 option', () => {
83
- const date = new Date('2026-02-11T15:30:00Z');
84
- const result12 = formatDateSpec(date, { timeStyle: 'short', hour12: true }, 'en-US');
85
- const result24 = formatDateSpec(date, { timeStyle: 'short', hour12: false }, 'en-US');
86
- expect(result12).toBeTruthy();
87
- expect(result24).toBeTruthy();
88
- });
89
- });
90
-
91
- // ============================================================================
92
- // NumberFormatSchema Consumer Tests
93
- // ============================================================================
94
-
95
- describe('formatNumberSpec', () => {
96
- it('should format decimal numbers', () => {
97
- const result = formatNumberSpec(1234.56, { style: 'decimal' }, 'en-US');
98
- expect(result).toContain('1,234');
99
- });
100
-
101
- it('should format currency', () => {
102
- const result = formatNumberSpec(42.99, {
103
- style: 'currency',
104
- currency: 'USD',
105
- }, 'en-US');
106
- expect(result).toContain('$');
107
- expect(result).toContain('42.99');
108
- });
109
-
110
- it('should format percentage', () => {
111
- const result = formatNumberSpec(0.75, { style: 'percent' }, 'en-US');
112
- expect(result).toContain('75');
113
- expect(result).toContain('%');
114
- });
115
-
116
- it('should respect fraction digits', () => {
117
- const result = formatNumberSpec(3.14159, {
118
- style: 'decimal',
119
- maximumFractionDigits: 2,
120
- }, 'en-US');
121
- expect(result).toBe('3.14');
122
- });
123
-
124
- it('should respect useGrouping', () => {
125
- const withGrouping = formatNumberSpec(1234567, {
126
- style: 'decimal',
127
- useGrouping: true,
128
- }, 'en-US');
129
- const withoutGrouping = formatNumberSpec(1234567, {
130
- style: 'decimal',
131
- useGrouping: false,
132
- }, 'en-US');
133
- expect(withGrouping).toContain(',');
134
- expect(withoutGrouping).not.toContain(',');
135
- });
136
- });
137
-
138
- // ============================================================================
139
- // LocaleConfigSchema Consumer Tests
140
- // ============================================================================
141
-
142
- describe('applyLocaleConfig', () => {
143
- it('should create bound formatting functions', () => {
144
- const locale = applyLocaleConfig({
145
- code: 'en-US',
146
- direction: 'ltr',
147
- numberFormat: { style: 'decimal', useGrouping: true },
148
- dateFormat: { dateStyle: 'medium' },
149
- });
150
-
151
- expect(locale.code).toBe('en-US');
152
- expect(locale.direction).toBe('ltr');
153
- expect(locale.fallbackChain).toEqual([]);
154
- });
155
-
156
- it('should format dates using configured locale', () => {
157
- const locale = applyLocaleConfig({
158
- code: 'en-US',
159
- dateFormat: { dateStyle: 'short' },
160
- });
161
-
162
- const result = locale.formatDate(new Date('2026-02-11'));
163
- expect(result).toBeTruthy();
164
- });
165
-
166
- it('should format numbers using configured locale', () => {
167
- const locale = applyLocaleConfig({
168
- code: 'en-US',
169
- numberFormat: { style: 'decimal', maximumFractionDigits: 2 },
170
- });
171
-
172
- const result = locale.formatNumber(1234.5678);
173
- expect(result).toBeTruthy();
174
- });
175
-
176
- it('should allow overrides in formatting calls', () => {
177
- const locale = applyLocaleConfig({
178
- code: 'en-US',
179
- numberFormat: { style: 'decimal' },
180
- });
181
-
182
- const result = locale.formatNumber(42.99, {
183
- style: 'currency',
184
- currency: 'EUR',
185
- });
186
- expect(result).toContain('€');
187
- });
188
-
189
- it('should resolve plurals with configured locale', () => {
190
- const locale = applyLocaleConfig({
191
- code: 'en',
192
- });
193
-
194
- const rule: SpecPluralRule = {
195
- key: 'items',
196
- one: '{count} item',
197
- other: '{count} items',
198
- };
199
-
200
- expect(locale.resolvePlural(rule, 1)).toBe('1 item');
201
- expect(locale.resolvePlural(rule, 5)).toBe('5 items');
202
- });
203
-
204
- it('should default direction to ltr', () => {
205
- const locale = applyLocaleConfig({ code: 'en' });
206
- expect(locale.direction).toBe('ltr');
207
- });
208
- });
@@ -1,131 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { renderHook } from '@testing-library/react';
3
- import React from 'react';
4
- import { I18nProvider } from '../provider';
5
- import { createI18n } from '../i18n';
6
- import { useObjectLabel } from '../useObjectLabel';
7
-
8
- /**
9
- * Create a wrapper with custom translations to simulate CRM locale bundles.
10
- */
11
- function createWrapper(lang: string, translations?: Record<string, unknown>) {
12
- const instance = createI18n({ defaultLanguage: lang, detectBrowserLanguage: false });
13
- if (translations) {
14
- instance.addResourceBundle(lang, 'translation', translations, true, true);
15
- }
16
- return ({ children }: { children: React.ReactNode }) =>
17
- React.createElement(I18nProvider, { instance }, children);
18
- }
19
-
20
- describe('useObjectLabel', () => {
21
- describe('objectLabel', () => {
22
- it('returns the plain label when no translation exists', () => {
23
- const wrapper = createWrapper('en');
24
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
25
-
26
- const label = result.current.objectLabel({ name: 'order_item', label: 'Order Item' });
27
- expect(label).toBe('Order Item');
28
- });
29
-
30
- it('returns the translated label when a translation exists', () => {
31
- const wrapper = createWrapper('zh', {
32
- crm: { objects: { order_item: { label: '订单明细' } } },
33
- });
34
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
35
-
36
- const label = result.current.objectLabel({ name: 'order_item', label: 'Order Item' });
37
- expect(label).toBe('订单明细');
38
- });
39
-
40
- it('falls back to plain label when translation key returns empty string', () => {
41
- const wrapper = createWrapper('zh', {
42
- crm: { objects: { order_item: { label: '' } } },
43
- });
44
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
45
-
46
- const label = result.current.objectLabel({ name: 'order_item', label: 'Order Item' });
47
- expect(label).toBe('Order Item');
48
- });
49
- });
50
-
51
- describe('objectDescription', () => {
52
- it('returns undefined when objectDef has no description', () => {
53
- const wrapper = createWrapper('en');
54
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
55
-
56
- const desc = result.current.objectDescription({ name: 'order_item' });
57
- expect(desc).toBeUndefined();
58
- });
59
-
60
- it('returns the plain description when no translation exists', () => {
61
- const wrapper = createWrapper('en');
62
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
63
-
64
- const desc = result.current.objectDescription({ name: 'order_item', description: 'Line items in an order' });
65
- expect(desc).toBe('Line items in an order');
66
- });
67
-
68
- it('returns the translated description when a translation exists', () => {
69
- const wrapper = createWrapper('zh', {
70
- crm: { objects: { order_item: { description: '订单中的商品条目' } } },
71
- });
72
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
73
-
74
- const desc = result.current.objectDescription({ name: 'order_item', description: 'Line items in an order' });
75
- expect(desc).toBe('订单中的商品条目');
76
- });
77
- });
78
-
79
- describe('fieldLabel', () => {
80
- it('returns the fallback label when no translation exists', () => {
81
- const wrapper = createWrapper('en');
82
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
83
-
84
- const label = result.current.fieldLabel('order_item', 'name', 'Line Item');
85
- expect(label).toBe('Line Item');
86
- });
87
-
88
- it('returns the translated label when a translation exists', () => {
89
- const wrapper = createWrapper('zh', {
90
- crm: { fields: { order_item: { name: '订单项' } } },
91
- });
92
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
93
-
94
- const label = result.current.fieldLabel('order_item', 'name', 'Line Item');
95
- expect(label).toBe('订单项');
96
- });
97
-
98
- it('falls back to plain label when translation key returns empty string', () => {
99
- const wrapper = createWrapper('zh', {
100
- crm: { fields: { order_item: { name: '' } } },
101
- });
102
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
103
-
104
- const label = result.current.fieldLabel('order_item', 'name', 'Line Item');
105
- expect(label).toBe('Line Item');
106
- });
107
- });
108
-
109
- describe('multiple objects', () => {
110
- it('resolves labels for different objects independently', () => {
111
- const wrapper = createWrapper('zh', {
112
- crm: {
113
- objects: {
114
- contact: { label: '联系人' },
115
- opportunity: { label: '商机' },
116
- },
117
- fields: {
118
- contact: { email: '邮箱' },
119
- opportunity: { stage: '阶段' },
120
- },
121
- },
122
- });
123
- const { result } = renderHook(() => useObjectLabel(), { wrapper });
124
-
125
- expect(result.current.objectLabel({ name: 'contact', label: 'Contact' })).toBe('联系人');
126
- expect(result.current.objectLabel({ name: 'opportunity', label: 'Opportunity' })).toBe('商机');
127
- expect(result.current.fieldLabel('contact', 'email', 'Email')).toBe('邮箱');
128
- expect(result.current.fieldLabel('opportunity', 'stage', 'Stage')).toBe('阶段');
129
- });
130
- });
131
- });
package/src/i18n.ts DELETED
@@ -1,98 +0,0 @@
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 DELETED
@@ -1,78 +0,0 @@
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
- // Convention-based object/field label i18n
41
- export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel';
42
-
43
- // Locale packs
44
- export { builtInLocales, isRTL, RTL_LANGUAGES } from './locales/index';
45
- export { default as en } from './locales/en';
46
- export { default as zh } from './locales/zh';
47
- export { default as ja } from './locales/ja';
48
- export { default as ko } from './locales/ko';
49
- export { default as de } from './locales/de';
50
- export { default as fr } from './locales/fr';
51
- export { default as es } from './locales/es';
52
- export { default as pt } from './locales/pt';
53
- export { default as ru } from './locales/ru';
54
- export { default as ar } from './locales/ar';
55
-
56
- // Formatting utilities
57
- export {
58
- formatDate,
59
- formatDateTime,
60
- formatRelativeTime,
61
- formatCurrency,
62
- formatNumber,
63
- type DateFormatOptions,
64
- type CurrencyFormatOptions,
65
- type NumberFormatOptions,
66
- } from './utils/index';
67
-
68
- // Spec-aligned formatters (v2.0.7)
69
- export {
70
- resolvePlural,
71
- formatDateSpec,
72
- formatNumberSpec,
73
- applyLocaleConfig,
74
- type SpecPluralRule,
75
- type SpecDateFormat,
76
- type SpecNumberFormat,
77
- type SpecLocaleConfig,
78
- } from './utils/index';