@nitronjs/framework 0.2.27 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +1 -32
- package/lib/Build/Manager.js +354 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +171 -23
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +103 -1
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +2 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +3 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
package/lib/Session/Manager.js
CHANGED
|
@@ -142,6 +142,10 @@ class SessionManager {
|
|
|
142
142
|
get cookieConfig() {
|
|
143
143
|
const config = { ...this.#config.cookie, signed: true };
|
|
144
144
|
|
|
145
|
+
if (!config.sameSite) {
|
|
146
|
+
config.sameSite = "Lax";
|
|
147
|
+
}
|
|
148
|
+
|
|
145
149
|
if (config.maxAge) {
|
|
146
150
|
config.maxAge = Math.floor(config.maxAge / 1000);
|
|
147
151
|
}
|
|
@@ -228,8 +232,9 @@ class SessionManager {
|
|
|
228
232
|
|
|
229
233
|
server.addHook("preHandler", async (request, response) => {
|
|
230
234
|
const lastSegment = request.url.split("/").pop().split("?")[0];
|
|
235
|
+
const dotIndex = lastSegment.lastIndexOf(".");
|
|
231
236
|
|
|
232
|
-
if (lastSegment.
|
|
237
|
+
if (dotIndex > 0 && STATIC_EXTENSIONS.has(lastSegment.slice(dotIndex + 1).toLowerCase())) {
|
|
233
238
|
return;
|
|
234
239
|
}
|
|
235
240
|
|
|
@@ -258,4 +263,101 @@ class SessionManager {
|
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
265
|
|
|
266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
// Private Constants
|
|
268
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
const STATIC_EXTENSIONS = new Set([
|
|
271
|
+
// ── Styles ──────────────────────────────────────────────────────────────
|
|
272
|
+
"css", "less", "scss", "sass", "styl", "stylus", "pcss", "postcss", "sss",
|
|
273
|
+
|
|
274
|
+
// ── Scripts & Source Maps ───────────────────────────────────────────────
|
|
275
|
+
"js", "mjs", "cjs", "ts", "mts", "cts", "jsx", "tsx",
|
|
276
|
+
"map", "coffee", "litcoffee", "elm", "dart", "vue", "svelte",
|
|
277
|
+
|
|
278
|
+
// ── Images (Raster) ────────────────────────────────────────────────────
|
|
279
|
+
"png", "jpg", "jpeg", "jpe", "jfif", "gif", "bmp", "dib",
|
|
280
|
+
"tiff", "tif", "webp", "avif", "heic", "heif", "ico", "cur",
|
|
281
|
+
"psd", "psb", "ai", "eps", "raw", "cr2", "nef", "orf", "sr2", "arw",
|
|
282
|
+
"dng", "raf", "rw2", "pef", "srw", "x3f", "jxl", "qoi", "hdr", "exr",
|
|
283
|
+
|
|
284
|
+
// ── Images (Vector) ────────────────────────────────────────────────────
|
|
285
|
+
"svg", "svgz", "cgm", "emf", "wmf",
|
|
286
|
+
|
|
287
|
+
// ── Fonts ───────────────────────────────────────────────────────────────
|
|
288
|
+
"woff", "woff2", "ttf", "otf", "eot", "pfb", "pfm",
|
|
289
|
+
|
|
290
|
+
// ── Video ───────────────────────────────────────────────────────────────
|
|
291
|
+
"mp4", "m4v", "webm", "ogv", "ogg", "avi", "mov", "mkv", "flv", "wmv",
|
|
292
|
+
"mpg", "mpeg", "mpe", "3gp", "3g2", "ts", "mts", "m2ts", "vob", "divx",
|
|
293
|
+
"asf", "rm", "rmvb", "f4v", "swf", "hevc", "av1",
|
|
294
|
+
|
|
295
|
+
// ── Audio ───────────────────────────────────────────────────────────────
|
|
296
|
+
"mp3", "wav", "flac", "aac", "m4a", "wma", "opus", "oga",
|
|
297
|
+
"aiff", "aif", "aifc", "mid", "midi", "amr", "ape", "dsf", "dff",
|
|
298
|
+
"ac3", "dts", "pcm", "au", "snd", "ra", "tak", "tta", "wv", "caf",
|
|
299
|
+
|
|
300
|
+
// ── Documents & Office ──────────────────────────────────────────────────
|
|
301
|
+
"pdf", "doc", "docx", "docm", "dot", "dotx", "dotm",
|
|
302
|
+
"xls", "xlsx", "xlsm", "xlsb", "xlt", "xltx", "xltm",
|
|
303
|
+
"ppt", "pptx", "pptm", "pot", "potx", "potm", "pps", "ppsx",
|
|
304
|
+
"odt", "ods", "odp", "odg", "odf", "odb", "odc",
|
|
305
|
+
"rtf", "tex", "latex", "bib", "epub", "mobi", "azw", "azw3",
|
|
306
|
+
"djvu", "djv", "xps", "oxps", "pages", "numbers", "key", "keynote",
|
|
307
|
+
|
|
308
|
+
// ── Data & Config ───────────────────────────────────────────────────────
|
|
309
|
+
"xml", "json", "jsonl", "ndjson", "json5", "jsonc",
|
|
310
|
+
"yaml", "yml", "toml", "ini", "cfg", "conf", "properties",
|
|
311
|
+
"csv", "tsv", "dsv", "parquet", "avro", "arrow", "feather",
|
|
312
|
+
"sql", "sqlite", "db", "mdb", "accdb",
|
|
313
|
+
|
|
314
|
+
// ── Text & Markup ───────────────────────────────────────────────────────
|
|
315
|
+
"txt", "text", "md", "markdown", "rst", "adoc", "asciidoc",
|
|
316
|
+
"log", "diff", "patch", "nfo", "diz",
|
|
317
|
+
"htm", "html", "xhtml", "mhtml", "mht", "shtml",
|
|
318
|
+
"ics", "ical", "vcf", "vcard", "ldif",
|
|
319
|
+
|
|
320
|
+
// ── Archives & Compression ──────────────────────────────────────────────
|
|
321
|
+
"zip", "rar", "7z", "tar", "gz", "gzip", "bz2", "bzip2",
|
|
322
|
+
"xz", "lzma", "lz", "lz4", "zst", "zstd", "br", "brotli",
|
|
323
|
+
"cab", "arj", "ace", "lzh", "lha", "cpio", "rpm", "deb",
|
|
324
|
+
"pkg", "dmg", "iso", "img", "vhd", "vhdx", "vmdk", "ova", "ovf",
|
|
325
|
+
"apk", "ipa", "xpi", "crx", "nupkg", "snap", "flatpak", "appimage",
|
|
326
|
+
"jar", "war", "ear",
|
|
327
|
+
|
|
328
|
+
// ── Executables & Libraries ─────────────────────────────────────────────
|
|
329
|
+
"exe", "msi", "dll", "sys", "so", "dylib", "lib", "a", "o", "obj",
|
|
330
|
+
"bin", "dat", "com", "bat", "cmd", "sh", "bash", "ps1", "psm1",
|
|
331
|
+
|
|
332
|
+
// ── 3D & CAD & Design ───────────────────────────────────────────────────
|
|
333
|
+
"glb", "gltf", "obj", "stl", "fbx", "dae", "3ds", "blend", "max",
|
|
334
|
+
"c4d", "ma", "mb", "lwo", "lws", "ply", "wrl", "vrml", "x3d",
|
|
335
|
+
"dwg", "dxf", "dgn", "step", "stp", "iges", "igs", "sat",
|
|
336
|
+
"skp", "3dm", "usd", "usda", "usdc", "usdz",
|
|
337
|
+
|
|
338
|
+
// ── GIS & Mapping ───────────────────────────────────────────────────────
|
|
339
|
+
"shp", "shx", "dbf", "prj", "cpg", "geojson", "topojson",
|
|
340
|
+
"kml", "kmz", "gpx", "osm", "pbf", "mbtiles",
|
|
341
|
+
|
|
342
|
+
// ── Scientific & Research ───────────────────────────────────────────────
|
|
343
|
+
"hdf5", "h5", "hdf", "nc", "netcdf", "fits", "mat", "sav",
|
|
344
|
+
|
|
345
|
+
// ── WebAssembly & Runtime ───────────────────────────────────────────────
|
|
346
|
+
"wasm", "wat", "swc",
|
|
347
|
+
|
|
348
|
+
// ── Certificates & Keys ─────────────────────────────────────────────────
|
|
349
|
+
"pem", "crt", "cer", "der", "p12", "pfx", "p7b", "p7c",
|
|
350
|
+
"csr", "key", "pub", "asc", "gpg", "sig",
|
|
351
|
+
|
|
352
|
+
// ── Web Manifests & PWA ─────────────────────────────────────────────────
|
|
353
|
+
"webmanifest", "manifest", "browserconfig",
|
|
354
|
+
|
|
355
|
+
// ── Misc ────────────────────────────────────────────────────────────────
|
|
356
|
+
"ani", "icns", "lnk", "url", "webloc", "desktop",
|
|
357
|
+
"torrent", "metalink", "magnet",
|
|
358
|
+
"sub", "srt", "ass", "ssa", "vtt", "sbv", "dfxp", "ttml",
|
|
359
|
+
"bak", "tmp", "temp", "swp", "swo", "lock",
|
|
360
|
+
"pid", "socket", "fifo"
|
|
361
|
+
]);
|
|
362
|
+
|
|
261
363
|
export default SessionManager;
|
package/lib/Session/Session.js
CHANGED
package/lib/Support/Str.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* String utility class with helper methods.
|
|
3
5
|
* Provides Laravel-style string manipulation functions.
|
|
4
|
-
*
|
|
6
|
+
*
|
|
5
7
|
* @example
|
|
6
8
|
* Str.slug("Hello World"); // "hello-world"
|
|
7
9
|
* Str.camel("user_name"); // "userName"
|
|
8
10
|
*/
|
|
9
11
|
class Str {
|
|
10
12
|
/**
|
|
11
|
-
* Generates a random alphanumeric string.
|
|
13
|
+
* Generates a cryptographically secure random alphanumeric string.
|
|
12
14
|
* @param {number} length - Desired string length
|
|
13
15
|
* @returns {string} Random string
|
|
14
16
|
*/
|
|
15
17
|
static random(length) {
|
|
16
18
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
19
|
+
const bytes = randomBytes(length);
|
|
17
20
|
let result = "";
|
|
18
|
-
const charactersLength = characters.length;
|
|
19
21
|
|
|
20
22
|
for (let i = 0; i < length; i++) {
|
|
21
|
-
result += characters.charAt(
|
|
23
|
+
result += characters.charAt(bytes[i] % characters.length);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
return result;
|
package/lib/Translation/Lang.js
CHANGED
|
@@ -1,54 +1,398 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
import Paths from "../Core/Paths.js";
|
|
5
|
+
import Config from "../Core/Config.js";
|
|
6
|
+
import Environment from "../Core/Environment.js";
|
|
7
|
+
import { pluralize, replaceParams } from "./pluralize.js";
|
|
8
|
+
|
|
9
|
+
const LangStore = new AsyncLocalStorage();
|
|
10
|
+
|
|
11
|
+
// Safety cap — prevents DoS via arbitrary locale strings in setLocale
|
|
12
|
+
const MAX_LOCALES = 50;
|
|
4
13
|
|
|
5
|
-
/**
|
|
6
|
-
* Translation manager for multi-language support.
|
|
7
|
-
* Loads translations from JSON files and supports parameter replacement.
|
|
8
|
-
*/
|
|
9
14
|
class Lang {
|
|
10
|
-
static #
|
|
15
|
+
static #translations = {};
|
|
16
|
+
static #flatCache = {};
|
|
17
|
+
static #mtimes = {};
|
|
18
|
+
static #fallbackLocale = null;
|
|
19
|
+
|
|
20
|
+
static setup(server) {
|
|
21
|
+
const rawLocale = Config.get("app.locale", "en");
|
|
22
|
+
const rawFallback = Config.get("app.fallback_locale") || null;
|
|
23
|
+
const defaultLocale = isValidLocale(rawLocale) ? rawLocale : "en";
|
|
24
|
+
|
|
25
|
+
Lang.#fallbackLocale = (rawFallback && isValidLocale(rawFallback)) ? rawFallback : null;
|
|
26
|
+
|
|
27
|
+
if (Environment.isDev) {
|
|
28
|
+
if (rawLocale !== defaultLocale) console.warn(`Lang: Invalid app.locale "${rawLocale}", falling back to "en"`);
|
|
29
|
+
if (rawFallback && !Lang.#fallbackLocale) console.warn(`Lang: Invalid app.fallback_locale "${rawFallback}", ignoring`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
server.addHook("onRequest", async (req) => {
|
|
33
|
+
LangStore.enterWith({ locale: defaultLocale, req, langChecked: new Set() });
|
|
34
|
+
req.locale = defaultLocale;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
globalThis.__nitron_lang_get = (key, params) => Lang.get(key, params);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static get(key, params = {}) {
|
|
41
|
+
const value = this.#resolveWithFallback(key);
|
|
42
|
+
|
|
43
|
+
if (!isResolvable(value)) return key;
|
|
44
|
+
|
|
45
|
+
let result = String(value);
|
|
46
|
+
|
|
47
|
+
// Pluralize only when count is provided and value has pipe-separated segments
|
|
48
|
+
if (params && typeof params === "object" && params.count !== undefined && result.includes("|")) {
|
|
49
|
+
result = pluralize(result, params.count);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return replaceParams(result, params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static has(key) {
|
|
56
|
+
return isResolvable(this.#resolveWithFallback(key));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static locale() {
|
|
60
|
+
const store = LangStore.getStore();
|
|
61
|
+
|
|
62
|
+
if (store) return store.locale;
|
|
63
|
+
|
|
64
|
+
return Config.get("app.locale", "en");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static setLocale(locale) {
|
|
68
|
+
if (!isValidLocale(locale)) {
|
|
69
|
+
if (Environment.isDev) {
|
|
70
|
+
console.warn(`Lang.setLocale: Invalid locale format "${locale}"`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const store = LangStore.getStore();
|
|
76
|
+
|
|
77
|
+
if (!store) return;
|
|
78
|
+
|
|
79
|
+
store.locale = locale;
|
|
80
|
+
|
|
81
|
+
if (store.req) {
|
|
82
|
+
store.req.locale = locale;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static getFilteredTranslations(locale, keys) {
|
|
87
|
+
this.#ensureLoaded(locale);
|
|
88
|
+
|
|
89
|
+
const flat = this.#getFlat(locale);
|
|
90
|
+
|
|
91
|
+
const fallback = Lang.#fallbackLocale;
|
|
92
|
+
let fallbackFlat = null;
|
|
93
|
+
|
|
94
|
+
if (fallback && fallback !== locale) {
|
|
95
|
+
this.#ensureLoaded(fallback);
|
|
96
|
+
fallbackFlat = this.#getFlat(fallback);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (keys === null || keys === undefined) {
|
|
100
|
+
if (fallbackFlat) {
|
|
101
|
+
return { ...fallbackFlat, ...flat };
|
|
102
|
+
}
|
|
103
|
+
return flat;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (keys.length === 0) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result = {};
|
|
111
|
+
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
if (flat[key] !== undefined) {
|
|
114
|
+
result[key] = flat[key];
|
|
115
|
+
}
|
|
116
|
+
else if (fallbackFlat && fallbackFlat[key] !== undefined) {
|
|
117
|
+
result[key] = fallbackFlat[key];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Internal: resolve + fallback ---
|
|
125
|
+
|
|
126
|
+
static #resolveWithFallback(key) {
|
|
127
|
+
const locale = this.locale();
|
|
128
|
+
|
|
129
|
+
this.#ensureLoaded(locale);
|
|
130
|
+
|
|
131
|
+
const value = this.#resolveKey(locale, key);
|
|
132
|
+
|
|
133
|
+
if (isResolvable(value)) return value;
|
|
134
|
+
|
|
135
|
+
const fallback = Lang.#fallbackLocale;
|
|
136
|
+
|
|
137
|
+
if (fallback && fallback !== locale) {
|
|
138
|
+
this.#ensureLoaded(fallback);
|
|
139
|
+
return this.#resolveKey(fallback, key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static #ensureLoaded(locale) {
|
|
146
|
+
if (Environment.isDev) {
|
|
147
|
+
const store = LangStore.getStore();
|
|
148
|
+
|
|
149
|
+
if (store && !store.langChecked.has(locale)) {
|
|
150
|
+
store.langChecked.add(locale);
|
|
151
|
+
this.#checkAndReload(locale);
|
|
152
|
+
}
|
|
153
|
+
else if (!this.#translations[locale]) {
|
|
154
|
+
this.#loadTranslations(locale);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!this.#translations[locale]) {
|
|
161
|
+
if (Object.keys(this.#translations).length >= MAX_LOCALES) return;
|
|
162
|
+
this.#loadTranslations(locale);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Internal: dev-mode hot reload ---
|
|
167
|
+
|
|
168
|
+
static #checkAndReload(locale) {
|
|
169
|
+
const langsDir = Paths.langs;
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(langsDir)) {
|
|
172
|
+
if (!this.#translations[locale]) {
|
|
173
|
+
this.#translations[locale] = {};
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const singleFile = path.join(langsDir, locale + ".json");
|
|
179
|
+
const folderDir = path.join(langsDir, locale);
|
|
180
|
+
|
|
181
|
+
let changed = false;
|
|
182
|
+
const currentPaths = new Set();
|
|
183
|
+
|
|
184
|
+
// Check single file mtime
|
|
185
|
+
if (fs.existsSync(singleFile)) {
|
|
186
|
+
currentPaths.add(singleFile);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const mtime = fs.statSync(singleFile).mtimeMs;
|
|
190
|
+
if (this.#mtimes[singleFile] !== mtime) changed = true;
|
|
191
|
+
}
|
|
192
|
+
catch {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check folder files mtime (always scan to keep currentPaths complete for deletion detection)
|
|
196
|
+
if (fs.existsSync(folderDir)) {
|
|
197
|
+
try {
|
|
198
|
+
if (fs.statSync(folderDir).isDirectory()) {
|
|
199
|
+
for (const file of fs.readdirSync(folderDir)) {
|
|
200
|
+
if (!file.endsWith(".json")) continue;
|
|
11
201
|
|
|
12
|
-
|
|
13
|
-
* Gets a translated string for the current request language.
|
|
14
|
-
* @param {import("fastify").FastifyRequest} req - Fastify request with language property
|
|
15
|
-
* @param {string} key - Translation key to look up
|
|
16
|
-
* @param {Object|null} params - Optional parameters to replace in translation (e.g., { name: "John" } for ":name")
|
|
17
|
-
* @returns {string} Translated string or the key if not found
|
|
18
|
-
*/
|
|
19
|
-
static get(req, key, params = null) {
|
|
20
|
-
const currentLang = req.language;
|
|
21
|
-
const langPath = path.join(Paths.langs, currentLang + ".json");
|
|
202
|
+
const fullPath = path.join(folderDir, file);
|
|
22
203
|
|
|
23
|
-
|
|
24
|
-
|
|
204
|
+
currentPaths.add(fullPath);
|
|
205
|
+
|
|
206
|
+
if (!changed) {
|
|
207
|
+
try {
|
|
208
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
209
|
+
if (this.#mtimes[fullPath] !== mtime) changed = true;
|
|
210
|
+
}
|
|
211
|
+
catch {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
if (Environment.isDev) console.warn(`Lang: Failed to scan ${folderDir}: ${err.message}`);
|
|
218
|
+
}
|
|
25
219
|
}
|
|
26
220
|
|
|
27
|
-
|
|
221
|
+
// Detect deleted files — tracked in #mtimes but no longer on disk
|
|
222
|
+
if (!changed) {
|
|
223
|
+
for (const tracked of Object.keys(this.#mtimes)) {
|
|
224
|
+
if (isLocalePath(tracked, singleFile, folderDir) && !currentPaths.has(tracked)) {
|
|
225
|
+
changed = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (changed || !this.#translations[locale]) {
|
|
232
|
+
this.#loadTranslations(locale);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Internal: file loading ---
|
|
237
|
+
|
|
238
|
+
static #loadTranslations(locale) {
|
|
239
|
+
const langsDir = Paths.langs;
|
|
240
|
+
const result = {};
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(langsDir)) {
|
|
243
|
+
this.#translations[locale] = result;
|
|
244
|
+
delete this.#flatCache[locale];
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const singleFile = path.join(langsDir, locale + ".json");
|
|
249
|
+
const folderDir = path.join(langsDir, locale);
|
|
250
|
+
|
|
251
|
+
// Purge stale mtime entries for this locale before repopulating
|
|
252
|
+
for (const tracked of Object.keys(this.#mtimes)) {
|
|
253
|
+
if (isLocalePath(tracked, singleFile, folderDir)) {
|
|
254
|
+
delete this.#mtimes[tracked];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 1. Single file: resources/langs/tr.json
|
|
259
|
+
if (fs.existsSync(singleFile)) {
|
|
260
|
+
try {
|
|
261
|
+
const mtime = fs.statSync(singleFile).mtimeMs;
|
|
262
|
+
const content = fs.readFileSync(singleFile, "utf-8");
|
|
263
|
+
const data = JSON.parse(content);
|
|
264
|
+
|
|
265
|
+
this.#mtimes[singleFile] = mtime;
|
|
266
|
+
this.#deepMerge(result, data);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.warn(`Lang: Failed to parse ${singleFile}: ${err.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2. Folder structure: resources/langs/tr/messages.json (wins over single file keys)
|
|
274
|
+
if (fs.existsSync(folderDir)) {
|
|
275
|
+
try {
|
|
276
|
+
if (fs.statSync(folderDir).isDirectory()) {
|
|
277
|
+
for (const file of fs.readdirSync(folderDir)) {
|
|
278
|
+
if (!file.endsWith(".json")) continue;
|
|
279
|
+
|
|
280
|
+
const fullPath = path.join(folderDir, file);
|
|
281
|
+
const namespace = file.replace(/\.json$/, "");
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
285
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
286
|
+
const data = JSON.parse(content);
|
|
287
|
+
|
|
288
|
+
this.#mtimes[fullPath] = mtime;
|
|
289
|
+
|
|
290
|
+
if (Environment.isDev && result[namespace]) {
|
|
291
|
+
console.warn(`Lang: Key "${namespace}" in ${fullPath} overrides single file`);
|
|
292
|
+
}
|
|
28
293
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
294
|
+
result[namespace] = data;
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.warn(`Lang: Failed to parse ${fullPath}: ${err.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
if (Environment.isDev) console.warn(`Lang: Failed to scan ${folderDir}: ${err.message}`);
|
|
304
|
+
}
|
|
33
305
|
}
|
|
34
306
|
|
|
35
|
-
|
|
307
|
+
this.#translations[locale] = result;
|
|
308
|
+
delete this.#flatCache[locale];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Internal: cache + key resolution ---
|
|
312
|
+
|
|
313
|
+
static #getFlat(locale) {
|
|
314
|
+
if (this.#flatCache[locale]) return this.#flatCache[locale];
|
|
315
|
+
|
|
316
|
+
const tree = this.#translations[locale];
|
|
317
|
+
const flat = tree ? this.#flattenTree(tree, "") : {};
|
|
318
|
+
|
|
319
|
+
this.#flatCache[locale] = flat;
|
|
320
|
+
return flat;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
static #resolveKey(locale, key) {
|
|
324
|
+
const tree = this.#translations[locale];
|
|
36
325
|
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
326
|
+
if (!tree) return undefined;
|
|
327
|
+
|
|
328
|
+
const parts = key.split(".");
|
|
329
|
+
let current = tree;
|
|
330
|
+
|
|
331
|
+
for (const part of parts) {
|
|
332
|
+
if (current === undefined || current === null || typeof current !== "object") {
|
|
333
|
+
return undefined;
|
|
40
334
|
}
|
|
335
|
+
|
|
336
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
current = current[part];
|
|
41
341
|
}
|
|
42
342
|
|
|
43
|
-
return
|
|
343
|
+
return current;
|
|
44
344
|
}
|
|
45
345
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
346
|
+
// --- Internal: tree utilities (input is always from JSON.parse — no circular refs) ---
|
|
347
|
+
|
|
348
|
+
static #deepMerge(target, source) {
|
|
349
|
+
for (const key of Object.keys(source)) {
|
|
350
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
351
|
+
|
|
352
|
+
if (
|
|
353
|
+
typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key]) &&
|
|
354
|
+
typeof target[key] === "object" && target[key] !== null && !Array.isArray(target[key])
|
|
355
|
+
) {
|
|
356
|
+
this.#deepMerge(target[key], source[key]);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
target[key] = source[key];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
static #flattenTree(tree, prefix) {
|
|
365
|
+
const result = {};
|
|
366
|
+
|
|
367
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
368
|
+
const fullKey = prefix ? prefix + "." + key : key;
|
|
369
|
+
|
|
370
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
371
|
+
Object.assign(result, this.#flattenTree(value, fullKey));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
result[fullKey] = value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return result;
|
|
51
379
|
}
|
|
52
380
|
}
|
|
53
381
|
|
|
382
|
+
// --- Module-level helpers ---
|
|
383
|
+
|
|
384
|
+
function isResolvable(value) {
|
|
385
|
+
return value !== undefined && value !== null && typeof value !== "object";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isLocalePath(tracked, singleFile, folderDir) {
|
|
389
|
+
return tracked === singleFile || tracked.startsWith(folderDir + path.sep);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const LOCALE_RE = /^[a-z]{2,3}(-[a-zA-Z0-9]{1,8})*$/i;
|
|
393
|
+
|
|
394
|
+
function isValidLocale(locale) {
|
|
395
|
+
return typeof locale === "string" && LOCALE_RE.test(locale);
|
|
396
|
+
}
|
|
397
|
+
|
|
54
398
|
export default Lang;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared translation utilities — single source of truth.
|
|
3
|
+
* Used by Lang.js (server) and jsxRuntime.js (SSR + client via .toString() embedding).
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: These functions are embedded client-side via .toString().
|
|
6
|
+
* Do NOT use arrow functions, template literals, const/let, or modern syntax.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function pluralize(value, count) {
|
|
10
|
+
count = typeof count === "number" ? count : Number(count) || 0;
|
|
11
|
+
if (count < 0) count = 0;
|
|
12
|
+
count = Math.floor(count);
|
|
13
|
+
|
|
14
|
+
var segments = value.split("|");
|
|
15
|
+
|
|
16
|
+
// Try explicit syntax first: {0} zero items | {1} one item | [2,*] many items
|
|
17
|
+
for (var i = 0; i < segments.length; i++) {
|
|
18
|
+
var trimmed = segments[i].trim();
|
|
19
|
+
|
|
20
|
+
// Exact match: {3} exactly three
|
|
21
|
+
var exactMatch = trimmed.match(/^\{(\d+)\}\s*(.*)/);
|
|
22
|
+
|
|
23
|
+
if (exactMatch) {
|
|
24
|
+
if (parseInt(exactMatch[1]) === count) return exactMatch[2];
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Range match: [2,5] between two and five | [6,*] six or more
|
|
29
|
+
var rangeMatch = trimmed.match(/^\[(\d+),(\d+|\*)\]\s*(.*)/);
|
|
30
|
+
|
|
31
|
+
if (rangeMatch) {
|
|
32
|
+
var min = parseInt(rangeMatch[1]);
|
|
33
|
+
var max = rangeMatch[2] === "*" ? Infinity : parseInt(rangeMatch[2]);
|
|
34
|
+
|
|
35
|
+
if (count >= min && count <= max) return rangeMatch[3];
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Simple fallback: "one item|many items" (no explicit ranges)
|
|
41
|
+
if (segments.length >= 2 && !hasExplicitRanges(segments)) {
|
|
42
|
+
return count === 1 ? segments[0].trim() : segments[1].trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function replaceParams(value, params) {
|
|
49
|
+
if (!params || typeof params !== "object") return value;
|
|
50
|
+
|
|
51
|
+
var keys = Object.keys(params);
|
|
52
|
+
|
|
53
|
+
if (keys.length === 0) return value;
|
|
54
|
+
|
|
55
|
+
// Escape regex special chars in param names
|
|
56
|
+
var escaped = [];
|
|
57
|
+
|
|
58
|
+
for (var i = 0; i < keys.length; i++) {
|
|
59
|
+
escaped.push(keys[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort longest-first so :name_full matches before :name
|
|
63
|
+
escaped.sort(function(a, b) { return b.length - a.length; });
|
|
64
|
+
|
|
65
|
+
var pattern = new RegExp(":(" + escaped.join("|") + ")", "g");
|
|
66
|
+
|
|
67
|
+
return value.replace(pattern, function(match, k) {
|
|
68
|
+
if (!Object.prototype.hasOwnProperty.call(params, k)) return match;
|
|
69
|
+
var v = params[k];
|
|
70
|
+
return (v !== null && v !== undefined) ? String(v) : match;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Internal helpers ---
|
|
75
|
+
|
|
76
|
+
function hasExplicitRanges(segments) {
|
|
77
|
+
for (var i = 0; i < segments.length; i++) {
|
|
78
|
+
if (/^\s*(\{\d+\}|\[\d+,(\d+|\*)\])/.test(segments[i])) return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|