@object-ui/i18n 3.3.0 → 3.3.2
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/CHANGELOG.md +4 -0
- package/README.md +21 -1
- package/dist/i18n.js +10 -1
- package/dist/locales/ar.d.ts +58 -0
- package/dist/locales/ar.js +58 -0
- package/dist/locales/de.d.ts +58 -0
- package/dist/locales/de.js +58 -0
- package/dist/locales/en.d.ts +78 -0
- package/dist/locales/en.js +78 -0
- package/dist/locales/es.d.ts +58 -0
- package/dist/locales/es.js +58 -0
- package/dist/locales/fr.d.ts +58 -0
- package/dist/locales/fr.js +58 -0
- package/dist/locales/index.d.ts +620 -0
- package/dist/locales/ja.d.ts +58 -0
- package/dist/locales/ja.js +58 -0
- package/dist/locales/ko.d.ts +58 -0
- package/dist/locales/ko.js +58 -0
- package/dist/locales/pt.d.ts +58 -0
- package/dist/locales/pt.js +58 -0
- package/dist/locales/ru.d.ts +58 -0
- package/dist/locales/ru.js +58 -0
- package/dist/locales/zh.d.ts +78 -0
- package/dist/locales/zh.js +78 -0
- package/dist/useObjectLabel.d.ts +46 -0
- package/dist/useObjectLabel.js +79 -12
- package/package.json +30 -7
- package/.turbo/turbo-build.log +0 -4
- package/src/__tests__/i18n.test.ts +0 -255
- package/src/__tests__/provider.test.tsx +0 -117
- package/src/__tests__/spec-formatters.test.ts +0 -208
- package/src/__tests__/useObjectLabel.test.tsx +0 -131
- package/src/i18n.ts +0 -98
- package/src/index.ts +0 -81
- package/src/locales/ar.ts +0 -742
- package/src/locales/de.ts +0 -755
- package/src/locales/en.ts +0 -836
- package/src/locales/es.ts +0 -741
- package/src/locales/fr.ts +0 -755
- package/src/locales/index.ts +0 -42
- package/src/locales/ja.ts +0 -752
- package/src/locales/ko.ts +0 -741
- package/src/locales/pt.ts +0 -741
- package/src/locales/ru.ts +0 -761
- package/src/locales/zh.ts +0 -835
- package/src/provider.tsx +0 -177
- package/src/useObjectLabel.ts +0 -122
- package/src/useSafeTranslation.ts +0 -49
- package/src/utils/formatting.ts +0 -136
- package/src/utils/index.ts +0 -21
- package/src/utils/spec-formatters.ts +0 -220
- package/tsconfig.json +0 -12
- 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
|
-
}
|
package/src/useObjectLabel.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/formatting.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -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';
|