@stacksjs/i18n 0.70.88 → 0.70.91

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Load translation files from the project `locales/` directory once.
3
+ * Locale subdirectories (`locales/de.yml`, `locales/en.yml`, …) are detected
4
+ * automatically by `loadFromDirectory`.
5
+ */
6
+ export declare function ensureLocalesLoaded(): Promise<void>;
7
+ /**
8
+ * Resolve the active locale for an HTTP request.
9
+ * Order: STX locale prefix (`/en/...`), `?locale=`, `locale` cookie,
10
+ * Accept-Language, then `config.app.locale`.
11
+ */
12
+ export declare function resolveRequestLocale(request?: Request): string;
13
+ /** Load `locales/` and activate the global translator for this request. */
14
+ export declare function applyRequestLocale(request?: Request): Promise<string>;
@@ -0,0 +1,70 @@
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
+ let localesLoaded = !1;
7
+ export async function ensureLocalesLoaded() {
8
+ if (localesLoaded)
9
+ return;
10
+ const directory = projectPath("locales");
11
+ if (existsSync(directory))
12
+ await loadFromDirectory({
13
+ directory,
14
+ extensions: [".yaml", ".yml", ".json"]
15
+ });
16
+ const app = config.app, defaultLocale = app?.locale ?? "en", fallback = app?.fallbackLocale ?? defaultLocale, available = getAvailableLocales();
17
+ configure({
18
+ locale: defaultLocale,
19
+ fallbackLocale: fallback,
20
+ availableLocales: available.length > 0 ? available : [defaultLocale],
21
+ warnMissing: (app?.env ?? process.env.APP_ENV) !== "production"
22
+ });
23
+ localesLoaded = !0;
24
+ }
25
+ export function resolveRequestLocale(request) {
26
+ const defaultLocale = config.app?.locale ?? "en", available = new Set(getAvailableLocales());
27
+ if (!available.size)
28
+ available.add(defaultLocale);
29
+ const pick = (code) => {
30
+ const normalized = code.trim().toLowerCase();
31
+ if (available.has(normalized))
32
+ return normalized;
33
+ const short = normalized.split("-")[0];
34
+ if (short && available.has(short))
35
+ return short;
36
+ return defaultLocale;
37
+ };
38
+ if (!request)
39
+ return defaultLocale;
40
+ const url = new URL(request.url), pathname = url.pathname, fromStx = request.headers.get("x-stx-locale");
41
+ if (fromStx)
42
+ return pick(fromStx);
43
+ for (const code of available) {
44
+ if (code === defaultLocale)
45
+ continue;
46
+ if (pathname === `/${code}` || pathname === `/${code}/`)
47
+ return pick(code);
48
+ if (pathname.startsWith(`/${code}/`))
49
+ return pick(code);
50
+ }
51
+ const fromQuery = url.searchParams.get("locale");
52
+ if (fromQuery)
53
+ return pick(fromQuery);
54
+ const match = (request.headers.get("cookie") ?? "").match(/(?:^|;\s*)locale=([a-z]{2}(?:-[a-z]{2})?)(?:;|$)/i);
55
+ if (match?.[1])
56
+ return pick(match[1]);
57
+ const accept = request.headers.get("accept-language") ?? "";
58
+ for (const part of accept.split(",")) {
59
+ const tag = part.split(";")[0]?.trim();
60
+ if (tag)
61
+ return pick(tag);
62
+ }
63
+ return defaultLocale;
64
+ }
65
+ export async function applyRequestLocale(request) {
66
+ await ensureLocalesLoaded();
67
+ const locale = resolveRequestLocale(request);
68
+ setLocale(locale);
69
+ return locale;
70
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Format a date
3
+ */
4
+ export declare function formatDate(date: Date | number | string, style?: DateStyle | DateFormatOptions, locale?: string): string;
5
+ /**
6
+ * Format a time
7
+ */
8
+ export declare function formatTime(date: Date | number | string, style?: TimeStyle | DateFormatOptions, locale?: string): string;
9
+ /**
10
+ * Format a date and time
11
+ */
12
+ export declare function formatDateTime(date: Date | number | string, dateStyle?: DateStyle, timeStyle?: TimeStyle, locale?: string): string;
13
+ /**
14
+ * Format relative time (e.g., "2 days ago", "in 3 hours")
15
+ */
16
+ export declare function formatRelativeTime(date: Date | number | string, baseDate?: Date | number | string, style?: 'long' | 'short' | 'narrow', locale?: string): string;
17
+ /**
18
+ * Format a number
19
+ */
20
+ export declare function formatNumber(value: number, options?: NumberFormatOptions, locale?: string): string;
21
+ /**
22
+ * Format a currency value
23
+ */
24
+ export declare function formatCurrency(value: number, currency?: string, options?: Omit<NumberFormatOptions, 'style' | 'currency'>, locale?: string): string;
25
+ /**
26
+ * Format a percentage
27
+ */
28
+ export declare function formatPercent(value: number, options?: Omit<NumberFormatOptions, 'style'>, locale?: string): string;
29
+ /**
30
+ * Format a number with units (e.g., "5 kilograms", "10 miles")
31
+ */
32
+ export declare function formatUnit(value: number, unit: string, unitDisplay?: 'long' | 'short' | 'narrow', locale?: string): string;
33
+ /**
34
+ * Format bytes to human-readable format
35
+ */
36
+ export declare function formatBytes(bytes: number, decimals?: number, locale?: string): string;
37
+ /**
38
+ * Format a list of items
39
+ */
40
+ export declare function formatList(items: string[], type?: ListType, style?: ListStyle, locale?: string): string;
41
+ /**
42
+ * Get the plural category for a number
43
+ */
44
+ export declare function formatPlural(value: number, locale?: string): Intl.LDMLPluralRule;
45
+ /**
46
+ * Select from plural options based on a number
47
+ */
48
+ export declare function selectPlural<T>(value: number, options: Partial<Record<Intl.LDMLPluralRule, T>> & { other: T }, locale?: string): T;
49
+ /**
50
+ * Get display name for a code
51
+ */
52
+ export declare function getDisplayName(code: string, type?: DisplayNameType, style?: 'long' | 'short' | 'narrow', locale?: string): string | undefined;
53
+ /**
54
+ * Get language name
55
+ */
56
+ export declare function getLanguageName(languageCode: string, locale?: string): string | undefined;
57
+ /**
58
+ * Get region/country name
59
+ */
60
+ export declare function getRegionName(regionCode: string, locale?: string): string | undefined;
61
+ /**
62
+ * Get currency name
63
+ */
64
+ export declare function getCurrencyName(currencyCode: string, locale?: string): string | undefined;
65
+ /**
66
+ * Compare strings in a locale-aware manner
67
+ */
68
+ export declare function compareStrings(a: string, b: string, options?: Intl.CollatorOptions, locale?: string): number;
69
+ /**
70
+ * Sort an array of strings in locale-aware order
71
+ */
72
+ export declare function sortStrings(strings: string[], options?: Intl.CollatorOptions, locale?: string): string[];
73
+ /**
74
+ * Get the text direction for a locale
75
+ */
76
+ export declare function getTextDirection(locale?: string): 'ltr' | 'rtl';
77
+ /**
78
+ * Check if a locale uses RTL text direction
79
+ */
80
+ export declare function isRTL(locale?: string): boolean;
81
+ export declare interface DateFormatOptions extends Intl.DateTimeFormatOptions {
82
+ dateStyle?: DateStyle
83
+ timeStyle?: TimeStyle
84
+ }
85
+ // =============================================================================
86
+ // Number Formatting
87
+ // =============================================================================
88
+ export declare interface NumberFormatOptions extends Intl.NumberFormatOptions {
89
+ compact?: boolean
90
+ precision?: number
91
+ }
92
+ // =============================================================================
93
+ // Date Formatting
94
+ // =============================================================================
95
+ export type DateStyle = 'full' | 'long' | 'medium' | 'short';
96
+ export type TimeStyle = 'full' | 'long' | 'medium' | 'short';
97
+ // =============================================================================
98
+ // List Formatting
99
+ // =============================================================================
100
+ export type ListType = 'conjunction' | 'disjunction' | 'unit';
101
+ export type ListStyle = 'long' | 'short' | 'narrow';
102
+ // =============================================================================
103
+ // Display Names
104
+ // =============================================================================
105
+ export type DisplayNameType = 'language' | 'region' | 'script' | 'currency' | 'calendar' | 'dateTimeField';
@@ -0,0 +1,128 @@
1
+ import { getLocale } from "./translator";
2
+ export function formatDate(date, style = "medium", locale) {
3
+ const targetLocale = locale || getLocale(), dateObj = toDate(date), options = typeof style === "string" ? { dateStyle: style } : style;
4
+ return new Intl.DateTimeFormat(targetLocale, options).format(dateObj);
5
+ }
6
+ export function formatTime(date, style = "short", locale) {
7
+ const targetLocale = locale || getLocale(), dateObj = toDate(date), options = typeof style === "string" ? { timeStyle: style } : style;
8
+ return new Intl.DateTimeFormat(targetLocale, options).format(dateObj);
9
+ }
10
+ export function formatDateTime(date, dateStyle = "medium", timeStyle = "short", locale) {
11
+ const targetLocale = locale || getLocale(), dateObj = toDate(date);
12
+ return new Intl.DateTimeFormat(targetLocale, {
13
+ dateStyle,
14
+ timeStyle
15
+ }).format(dateObj);
16
+ }
17
+ export function formatRelativeTime(date, baseDate = new Date, style = "long", locale) {
18
+ const targetLocale = locale || getLocale(), dateObj = toDate(date), baseDateObj = toDate(baseDate), diffMs = dateObj.getTime() - baseDateObj.getTime(), diffSeconds = Math.round(diffMs / 1000), diffMinutes = Math.round(diffSeconds / 60), diffHours = Math.round(diffMinutes / 60), diffDays = Math.round(diffHours / 24), diffWeeks = Math.round(diffDays / 7), diffMonths = Math.round(diffDays / 30), diffYears = Math.round(diffDays / 365), rtf = new Intl.RelativeTimeFormat(targetLocale, { style });
19
+ if (Math.abs(diffSeconds) < 60)
20
+ return rtf.format(diffSeconds, "second");
21
+ if (Math.abs(diffMinutes) < 60)
22
+ return rtf.format(diffMinutes, "minute");
23
+ if (Math.abs(diffHours) < 24)
24
+ return rtf.format(diffHours, "hour");
25
+ if (Math.abs(diffDays) < 7)
26
+ return rtf.format(diffDays, "day");
27
+ if (Math.abs(diffWeeks) < 4)
28
+ return rtf.format(diffWeeks, "week");
29
+ if (Math.abs(diffMonths) < 12)
30
+ return rtf.format(diffMonths, "month");
31
+ return rtf.format(diffYears, "year");
32
+ }
33
+ export function formatNumber(value, options = {}, locale) {
34
+ const targetLocale = locale || getLocale(), formatOptions = { ...options };
35
+ if (options.compact) {
36
+ formatOptions.notation = "compact";
37
+ formatOptions.compactDisplay = "short";
38
+ }
39
+ if (options.precision !== void 0) {
40
+ formatOptions.minimumFractionDigits = options.precision;
41
+ formatOptions.maximumFractionDigits = options.precision;
42
+ }
43
+ return new Intl.NumberFormat(targetLocale, formatOptions).format(value);
44
+ }
45
+ export function formatCurrency(value, currency = "USD", options = {}, locale) {
46
+ const targetLocale = locale || getLocale();
47
+ return new Intl.NumberFormat(targetLocale, {
48
+ style: "currency",
49
+ currency,
50
+ ...options
51
+ }).format(value);
52
+ }
53
+ export function formatPercent(value, options = {}, locale) {
54
+ const targetLocale = locale || getLocale();
55
+ return new Intl.NumberFormat(targetLocale, {
56
+ style: "percent",
57
+ ...options
58
+ }).format(value);
59
+ }
60
+ export function formatUnit(value, unit, unitDisplay = "short", locale) {
61
+ const targetLocale = locale || getLocale();
62
+ return new Intl.NumberFormat(targetLocale, {
63
+ style: "unit",
64
+ unit,
65
+ unitDisplay
66
+ }).format(value);
67
+ }
68
+ export function formatBytes(bytes, decimals = 2, locale) {
69
+ const targetLocale = locale || getLocale();
70
+ if (bytes === 0)
71
+ return "0 B";
72
+ const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], i = Math.floor(Math.log(bytes) / Math.log(k)), value = bytes / Math.pow(k, i);
73
+ return `${new Intl.NumberFormat(targetLocale, {
74
+ minimumFractionDigits: 0,
75
+ maximumFractionDigits: decimals
76
+ }).format(value)} ${sizes[i]}`;
77
+ }
78
+ export function formatList(items, type = "conjunction", style = "long", locale) {
79
+ const targetLocale = locale || getLocale();
80
+ return new Intl.ListFormat(targetLocale, { type, style }).format(items);
81
+ }
82
+ export function formatPlural(value, locale) {
83
+ const targetLocale = locale || getLocale();
84
+ return new Intl.PluralRules(targetLocale).select(value);
85
+ }
86
+ export function selectPlural(value, options, locale) {
87
+ const category = formatPlural(value, locale);
88
+ return options[category] ?? options.other;
89
+ }
90
+ export function getDisplayName(code, type = "language", style = "long", locale) {
91
+ const targetLocale = locale || getLocale();
92
+ try {
93
+ return new Intl.DisplayNames(targetLocale, { type, style }).of(code);
94
+ } catch {
95
+ return;
96
+ }
97
+ }
98
+ export function getLanguageName(languageCode, locale) {
99
+ return getDisplayName(languageCode, "language", "long", locale);
100
+ }
101
+ export function getRegionName(regionCode, locale) {
102
+ return getDisplayName(regionCode, "region", "long", locale);
103
+ }
104
+ export function getCurrencyName(currencyCode, locale) {
105
+ return getDisplayName(currencyCode, "currency", "long", locale);
106
+ }
107
+ export function compareStrings(a, b, options = {}, locale) {
108
+ const targetLocale = locale || getLocale();
109
+ return new Intl.Collator(targetLocale, options).compare(a, b);
110
+ }
111
+ export function sortStrings(strings, options = {}, locale) {
112
+ const targetLocale = locale || getLocale(), collator = new Intl.Collator(targetLocale, options);
113
+ return [...strings].sort((a, b) => collator.compare(a, b));
114
+ }
115
+ function toDate(date) {
116
+ if (date instanceof Date)
117
+ return date;
118
+ if (typeof date === "number")
119
+ return new Date(date);
120
+ return new Date(date);
121
+ }
122
+ export function getTextDirection(locale) {
123
+ const targetLocale = locale || getLocale(), lang = (targetLocale.split("-")[0] ?? targetLocale).toLowerCase();
124
+ return ["ar", "he", "fa", "ur", "yi", "ps", "sd", "ug", "ku", "ckb"].includes(lang) ? "rtl" : "ltr";
125
+ }
126
+ export function isRTL(locale) {
127
+ return getTextDirection(locale) === "rtl";
128
+ }
@@ -0,0 +1,94 @@
1
+ export type { I18nConfig as TsI18nConfig, TranslationTree, TranslationValue } from '@stacksjs/ts-i18n';
2
+ /**
3
+ * Stacks i18n - Internationalization System
4
+ *
5
+ * A comprehensive internationalization system with support for:
6
+ * - Translation loading from JSON/YAML files
7
+ * - Plural forms
8
+ * - Variable interpolation
9
+ * - Nested translations
10
+ * - Date/time formatting
11
+ * - Number formatting
12
+ * - Currency formatting
13
+ * - Relative time formatting
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { t, setLocale, addTranslations } from '@stacksjs/i18n'
18
+ *
19
+ * // Add translations
20
+ * addTranslations('en', {
21
+ * greeting: 'Hello, {name}!',
22
+ * items: '{count} item | {count} items',
23
+ * })
24
+ *
25
+ * // Use translations
26
+ * t('greeting', { name: 'World' }) // "Hello, World!"
27
+ * t('items', { count: 1 }) // "1 item"
28
+ * t('items', { count: 5 }) // "5 items"
29
+ *
30
+ * // Format dates and numbers
31
+ * formatDate(new Date(), 'long') // "January 8, 2026"
32
+ * formatNumber(1234567.89) // "1,234,567.89"
33
+ * formatCurrency(99.99, 'USD') // "$99.99"
34
+ * ```
35
+ */
36
+ export * from './translator';
37
+ export * from './formatter';
38
+ export * from './pluralization';
39
+ export * from './loader';
40
+ export * from './types';
41
+ // Re-export the file-loading and type-generation surface from
42
+ // `@stacksjs/ts-i18n`. The library handles disk loading (YAML / TS /
43
+ // JSON), namespace resolution, and `.d.ts` codegen for translation
44
+ // keys; the Stacks-side translator/formatter sits on top of that
45
+ // data with locale management, ICU pluralization, and Intl-backed
46
+ // formatters. Importing both names from `@stacksjs/i18n` keeps the
47
+ // app side from caring which symbol comes from which package.
48
+ export {
49
+ loadTranslations as loadTranslationsFromDisk,
50
+ generateTypes as generateI18nTypes,
51
+ generateTypesFromModule as generateI18nTypesFromModule,
52
+ generateSampleConfig as generateI18nSampleConfig,
53
+ writeOutputs as writeI18nOutputs,
54
+ createTranslator as createSimpleTranslator,
55
+ } from '@stacksjs/ts-i18n';
56
+ // Re-export main functions for convenience
57
+ export {
58
+ t,
59
+ trans,
60
+ tc,
61
+ te,
62
+ tm,
63
+ setLocale,
64
+ getLocale,
65
+ addTranslations,
66
+ loadTranslations,
67
+ getAvailableLocales,
68
+ hasTranslation,
69
+ I18n,
70
+ createI18n,
71
+ useI18n,
72
+ } from './translator';
73
+ export {
74
+ ensureLocalesLoaded,
75
+ resolveRequestLocale,
76
+ applyRequestLocale,
77
+ } from './bootstrap';
78
+ export {
79
+ createLocaleSwitchResponse,
80
+ localizePath,
81
+ stripLocalePrefix,
82
+ type LocaleSwitchConfig,
83
+ } from './locale-switch';
84
+ export {
85
+ formatDate,
86
+ formatTime,
87
+ formatDateTime,
88
+ formatNumber,
89
+ formatCurrency,
90
+ formatPercent,
91
+ formatRelativeTime,
92
+ formatList,
93
+ formatPlural,
94
+ } from './formatter';
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ export * from "./translator";
2
+ export * from "./formatter";
3
+ export * from "./pluralization";
4
+ export * from "./loader";
5
+ export * from "./types";
6
+ export {
7
+ loadTranslations as loadTranslationsFromDisk,
8
+ generateTypes as generateI18nTypes,
9
+ generateTypesFromModule as generateI18nTypesFromModule,
10
+ generateSampleConfig as generateI18nSampleConfig,
11
+ writeOutputs as writeI18nOutputs,
12
+ createTranslator as createSimpleTranslator
13
+ } from "@stacksjs/ts-i18n";
14
+ export {
15
+ t,
16
+ trans,
17
+ tc,
18
+ te,
19
+ tm,
20
+ setLocale,
21
+ getLocale,
22
+ addTranslations,
23
+ loadTranslations,
24
+ getAvailableLocales,
25
+ hasTranslation,
26
+ I18n,
27
+ createI18n,
28
+ useI18n
29
+ } from "./translator";
30
+ export {
31
+ ensureLocalesLoaded,
32
+ resolveRequestLocale,
33
+ applyRequestLocale
34
+ } from "./bootstrap";
35
+ export {
36
+ createLocaleSwitchResponse,
37
+ localizePath,
38
+ stripLocalePrefix
39
+ } from "./locale-switch";
40
+ export {
41
+ formatDate,
42
+ formatTime,
43
+ formatDateTime,
44
+ formatNumber,
45
+ formatCurrency,
46
+ formatPercent,
47
+ formatRelativeTime,
48
+ formatList,
49
+ formatPlural
50
+ } from "./formatter";
@@ -0,0 +1,52 @@
1
+ import type { TranslationMessages, Translations } from './types';
2
+ /**
3
+ * Load translations from a directory
4
+ */
5
+ export declare function loadFromDirectory(options: LoaderOptions): Promise<Translations>;
6
+ /**
7
+ * Load a single translation file
8
+ */
9
+ export declare function loadFile(filePath: string): Promise<TranslationMessages>;
10
+ /**
11
+ * Load a single locale from a file or directory
12
+ */
13
+ export declare function loadLocale(locale: string, path: string): Promise<TranslationMessages>;
14
+ /**
15
+ * Create a translation loader for a specific directory
16
+ */
17
+ export declare function createLoader(directory: string, options?: Partial<Omit<LoaderOptions, 'directory'>>): {
18
+ load: () => unknown;
19
+ loadLocale: (locale: string, filename: string) => unknown
20
+ };
21
+ /**
22
+ * Load translations via `@stacksjs/ts-i18n` and register them with the
23
+ * global translator. This is the recommended path for new projects —
24
+ * it gets you:
25
+ *
26
+ * - Per-locale subdirectory namespacing (en/auth.yaml → en.auth.*)
27
+ * - First-class .ts/.json/.yaml support with consistent error
28
+ * messages
29
+ * - Optional `.d.ts` codegen via `generateI18nTypes()` so message
30
+ * keys autocomplete in `t('…')` calls
31
+ *
32
+ * The legacy `loadFromDirectory()` remains for projects that depend
33
+ * on its specific behavior; new code should prefer this entry.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * await loadFromTsI18n({
38
+ * translationsDir: path.userI18nPath(),
39
+ * defaultLocale: 'en',
40
+ * fallbackLocale: 'en',
41
+ * sources: ['yaml', 'ts', 'json'],
42
+ * optional: true,
43
+ * })
44
+ * ```
45
+ */
46
+ export declare function loadFromTsI18n(config: import('@stacksjs/ts-i18n').I18nConfig): Promise<Translations>;
47
+ export declare interface LoaderOptions {
48
+ directory: string
49
+ extensions?: string[]
50
+ recursive?: boolean
51
+ namespaceSeparator?: string
52
+ }
package/dist/loader.js ADDED
@@ -0,0 +1,144 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, extname, join } from "node:path";
4
+ import { log } from "@stacksjs/logging";
5
+ import { addTranslations, loadTranslations } from "./translator";
6
+ export async function loadFromDirectory(options) {
7
+ const {
8
+ directory,
9
+ extensions = [".json", ".yaml", ".yml", ".ts", ".js"],
10
+ recursive = !1,
11
+ namespaceSeparator = "."
12
+ } = options, translations = {};
13
+ if (!existsSync(directory)) {
14
+ log.warn(`[i18n] Translation directory not found: ${directory}`);
15
+ return translations;
16
+ }
17
+ await loadDirectory(directory, translations, extensions, recursive, namespaceSeparator, "");
18
+ loadTranslations(translations);
19
+ return translations;
20
+ }
21
+ async function loadDirectory(dir, translations, extensions, recursive, namespaceSeparator, namespace) {
22
+ const entries = readdirSync(dir, { withFileTypes: !0 });
23
+ for (const entry of entries) {
24
+ const fullPath = join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ const locale = entry.name;
27
+ if (isLocaleCode(locale))
28
+ await loadLocaleDirectory(fullPath, locale, translations, extensions);
29
+ else if (recursive) {
30
+ const newNamespace = namespace ? `${namespace}${namespaceSeparator}${entry.name}` : entry.name;
31
+ await loadDirectory(fullPath, translations, extensions, recursive, namespaceSeparator, newNamespace);
32
+ }
33
+ } else if (entry.isFile()) {
34
+ const ext = extname(entry.name);
35
+ if (extensions.includes(ext)) {
36
+ const locale = basename(entry.name, ext);
37
+ if (!translations[locale])
38
+ translations[locale] = {};
39
+ const messages = await loadFile(fullPath);
40
+ if (namespace)
41
+ setNested(translations[locale], namespace, messages, namespaceSeparator);
42
+ else
43
+ translations[locale] = deepMerge(translations[locale], messages);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ async function loadLocaleDirectory(dir, locale, translations, extensions) {
49
+ if (!translations[locale])
50
+ translations[locale] = {};
51
+ const entries = readdirSync(dir, { withFileTypes: !0 });
52
+ for (const entry of entries) {
53
+ if (!entry.isFile())
54
+ continue;
55
+ const ext = extname(entry.name);
56
+ if (!extensions.includes(ext))
57
+ continue;
58
+ const fullPath = join(dir, entry.name), namespace = basename(entry.name, ext), messages = await loadFile(fullPath);
59
+ if (namespace === locale)
60
+ translations[locale] = deepMerge(translations[locale], messages);
61
+ else
62
+ translations[locale][namespace] = messages;
63
+ }
64
+ }
65
+ export async function loadFile(filePath) {
66
+ const ext = extname(filePath).toLowerCase(), content = await readFile(filePath, "utf8");
67
+ switch (ext) {
68
+ case ".json":
69
+ try {
70
+ return JSON.parse(content);
71
+ } catch {
72
+ throw Error(`Invalid JSON in translation file: ${filePath}`);
73
+ }
74
+ case ".yaml":
75
+ case ".yml":
76
+ return parseYaml(content);
77
+ case ".ts":
78
+ case ".js": {
79
+ const module = await import(filePath);
80
+ return module.default || module;
81
+ }
82
+ default:
83
+ throw Error(`Unsupported file type: ${ext}`);
84
+ }
85
+ }
86
+ export async function loadLocale(locale, path) {
87
+ const messages = await loadFile(path);
88
+ addTranslations(locale, messages);
89
+ return messages;
90
+ }
91
+ function parseYaml(content) {
92
+ try {
93
+ const parsed = Bun.YAML.parse(content);
94
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
95
+ throw Error("YAML content must be an object at the root level");
96
+ return parsed;
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : "Unknown YAML parse error";
99
+ throw Error(`Invalid YAML translation file: ${message}`);
100
+ }
101
+ }
102
+ function isLocaleCode(str) {
103
+ return /^[a-z]{2}(?:-[A-Z]{2})?(?:-[A-Za-z]+)?$/i.test(str);
104
+ }
105
+ function setNested(obj, path, value, separator) {
106
+ const parts = path.split(separator);
107
+ let current = obj;
108
+ for (let i = 0;i < parts.length - 1; i++) {
109
+ const part = parts[i];
110
+ if (!part)
111
+ continue;
112
+ if (!current[part] || typeof current[part] === "string")
113
+ current[part] = {};
114
+ current = current[part];
115
+ }
116
+ const lastPart = parts[parts.length - 1];
117
+ if (lastPart)
118
+ current[lastPart] = value;
119
+ }
120
+ function deepMerge(target, source) {
121
+ const result = { ...target };
122
+ for (const key of Object.keys(source)) {
123
+ const sourceValue = source[key];
124
+ if (sourceValue === void 0)
125
+ continue;
126
+ const targetValue = result[key];
127
+ if (typeof sourceValue === "object" && sourceValue !== null && typeof targetValue === "object" && targetValue !== null)
128
+ result[key] = deepMerge(targetValue, sourceValue);
129
+ else
130
+ result[key] = sourceValue;
131
+ }
132
+ return result;
133
+ }
134
+ export function createLoader(directory, options = {}) {
135
+ return {
136
+ load: () => loadFromDirectory({ directory, ...options }),
137
+ loadLocale: (locale, filename) => loadLocale(locale, join(directory, filename))
138
+ };
139
+ }
140
+ export async function loadFromTsI18n(config) {
141
+ const { loadTranslations: tsLoad } = await import("@stacksjs/ts-i18n"), localeMap = await tsLoad(config);
142
+ loadTranslations(localeMap);
143
+ return localeMap;
144
+ }
@@ -0,0 +1,16 @@
1
+ export declare function stripLocalePrefix(path: string, locales: string[]): { locale: string | null, path: string };
2
+ export declare function localizePath(path: string, locale: string, defaultLocale: string): string;
3
+ /**
4
+ * Build a redirect Response that sets the `locale` cookie and sends the
5
+ * visitor to the equivalent path in the requested locale (STX-style).
6
+ */
7
+ export declare function createLocaleSwitchResponse(request: Request, localeCode: string, config: LocaleSwitchConfig): Response;
8
+ /**
9
+ * Locale switch redirect — mirrors STX `buildLangPickerScript` path logic
10
+ * (`/<code>/…` prefixes) so cookie-based fallbacks and `/locale/{code}`
11
+ * redirects stay consistent with stx-serve i18n routing.
12
+ */
13
+ export declare interface LocaleSwitchConfig {
14
+ locales: string[]
15
+ defaultLocale: string
16
+ }