@stacksjs/i18n 0.70.85 → 0.70.87

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacksjs/i18n",
3
- "version": "0.70.85",
3
+ "version": "0.70.87",
4
4
  "description": "Internationalization system for Stacks.js applications",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,8 +27,7 @@
27
27
  "module": "./dist/index.js",
28
28
  "types": "./dist/index.d.ts",
29
29
  "files": [
30
- "dist",
31
- "src"
30
+ "dist"
32
31
  ],
33
32
  "scripts": {
34
33
  "build": "bun build.ts",
@@ -41,6 +40,16 @@
41
40
  "@stacksjs/ts-i18n": "^0.1.9"
42
41
  },
43
42
  "devDependencies": {
44
- "@stacksjs/development": "0.70.85"
43
+ "@stacksjs/development": "0.70.87"
44
+ },
45
+ "publishConfig": {
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist/index.d.ts",
49
+ "bun": "./dist/index.js",
50
+ "import": "./dist/index.js",
51
+ "default": "./dist/index.js"
52
+ }
53
+ }
45
54
  }
46
55
  }
package/src/bootstrap.ts DELETED
@@ -1,107 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { config } from '@stacksjs/config'
3
- import { projectPath } from '@stacksjs/path'
4
- import { loadFromDirectory } from './loader'
5
- import { configure, getAvailableLocales, setLocale } from './translator'
6
-
7
- let localesLoaded = false
8
-
9
- /**
10
- * Load translation files from the project `locales/` directory once.
11
- * Locale subdirectories (`locales/de.yml`, `locales/en.yml`, …) are detected
12
- * automatically by `loadFromDirectory`.
13
- */
14
- export async function ensureLocalesLoaded(): Promise<void> {
15
- if (localesLoaded)
16
- return
17
-
18
- const directory = projectPath('locales')
19
- if (existsSync(directory)) {
20
- await loadFromDirectory({
21
- directory,
22
- extensions: ['.yaml', '.yml', '.json'],
23
- })
24
- }
25
-
26
- const app = (config as { app?: { locale?: string, fallbackLocale?: string, env?: string } }).app
27
- const defaultLocale = app?.locale ?? 'en'
28
- const fallback = app?.fallbackLocale ?? defaultLocale
29
- const available = getAvailableLocales()
30
- configure({
31
- locale: defaultLocale,
32
- fallbackLocale: fallback,
33
- availableLocales: available.length > 0 ? available : [defaultLocale],
34
- warnMissing: (app?.env ?? process.env.APP_ENV) !== 'production',
35
- })
36
-
37
- localesLoaded = true
38
- }
39
-
40
- /**
41
- * Resolve the active locale for an HTTP request.
42
- * Order: STX locale prefix (`/en/...`), `?locale=`, `locale` cookie,
43
- * Accept-Language, then `config.app.locale`.
44
- */
45
- export function resolveRequestLocale(request?: Request): string {
46
- const app = (config as { app?: { locale?: string, fallbackLocale?: string } }).app
47
- const defaultLocale = app?.locale ?? 'en'
48
- const available = new Set(getAvailableLocales())
49
- if (!available.size)
50
- available.add(defaultLocale)
51
-
52
- const pick = (code: string): string => {
53
- const normalized = code.trim().toLowerCase()
54
- if (available.has(normalized))
55
- return normalized
56
- const short = normalized.split('-')[0]
57
- if (short && available.has(short))
58
- return short
59
- return defaultLocale
60
- }
61
-
62
- if (!request)
63
- return defaultLocale
64
-
65
- const url = new URL(request.url)
66
- const pathname = url.pathname
67
-
68
- const fromStx = request.headers.get('x-stx-locale')
69
- if (fromStx)
70
- return pick(fromStx)
71
-
72
- // Non-default locales are served under `/<code>/…` by stx-serve i18n routing.
73
- for (const code of available) {
74
- if (code === defaultLocale)
75
- continue
76
- if (pathname === `/${code}` || pathname === `/${code}/`)
77
- return pick(code)
78
- if (pathname.startsWith(`/${code}/`))
79
- return pick(code)
80
- }
81
-
82
- const fromQuery = url.searchParams.get('locale')
83
- if (fromQuery)
84
- return pick(fromQuery)
85
-
86
- const cookie = request.headers.get('cookie') ?? ''
87
- const match = cookie.match(/(?:^|;\s*)locale=([a-z]{2}(?:-[a-z]{2})?)(?:;|$)/i)
88
- if (match?.[1])
89
- return pick(match[1])
90
-
91
- const accept = request.headers.get('accept-language') ?? ''
92
- for (const part of accept.split(',')) {
93
- const tag = part.split(';')[0]?.trim()
94
- if (tag)
95
- return pick(tag)
96
- }
97
-
98
- return defaultLocale
99
- }
100
-
101
- /** Load `locales/` and activate the global translator for this request. */
102
- export async function applyRequestLocale(request?: Request): Promise<string> {
103
- await ensureLocalesLoaded()
104
- const locale = resolveRequestLocale(request)
105
- setLocale(locale)
106
- return locale
107
- }
package/src/formatter.ts DELETED
@@ -1,399 +0,0 @@
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] ?? targetLocale).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 DELETED
@@ -1,100 +0,0 @@
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
- ensureLocalesLoaded,
79
- resolveRequestLocale,
80
- applyRequestLocale,
81
- } from './bootstrap'
82
-
83
- export {
84
- createLocaleSwitchResponse,
85
- localizePath,
86
- stripLocalePrefix,
87
- type LocaleSwitchConfig,
88
- } from './locale-switch'
89
-
90
- export {
91
- formatDate,
92
- formatTime,
93
- formatDateTime,
94
- formatNumber,
95
- formatCurrency,
96
- formatPercent,
97
- formatRelativeTime,
98
- formatList,
99
- formatPlural,
100
- } from './formatter'