@nitronjs/framework 0.3.1 → 0.3.3

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.
@@ -94,7 +94,16 @@ class FileAnalyzer {
94
94
  const imported = new Set();
95
95
  const importedBy = new Map();
96
96
 
97
- for (const file of files) {
97
+ // Start with .tsx files, then follow imports into .ts files (barrel exports, utils, types)
98
+ // so the importedBy chain stays complete across .ts intermediaries.
99
+ const queue = [...files];
100
+ let i = 0;
101
+
102
+ while (i < queue.length) {
103
+ const file = queue[i++];
104
+
105
+ if (graph.has(file)) continue;
106
+
98
107
  const meta = this.analyzeFile(file);
99
108
  graph.set(file, meta);
100
109
 
@@ -108,6 +117,12 @@ class FileAnalyzer {
108
117
  importedBy.set(resolvedPath, []);
109
118
  }
110
119
  importedBy.get(resolvedPath).push(file);
120
+
121
+ // If the resolved file is .ts (not .tsx) and not yet in graph, queue it
122
+ // so its own imports get tracked in the dependency chain.
123
+ if (resolvedPath.endsWith(".ts") && !graph.has(resolvedPath)) {
124
+ queue.push(resolvedPath);
125
+ }
111
126
  }
112
127
  }
113
128
  }
@@ -215,7 +230,10 @@ class FileAnalyzer {
215
230
  JSXFragment: () => { meta.jsx = true; }
216
231
  });
217
232
 
218
- if (meta.needsClient && !meta.isClient) {
233
+ // Only enforce "use client" requirement on .tsx files.
234
+ // .ts files (hooks, utils) are pure logic modules — they don't define components
235
+ // and are safe to import from either server or client code.
236
+ if (meta.needsClient && !meta.isClient && filePath.endsWith(".tsx")) {
219
237
  throw this.#createError('Missing "use client"', {
220
238
  File: path.relative(Paths.project, filePath),
221
239
  Fix: 'Add "use client" at top'
@@ -234,7 +252,15 @@ class FileAnalyzer {
234
252
  }
235
253
 
236
254
  #extractNamedExports(path, meta) {
237
- const declaration = path.node.declaration;
255
+ const node = path.node;
256
+
257
+ // Re-exports: export { X } from './Y' — track the source as an import
258
+ // so the dependency graph stays complete through barrel files.
259
+ if (node.source && node.source.value && node.source.value.startsWith(".")) {
260
+ meta.imports.add(node.source.value);
261
+ }
262
+
263
+ const declaration = node.declaration;
238
264
 
239
265
  if (declaration?.type === "VariableDeclaration") {
240
266
  for (const decl of declaration.declarations) {
@@ -344,7 +370,9 @@ class FileAnalyzer {
344
370
  const resolvedPath = this.resolveImport(filePath, importPath);
345
371
  const resolvedMeta = graph.get(resolvedPath);
346
372
 
347
- if (resolvedMeta && !resolvedMeta.isClient) {
373
+ // .ts files (hooks, utils, types) are pure logic — no server/client boundary.
374
+ // Only .tsx files with JSX can be server components that violate the boundary.
375
+ if (resolvedMeta && !resolvedMeta.isClient && resolvedPath.endsWith(".tsx")) {
348
376
  throw this.#createError("Boundary Violation", {
349
377
  Client: relativePath(filePath),
350
378
  Server: relativePath(resolvedPath),
@@ -258,16 +258,30 @@ class Builder {
258
258
  ...(frameworkBundle.changedFiles || [])
259
259
  ]);
260
260
 
261
- // Include changed client component sources so their hydration bundles rebuild
261
+ // Include changed client component sources so their hydration bundles rebuild.
262
+ // Also include any client components that transitively import the changed file,
263
+ // because esbuild bundles all imports into each client chunk. If a dependency
264
+ // changes, every client chunk that bundles it must be rebuilt too.
262
265
  for (const bundle of [userBundle, frameworkBundle]) {
263
- if (!bundle?.changedSources) continue;
266
+ if (!bundle?.changedSources || !bundle.meta) continue;
264
267
 
265
268
  for (const file of bundle.changedSources) {
266
- const fileMeta = bundle.meta?.get(file);
269
+ const fileMeta = bundle.meta.get(file);
267
270
  if (fileMeta?.isClient) changedViews.add(file);
271
+
272
+ const dependents = this.#getTransitiveDependents(file, bundle.importedBy || new Map());
273
+
274
+ for (const dep of dependents) {
275
+ const depMeta = bundle.meta.get(dep);
276
+ if (depMeta?.isClient) changedViews.add(dep);
277
+ }
268
278
  }
269
279
  }
270
280
 
281
+ if (changedViews.size > 0) {
282
+ this.#cache.viewsChanged = true;
283
+ }
284
+
271
285
  for (const file of changedViews) {
272
286
  this.#changedFiles.add(file);
273
287
  }
@@ -366,13 +380,13 @@ class Builder {
366
380
 
367
381
  async #buildViewBundle(namespace, srcDir, outDir) {
368
382
  if (!fs.existsSync(srcDir)) {
369
- return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
383
+ return { entries: [], layouts: [], meta: new Map(), importedBy: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
370
384
  }
371
385
 
372
386
  const { entries, layouts, meta, importedBy } = this.#analyzer.discoverEntries(srcDir);
373
387
 
374
388
  if (!entries.length && !layouts.length) {
375
- return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
389
+ return { entries: [], layouts: [], meta: new Map(), importedBy: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
376
390
  }
377
391
 
378
392
  // Pre-scan: compute effective prop usage for server→client boundary filtering.
@@ -435,7 +449,7 @@ class Builder {
435
449
 
436
450
  this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
437
451
 
438
- return { entries, layouts, meta, srcDir, namespace, changedFiles, changedSources };
452
+ return { entries, layouts, meta, importedBy, srcDir, namespace, changedFiles, changedSources };
439
453
  }
440
454
 
441
455
  async #postProcessMeta(entries, srcDir, outDir) {
@@ -737,7 +751,8 @@ class Builder {
737
751
 
738
752
  this.#manifest[key] = {
739
753
  css: cssFiles,
740
- isLayout: true
754
+ isLayout: true,
755
+ translationKeys: this.#collectTranslationKeys(layout, meta)
741
756
  };
742
757
  }
743
758
  }
@@ -634,6 +634,7 @@ function trackVariableAccess(varName, funcPath, bodyStart) {
634
634
  let usedAsWhole = false;
635
635
  const chains = [];
636
636
  const arrayIterations = [];
637
+ const derivedArrayVars = [];
637
638
 
638
639
  funcPath.traverse({
639
640
  Identifier(idPath) {
@@ -690,6 +691,17 @@ function trackVariableAccess(varName, funcPath, bodyStart) {
690
691
 
691
692
  if (callbacks.length > 0) {
692
693
  arrayIterations.push({ prefixChain, callbacks });
694
+
695
+ const lastCall = getLastChainedCall(outermost.parentPath);
696
+
697
+ if (lastCall.parentPath.isVariableDeclarator() &&
698
+ lastCall.parentPath.node.id.type === "Identifier") {
699
+ derivedArrayVars.push({
700
+ derivedName: lastCall.parentPath.node.id.name,
701
+ prefixChain
702
+ });
703
+ }
704
+
693
705
  return;
694
706
  }
695
707
 
@@ -710,7 +722,6 @@ function trackVariableAccess(varName, funcPath, bodyStart) {
710
722
  return;
711
723
  }
712
724
 
713
- // Check direct for...of: for (const item of varName)
714
725
  if (idPath.parentPath.isForOfStatement() &&
715
726
  idPath.parentPath.node.right === idPath.node) {
716
727
  const loopVar = extractForOfVariable(idPath.parentPath.node);
@@ -721,19 +732,26 @@ function trackVariableAccess(varName, funcPath, bodyStart) {
721
732
  }
722
733
  }
723
734
 
724
- // Guard/truthy checks (isAdmin && ..., isAdmin ? ... : ...) need the
725
- // value itself but not its sub-properties. Mark as whole usage so
726
- // boolean props are kept in the filtered output.
727
735
  if (isGuardCheck(idPath)) {
728
736
  usedAsWhole = true;
729
737
  return;
730
738
  }
731
739
 
732
- // Used directly (as argument, JSX attribute value, assignment, etc.)
733
740
  usedAsWhole = true;
734
741
  }
735
742
  });
736
743
 
744
+ for (const derived of derivedArrayVars) {
745
+ const derivedIters = collectDerivedArrayUsage(derived.derivedName, funcPath, bodyStart);
746
+
747
+ for (const iter of derivedIters) {
748
+ arrayIterations.push({
749
+ prefixChain: derived.prefixChain,
750
+ ...iter
751
+ });
752
+ }
753
+ }
754
+
737
755
  if (usedAsWhole) return true;
738
756
  if (chains.length === 0 && arrayIterations.length === 0) return false;
739
757
 
@@ -919,12 +937,83 @@ function mergeArrayUsage(tree, prefixChain, elementUsage) {
919
937
  }
920
938
  }
921
939
 
922
- /**
923
- * Collects property names from a MemberExpression chain.
924
- * admin.mfa.status → ["mfa", "status"]
925
- * @param {import("@babel/traverse").NodePath} memberPath
926
- * @returns {string[]}
927
- */
940
+ function getLastChainedCall(callExprPath) {
941
+ let current = callExprPath;
942
+
943
+ while (true) {
944
+ const parent = current.parentPath;
945
+
946
+ if (!parent || (!parent.isMemberExpression() && !parent.isOptionalMemberExpression())) break;
947
+ if (parent.node.object !== current.node || parent.node.computed) break;
948
+
949
+ const method = parent.node.property?.name;
950
+ const gp = parent.parentPath;
951
+
952
+ if ((gp.isCallExpression() || gp.isOptionalCallExpression()) &&
953
+ gp.node.callee === parent.node &&
954
+ ARRAY_METHODS.has(method)) {
955
+ current = gp;
956
+ }
957
+ else {
958
+ break;
959
+ }
960
+ }
961
+
962
+ return current;
963
+ }
964
+
965
+ function collectDerivedArrayUsage(derivedName, funcPath, bodyStart) {
966
+ const iterations = [];
967
+
968
+ funcPath.traverse({
969
+ Identifier(idPath) {
970
+ if (idPath.node.start < bodyStart) return;
971
+ if (idPath.node.name !== derivedName) return;
972
+ if (isInTypeAnnotation(idPath)) return;
973
+
974
+ if (idPath.parentPath.isObjectProperty() &&
975
+ idPath.parentPath.node.key === idPath.node &&
976
+ !idPath.parentPath.node.computed) return;
977
+
978
+ if (idPath.parentPath.isVariableDeclarator() &&
979
+ idPath.parentPath.node.id === idPath.node) return;
980
+
981
+ const isMember = idPath.parentPath.isMemberExpression() || idPath.parentPath.isOptionalMemberExpression();
982
+
983
+ if (isMember &&
984
+ idPath.parentPath.node.object === idPath.node &&
985
+ !idPath.parentPath.node.computed) {
986
+
987
+ const propName = idPath.parentPath.node.property?.name;
988
+ const grandParent = idPath.parentPath.parentPath;
989
+ const isCall = (grandParent.isCallExpression() || grandParent.isOptionalCallExpression()) &&
990
+ grandParent.node.callee === idPath.parentPath.node;
991
+
992
+ if (isCall && ARRAY_METHODS.has(propName)) {
993
+ const callbacks = collectArrayCallbacks(grandParent, propName);
994
+
995
+ if (callbacks.length > 0) {
996
+ iterations.push({ callbacks });
997
+ }
998
+ }
999
+
1000
+ return;
1001
+ }
1002
+
1003
+ if (idPath.parentPath.isForOfStatement() &&
1004
+ idPath.parentPath.node.right === idPath.node) {
1005
+ const loopVar = extractForOfVariable(idPath.parentPath.node);
1006
+
1007
+ if (loopVar) {
1008
+ iterations.push({ loopVar, scopePath: idPath.parentPath });
1009
+ }
1010
+ }
1011
+ }
1012
+ });
1013
+
1014
+ return iterations;
1015
+ }
1016
+
928
1017
  function collectMemberChain(memberPath) {
929
1018
  const chain = [];
930
1019
  let current = memberPath;
@@ -976,6 +1065,7 @@ function analyzePropsIdentifier(propsName, funcPath) {
976
1065
  let usedAsWhole = false;
977
1066
  const chains = [];
978
1067
  const arrayIterations = [];
1068
+ const derivedArrayVars = [];
979
1069
  const bodyStart = funcPath.node.body?.start ?? 0;
980
1070
 
981
1071
  funcPath.traverse({
@@ -1013,6 +1103,17 @@ function analyzePropsIdentifier(propsName, funcPath) {
1013
1103
 
1014
1104
  if (callbacks.length > 0) {
1015
1105
  arrayIterations.push({ prefixChain, callbacks });
1106
+
1107
+ const lastCall = getLastChainedCall(outermost.parentPath);
1108
+
1109
+ if (lastCall.parentPath.isVariableDeclarator() &&
1110
+ lastCall.parentPath.node.id.type === "Identifier") {
1111
+ derivedArrayVars.push({
1112
+ derivedName: lastCall.parentPath.node.id.name,
1113
+ prefixChain
1114
+ });
1115
+ }
1116
+
1016
1117
  return;
1017
1118
  }
1018
1119
 
@@ -1033,11 +1134,21 @@ function analyzePropsIdentifier(propsName, funcPath) {
1033
1134
  return;
1034
1135
  }
1035
1136
 
1036
- // props used as a whole (destructured in body, passed as arg, etc.)
1037
1137
  usedAsWhole = true;
1038
1138
  }
1039
1139
  });
1040
1140
 
1141
+ for (const derived of derivedArrayVars) {
1142
+ const derivedIters = collectDerivedArrayUsage(derived.derivedName, funcPath, bodyStart);
1143
+
1144
+ for (const iter of derivedIters) {
1145
+ arrayIterations.push({
1146
+ prefixChain: derived.prefixChain,
1147
+ ...iter
1148
+ });
1149
+ }
1150
+ }
1151
+
1041
1152
  if (usedAsWhole) return null;
1042
1153
  if (chains.length === 0 && arrayIterations.length === 0) return {};
1043
1154
 
@@ -21,6 +21,6 @@ if (isMain) {
21
21
  const onlyArg = process.argv.find(arg => arg.startsWith("--only="));
22
22
  const only = onlyArg?.split("=")[1] || null;
23
23
  Build(only, false) // Production build
24
- .then(success => process.exit(success ? 0 : 1))
25
- .catch(() => process.exit(1));
24
+ .then(success => { process.exitCode = success ? 0 : 1; })
25
+ .catch(() => { process.exitCode = 1; });
26
26
  }
@@ -25,7 +25,7 @@ const ICONS = {
25
25
  };
26
26
 
27
27
  const PATTERNS = {
28
- views: ["resources/views/**/*.tsx"],
28
+ views: ["resources/views/**/*.tsx", "resources/views/**/*.ts"],
29
29
  css: ["resources/css/**/*.css"],
30
30
  restart: ["config/**/*.js", "app/Kernel.js", "app/Models/**/*.js", "routes/**/*.js", "app.js", "app/Controllers/**/*.js", "app/Middlewares/**/*.js"],
31
31
  framework: ["lib/View/Templates/**/*.tsx"]
@@ -150,12 +150,21 @@ class DevServer {
150
150
  this.#debounce.build = setTimeout(async () => {
151
151
  const r = await this.#build();
152
152
  if (r.success) {
153
- const changeType = detectChangeType(filePath);
154
- this.#send("change", {
155
- changeType,
156
- cssChanged: r.cssChanged || false,
157
- file: rel
158
- });
153
+ // When client component JS chunks are rebuilt, the browser
154
+ // still has the old JS modules loaded in memory. RSC refetch
155
+ // only updates the React tree data, not the actual JS code.
156
+ // A full page reload is needed to load the new JS chunks.
157
+ if (r.viewsChanged) {
158
+ this.#send("reload", { reason: "Client component changed" });
159
+ }
160
+ else {
161
+ const changeType = detectChangeType(filePath);
162
+ this.#send("change", {
163
+ changeType,
164
+ cssChanged: r.cssChanged || false,
165
+ file: rel
166
+ });
167
+ }
159
168
  }
160
169
  }, 100);
161
170
  return;
@@ -224,9 +233,9 @@ class DevServer {
224
233
 
225
234
  const exit = async () => {
226
235
  console.log();
227
- watcher.close();
236
+ await watcher.close();
228
237
  await this.#stop();
229
- process.exit(0);
238
+ process.exitCode = 0;
230
239
  };
231
240
 
232
241
  process.on("SIGINT", exit);
@@ -245,5 +254,5 @@ export default async function Dev() {
245
254
  }
246
255
 
247
256
  if (process.argv[1]?.endsWith("DevCommand.js")) {
248
- Dev().catch(e => { console.error(e); process.exit(1); });
257
+ Dev().catch(e => { console.error(e); process.exitCode = 1; });
249
258
  }
@@ -123,9 +123,9 @@ const isMain = process.argv[1]?.endsWith("MakeCommand.js");
123
123
  if (isMain) {
124
124
  const [, , type, rawName] = process.argv;
125
125
  make(type, rawName)
126
- .then(success => process.exit(success ? 0 : 1))
126
+ .then(success => { process.exitCode = success ? 0 : 1; })
127
127
  .catch(err => {
128
128
  console.error(err);
129
- process.exit(1);
129
+ process.exitCode = 1;
130
130
  });
131
131
  }
@@ -50,7 +50,7 @@ if (isMain) {
50
50
  const shouldSeed = args.includes("--seed");
51
51
 
52
52
  migrate({ seed: shouldSeed })
53
- .then(success => process.exit(success ? 0 : 1))
53
+ .then(success => { process.exit(success ? 0 : 1); })
54
54
  .catch(err => {
55
55
  console.error(err);
56
56
  process.exit(1);
@@ -68,7 +68,7 @@ if (isMain) {
68
68
  const shouldSeed = args.includes("--seed");
69
69
 
70
70
  migrateFresh({ seed: shouldSeed })
71
- .then(success => process.exit(success ? 0 : 1))
71
+ .then(success => { process.exit(success ? 0 : 1); })
72
72
  .catch(err => {
73
73
  console.error(err);
74
74
  process.exit(1);
@@ -50,7 +50,7 @@ if (isMain) {
50
50
  const all = args.includes('--all');
51
51
 
52
52
  rollback({ step, all })
53
- .then(success => process.exit(success ? 0 : 1))
53
+ .then(success => { process.exit(success ? 0 : 1); })
54
54
  .catch(err => {
55
55
  console.error(err);
56
56
  process.exit(1);
@@ -36,7 +36,7 @@ export default async function status() {
36
36
  const isMain = process.argv[1]?.endsWith("MigrateStatusCommand.js");
37
37
  if (isMain) {
38
38
  status()
39
- .then(success => process.exit(success ? 0 : 1))
39
+ .then(success => { process.exit(success ? 0 : 1); })
40
40
  .catch(err => {
41
41
  console.error(err);
42
42
  process.exit(1);
@@ -37,7 +37,7 @@ if (isMain) {
37
37
  const seederName = args.find(a => !a.startsWith('--')) || null;
38
38
 
39
39
  seed(seederName)
40
- .then(success => process.exit(success ? 0 : 1))
40
+ .then(success => { process.exit(success ? 0 : 1); })
41
41
  .catch(err => {
42
42
  console.error(err);
43
43
  process.exit(1);
@@ -14,7 +14,8 @@ export default async function Start() {
14
14
 
15
15
  if (!success) {
16
16
  Output.error("Build failed. Cannot start server.");
17
- process.exit(1);
17
+ process.exitCode = 1;
18
+ return;
18
19
  }
19
20
  }
20
21
 
@@ -1,10 +1,11 @@
1
- import fs from "fs";
1
+ import fs from "fs/promises";
2
2
  import path from "path";
3
3
  import os from "os";
4
4
  import Output from "../Output.js";
5
5
 
6
6
  /**
7
7
  * Creates a symbolic link from storage/app/public to public/storage.
8
+ * On Windows, uses "junction" type because it doesn't require admin privileges.
8
9
  * @returns {Promise<boolean>}
9
10
  */
10
11
  export default async function storageLink() {
@@ -12,35 +13,23 @@ export default async function storageLink() {
12
13
  const target = path.join(process.cwd(), "public", "storage");
13
14
  const linkType = os.platform() === "win32" ? "junction" : "dir";
14
15
 
15
- return new Promise((resolve, reject) => {
16
- fs.symlink(source, target, linkType, (err) => {
17
- if (err) {
18
- if (err.code === "EEXIST") {
19
- Output.warn("Symbolic link already exists");
20
- Output.dim(` ${source} → ${target}`);
21
- Output.newline();
22
- resolve(true);
23
- }
24
- else {
25
- Output.error("Failed to create symbolic link");
26
- Output.dim(` ${err.message}`);
27
- reject(err);
28
- }
29
- }
30
- else {
31
- Output.success("Symbolic link created");
32
- Output.dim(` ${source} → ${target}`);
33
- Output.newline();
34
- resolve(true);
35
- }
36
- });
37
- });
38
- }
16
+ try {
17
+ await fs.symlink(source, target, linkType);
18
+ Output.success("Symbolic link created");
19
+ Output.dim(` ${source} ${target}`);
20
+ Output.newline();
21
+ return true;
22
+ }
23
+ catch (err) {
24
+ if (err.code === "EEXIST") {
25
+ Output.warn("Symbolic link already exists");
26
+ Output.dim(` ${source} ${target}`);
27
+ Output.newline();
28
+ return true;
29
+ }
39
30
 
40
- // Auto-run when called directly
41
- const isMain = process.argv[1]?.endsWith("StorageLinkCommand.js");
42
- if (isMain) {
43
- storageLink()
44
- .then(() => process.exit(0))
45
- .catch(() => process.exit(1));
46
- }
31
+ Output.error("Failed to create symbolic link");
32
+ Output.dim(` ${err.message}`);
33
+ return false;
34
+ }
35
+ }
@@ -39,7 +39,7 @@ function navigateWithPayload(payload: string) {
39
39
  rsc.root.render(React.createElement(Root));
40
40
  }
41
41
 
42
- function mount() {
42
+ async function mount() {
43
43
  const payload = window.__NITRON_FLIGHT__;
44
44
 
45
45
  if (!payload) return;
@@ -47,6 +47,22 @@ function mount() {
47
47
  const stream = payloadToStream(payload);
48
48
  const rscResponse = createFromReadableStream(stream);
49
49
 
50
+ // Wait for the RSC response to fully resolve before hydrating.
51
+ //
52
+ // Without this, hydrateRoot starts rendering AFTER the ReadableStream
53
+ // has already closed. React then tries to look up component chunks
54
+ // that no longer exist in the stream, causing "Connection closed" errors.
55
+ //
56
+ // By awaiting here, all component modules are loaded and ready BEFORE
57
+ // React starts rendering. This prevents the race condition entirely.
58
+ try {
59
+ await rscResponse;
60
+ }
61
+ catch (err) {
62
+ console.error('[rsc-consumer] RSC response failed to resolve:', err);
63
+ return;
64
+ }
65
+
50
66
  function Root(): React.ReactNode {
51
67
  return React.use(rscResponse) as React.ReactNode;
52
68
  }
@@ -86,3 +86,34 @@ Object.assign(window, {
86
86
  __webpack_get_script_filename__: webpackRequire.u
87
87
  });
88
88
 
89
+ // Client-side translation function
90
+ // Reads translations from window.__NITRON_TRANSLATIONS__ injected by the server
91
+ (globalThis as any).__ = function(key: string, params?: Record<string, any>): string {
92
+ const translations = (window as any).__NITRON_TRANSLATIONS__;
93
+
94
+ if (!translations) return key;
95
+
96
+ let value = translations[key];
97
+
98
+ if (value === undefined) return key;
99
+
100
+ value = String(value);
101
+
102
+ // Replace :param placeholders with actual values
103
+ if (params && typeof params === "object") {
104
+ const keys = Object.keys(params);
105
+
106
+ for (let i = 0; i < keys.length; i++) {
107
+ const v = params[keys[i]];
108
+
109
+ if (v !== null && v !== undefined) {
110
+ value = value.split(":" + keys[i]).join(String(v));
111
+ }
112
+ }
113
+ }
114
+
115
+ return value;
116
+ };
117
+
118
+ (globalThis as any).lang = (globalThis as any).__;
119
+
@@ -68,3 +68,34 @@ Object.assign(window, {
68
68
  __webpack_chunk_load__: webpackChunkLoad,
69
69
  __webpack_get_script_filename__: webpackRequire.u
70
70
  });
71
+
72
+ // Client-side translation function
73
+ // Reads translations from window.__NITRON_TRANSLATIONS__ injected by the server
74
+ (globalThis as any).__ = function(key: string, params?: Record<string, any>): string {
75
+ const translations = (window as any).__NITRON_TRANSLATIONS__;
76
+
77
+ if (!translations) return key;
78
+
79
+ let value = translations[key];
80
+
81
+ if (value === undefined) return key;
82
+
83
+ value = String(value);
84
+
85
+ // Replace :param placeholders with actual values
86
+ if (params && typeof params === "object") {
87
+ const keys = Object.keys(params);
88
+
89
+ for (let i = 0; i < keys.length; i++) {
90
+ const v = params[keys[i]];
91
+
92
+ if (v !== null && v !== undefined) {
93
+ value = value.split(":" + keys[i]).join(String(v));
94
+ }
95
+ }
96
+ }
97
+
98
+ return value;
99
+ };
100
+
101
+ (globalThis as any).lang = (globalThis as any).__;