@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,45 @@
1
+ function normalizeLocale(code, config) {
2
+ const normalized = code.trim().toLowerCase();
3
+ if (config.locales.includes(normalized))
4
+ return normalized;
5
+ const short = normalized.split("-")[0];
6
+ if (short && config.locales.includes(short))
7
+ return short;
8
+ return null;
9
+ }
10
+ export function stripLocalePrefix(path, locales) {
11
+ for (const loc of locales) {
12
+ if (path === `/${loc}` || path === `/${loc}/`)
13
+ return { locale: loc, path: "/" };
14
+ if (path.startsWith(`/${loc}/`))
15
+ return { locale: loc, path: path.slice(loc.length + 1) };
16
+ }
17
+ return { locale: null, path };
18
+ }
19
+ export function localizePath(path, locale, defaultLocale) {
20
+ if (locale === defaultLocale)
21
+ return path;
22
+ if (path === "/")
23
+ return `/${locale}/`;
24
+ if (path === "/404.html")
25
+ return `/${locale}/404.html`;
26
+ return `/${locale}${path}`;
27
+ }
28
+ export function createLocaleSwitchResponse(request, localeCode, config) {
29
+ const locale = normalizeLocale(localeCode, config);
30
+ if (!locale)
31
+ return Response.redirect(new URL("/", request.url).href, 302);
32
+ const referer = request.headers.get("referer");
33
+ let basePath = "/";
34
+ if (referer)
35
+ try {
36
+ const ref = new URL(referer);
37
+ if (!ref.pathname.startsWith("/locale/"))
38
+ basePath = stripLocalePrefix(ref.pathname, config.locales).path || "/";
39
+ } catch {
40
+ basePath = "/";
41
+ }
42
+ const targetPath = localizePath(basePath, locale, config.defaultLocale), target = new URL(targetPath, request.url), res = Response.redirect(target.href, 302);
43
+ res.headers.append("Set-Cookie", `locale=${locale}; Path=/; Max-Age=31536000; SameSite=Lax`);
44
+ return res;
45
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Get the plural category for a number in a given locale
3
+ */
4
+ export declare function getPluralCategory(n: number, locale: string): PluralCategory;
5
+ /**
6
+ * Get the plural rule function for a locale
7
+ */
8
+ export declare function getPluralRule(locale: string): PluralRule;
9
+ /**
10
+ * Parse a plural string into its forms
11
+ *
12
+ * Supports formats:
13
+ * - "one | other" (simple)
14
+ * - "zero | one | other" (with zero)
15
+ * - "{0} item | {0} items" (with placeholders)
16
+ * - "no items | one item | {n} items" (with special forms)
17
+ */
18
+ export declare function parsePluralForms(str: string, separator?: string): Map<PluralCategory, string>;
19
+ /**
20
+ * Select the appropriate plural form for a count
21
+ */
22
+ export declare function selectPluralForm(forms: Map<PluralCategory, string>, count: number, locale: string): string;
23
+ /**
24
+ * Get all supported locales for pluralization
25
+ */
26
+ export declare function getSupportedPluralLocales(): string[];
27
+ /**
28
+ * Check if a locale has custom plural rules
29
+ */
30
+ export declare function hasPluralRule(locale: string): boolean;
31
+ /**
32
+ * Add a custom plural rule for a locale
33
+ */
34
+ export declare function addPluralRule(locale: string, rule: PluralRule): void;
35
+ /**
36
+ * Pluralization Rules
37
+ *
38
+ * Implements CLDR plural rules for various languages.
39
+ * @see https://cldr.unicode.org/index/cldr-spec/plural-rules
40
+ */
41
+ export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
42
+ export type PluralRule = (_n: number) => PluralCategory;
@@ -0,0 +1,190 @@
1
+ export function getPluralCategory(n, locale) {
2
+ return getPluralRule(locale)(n);
3
+ }
4
+ export function getPluralRule(locale) {
5
+ const lang = locale.split("-")[0]?.toLowerCase() ?? locale.toLowerCase(), rule = pluralRules[lang];
6
+ if (rule)
7
+ return rule;
8
+ return () => "other";
9
+ }
10
+ export function parsePluralForms(str, separator = "|") {
11
+ const parts = str.split(separator).map((s) => s.trim()), forms = new Map;
12
+ if (parts.length === 1)
13
+ forms.set("other", parts[0] ?? "");
14
+ else if (parts.length === 2) {
15
+ forms.set("one", parts[0] ?? "");
16
+ forms.set("other", parts[1] ?? "");
17
+ } else if (parts.length === 3) {
18
+ forms.set("zero", parts[0] ?? "");
19
+ forms.set("one", parts[1] ?? "");
20
+ forms.set("other", parts[2] ?? "");
21
+ } else if (parts.length >= 4) {
22
+ const categories = ["zero", "one", "two", "few", "many", "other"];
23
+ for (let i = 0;i < parts.length && i < categories.length; i++) {
24
+ const category = categories[i], part = parts[i];
25
+ if (category !== void 0 && part !== void 0)
26
+ forms.set(category, part);
27
+ }
28
+ }
29
+ return forms;
30
+ }
31
+ export function selectPluralForm(forms, count, locale) {
32
+ const category = getPluralCategory(count, locale);
33
+ if (forms.has(category))
34
+ return forms.get(category);
35
+ if (forms.has("other"))
36
+ return forms.get("other");
37
+ const first = forms.values().next();
38
+ return first.done ? "" : first.value;
39
+ }
40
+ const pluralRules = {
41
+ en: (n) => n === 1 ? "one" : "other",
42
+ de: (n) => n === 1 ? "one" : "other",
43
+ nl: (n) => n === 1 ? "one" : "other",
44
+ es: (n) => n === 1 ? "one" : "other",
45
+ it: (n) => n === 1 ? "one" : "other",
46
+ pt: (n) => n === 1 ? "one" : "other",
47
+ sv: (n) => n === 1 ? "one" : "other",
48
+ da: (n) => n === 1 ? "one" : "other",
49
+ no: (n) => n === 1 ? "one" : "other",
50
+ fi: (n) => n === 1 ? "one" : "other",
51
+ el: (n) => n === 1 ? "one" : "other",
52
+ he: (n) => n === 1 ? "one" : "other",
53
+ hu: (n) => n === 1 ? "one" : "other",
54
+ tr: (n) => n === 1 ? "one" : "other",
55
+ zh: () => "other",
56
+ ja: () => "other",
57
+ ko: () => "other",
58
+ vi: () => "other",
59
+ th: () => "other",
60
+ id: () => "other",
61
+ ms: () => "other",
62
+ fr: (n) => n === 0 || n === 1 ? "one" : "other",
63
+ ru: (n) => {
64
+ const mod10 = n % 10, mod100 = n % 100;
65
+ if (mod10 === 1 && mod100 !== 11)
66
+ return "one";
67
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14))
68
+ return "few";
69
+ if (mod10 === 0 || mod10 >= 5 && mod10 <= 9 || mod100 >= 11 && mod100 <= 14)
70
+ return "many";
71
+ return "other";
72
+ },
73
+ uk: (n) => {
74
+ const mod10 = n % 10, mod100 = n % 100;
75
+ if (mod10 === 1 && mod100 !== 11)
76
+ return "one";
77
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14))
78
+ return "few";
79
+ if (mod10 === 0 || mod10 >= 5 && mod10 <= 9 || mod100 >= 11 && mod100 <= 14)
80
+ return "many";
81
+ return "other";
82
+ },
83
+ pl: (n) => {
84
+ const mod10 = n % 10, mod100 = n % 100;
85
+ if (n === 1)
86
+ return "one";
87
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14))
88
+ return "few";
89
+ if (mod10 === 0 || mod10 === 1 || mod10 >= 5 && mod10 <= 9 || mod100 >= 12 && mod100 <= 14)
90
+ return "many";
91
+ return "other";
92
+ },
93
+ cs: (n) => {
94
+ if (n === 1)
95
+ return "one";
96
+ if (n >= 2 && n <= 4)
97
+ return "few";
98
+ return "other";
99
+ },
100
+ sk: (n) => {
101
+ if (n === 1)
102
+ return "one";
103
+ if (n >= 2 && n <= 4)
104
+ return "few";
105
+ return "other";
106
+ },
107
+ ar: (n) => {
108
+ if (n === 0)
109
+ return "zero";
110
+ if (n === 1)
111
+ return "one";
112
+ if (n === 2)
113
+ return "two";
114
+ const mod100 = n % 100;
115
+ if (mod100 >= 3 && mod100 <= 10)
116
+ return "few";
117
+ if (mod100 >= 11 && mod100 <= 99)
118
+ return "many";
119
+ return "other";
120
+ },
121
+ cy: (n) => {
122
+ if (n === 0)
123
+ return "zero";
124
+ if (n === 1)
125
+ return "one";
126
+ if (n === 2)
127
+ return "two";
128
+ if (n === 3)
129
+ return "few";
130
+ if (n === 6)
131
+ return "many";
132
+ return "other";
133
+ },
134
+ sl: (n) => {
135
+ const mod100 = n % 100;
136
+ if (mod100 === 1)
137
+ return "one";
138
+ if (mod100 === 2)
139
+ return "two";
140
+ if (mod100 === 3 || mod100 === 4)
141
+ return "few";
142
+ return "other";
143
+ },
144
+ ga: (n) => {
145
+ if (n === 1)
146
+ return "one";
147
+ if (n === 2)
148
+ return "two";
149
+ if (n >= 3 && n <= 6)
150
+ return "few";
151
+ if (n >= 7 && n <= 10)
152
+ return "many";
153
+ return "other";
154
+ },
155
+ lt: (n) => {
156
+ const mod10 = n % 10, mod100 = n % 100;
157
+ if (mod10 === 1 && (mod100 < 11 || mod100 > 19))
158
+ return "one";
159
+ if (mod10 >= 2 && mod10 <= 9 && (mod100 < 11 || mod100 > 19))
160
+ return "few";
161
+ return "other";
162
+ },
163
+ lv: (n) => {
164
+ const mod10 = n % 10, mod100 = n % 100;
165
+ if (n === 0)
166
+ return "zero";
167
+ if (mod10 === 1 && mod100 !== 11)
168
+ return "one";
169
+ return "other";
170
+ },
171
+ ro: (n) => {
172
+ if (n === 1)
173
+ return "one";
174
+ const mod100 = n % 100;
175
+ if (n === 0 || mod100 >= 1 && mod100 <= 19)
176
+ return "few";
177
+ return "other";
178
+ },
179
+ hi: (n) => n === 0 || n === 1 ? "one" : "other"
180
+ };
181
+ export function getSupportedPluralLocales() {
182
+ return Object.keys(pluralRules);
183
+ }
184
+ export function hasPluralRule(locale) {
185
+ return (locale.split("-")[0]?.toLowerCase() ?? locale.toLowerCase()) in pluralRules;
186
+ }
187
+ export function addPluralRule(locale, rule) {
188
+ const lang = locale.split("-")[0]?.toLowerCase() ?? locale.toLowerCase();
189
+ pluralRules[lang] = rule;
190
+ }
@@ -0,0 +1,79 @@
1
+ import type { I18nConfig, I18nInstance, InterpolationValues, TranslationMessages, Translations } from './types';
2
+ /**
3
+ * Set the current locale
4
+ */
5
+ export declare function setLocale(locale: string): void;
6
+ /**
7
+ * Get the current locale
8
+ */
9
+ export declare function getLocale(): string;
10
+ /**
11
+ * Set the fallback locale
12
+ */
13
+ export declare function setFallbackLocale(locale: string): void;
14
+ /**
15
+ * Get available locales
16
+ */
17
+ export declare function getAvailableLocales(): string[];
18
+ /**
19
+ * Add translations for a locale
20
+ */
21
+ export declare function addTranslations(locale: string, messages: TranslationMessages): void;
22
+ /**
23
+ * Load translations (alias for addTranslations for multiple locales)
24
+ */
25
+ export declare function loadTranslations(messages: Translations): void;
26
+ /**
27
+ * Check if a translation exists
28
+ */
29
+ export declare function hasTranslation(key: string, locale?: string): boolean;
30
+ /**
31
+ * Translate a key
32
+ */
33
+ export declare function t(key: string, values?: InterpolationValues, locale?: string): string;
34
+ /**
35
+ * Translate with pluralization
36
+ */
37
+ export declare function tc(key: string, count: number, values?: InterpolationValues, locale?: string): string;
38
+ /**
39
+ * Check if translation exists
40
+ */
41
+ export declare function te(key: string, locale?: string): boolean;
42
+ /**
43
+ * Get translation message object
44
+ */
45
+ export declare function tm(key: string, locale?: string): TranslationMessages | string | undefined;
46
+ /**
47
+ * Create a new i18n instance
48
+ */
49
+ export declare function createI18n(options?: Partial<I18nConfig>): I18n;
50
+ /**
51
+ * Use i18n composable (for Vue-like usage)
52
+ */
53
+ export declare function useI18n(options?: Partial<I18nConfig>): I18nInstance;
54
+ /**
55
+ * Configure the global i18n instance
56
+ */
57
+ export declare function configure(options: Partial<I18nConfig>): void;
58
+ /**
59
+ * Alias for t()
60
+ */
61
+ export declare const trans: unknown;
62
+ /**
63
+ * I18n class for creating isolated instances
64
+ */
65
+ export declare class I18n implements I18nInstance {
66
+ constructor(options?: Partial<I18nConfig>);
67
+ get locale(): string;
68
+ set locale(value: string);
69
+ get fallbackLocale(): string;
70
+ get availableLocales(): string[];
71
+ t: unknown;
72
+ tc: unknown;
73
+ te: unknown;
74
+ tm: unknown;
75
+ setLocale: unknown;
76
+ addTranslations: unknown;
77
+ d: unknown;
78
+ n: unknown;
79
+ }
@@ -0,0 +1,249 @@
1
+ import { parsePluralForms, selectPluralForm, getPluralCategory } from "./pluralization";
2
+ const defaultConfig = {
3
+ locale: "en",
4
+ fallbackLocale: "en",
5
+ availableLocales: ["en"],
6
+ messages: {},
7
+ warnMissing: !0,
8
+ escapeValues: !1,
9
+ keySeparator: ".",
10
+ pluralSeparator: "|"
11
+ };
12
+ let currentLocale = "en", fallbackLocale = "en";
13
+ const translations = {};
14
+ let config = { ...defaultConfig };
15
+
16
+ export class I18n {
17
+ _locale;
18
+ _fallbackLocale;
19
+ _messages;
20
+ _config;
21
+ constructor(options = {}) {
22
+ this._config = { ...defaultConfig, ...options };
23
+ this._locale = this._config.locale;
24
+ this._fallbackLocale = this._config.fallbackLocale;
25
+ this._messages = this._config.messages || {};
26
+ }
27
+ get locale() {
28
+ return this._locale;
29
+ }
30
+ set locale(value) {
31
+ this._locale = value;
32
+ }
33
+ get fallbackLocale() {
34
+ return this._fallbackLocale;
35
+ }
36
+ get availableLocales() {
37
+ return Object.keys(this._messages);
38
+ }
39
+ t = (key, values, locale) => {
40
+ return this.translate(key, values, locale);
41
+ };
42
+ tc = (key, count, values, locale) => {
43
+ return this.translatePlural(key, count, values, locale);
44
+ };
45
+ te = (key, locale) => {
46
+ return this.hasTranslation(key, locale);
47
+ };
48
+ tm = (key, locale) => {
49
+ return this.getMessage(key, locale ?? this.locale);
50
+ };
51
+ setLocale = (locale) => {
52
+ this._locale = locale;
53
+ };
54
+ addTranslations = (locale, messages) => {
55
+ if (!this._messages[locale])
56
+ this._messages[locale] = {};
57
+ this._messages[locale] = deepMerge(this._messages[locale], messages);
58
+ };
59
+ d = (value, format) => {
60
+ const date = typeof value === "number" ? new Date(value) : value, options = this._config.dateTimeFormats?.[this._locale]?.[format || "short"] || {};
61
+ return new Intl.DateTimeFormat(this._locale, options).format(date);
62
+ };
63
+ n = (value, format) => {
64
+ const options = this._config.numberFormats?.[this._locale]?.[format || "decimal"] || {};
65
+ return new Intl.NumberFormat(this._locale, options).format(value);
66
+ };
67
+ translate(key, values, locale) {
68
+ const targetLocale = locale || this._locale;
69
+ let message = this.getMessage(key, targetLocale);
70
+ if (message === void 0) {
71
+ const dash = targetLocale.indexOf("-");
72
+ if (dash > 0) {
73
+ const langOnly = targetLocale.slice(0, dash);
74
+ if (langOnly !== this._fallbackLocale)
75
+ message = this.getMessage(key, langOnly);
76
+ }
77
+ }
78
+ if (message === void 0 && targetLocale !== this._fallbackLocale)
79
+ message = this.getMessage(key, this._fallbackLocale);
80
+ if (message === void 0) {
81
+ if (this._config.warnMissing)
82
+ console.warn(`[i18n] Missing translation: "${key}" for locale "${targetLocale}"`);
83
+ if (this._config.missingHandler) {
84
+ const result = this._config.missingHandler(targetLocale, key);
85
+ if (result !== void 0)
86
+ return result;
87
+ }
88
+ return key;
89
+ }
90
+ if (typeof message !== "string")
91
+ return key;
92
+ return this.interpolate(message, values);
93
+ }
94
+ translatePlural(key, count, values, locale) {
95
+ const targetLocale = locale || this._locale;
96
+ let message = this.getMessage(key, targetLocale);
97
+ if (message === void 0 && targetLocale !== this._fallbackLocale)
98
+ message = this.getMessage(key, this._fallbackLocale);
99
+ if (message === void 0 || typeof message !== "string") {
100
+ if (this._config.warnMissing)
101
+ console.warn(`[i18n] Missing plural translation: "${key}" for locale "${targetLocale}"`);
102
+ return key;
103
+ }
104
+ const forms = parsePluralForms(message, this._config.pluralSeparator), selectedForm = selectPluralForm(forms, count, targetLocale), allValues = { count, n: count, ...values };
105
+ return this.interpolate(selectedForm, allValues);
106
+ }
107
+ hasTranslation(key, locale) {
108
+ const targetLocale = locale || this._locale;
109
+ return this.getMessage(key, targetLocale) !== void 0;
110
+ }
111
+ getMessage(key, locale) {
112
+ const messages = this._messages[locale];
113
+ if (!messages)
114
+ return;
115
+ const parts = key.split(this._config.keySeparator || ".");
116
+ let current = messages;
117
+ for (const part of parts) {
118
+ if (current === void 0 || typeof current === "string")
119
+ return;
120
+ current = current[part];
121
+ }
122
+ return current;
123
+ }
124
+ interpolate(message, values) {
125
+ if (!values)
126
+ return message;
127
+ return message.replace(/\{(\w+)\}/g, (match, key) => {
128
+ const value = values[key];
129
+ if (value === void 0 || value === null)
130
+ return match;
131
+ const str = String(value);
132
+ return this._config.escapeValues ? escapeHtml(str) : str;
133
+ });
134
+ }
135
+ }
136
+ export function setLocale(locale) {
137
+ currentLocale = locale;
138
+ }
139
+ export function getLocale() {
140
+ return currentLocale;
141
+ }
142
+ export function setFallbackLocale(locale) {
143
+ fallbackLocale = locale;
144
+ }
145
+ export function getAvailableLocales() {
146
+ return Object.keys(translations);
147
+ }
148
+ export function addTranslations(locale, messages) {
149
+ if (!translations[locale])
150
+ translations[locale] = {};
151
+ translations[locale] = deepMerge(translations[locale], messages);
152
+ }
153
+ export function loadTranslations(messages) {
154
+ for (const [locale, localeMessages] of Object.entries(messages))
155
+ addTranslations(locale, localeMessages);
156
+ }
157
+ export function hasTranslation(key, locale) {
158
+ return getMessage(key, locale || currentLocale) !== void 0;
159
+ }
160
+ function getMessage(key, locale) {
161
+ const messages = translations[locale];
162
+ if (!messages)
163
+ return;
164
+ const parts = key.split(config.keySeparator || ".");
165
+ let current = messages;
166
+ for (const part of parts) {
167
+ if (current === void 0 || typeof current === "string")
168
+ return;
169
+ current = current[part];
170
+ }
171
+ return current;
172
+ }
173
+ function interpolate(message, values) {
174
+ if (!values)
175
+ return message;
176
+ return message.replace(/\{(\w+)\}/g, (match, key) => {
177
+ const value = values[key];
178
+ if (value === void 0 || value === null)
179
+ return match;
180
+ return String(value);
181
+ });
182
+ }
183
+ export function t(key, values, locale) {
184
+ const targetLocale = locale || currentLocale;
185
+ let message = getMessage(key, targetLocale);
186
+ if (message === void 0 && targetLocale !== fallbackLocale)
187
+ message = getMessage(key, fallbackLocale);
188
+ if (message === void 0) {
189
+ if (config.warnMissing)
190
+ console.warn(`[i18n] Missing translation: "${key}" for locale "${targetLocale}"`);
191
+ return key;
192
+ }
193
+ if (typeof message !== "string")
194
+ return key;
195
+ return interpolate(message, values);
196
+ }
197
+ export const trans = t;
198
+ export function tc(key, count, values, locale) {
199
+ const targetLocale = locale || currentLocale;
200
+ let message = getMessage(key, targetLocale);
201
+ if (message === void 0 && targetLocale !== fallbackLocale)
202
+ message = getMessage(key, fallbackLocale);
203
+ if (message === void 0 || typeof message !== "string")
204
+ return key;
205
+ const forms = parsePluralForms(message, config.pluralSeparator), selectedForm = selectPluralForm(forms, count, targetLocale), allValues = { count, n: count, ...values };
206
+ return interpolate(selectedForm, allValues);
207
+ }
208
+ export function te(key, locale) {
209
+ return hasTranslation(key, locale);
210
+ }
211
+ export function tm(key, locale) {
212
+ return getMessage(key, locale || currentLocale);
213
+ }
214
+ export function createI18n(options = {}) {
215
+ return new I18n(options);
216
+ }
217
+ export function useI18n(options = {}) {
218
+ return createI18n(options);
219
+ }
220
+ export function configure(options) {
221
+ config = { ...config, ...options };
222
+ if (options.locale)
223
+ currentLocale = options.locale;
224
+ if (options.fallbackLocale)
225
+ fallbackLocale = options.fallbackLocale;
226
+ if (options.messages)
227
+ loadTranslations(options.messages);
228
+ }
229
+ function deepMerge(target, source) {
230
+ const result = { ...target };
231
+ for (const key of Object.keys(source)) {
232
+ const sourceValue = source[key], targetValue = result[key];
233
+ if (typeof sourceValue === "object" && sourceValue !== null && typeof targetValue === "object" && targetValue !== null)
234
+ result[key] = deepMerge(targetValue, sourceValue);
235
+ else if (sourceValue !== void 0)
236
+ result[key] = sourceValue;
237
+ }
238
+ return result;
239
+ }
240
+ function escapeHtml(str) {
241
+ const htmlEscapes = {
242
+ "&": "&amp;",
243
+ "<": "&lt;",
244
+ ">": "&gt;",
245
+ '"': "&quot;",
246
+ "'": "&#39;"
247
+ };
248
+ return str.replace(/[&<>"']/g, (char) => htmlEscapes[char] || char);
249
+ }