@object-ui/i18n 3.3.0 → 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 (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +21 -1
  3. package/dist/i18n.js +10 -1
  4. package/dist/locales/ar.d.ts +58 -0
  5. package/dist/locales/ar.js +58 -0
  6. package/dist/locales/de.d.ts +58 -0
  7. package/dist/locales/de.js +58 -0
  8. package/dist/locales/en.d.ts +78 -0
  9. package/dist/locales/en.js +78 -0
  10. package/dist/locales/es.d.ts +58 -0
  11. package/dist/locales/es.js +58 -0
  12. package/dist/locales/fr.d.ts +58 -0
  13. package/dist/locales/fr.js +58 -0
  14. package/dist/locales/index.d.ts +620 -0
  15. package/dist/locales/ja.d.ts +58 -0
  16. package/dist/locales/ja.js +58 -0
  17. package/dist/locales/ko.d.ts +58 -0
  18. package/dist/locales/ko.js +58 -0
  19. package/dist/locales/pt.d.ts +58 -0
  20. package/dist/locales/pt.js +58 -0
  21. package/dist/locales/ru.d.ts +58 -0
  22. package/dist/locales/ru.js +58 -0
  23. package/dist/locales/zh.d.ts +78 -0
  24. package/dist/locales/zh.js +78 -0
  25. package/dist/useObjectLabel.d.ts +46 -0
  26. package/dist/useObjectLabel.js +79 -12
  27. package/package.json +30 -7
  28. package/.turbo/turbo-build.log +0 -4
  29. package/src/__tests__/i18n.test.ts +0 -255
  30. package/src/__tests__/provider.test.tsx +0 -117
  31. package/src/__tests__/spec-formatters.test.ts +0 -208
  32. package/src/__tests__/useObjectLabel.test.tsx +0 -131
  33. package/src/i18n.ts +0 -98
  34. package/src/index.ts +0 -81
  35. package/src/locales/ar.ts +0 -742
  36. package/src/locales/de.ts +0 -755
  37. package/src/locales/en.ts +0 -836
  38. package/src/locales/es.ts +0 -741
  39. package/src/locales/fr.ts +0 -755
  40. package/src/locales/index.ts +0 -42
  41. package/src/locales/ja.ts +0 -752
  42. package/src/locales/ko.ts +0 -741
  43. package/src/locales/pt.ts +0 -741
  44. package/src/locales/ru.ts +0 -761
  45. package/src/locales/zh.ts +0 -835
  46. package/src/provider.tsx +0 -177
  47. package/src/useObjectLabel.ts +0 -122
  48. package/src/useSafeTranslation.ts +0 -49
  49. package/src/utils/formatting.ts +0 -136
  50. package/src/utils/index.ts +0 -21
  51. package/src/utils/spec-formatters.ts +0 -220
  52. package/tsconfig.json +0 -12
  53. package/tsconfig.tsbuildinfo +0 -1
package/src/provider.tsx DELETED
@@ -1,177 +0,0 @@
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, useRef, useState } from 'react';
7
- import { I18nextProvider, useTranslation } from 'react-i18next';
8
- import type { i18n as I18nInstance } from 'i18next';
9
- import { createI18n, getDirection, type I18nConfig } from './i18n';
10
-
11
- interface I18nContextValue {
12
- /** Current language code */
13
- language: string;
14
- /** Change the active language */
15
- changeLanguage: (lang: string) => Promise<void>;
16
- /** Current text direction ('ltr' or 'rtl') */
17
- direction: 'ltr' | 'rtl';
18
- /** The underlying i18next instance */
19
- i18n: I18nInstance;
20
- }
21
-
22
- const ObjectI18nContext = createContext<I18nContextValue | null>(null);
23
-
24
- export interface I18nProviderProps {
25
- /** i18n configuration options */
26
- config?: I18nConfig;
27
- /** Pre-created i18next instance (overrides config) */
28
- instance?: I18nInstance;
29
- /**
30
- * Dynamic language pack loader (v2.0.7).
31
- * When set, language packs are loaded lazily instead of being bundled.
32
- * Should return a translation resource object for the given language code.
33
- *
34
- * @example
35
- * ```tsx
36
- * <I18nProvider
37
- * loadLanguage={async (lang) => {
38
- * const mod = await import(`./locales/${lang}.json`);
39
- * return mod.default;
40
- * }}
41
- * >
42
- * <App />
43
- * </I18nProvider>
44
- * ```
45
- */
46
- loadLanguage?: (lang: string) => Promise<Record<string, unknown>>;
47
- /** Children to render */
48
- children: React.ReactNode;
49
- }
50
-
51
- /**
52
- * I18nProvider - Wraps your app with i18n support
53
- *
54
- * @example
55
- * ```tsx
56
- * <I18nProvider config={{ defaultLanguage: 'zh' }}>
57
- * <App />
58
- * </I18nProvider>
59
- * ```
60
- */
61
- export function I18nProvider({ config, instance: externalInstance, loadLanguage, children }: I18nProviderProps) {
62
- const i18nInstance = useMemo(
63
- () => externalInstance || createI18n(config),
64
- [externalInstance, config],
65
- );
66
-
67
- const [language, setLanguage] = useState(i18nInstance.language || 'en');
68
- const direction = getDirection(language);
69
-
70
- // Track which languages have had app-specific translations loaded
71
- // (separate from built-in locales that ship with the library)
72
- const loadedAppLangs = useRef<Set<string>>(new Set());
73
-
74
- useEffect(() => {
75
- const handleLanguageChanged = (lng: string) => {
76
- setLanguage(lng);
77
- // Update document direction for RTL support
78
- if (typeof document !== 'undefined') {
79
- document.documentElement.dir = getDirection(lng);
80
- document.documentElement.lang = lng;
81
- }
82
- };
83
-
84
- i18nInstance.on('languageChanged', handleLanguageChanged);
85
- return () => {
86
- i18nInstance.off('languageChanged', handleLanguageChanged);
87
- };
88
- }, [i18nInstance]);
89
-
90
- // Load app-specific translations for the initial language on mount
91
- useEffect(() => {
92
- if (!loadLanguage) return;
93
- const currentLang = i18nInstance.language || 'en';
94
- if (loadedAppLangs.current.has(currentLang)) return;
95
- loadedAppLangs.current.add(currentLang);
96
- loadLanguage(currentLang).then((resources) => {
97
- if (resources && Object.keys(resources).length > 0) {
98
- i18nInstance.addResourceBundle(currentLang, 'translation', resources, true, true);
99
- // Force re-render so components pick up newly loaded translations
100
- setLanguage(currentLang);
101
- }
102
- }).catch((err) => {
103
- // Allow retry on failure by removing from loaded set
104
- loadedAppLangs.current.delete(currentLang);
105
- console.warn(`[i18n] Failed to load app translations for '${currentLang}':`, err);
106
- });
107
- }, [i18nInstance, loadLanguage]);
108
-
109
- const contextValue = useMemo<I18nContextValue>(
110
- () => ({
111
- language,
112
- changeLanguage: async (lang: string) => {
113
- // Dynamic language pack loading (v2.0.7)
114
- if (loadLanguage && !loadedAppLangs.current.has(lang)) {
115
- loadedAppLangs.current.add(lang);
116
- try {
117
- const resources = await loadLanguage(lang);
118
- i18nInstance.addResourceBundle(lang, 'translation', resources, true, true);
119
- } catch (err) {
120
- loadedAppLangs.current.delete(lang);
121
- console.warn(`[i18n] Failed to load app translations for '${lang}':`, err);
122
- }
123
- }
124
- await i18nInstance.changeLanguage(lang);
125
- },
126
- direction,
127
- i18n: i18nInstance,
128
- }),
129
- [language, direction, i18nInstance, loadLanguage],
130
- );
131
-
132
- return React.createElement(
133
- ObjectI18nContext.Provider,
134
- { value: contextValue },
135
- React.createElement(I18nextProvider, { i18n: i18nInstance }, children),
136
- );
137
- }
138
-
139
- /**
140
- * Hook to access Object UI i18n context
141
- *
142
- * @example
143
- * ```tsx
144
- * function MyComponent() {
145
- * const { t, language, changeLanguage, direction } = useObjectTranslation();
146
- * return <div dir={direction}>{t('common.save')}</div>;
147
- * }
148
- * ```
149
- */
150
- export function useObjectTranslation(ns?: string) {
151
- const context = useContext(ObjectI18nContext);
152
- const { t, i18n } = useTranslation(ns);
153
-
154
- return {
155
- /** Translation function */
156
- t,
157
- /** Current language code */
158
- language: context?.language || i18n.language || 'en',
159
- /** Change the active language */
160
- changeLanguage: context?.changeLanguage || (async (lang: string) => { await i18n.changeLanguage(lang); }),
161
- /** Current text direction */
162
- direction: context?.direction || 'ltr',
163
- /** The underlying i18next instance */
164
- i18n,
165
- };
166
- }
167
-
168
- /**
169
- * Hook to access the i18n context directly
170
- */
171
- export function useI18nContext(): I18nContextValue {
172
- const context = useContext(ObjectI18nContext);
173
- if (!context) {
174
- throw new Error('useI18nContext must be used within an I18nProvider');
175
- }
176
- return context;
177
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * @object-ui/i18n - Convention-based Object & Field Label i18n
3
- *
4
- * Provides automatic translation resolution for object metadata labels
5
- * using Salesforce-style convention-based key generation.
6
- *
7
- * The app namespace (e.g. "crm") is discovered dynamically from loaded
8
- * i18next resources — no hardcoded app names in platform code.
9
- *
10
- * Convention Rules:
11
- * | What | Auto-generated key | Fallback |
12
- * |--------------------|-------------------------------------------------|-----------------------|
13
- * | Object label | {ns}.objects.{objectName}.label | objectDef.label |
14
- * | Object description | {ns}.objects.{objectName}.description | objectDef.description |
15
- * | Field label | {ns}.fields.{objectName}.{fieldName} | field.label |
16
- *
17
- * @module useObjectLabel
18
- */
19
-
20
- import { useObjectTranslation } from './provider';
21
-
22
- /**
23
- * Built-in Object UI top-level locale keys — not app namespaces.
24
- * Update this set when new top-level platform translation keys are added
25
- * to `packages/i18n/src/locales/en.ts` to prevent them from being treated
26
- * as app namespaces during dynamic namespace discovery.
27
- */
28
- const BUILTIN_KEYS = new Set([
29
- 'common', 'validation', 'form', 'table', 'grid', 'calendar',
30
- 'list', 'kanban', 'chart', 'dashboard', 'configPanel',
31
- 'appDesigner', 'console', 'errors', 'detail',
32
- ]);
33
-
34
- /**
35
- * Hook for convention-based auto-resolution of object and field labels.
36
- *
37
- * Automatically constructs i18n keys from object/field names and looks up
38
- * translations, falling back to the plain-string label when no translation exists.
39
- *
40
- * The app namespace is discovered dynamically from loaded i18next resources
41
- * by finding top-level keys that contain an `objects` sub-key.
42
- *
43
- * @example
44
- * ```tsx
45
- * const { objectLabel, fieldLabel } = useObjectLabel();
46
- * <h1>{objectLabel(objectDef)}</h1>
47
- * ```
48
- */
49
- export function useObjectLabel() {
50
- const { t, i18n } = useObjectTranslation();
51
-
52
- /**
53
- * Discover app namespace(s) from loaded i18next resources.
54
- * Returns top-level keys (outside built-in Object UI keys) that contain
55
- * an `objects` or `fields` sub-key — e.g. "crm" when resources include crm.objects.*.
56
- */
57
- const getAppNamespaces = (): string[] => {
58
- if (!i18n || typeof i18n.getResourceBundle !== 'function') return [];
59
- const lang = i18n.language || 'en';
60
- const bundle = i18n.getResourceBundle(lang, 'translation') as Record<string, any> | undefined;
61
- if (!bundle) return [];
62
- return Object.keys(bundle).filter(
63
- (key) => !BUILTIN_KEYS.has(key) && bundle[key] && typeof bundle[key] === 'object'
64
- && (bundle[key].objects || bundle[key].fields),
65
- );
66
- };
67
-
68
- /** Try resolving a key across all discovered app namespaces. */
69
- const resolve = (suffix: string, fallback: string): string => {
70
- try {
71
- const namespaces = getAppNamespaces();
72
- for (const ns of namespaces) {
73
- const key = `${ns}.${suffix}`;
74
- const translated = t(key, { defaultValue: '' });
75
- if (translated && translated !== key && translated !== '') {
76
- return translated;
77
- }
78
- }
79
- } catch {
80
- // Graceful degradation when i18n provider is not available
81
- }
82
- return fallback;
83
- };
84
-
85
- return {
86
- /**
87
- * Resolve translated object label, falling back to objectDef.label.
88
- */
89
- objectLabel: (objectDef: { name: string; label: string }) =>
90
- resolve(`objects.${objectDef.name}.label`, objectDef.label),
91
-
92
- /**
93
- * Resolve translated object description, falling back to objectDef.description.
94
- */
95
- objectDescription: (objectDef: { name: string; description?: string }) => {
96
- if (!objectDef.description) return undefined;
97
- return resolve(`objects.${objectDef.name}.description`, objectDef.description);
98
- },
99
-
100
- /**
101
- * Resolve translated field label, falling back to the provided fallback string.
102
- */
103
- fieldLabel: (objectName: string, fieldName: string, fallback: string) =>
104
- resolve(`fields.${objectName}.${fieldName}`, fallback),
105
- };
106
- }
107
-
108
- /**
109
- * Safe wrapper for useObjectLabel that falls back to identity functions
110
- * when no I18nProvider is available. Suitable for plugin components that
111
- * may be rendered outside an i18n context.
112
- */
113
- export function useSafeFieldLabel() {
114
- try {
115
- const { fieldLabel } = useObjectLabel();
116
- return { fieldLabel };
117
- } catch {
118
- return {
119
- fieldLabel: (_objectName: string, _fieldName: string, fallback: string) => fallback,
120
- };
121
- }
122
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Safe translation hook with fallback to default strings.
3
- *
4
- * When no I18nProvider is available (e.g., in tests or standalone usage),
5
- * this hook falls back to the provided default translations instead of
6
- * returning raw i18n keys.
7
- *
8
- * @param defaults - Fallback English translations keyed by i18n key
9
- * @param testKey - A key to test if i18n is properly configured (must be in defaults)
10
- */
11
- import { useObjectTranslation } from './provider';
12
-
13
- export function createSafeTranslation(
14
- defaults: Record<string, string>,
15
- testKey: string,
16
- ) {
17
- return function useSafeTranslation() {
18
- try {
19
- const result = useObjectTranslation();
20
- const testValue = result.t(testKey);
21
- if (testValue === testKey) {
22
- return {
23
- t: (key: string, options?: Record<string, unknown>) => {
24
- let value = defaults[key] || key;
25
- if (options) {
26
- for (const [k, v] of Object.entries(options)) {
27
- value = value.replace(`{{${k}}}`, String(v));
28
- }
29
- }
30
- return value;
31
- },
32
- };
33
- }
34
- return { t: result.t };
35
- } catch {
36
- return {
37
- t: (key: string, options?: Record<string, unknown>) => {
38
- let value = defaults[key] || key;
39
- if (options) {
40
- for (const [k, v] of Object.entries(options)) {
41
- value = value.replace(`{{${k}}}`, String(v));
42
- }
43
- }
44
- return value;
45
- },
46
- };
47
- }
48
- };
49
- }
@@ -1,136 +0,0 @@
1
- /**
2
- * @object-ui/i18n - Date and currency formatting utilities
3
- *
4
- * Uses the native Intl API for locale-aware formatting.
5
- */
6
-
7
- export interface DateFormatOptions {
8
- locale?: string;
9
- style?: 'short' | 'medium' | 'long' | 'full';
10
- dateStyle?: Intl.DateTimeFormatOptions['dateStyle'];
11
- timeStyle?: Intl.DateTimeFormatOptions['timeStyle'];
12
- }
13
-
14
- export interface CurrencyFormatOptions {
15
- locale?: string;
16
- currency?: string;
17
- style?: 'currency' | 'decimal' | 'percent';
18
- minimumFractionDigits?: number;
19
- maximumFractionDigits?: number;
20
- }
21
-
22
- export interface NumberFormatOptions {
23
- locale?: string;
24
- style?: 'decimal' | 'percent' | 'unit';
25
- minimumFractionDigits?: number;
26
- maximumFractionDigits?: number;
27
- notation?: 'standard' | 'scientific' | 'engineering' | 'compact';
28
- }
29
-
30
- /**
31
- * Format a date according to locale conventions
32
- */
33
- export function formatDate(
34
- date: Date | string | number,
35
- options: DateFormatOptions = {},
36
- ): string {
37
- const { locale = 'en', style = 'medium' } = options;
38
- const d = date instanceof Date ? date : new Date(date);
39
-
40
- if (isNaN(d.getTime())) {
41
- return String(date);
42
- }
43
-
44
- const styleMap: Record<string, Intl.DateTimeFormatOptions> = {
45
- short: { year: '2-digit', month: 'numeric', day: 'numeric' },
46
- medium: { year: 'numeric', month: 'short', day: 'numeric' },
47
- long: { year: 'numeric', month: 'long', day: 'numeric' },
48
- full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
49
- };
50
-
51
- const formatOptions = options.dateStyle
52
- ? { dateStyle: options.dateStyle, timeStyle: options.timeStyle }
53
- : styleMap[style] || styleMap.medium;
54
-
55
- return new Intl.DateTimeFormat(locale, formatOptions).format(d);
56
- }
57
-
58
- /**
59
- * Format a date and time according to locale conventions
60
- */
61
- export function formatDateTime(
62
- date: Date | string | number,
63
- options: DateFormatOptions = {},
64
- ): string {
65
- const { locale = 'en', style = 'medium' } = options;
66
- const d = date instanceof Date ? date : new Date(date);
67
-
68
- if (isNaN(d.getTime())) {
69
- return String(date);
70
- }
71
-
72
- const styleMap: Record<string, Intl.DateTimeFormatOptions> = {
73
- short: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },
74
- medium: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' },
75
- long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
76
- full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' },
77
- };
78
-
79
- return new Intl.DateTimeFormat(locale, styleMap[style] || styleMap.medium).format(d);
80
- }
81
-
82
- /**
83
- * Format a relative time (e.g., "2 days ago", "in 3 hours")
84
- */
85
- export function formatRelativeTime(
86
- date: Date | string | number,
87
- locale = 'en',
88
- ): string {
89
- const d = date instanceof Date ? date : new Date(date);
90
- const now = new Date();
91
- const diffMs = d.getTime() - now.getTime();
92
- const diffSec = Math.round(diffMs / 1000);
93
- const diffMin = Math.round(diffSec / 60);
94
- const diffHour = Math.round(diffMin / 60);
95
- const diffDay = Math.round(diffHour / 24);
96
-
97
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
98
-
99
- if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
100
- if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
101
- if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
102
- if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
103
-
104
- const diffMonth = Math.round(diffDay / 30);
105
- if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, 'month');
106
-
107
- return rtf.format(Math.round(diffDay / 365), 'year');
108
- }
109
-
110
- /**
111
- * Format a currency value according to locale conventions
112
- */
113
- export function formatCurrency(
114
- value: number,
115
- options: CurrencyFormatOptions = {},
116
- ): string {
117
- const { locale = 'en', currency = 'USD', minimumFractionDigits, maximumFractionDigits } = options;
118
-
119
- return new Intl.NumberFormat(locale, {
120
- style: 'currency',
121
- currency,
122
- minimumFractionDigits,
123
- maximumFractionDigits,
124
- }).format(value);
125
- }
126
-
127
- /**
128
- * Format a number according to locale conventions
129
- */
130
- export function formatNumber(
131
- value: number,
132
- options: NumberFormatOptions = {},
133
- ): string {
134
- const { locale = 'en', ...rest } = options;
135
- return new Intl.NumberFormat(locale, rest).format(value);
136
- }
@@ -1,21 +0,0 @@
1
- export {
2
- formatDate,
3
- formatDateTime,
4
- formatRelativeTime,
5
- formatCurrency,
6
- formatNumber,
7
- type DateFormatOptions,
8
- type CurrencyFormatOptions,
9
- type NumberFormatOptions,
10
- } from './formatting';
11
-
12
- export {
13
- resolvePlural,
14
- formatDateSpec,
15
- formatNumberSpec,
16
- applyLocaleConfig,
17
- type SpecPluralRule,
18
- type SpecDateFormat,
19
- type SpecNumberFormat,
20
- type SpecLocaleConfig,
21
- } from './spec-formatters';