@stacksjs/i18n 0.70.87 → 0.70.90

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,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,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
+ }
package/dist/index.js CHANGED
@@ -1,2 +1,50 @@
1
- // @bun
2
- var x=import.meta.require;function M(q,B){return D(B)(q)}function D(q){let B=q.split("-")[0]?.toLowerCase()??q.toLowerCase(),G=E[B];if(G)return G;return()=>"other"}function c(q,B="|"){let G=q.split(B).map((K)=>K.trim()),J=new Map;if(G.length===1)J.set("other",G[0]??"");else if(G.length===2)J.set("one",G[0]??""),J.set("other",G[1]??"");else if(G.length===3)J.set("zero",G[0]??""),J.set("one",G[1]??""),J.set("other",G[2]??"");else if(G.length>=4){let K=["zero","one","two","few","many","other"];for(let Q=0;Q<G.length&&Q<K.length;Q++){let X=K[Q],U=G[Q];if(X!==void 0&&U!==void 0)J.set(X,U)}}return J}function r(q,B,G){let J=M(B,G);if(q.has(J))return q.get(J);if(q.has("other"))return q.get("other");let K=q.values().next();return K.done?"":K.value}var E={en:(q)=>q===1?"one":"other",de:(q)=>q===1?"one":"other",nl:(q)=>q===1?"one":"other",es:(q)=>q===1?"one":"other",it:(q)=>q===1?"one":"other",pt:(q)=>q===1?"one":"other",sv:(q)=>q===1?"one":"other",da:(q)=>q===1?"one":"other",no:(q)=>q===1?"one":"other",fi:(q)=>q===1?"one":"other",el:(q)=>q===1?"one":"other",he:(q)=>q===1?"one":"other",hu:(q)=>q===1?"one":"other",tr:(q)=>q===1?"one":"other",zh:()=>"other",ja:()=>"other",ko:()=>"other",vi:()=>"other",th:()=>"other",id:()=>"other",ms:()=>"other",fr:(q)=>q===0||q===1?"one":"other",ru:(q)=>{let B=q%10,G=q%100;if(B===1&&G!==11)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B>=5&&B<=9||G>=11&&G<=14)return"many";return"other"},uk:(q)=>{let B=q%10,G=q%100;if(B===1&&G!==11)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B>=5&&B<=9||G>=11&&G<=14)return"many";return"other"},pl:(q)=>{let B=q%10,G=q%100;if(q===1)return"one";if(B>=2&&B<=4&&(G<12||G>14))return"few";if(B===0||B===1||B>=5&&B<=9||G>=12&&G<=14)return"many";return"other"},cs:(q)=>{if(q===1)return"one";if(q>=2&&q<=4)return"few";return"other"},sk:(q)=>{if(q===1)return"one";if(q>=2&&q<=4)return"few";return"other"},ar:(q)=>{if(q===0)return"zero";if(q===1)return"one";if(q===2)return"two";let B=q%100;if(B>=3&&B<=10)return"few";if(B>=11&&B<=99)return"many";return"other"},cy:(q)=>{if(q===0)return"zero";if(q===1)return"one";if(q===2)return"two";if(q===3)return"few";if(q===6)return"many";return"other"},sl:(q)=>{let B=q%100;if(B===1)return"one";if(B===2)return"two";if(B===3||B===4)return"few";return"other"},ga:(q)=>{if(q===1)return"one";if(q===2)return"two";if(q>=3&&q<=6)return"few";if(q>=7&&q<=10)return"many";return"other"},lt:(q)=>{let B=q%10,G=q%100;if(B===1&&(G<11||G>19))return"one";if(B>=2&&B<=9&&(G<11||G>19))return"few";return"other"},lv:(q)=>{let B=q%10,G=q%100;if(q===0)return"zero";if(B===1&&G!==11)return"one";return"other"},ro:(q)=>{if(q===1)return"one";let B=q%100;if(q===0||B>=1&&B<=19)return"few";return"other"},hi:(q)=>q===0||q===1?"one":"other"};function n(){return Object.keys(E)}function s(q){return(q.split("-")[0]?.toLowerCase()??q.toLowerCase())in E}function t(q,B){let G=q.split("-")[0]?.toLowerCase()??q.toLowerCase();E[G]=B}var b={locale:"en",fallbackLocale:"en",availableLocales:["en"],messages:{},warnMissing:!0,escapeValues:!1,keySeparator:".",pluralSeparator:"|"},C="en",O="en",W={},F={...b};function $(){return C}function l(q){O=q}function z(q,B){if(!W[q])W[q]={};W[q]=j(W[q],B)}function Y(q){for(let[B,G]of Object.entries(q))z(B,G)}function a(q){if(F={...F,...q},q.locale)C=q.locale;if(q.fallbackLocale)O=q.fallbackLocale;if(q.messages)Y(q.messages)}function j(q,B){let G={...q};for(let J of Object.keys(B)){let K=B[J],Q=G[J];if(typeof K==="object"&&K!==null&&typeof Q==="object"&&Q!==null)G[J]=j(Q,K);else if(K!==void 0)G[J]=K}return G}function Bq(q,B,G="short",J){let K=J||$();return new Intl.NumberFormat(K,{style:"unit",unit:B,unitDisplay:G}).format(q)}function Gq(q,B=2,G){let J=G||$();if(q===0)return"0 B";let K=1024,Q=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],X=Math.floor(Math.log(q)/Math.log(K)),U=q/Math.pow(K,X);return`${new Intl.NumberFormat(J,{minimumFractionDigits:0,maximumFractionDigits:B}).format(U)} ${Q[X]}`}function h(q,B){let G=B||$();return new Intl.PluralRules(G).select(q)}function Jq(q,B,G){let J=h(q,G);return B[J]??B.other}function I(q,B="language",G="long",J){let K=J||$();try{return new Intl.DisplayNames(K,{type:B,style:G}).of(q)}catch{return}}function Kq(q,B){return I(q,"language","long",B)}function Qq(q,B){return I(q,"region","long",B)}function Uq(q,B){return I(q,"currency","long",B)}function Xq(q,B,G={},J){let K=J||$();return new Intl.Collator(K,G).compare(q,B)}function Zq(q,B={},G){let J=G||$(),K=new Intl.Collator(J,B);return[...q].sort((Q,X)=>K.compare(Q,X))}function v(q){let B=q||$(),G=(B.split("-")[0]??B).toLowerCase();return["ar","he","fa","ur","yi","ps","sd","ug","ku","ckb"].includes(G)?"rtl":"ltr"}function _q(q){return v(q)==="rtl"}import{existsSync as L,readdirSync as S}from"fs";import{readFile as k}from"fs/promises";import{basename as V,extname as w,join as H}from"path";import{log as f}from"@stacksjs/logging";async function y(q){let{directory:B,extensions:G=[".json",".yaml",".yml",".ts",".js"],recursive:J=!1,namespaceSeparator:K="."}=q,Q={};if(!L(B))return f.warn(`[i18n] Translation directory not found: ${B}`),Q;return await P(B,Q,G,J,K,""),Y(Q),Q}async function P(q,B,G,J,K,Q){let X=S(q,{withFileTypes:!0});for(let U of X){let Z=H(q,U.name);if(U.isDirectory()){let _=U.name;if(m(_))await p(Z,_,B,G);else if(J){let A=Q?`${Q}${K}${U.name}`:U.name;await P(Z,B,G,J,K,A)}}else if(U.isFile()){let _=w(U.name);if(G.includes(_)){let A=V(U.name,_);if(!B[A])B[A]={};let T=await N(Z);if(Q)u(B[A],Q,T,K);else B[A]=R(B[A],T)}}}}async function p(q,B,G,J){if(!G[B])G[B]={};let K=S(q,{withFileTypes:!0});for(let Q of K){if(!Q.isFile())continue;let X=w(Q.name);if(!J.includes(X))continue;let U=H(q,Q.name),Z=V(Q.name,X),_=await N(U);if(Z===B)G[B]=R(G[B],_);else G[B][Z]=_}}async function N(q){let B=w(q).toLowerCase(),G=await k(q,"utf8");switch(B){case".json":try{return JSON.parse(G)}catch{throw Error(`Invalid JSON in translation file: ${q}`)}case".yaml":case".yml":return d(G);case".ts":case".js":{let J=await import(q);return J.default||J}default:throw Error(`Unsupported file type: ${B}`)}}async function g(q,B){let G=await N(B);return z(q,G),G}function d(q){try{let B=Bun.YAML.parse(q);if(!B||typeof B!=="object"||Array.isArray(B))throw Error("YAML content must be an object at the root level");return B}catch(B){let G=B instanceof Error?B.message:"Unknown YAML parse error";throw Error(`Invalid YAML translation file: ${G}`)}}function m(q){return/^[a-z]{2}(?:-[A-Z]{2})?(?:-[A-Za-z]+)?$/i.test(q)}function u(q,B,G,J){let K=B.split(J),Q=q;for(let U=0;U<K.length-1;U++){let Z=K[U];if(!Z)continue;if(!Q[Z]||typeof Q[Z]==="string")Q[Z]={};Q=Q[Z]}let X=K[K.length-1];if(X)Q[X]=G}function R(q,B){let G={...q};for(let J of Object.keys(B)){let K=B[J];if(K===void 0)continue;let Q=G[J];if(typeof K==="object"&&K!==null&&typeof Q==="object"&&Q!==null)G[J]=R(Q,K);else G[J]=K}return G}function Iq(q,B={}){return{load:()=>y({directory:q,...B}),loadLocale:(G,J)=>g(G,H(q,J))}}async function wq(q){let{loadTranslations:B}=await import("@stacksjs/ts-i18n"),G=await B(q);return Y(G),G}var Nq={en:{code:"en",name:"English",nativeName:"English",direction:"ltr"},"en-US":{code:"en-US",name:"English (US)",nativeName:"English (US)",direction:"ltr",region:"US"},"en-GB":{code:"en-GB",name:"English (UK)",nativeName:"English (UK)",direction:"ltr",region:"GB"},es:{code:"es",name:"Spanish",nativeName:"Espa\xF1ol",direction:"ltr"},fr:{code:"fr",name:"French",nativeName:"Fran\xE7ais",direction:"ltr"},de:{code:"de",name:"German",nativeName:"Deutsch",direction:"ltr"},it:{code:"it",name:"Italian",nativeName:"Italiano",direction:"ltr"},pt:{code:"pt",name:"Portuguese",nativeName:"Portugu\xEAs",direction:"ltr"},"pt-BR":{code:"pt-BR",name:"Portuguese (Brazil)",nativeName:"Portugu\xEAs (Brasil)",direction:"ltr",region:"BR"},zh:{code:"zh",name:"Chinese",nativeName:"\u4E2D\u6587",direction:"ltr"},"zh-CN":{code:"zh-CN",name:"Chinese (Simplified)",nativeName:"\u7B80\u4F53\u4E2D\u6587",direction:"ltr",region:"CN"},"zh-TW":{code:"zh-TW",name:"Chinese (Traditional)",nativeName:"\u7E41\u9AD4\u4E2D\u6587",direction:"ltr",region:"TW"},ja:{code:"ja",name:"Japanese",nativeName:"\u65E5\u672C\u8A9E",direction:"ltr"},ko:{code:"ko",name:"Korean",nativeName:"\uD55C\uAD6D\uC5B4",direction:"ltr"},ar:{code:"ar",name:"Arabic",nativeName:"\u0627\u0644\u0639\u0631\u0628\u064A\u0629",direction:"rtl"},he:{code:"he",name:"Hebrew",nativeName:"\u05E2\u05D1\u05E8\u05D9\u05EA",direction:"rtl"},ru:{code:"ru",name:"Russian",nativeName:"\u0420\u0443\u0441\u0441\u043A\u0438\u0439",direction:"ltr"},nl:{code:"nl",name:"Dutch",nativeName:"Nederlands",direction:"ltr"},pl:{code:"pl",name:"Polish",nativeName:"Polski",direction:"ltr"},tr:{code:"tr",name:"Turkish",nativeName:"T\xFCrk\xE7e",direction:"ltr"},vi:{code:"vi",name:"Vietnamese",nativeName:"Ti\u1EBFng Vi\u1EC7t",direction:"ltr"},th:{code:"th",name:"Thai",nativeName:"\u0E44\u0E17\u0E22",direction:"ltr"},hi:{code:"hi",name:"Hindi",nativeName:"\u0939\u093F\u0928\u094D\u0926\u0940",direction:"ltr"}};export{bq as writeI18nOutputs,sq as useI18n,kq as trans,pq as tm,yq as te,fq as tc,Lq as t,GB as stripLocalePrefix,Zq as sortStrings,gq as setLocale,l as setFallbackLocale,r as selectPluralForm,Jq as selectPlural,lq as resolveRequestLocale,c as parsePluralForms,BB as localizePath,Pq as loadTranslationsFromDisk,uq as loadTranslations,g as loadLocale,wq as loadFromTsI18n,y as loadFromDirectory,N as loadFile,_q as isRTL,cq as hasTranslation,s as hasPluralRule,v as getTextDirection,n as getSupportedPluralLocales,Qq as getRegionName,D as getPluralRule,M as getPluralCategory,dq as getLocale,Kq as getLanguageName,I as getDisplayName,Uq as getCurrencyName,iq as getAvailableLocales,Mq as generateI18nTypesFromModule,xq as generateI18nTypes,Dq as generateI18nSampleConfig,Bq as formatUnit,QB as formatTime,$B as formatRelativeTime,EB as formatPlural,_B as formatPercent,XB as formatNumber,AB as formatList,UB as formatDateTime,KB as formatDate,ZB as formatCurrency,Gq as formatBytes,oq as ensureLocalesLoaded,hq as createSimpleTranslator,qB as createLocaleSwitchResponse,Iq as createLoader,nq as createI18n,a as configure,Xq as compareStrings,aq as applyRequestLocale,mq as addTranslations,t as addPluralRule,Nq as LOCALE_INFO,rq as I18n};
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";
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,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,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,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
+ }
package/dist/types.js ADDED
@@ -0,0 +1,25 @@
1
+ export const LOCALE_INFO = {
2
+ en: { code: "en", name: "English", nativeName: "English", direction: "ltr" },
3
+ "en-US": { code: "en-US", name: "English (US)", nativeName: "English (US)", direction: "ltr", region: "US" },
4
+ "en-GB": { code: "en-GB", name: "English (UK)", nativeName: "English (UK)", direction: "ltr", region: "GB" },
5
+ es: { code: "es", name: "Spanish", nativeName: "Espa\xF1ol", direction: "ltr" },
6
+ fr: { code: "fr", name: "French", nativeName: "Fran\xE7ais", direction: "ltr" },
7
+ de: { code: "de", name: "German", nativeName: "Deutsch", direction: "ltr" },
8
+ it: { code: "it", name: "Italian", nativeName: "Italiano", direction: "ltr" },
9
+ pt: { code: "pt", name: "Portuguese", nativeName: "Portugu\xEAs", direction: "ltr" },
10
+ "pt-BR": { code: "pt-BR", name: "Portuguese (Brazil)", nativeName: "Portugu\xEAs (Brasil)", direction: "ltr", region: "BR" },
11
+ zh: { code: "zh", name: "Chinese", nativeName: "\u4E2D\u6587", direction: "ltr" },
12
+ "zh-CN": { code: "zh-CN", name: "Chinese (Simplified)", nativeName: "\u7B80\u4F53\u4E2D\u6587", direction: "ltr", region: "CN" },
13
+ "zh-TW": { code: "zh-TW", name: "Chinese (Traditional)", nativeName: "\u7E41\u9AD4\u4E2D\u6587", direction: "ltr", region: "TW" },
14
+ ja: { code: "ja", name: "Japanese", nativeName: "\u65E5\u672C\u8A9E", direction: "ltr" },
15
+ ko: { code: "ko", name: "Korean", nativeName: "\uD55C\uAD6D\uC5B4", direction: "ltr" },
16
+ ar: { code: "ar", name: "Arabic", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", direction: "rtl" },
17
+ he: { code: "he", name: "Hebrew", nativeName: "\u05E2\u05D1\u05E8\u05D9\u05EA", direction: "rtl" },
18
+ ru: { code: "ru", name: "Russian", nativeName: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439", direction: "ltr" },
19
+ nl: { code: "nl", name: "Dutch", nativeName: "Nederlands", direction: "ltr" },
20
+ pl: { code: "pl", name: "Polish", nativeName: "Polski", direction: "ltr" },
21
+ tr: { code: "tr", name: "Turkish", nativeName: "T\xFCrk\xE7e", direction: "ltr" },
22
+ vi: { code: "vi", name: "Vietnamese", nativeName: "Ti\u1EBFng Vi\u1EC7t", direction: "ltr" },
23
+ th: { code: "th", name: "Thai", nativeName: "\u0E44\u0E17\u0E22", direction: "ltr" },
24
+ hi: { code: "hi", name: "Hindi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940", direction: "ltr" }
25
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacksjs/i18n",
3
- "version": "0.70.87",
3
+ "version": "0.70.90",
4
4
  "description": "Internationalization system for Stacks.js applications",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -40,7 +40,7 @@
40
40
  "@stacksjs/ts-i18n": "^0.1.9"
41
41
  },
42
42
  "devDependencies": {
43
- "@stacksjs/development": "0.70.87"
43
+ "@stacksjs/development": "0.70.90"
44
44
  },
45
45
  "publishConfig": {
46
46
  "exports": {