@stacksjs/i18n 0.70.23

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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@stacksjs/i18n",
3
+ "version": "0.70.23",
4
+ "description": "Internationalization system for Stacks.js applications",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/i18n#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/stacksjs/stacks.git",
11
+ "directory": "./storage/framework/core/i18n"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/stacksjs/stacks/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "bun": "./src/index.ts",
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "main": "./dist/index.js",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist",
28
+ "src"
29
+ ],
30
+ "scripts": {
31
+ "build": "bun build.ts",
32
+ "lint": "bunx --bun pickier lint .",
33
+ "lint:fix": "bunx --bun pickier lint . --fix",
34
+ "typecheck": "bunx --bun tsc --noEmit",
35
+ "prepublishOnly": "bun run build"
36
+ },
37
+ "dependencies": {
38
+ "@stacksjs/ts-i18n": "^0.1.7"
39
+ },
40
+ "devDependencies": {
41
+ "@stacksjs/development": "0.70.23"
42
+ }
43
+ }
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Internationalized Formatting
3
+ *
4
+ * Provides locale-aware formatting for dates, numbers, currencies, and more.
5
+ */
6
+
7
+ import { getLocale } from './translator'
8
+
9
+ // =============================================================================
10
+ // Date Formatting
11
+ // =============================================================================
12
+
13
+ export type DateStyle = 'full' | 'long' | 'medium' | 'short'
14
+ export type TimeStyle = 'full' | 'long' | 'medium' | 'short'
15
+
16
+ export interface DateFormatOptions extends Intl.DateTimeFormatOptions {
17
+ dateStyle?: DateStyle
18
+ timeStyle?: TimeStyle
19
+ }
20
+
21
+ /**
22
+ * Format a date
23
+ */
24
+ export function formatDate(
25
+ date: Date | number | string,
26
+ style: DateStyle | DateFormatOptions = 'medium',
27
+ locale?: string,
28
+ ): string {
29
+ const targetLocale = locale || getLocale()
30
+ const dateObj = toDate(date)
31
+
32
+ const options: Intl.DateTimeFormatOptions = typeof style === 'string'
33
+ ? { dateStyle: style }
34
+ : style
35
+
36
+ return new Intl.DateTimeFormat(targetLocale, options).format(dateObj)
37
+ }
38
+
39
+ /**
40
+ * Format a time
41
+ */
42
+ export function formatTime(
43
+ date: Date | number | string,
44
+ style: TimeStyle | DateFormatOptions = 'short',
45
+ locale?: string,
46
+ ): string {
47
+ const targetLocale = locale || getLocale()
48
+ const dateObj = toDate(date)
49
+
50
+ const options: Intl.DateTimeFormatOptions = typeof style === 'string'
51
+ ? { timeStyle: style }
52
+ : style
53
+
54
+ return new Intl.DateTimeFormat(targetLocale, options).format(dateObj)
55
+ }
56
+
57
+ /**
58
+ * Format a date and time
59
+ */
60
+ export function formatDateTime(
61
+ date: Date | number | string,
62
+ dateStyle: DateStyle = 'medium',
63
+ timeStyle: TimeStyle = 'short',
64
+ locale?: string,
65
+ ): string {
66
+ const targetLocale = locale || getLocale()
67
+ const dateObj = toDate(date)
68
+
69
+ return new Intl.DateTimeFormat(targetLocale, {
70
+ dateStyle,
71
+ timeStyle,
72
+ }).format(dateObj)
73
+ }
74
+
75
+ /**
76
+ * Format relative time (e.g., "2 days ago", "in 3 hours")
77
+ */
78
+ export function formatRelativeTime(
79
+ date: Date | number | string,
80
+ baseDate: Date | number | string = new Date(),
81
+ style: 'long' | 'short' | 'narrow' = 'long',
82
+ locale?: string,
83
+ ): string {
84
+ const targetLocale = locale || getLocale()
85
+ const dateObj = toDate(date)
86
+ const baseDateObj = toDate(baseDate)
87
+
88
+ const diffMs = dateObj.getTime() - baseDateObj.getTime()
89
+ const diffSeconds = Math.round(diffMs / 1000)
90
+ const diffMinutes = Math.round(diffSeconds / 60)
91
+ const diffHours = Math.round(diffMinutes / 60)
92
+ const diffDays = Math.round(diffHours / 24)
93
+ const diffWeeks = Math.round(diffDays / 7)
94
+ const diffMonths = Math.round(diffDays / 30)
95
+ const diffYears = Math.round(diffDays / 365)
96
+
97
+ const rtf = new Intl.RelativeTimeFormat(targetLocale, { style })
98
+
99
+ // Choose appropriate unit
100
+ if (Math.abs(diffSeconds) < 60) {
101
+ return rtf.format(diffSeconds, 'second')
102
+ }
103
+ if (Math.abs(diffMinutes) < 60) {
104
+ return rtf.format(diffMinutes, 'minute')
105
+ }
106
+ if (Math.abs(diffHours) < 24) {
107
+ return rtf.format(diffHours, 'hour')
108
+ }
109
+ if (Math.abs(diffDays) < 7) {
110
+ return rtf.format(diffDays, 'day')
111
+ }
112
+ if (Math.abs(diffWeeks) < 4) {
113
+ return rtf.format(diffWeeks, 'week')
114
+ }
115
+ if (Math.abs(diffMonths) < 12) {
116
+ return rtf.format(diffMonths, 'month')
117
+ }
118
+ return rtf.format(diffYears, 'year')
119
+ }
120
+
121
+ // =============================================================================
122
+ // Number Formatting
123
+ // =============================================================================
124
+
125
+ export interface NumberFormatOptions extends Intl.NumberFormatOptions {
126
+ compact?: boolean
127
+ precision?: number
128
+ }
129
+
130
+ /**
131
+ * Format a number
132
+ */
133
+ export function formatNumber(
134
+ value: number,
135
+ options: NumberFormatOptions = {},
136
+ locale?: string,
137
+ ): string {
138
+ const targetLocale = locale || getLocale()
139
+
140
+ const formatOptions: Intl.NumberFormatOptions = { ...options }
141
+
142
+ if (options.compact) {
143
+ formatOptions.notation = 'compact'
144
+ formatOptions.compactDisplay = 'short'
145
+ }
146
+
147
+ if (options.precision !== undefined) {
148
+ formatOptions.minimumFractionDigits = options.precision
149
+ formatOptions.maximumFractionDigits = options.precision
150
+ }
151
+
152
+ return new Intl.NumberFormat(targetLocale, formatOptions).format(value)
153
+ }
154
+
155
+ /**
156
+ * Format a currency value
157
+ */
158
+ export function formatCurrency(
159
+ value: number,
160
+ currency = 'USD',
161
+ options: Omit<NumberFormatOptions, 'style' | 'currency'> = {},
162
+ locale?: string,
163
+ ): string {
164
+ const targetLocale = locale || getLocale()
165
+
166
+ return new Intl.NumberFormat(targetLocale, {
167
+ style: 'currency',
168
+ currency,
169
+ ...options,
170
+ }).format(value)
171
+ }
172
+
173
+ /**
174
+ * Format a percentage
175
+ */
176
+ export function formatPercent(
177
+ value: number,
178
+ options: Omit<NumberFormatOptions, 'style'> = {},
179
+ locale?: string,
180
+ ): string {
181
+ const targetLocale = locale || getLocale()
182
+
183
+ return new Intl.NumberFormat(targetLocale, {
184
+ style: 'percent',
185
+ ...options,
186
+ }).format(value)
187
+ }
188
+
189
+ /**
190
+ * Format a number with units (e.g., "5 kilograms", "10 miles")
191
+ */
192
+ export function formatUnit(
193
+ value: number,
194
+ unit: string,
195
+ unitDisplay: 'long' | 'short' | 'narrow' = 'short',
196
+ locale?: string,
197
+ ): string {
198
+ const targetLocale = locale || getLocale()
199
+
200
+ return new Intl.NumberFormat(targetLocale, {
201
+ style: 'unit',
202
+ unit,
203
+ unitDisplay,
204
+ }).format(value)
205
+ }
206
+
207
+ /**
208
+ * Format bytes to human-readable format
209
+ */
210
+ export function formatBytes(
211
+ bytes: number,
212
+ decimals = 2,
213
+ locale?: string,
214
+ ): string {
215
+ const targetLocale = locale || getLocale()
216
+
217
+ if (bytes === 0) return '0 B'
218
+
219
+ const k = 1024
220
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
221
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
222
+
223
+ const value = bytes / Math.pow(k, i)
224
+ const formatted = new Intl.NumberFormat(targetLocale, {
225
+ minimumFractionDigits: 0,
226
+ maximumFractionDigits: decimals,
227
+ }).format(value)
228
+
229
+ return `${formatted} ${sizes[i]}`
230
+ }
231
+
232
+ // =============================================================================
233
+ // List Formatting
234
+ // =============================================================================
235
+
236
+ export type ListType = 'conjunction' | 'disjunction' | 'unit'
237
+ export type ListStyle = 'long' | 'short' | 'narrow'
238
+
239
+ /**
240
+ * Format a list of items
241
+ */
242
+ export function formatList(
243
+ items: string[],
244
+ type: ListType = 'conjunction',
245
+ style: ListStyle = 'long',
246
+ locale?: string,
247
+ ): string {
248
+ const targetLocale = locale || getLocale()
249
+
250
+ return new Intl.ListFormat(targetLocale, { type, style }).format(items)
251
+ }
252
+
253
+ // =============================================================================
254
+ // Plural Formatting
255
+ // =============================================================================
256
+
257
+ /**
258
+ * Get the plural category for a number
259
+ */
260
+ export function formatPlural(
261
+ value: number,
262
+ locale?: string,
263
+ ): Intl.LDMLPluralRule {
264
+ const targetLocale = locale || getLocale()
265
+ const pr = new Intl.PluralRules(targetLocale)
266
+ return pr.select(value)
267
+ }
268
+
269
+ /**
270
+ * Select from plural options based on a number
271
+ */
272
+ export function selectPlural<T>(
273
+ value: number,
274
+ options: Partial<Record<Intl.LDMLPluralRule, T>> & { other: T },
275
+ locale?: string,
276
+ ): T {
277
+ const category = formatPlural(value, locale)
278
+ return options[category] ?? options.other
279
+ }
280
+
281
+ // =============================================================================
282
+ // Display Names
283
+ // =============================================================================
284
+
285
+ export type DisplayNameType = 'language' | 'region' | 'script' | 'currency' | 'calendar' | 'dateTimeField'
286
+
287
+ /**
288
+ * Get display name for a code
289
+ */
290
+ export function getDisplayName(
291
+ code: string,
292
+ type: DisplayNameType = 'language',
293
+ style: 'long' | 'short' | 'narrow' = 'long',
294
+ locale?: string,
295
+ ): string | undefined {
296
+ const targetLocale = locale || getLocale()
297
+
298
+ try {
299
+ const dn = new Intl.DisplayNames(targetLocale, { type, style })
300
+ return dn.of(code)
301
+ }
302
+ catch {
303
+ return undefined
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get language name
309
+ */
310
+ export function getLanguageName(
311
+ languageCode: string,
312
+ locale?: string,
313
+ ): string | undefined {
314
+ return getDisplayName(languageCode, 'language', 'long', locale)
315
+ }
316
+
317
+ /**
318
+ * Get region/country name
319
+ */
320
+ export function getRegionName(
321
+ regionCode: string,
322
+ locale?: string,
323
+ ): string | undefined {
324
+ return getDisplayName(regionCode, 'region', 'long', locale)
325
+ }
326
+
327
+ /**
328
+ * Get currency name
329
+ */
330
+ export function getCurrencyName(
331
+ currencyCode: string,
332
+ locale?: string,
333
+ ): string | undefined {
334
+ return getDisplayName(currencyCode, 'currency', 'long', locale)
335
+ }
336
+
337
+ // =============================================================================
338
+ // Collation
339
+ // =============================================================================
340
+
341
+ /**
342
+ * Compare strings in a locale-aware manner
343
+ */
344
+ export function compareStrings(
345
+ a: string,
346
+ b: string,
347
+ options: Intl.CollatorOptions = {},
348
+ locale?: string,
349
+ ): number {
350
+ const targetLocale = locale || getLocale()
351
+ const collator = new Intl.Collator(targetLocale, options)
352
+ return collator.compare(a, b)
353
+ }
354
+
355
+ /**
356
+ * Sort an array of strings in locale-aware order
357
+ */
358
+ export function sortStrings(
359
+ strings: string[],
360
+ options: Intl.CollatorOptions = {},
361
+ locale?: string,
362
+ ): string[] {
363
+ const targetLocale = locale || getLocale()
364
+ const collator = new Intl.Collator(targetLocale, options)
365
+ return [...strings].sort((a, b) => collator.compare(a, b))
366
+ }
367
+
368
+ // =============================================================================
369
+ // Helpers
370
+ // =============================================================================
371
+
372
+ /**
373
+ * Convert various date formats to Date object
374
+ */
375
+ function toDate(date: Date | number | string): Date {
376
+ if (date instanceof Date) return date
377
+ if (typeof date === 'number') return new Date(date)
378
+ return new Date(date)
379
+ }
380
+
381
+ /**
382
+ * Get the text direction for a locale
383
+ */
384
+ export function getTextDirection(locale?: string): 'ltr' | 'rtl' {
385
+ const targetLocale = locale || getLocale()
386
+ const lang = targetLocale.split('-')[0].toLowerCase()
387
+
388
+ // RTL languages
389
+ const rtlLanguages = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug', 'ku', 'ckb']
390
+
391
+ return rtlLanguages.includes(lang) ? 'rtl' : 'ltr'
392
+ }
393
+
394
+ /**
395
+ * Check if a locale uses RTL text direction
396
+ */
397
+ export function isRTL(locale?: string): boolean {
398
+ return getTextDirection(locale) === 'rtl'
399
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Stacks i18n - Internationalization System
3
+ *
4
+ * A comprehensive internationalization system with support for:
5
+ * - Translation loading from JSON/YAML files
6
+ * - Plural forms
7
+ * - Variable interpolation
8
+ * - Nested translations
9
+ * - Date/time formatting
10
+ * - Number formatting
11
+ * - Currency formatting
12
+ * - Relative time formatting
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { t, setLocale, addTranslations } from '@stacksjs/i18n'
17
+ *
18
+ * // Add translations
19
+ * addTranslations('en', {
20
+ * greeting: 'Hello, {name}!',
21
+ * items: '{count} item | {count} items',
22
+ * })
23
+ *
24
+ * // Use translations
25
+ * t('greeting', { name: 'World' }) // "Hello, World!"
26
+ * t('items', { count: 1 }) // "1 item"
27
+ * t('items', { count: 5 }) // "5 items"
28
+ *
29
+ * // Format dates and numbers
30
+ * formatDate(new Date(), 'long') // "January 8, 2026"
31
+ * formatNumber(1234567.89) // "1,234,567.89"
32
+ * formatCurrency(99.99, 'USD') // "$99.99"
33
+ * ```
34
+ */
35
+
36
+ export * from './translator'
37
+ export * from './formatter'
38
+ export * from './pluralization'
39
+ export * from './loader'
40
+ export * from './types'
41
+
42
+ // Re-export the file-loading and type-generation surface from
43
+ // `@stacksjs/ts-i18n`. The library handles disk loading (YAML / TS /
44
+ // JSON), namespace resolution, and `.d.ts` codegen for translation
45
+ // keys; the Stacks-side translator/formatter sits on top of that
46
+ // data with locale management, ICU pluralization, and Intl-backed
47
+ // formatters. Importing both names from `@stacksjs/i18n` keeps the
48
+ // app side from caring which symbol comes from which package.
49
+ export {
50
+ loadTranslations as loadTranslationsFromDisk,
51
+ generateTypes as generateI18nTypes,
52
+ generateTypesFromModule as generateI18nTypesFromModule,
53
+ generateSampleConfig as generateI18nSampleConfig,
54
+ writeOutputs as writeI18nOutputs,
55
+ createTranslator as createSimpleTranslator,
56
+ } from '@stacksjs/ts-i18n'
57
+ export type { I18nConfig as TsI18nConfig, TranslationTree, TranslationValue } from '@stacksjs/ts-i18n'
58
+
59
+ // Re-export main functions for convenience
60
+ export {
61
+ t,
62
+ trans,
63
+ tc,
64
+ te,
65
+ tm,
66
+ setLocale,
67
+ getLocale,
68
+ addTranslations,
69
+ loadTranslations,
70
+ getAvailableLocales,
71
+ hasTranslation,
72
+ I18n,
73
+ createI18n,
74
+ useI18n,
75
+ } from './translator'
76
+
77
+ export {
78
+ formatDate,
79
+ formatTime,
80
+ formatDateTime,
81
+ formatNumber,
82
+ formatCurrency,
83
+ formatPercent,
84
+ formatRelativeTime,
85
+ formatList,
86
+ formatPlural,
87
+ } from './formatter'