@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
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,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';
@@ -1,220 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * @object-ui/i18n - Spec-aligned i18n utilities
11
- *
12
- * Runtime consumers for @objectstack/spec v2.0.7 i18n types:
13
- * - PluralRuleSchema → resolvePlural()
14
- * - DateFormatSchema → formatDateSpec()
15
- * - NumberFormatSchema → formatNumberSpec()
16
- * - LocaleConfigSchema → applyLocaleConfig()
17
- *
18
- * @module spec-formatters
19
- */
20
-
21
- // ============================================================================
22
- // PluralRuleSchema Consumer
23
- // ============================================================================
24
-
25
- /**
26
- * Spec-aligned PluralRule (mirrors @objectstack/spec PluralRuleSchema).
27
- */
28
- export interface SpecPluralRule {
29
- /** Translation key */
30
- key: string;
31
- /** Form for zero items */
32
- zero?: string;
33
- /** Form for exactly one item */
34
- one?: string;
35
- /** Form for exactly two items */
36
- two?: string;
37
- /** Form for few items (language-specific) */
38
- few?: string;
39
- /** Form for many items (language-specific) */
40
- many?: string;
41
- /** Default/fallback form (required) */
42
- other: string;
43
- }
44
-
45
- /**
46
- * Resolve a plural form based on count, following CLDR plural rules.
47
- * Uses the Intl.PluralRules API for correct locale-aware pluralization.
48
- *
49
- * @example
50
- * ```ts
51
- * const rule: SpecPluralRule = {
52
- * key: 'items.count',
53
- * zero: 'No items',
54
- * one: '{count} item',
55
- * other: '{count} items',
56
- * };
57
- * resolvePlural(rule, 0, 'en'); // → 'No items'
58
- * resolvePlural(rule, 1, 'en'); // → '1 item'
59
- * resolvePlural(rule, 5, 'en'); // → '5 items'
60
- * ```
61
- */
62
- export function resolvePlural(
63
- rule: SpecPluralRule,
64
- count: number,
65
- locale = 'en',
66
- ): string {
67
- const pr = new Intl.PluralRules(locale);
68
- const category = pr.select(count);
69
-
70
- // Prefer the explicit form, fall back to 'other'
71
- const template =
72
- (category === 'zero' && rule.zero) ||
73
- (category === 'one' && rule.one) ||
74
- (category === 'two' && rule.two) ||
75
- (category === 'few' && rule.few) ||
76
- (category === 'many' && rule.many) ||
77
- rule.other;
78
-
79
- // Replace {count} placeholder
80
- return template.replace(/\{count\}/g, String(count));
81
- }
82
-
83
- // ============================================================================
84
- // DateFormatSchema Consumer
85
- // ============================================================================
86
-
87
- /**
88
- * Spec-aligned DateFormat (mirrors @objectstack/spec DateFormatSchema).
89
- */
90
- export interface SpecDateFormat {
91
- dateStyle?: 'full' | 'long' | 'medium' | 'short';
92
- timeStyle?: 'full' | 'long' | 'medium' | 'short';
93
- timeZone?: string;
94
- hour12?: boolean;
95
- }
96
-
97
- /**
98
- * Format a date using @objectstack/spec DateFormatSchema configuration.
99
- *
100
- * @example
101
- * ```ts
102
- * formatDateSpec(new Date(), {
103
- * dateStyle: 'medium',
104
- * timeStyle: 'short',
105
- * timeZone: 'America/New_York',
106
- * }, 'en-US');
107
- * ```
108
- */
109
- export function formatDateSpec(
110
- date: Date | string | number,
111
- format: SpecDateFormat,
112
- locale = 'en',
113
- ): string {
114
- const d = date instanceof Date ? date : new Date(date);
115
- if (isNaN(d.getTime())) return String(date);
116
-
117
- const options: Intl.DateTimeFormatOptions = {};
118
- if (format.dateStyle) options.dateStyle = format.dateStyle;
119
- if (format.timeStyle) options.timeStyle = format.timeStyle;
120
- if (format.timeZone) options.timeZone = format.timeZone;
121
- if (format.hour12 !== undefined) options.hour12 = format.hour12;
122
-
123
- return new Intl.DateTimeFormat(locale, options).format(d);
124
- }
125
-
126
- // ============================================================================
127
- // NumberFormatSchema Consumer
128
- // ============================================================================
129
-
130
- /**
131
- * Spec-aligned NumberFormat (mirrors @objectstack/spec NumberFormatSchema).
132
- */
133
- export interface SpecNumberFormat {
134
- style?: 'currency' | 'percent' | 'decimal' | 'unit';
135
- currency?: string;
136
- unit?: string;
137
- minimumFractionDigits?: number;
138
- maximumFractionDigits?: number;
139
- useGrouping?: boolean;
140
- }
141
-
142
- /**
143
- * Format a number using @objectstack/spec NumberFormatSchema configuration.
144
- *
145
- * @example
146
- * ```ts
147
- * formatNumberSpec(1234.56, {
148
- * style: 'currency',
149
- * currency: 'USD',
150
- * maximumFractionDigits: 2,
151
- * }, 'en-US'); // → '$1,234.56'
152
- * ```
153
- */
154
- export function formatNumberSpec(
155
- value: number,
156
- format: SpecNumberFormat,
157
- locale = 'en',
158
- ): string {
159
- const options: Intl.NumberFormatOptions = {
160
- style: format.style || 'decimal',
161
- };
162
-
163
- if (format.currency) options.currency = format.currency;
164
- if (format.unit) options.unit = format.unit;
165
- if (format.minimumFractionDigits !== undefined) options.minimumFractionDigits = format.minimumFractionDigits;
166
- if (format.maximumFractionDigits !== undefined) options.maximumFractionDigits = format.maximumFractionDigits;
167
- if (format.useGrouping !== undefined) options.useGrouping = format.useGrouping;
168
-
169
- return new Intl.NumberFormat(locale, options).format(value);
170
- }
171
-
172
- // ============================================================================
173
- // LocaleConfigSchema Consumer
174
- // ============================================================================
175
-
176
- /**
177
- * Spec-aligned LocaleConfig (mirrors @objectstack/spec LocaleConfigSchema).
178
- */
179
- export interface SpecLocaleConfig {
180
- /** BCP 47 language code (e.g., 'en-US', 'zh-CN') */
181
- code: string;
182
- /** Fallback locale chain */
183
- fallbackChain?: string[];
184
- /** Text direction */
185
- direction?: 'ltr' | 'rtl';
186
- /** Number formatting defaults */
187
- numberFormat?: SpecNumberFormat;
188
- /** Date formatting defaults */
189
- dateFormat?: SpecDateFormat;
190
- }
191
-
192
- /**
193
- * Apply a LocaleConfigSchema to configure i18n formatting defaults.
194
- * Returns resolved formatting functions bound to the locale config.
195
- *
196
- * @example
197
- * ```ts
198
- * const locale = applyLocaleConfig({
199
- * code: 'zh-CN',
200
- * direction: 'ltr',
201
- * numberFormat: { style: 'decimal', useGrouping: true },
202
- * dateFormat: { dateStyle: 'medium' },
203
- * });
204
- * locale.formatDate(new Date()); // Chinese medium date
205
- * locale.formatNumber(1234.5); // 1,234.5
206
- * ```
207
- */
208
- export function applyLocaleConfig(config: SpecLocaleConfig) {
209
- return {
210
- code: config.code,
211
- direction: config.direction || 'ltr',
212
- fallbackChain: config.fallbackChain || [],
213
- formatDate: (date: Date | string | number, overrides?: Partial<SpecDateFormat>) =>
214
- formatDateSpec(date, { ...config.dateFormat, ...overrides }, config.code),
215
- formatNumber: (value: number, overrides?: Partial<SpecNumberFormat>) =>
216
- formatNumberSpec(value, { ...config.numberFormat, ...overrides }, config.code),
217
- resolvePlural: (rule: SpecPluralRule, count: number) =>
218
- resolvePlural(rule, count, config.code),
219
- };
220
- }
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "noEmit": false,
7
- "declaration": true,
8
- "composite": true
9
- },
10
- "include": ["src"],
11
- "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
12
- }