@erickxavier/no-js 1.0.2 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erickxavier/no-js",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "The HTML-first reactive framework — build dynamic web apps with just HTML attributes, no JavaScript required",
5
5
  "main": "dist/cjs/no.js",
6
6
  "module": "dist/esm/no.js",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "node build.js",
25
- "start": "npx serve .",
25
+ "start": "node docs/dev-server.js",
26
26
  "test": "jest",
27
27
  "test:watch": "jest --watch",
28
28
  "test:coverage": "jest --coverage",
package/src/context.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // REACTIVE CONTEXT
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
- import { _stores, _refs, _routerInstance } from "./globals.js";
5
+ import { _stores, _refs, _routerInstance, _currentEl } from "./globals.js";
6
6
  import { _i18n } from "./i18n.js";
7
7
 
8
8
  let _batchDepth = 0;
@@ -17,7 +17,10 @@ export function _endBatch() {
17
17
  if (_batchDepth === 0 && _batchQueue.size > 0) {
18
18
  const fns = [..._batchQueue];
19
19
  _batchQueue.clear();
20
- fns.forEach((fn) => fn());
20
+ fns.forEach((fn) => {
21
+ if (fn._el && !fn._el.isConnected) return;
22
+ fn();
23
+ });
21
24
  }
22
25
  }
23
26
 
@@ -32,9 +35,15 @@ export function createContext(data = {}, parent = null) {
32
35
  notifying = true;
33
36
  try {
34
37
  if (_batchDepth > 0) {
35
- listeners.forEach((fn) => _batchQueue.add(fn));
38
+ for (const fn of listeners) {
39
+ if (fn._el && !fn._el.isConnected) { listeners.delete(fn); continue; }
40
+ _batchQueue.add(fn);
41
+ }
36
42
  } else {
37
- listeners.forEach((fn) => fn());
43
+ for (const fn of listeners) {
44
+ if (fn._el && !fn._el.isConnected) { listeners.delete(fn); continue; }
45
+ fn();
46
+ }
38
47
  }
39
48
  } finally {
40
49
  notifying = false;
@@ -48,6 +57,7 @@ export function createContext(data = {}, parent = null) {
48
57
  if (key === "__listeners") return listeners;
49
58
  if (key === "$watch")
50
59
  return (fn) => {
60
+ if (_currentEl) fn._el = _currentEl;
51
61
  listeners.add(fn);
52
62
  return () => listeners.delete(fn);
53
63
  };
@@ -5,6 +5,7 @@
5
5
  import { evaluate, _execStatement } from "../evaluate.js";
6
6
  import { findContext } from "../dom.js";
7
7
  import { registerDirective } from "../registry.js";
8
+ import { _onDispose } from "../globals.js";
8
9
 
9
10
  registerDirective("on:*", {
10
11
  priority: 20,
@@ -28,14 +29,17 @@ registerDirective("on:*", {
28
29
  _execStatement(expr, ctx, { $el: el });
29
30
  });
30
31
  updatedObserver.observe(el, { childList: true, subtree: true, characterData: true, attributes: true });
32
+ _onDispose(() => updatedObserver.disconnect());
31
33
  return;
32
34
  }
33
35
  if (event === "error") {
34
- window.addEventListener("error", (e) => {
36
+ const errorHandler = (e) => {
35
37
  if (el.contains(e.target) || e.target === el) {
36
38
  _execStatement(expr, ctx, { $el: el, $error: e.error || e.message });
37
39
  }
38
- });
40
+ };
41
+ window.addEventListener("error", errorHandler);
42
+ _onDispose(() => window.removeEventListener("error", errorHandler));
39
43
  return;
40
44
  }
41
45
  if (event === "unmounted") {
@@ -10,6 +10,7 @@ import {
10
10
  _notifyStoreWatchers,
11
11
  _emitEvent,
12
12
  _routerInstance,
13
+ _onDispose,
13
14
  } from "../globals.js";
14
15
  import { createContext } from "../context.js";
15
16
  import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
@@ -281,7 +282,8 @@ for (const method of HTTP_METHODS) {
281
282
 
282
283
  // Polling
283
284
  if (refreshInterval > 0) {
284
- setInterval(doRequest, refreshInterval);
285
+ const id = setInterval(doRequest, refreshInterval);
286
+ _onDispose(() => clearInterval(id));
285
287
  }
286
288
  },
287
289
  });
@@ -1,26 +1,33 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════
2
2
  // DIRECTIVE: t (i18n translations)
3
+ // DIRECTIVE: i18n-ns (load namespace before children)
3
4
  // ═══════════════════════════════════════════════════════════════════════
4
5
 
5
- import { _i18n, _watchI18n } from "../i18n.js";
6
+ import { _i18n, _watchI18n, _loadI18nNamespace, _notifyI18n } from "../i18n.js";
6
7
  import { evaluate } from "../evaluate.js";
7
8
  import { findContext } from "../dom.js";
8
- import { registerDirective } from "../registry.js";
9
+ import { registerDirective, processTree } from "../registry.js";
9
10
 
10
11
  registerDirective("t", {
11
12
  priority: 20,
12
13
  init(el, name, key) {
13
14
  const ctx = findContext(el);
15
+ const useHtml = el.hasAttribute("t-html");
14
16
 
15
17
  function update() {
16
18
  const params = {};
17
19
  for (const attr of [...el.attributes]) {
18
- if (attr.name.startsWith("t-") && attr.name !== "t") {
20
+ if (attr.name.startsWith("t-") && attr.name !== "t" && attr.name !== "t-html") {
19
21
  const paramName = attr.name.replace("t-", "");
20
22
  params[paramName] = evaluate(attr.value, ctx) ?? attr.value;
21
23
  }
22
24
  }
23
- el.textContent = _i18n.t(key, params);
25
+ const text = _i18n.t(key, params);
26
+ if (useHtml) {
27
+ el.innerHTML = text;
28
+ } else {
29
+ el.textContent = text;
30
+ }
24
31
  }
25
32
 
26
33
  ctx.$watch(update);
@@ -28,3 +35,18 @@ registerDirective("t", {
28
35
  update();
29
36
  },
30
37
  });
38
+
39
+ registerDirective("i18n-ns", {
40
+ priority: 1,
41
+ init(el, name, ns) {
42
+ // Save children to prevent premature t resolution
43
+ const saved = document.createDocumentFragment();
44
+ while (el.firstChild) saved.appendChild(el.firstChild);
45
+
46
+ _loadI18nNamespace(ns).then(() => {
47
+ el.appendChild(saved);
48
+ processTree(el);
49
+ _notifyI18n();
50
+ });
51
+ },
52
+ });
@@ -5,6 +5,7 @@
5
5
  import { evaluate } from "../evaluate.js";
6
6
  import { findContext } from "../dom.js";
7
7
  import { registerDirective } from "../registry.js";
8
+ import { _watchI18n } from "../i18n.js";
8
9
 
9
10
  registerDirective("class-*", {
10
11
  priority: 20,
@@ -51,6 +52,7 @@ registerDirective("class-*", {
51
52
  el.classList.toggle(suffix, !!evaluate(expr, ctx));
52
53
  }
53
54
  ctx.$watch(update);
55
+ if (expr.includes("NoJS.locale")) _watchI18n(update);
54
56
  update();
55
57
  },
56
58
  });
@@ -3,7 +3,7 @@
3
3
  // HELPER: _validateField
4
4
  // ═══════════════════════════════════════════════════════════════════════
5
5
 
6
- import { _validators } from "../globals.js";
6
+ import { _validators, _onDispose } from "../globals.js";
7
7
  import { createContext } from "../context.js";
8
8
  import { findContext, _cloneTemplate } from "../dom.js";
9
9
  import { registerDirective, processTree } from "../registry.js";
@@ -223,10 +223,12 @@ registerDirective("error-boundary", {
223
223
  });
224
224
 
225
225
  // Listen for window-level errors (resource load failures, etc.)
226
- window.addEventListener("error", (e) => {
226
+ const errorHandler = (e) => {
227
227
  if (el.contains(e.target) || el === e.target) {
228
228
  showFallback(e.message || "An error occurred");
229
229
  }
230
- });
230
+ };
231
+ window.addEventListener("error", errorHandler);
232
+ _onDispose(() => window.removeEventListener("error", errorHandler));
231
233
  },
232
234
  });
package/src/globals.js CHANGED
@@ -12,8 +12,8 @@ export const _config = {
12
12
  csrf: null,
13
13
  cache: { strategy: "none", ttl: 300000 },
14
14
  templates: { cache: true },
15
- router: { mode: "history", base: "/", scrollBehavior: "top" },
16
- i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false },
15
+ router: { mode: "history", base: "/", scrollBehavior: "top", templates: "pages", ext: ".tpl" },
16
+ i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false, loadPath: null, ns: [], cache: true, persist: false },
17
17
  debug: false,
18
18
  devtools: false,
19
19
  csp: null,
@@ -30,6 +30,15 @@ export const _cache = new Map();
30
30
  export const _refs = {};
31
31
  export let _routerInstance = null;
32
32
 
33
+ // ─── Lifecycle: tracks the element being processed by processElement ────────
34
+ // Used by ctx.$watch and _onDispose to transparently tag watchers/disposers
35
+ // with the owning DOM element — no changes needed in directive files.
36
+ export let _currentEl = null;
37
+
38
+ export function _setCurrentEl(el) {
39
+ _currentEl = el;
40
+ }
41
+
33
42
  export function setRouterInstance(r) {
34
43
  _routerInstance = r;
35
44
  }
@@ -43,7 +52,13 @@ export function _warn(...args) {
43
52
  }
44
53
 
45
54
  export function _notifyStoreWatchers() {
46
- for (const fn of _storeWatchers) fn();
55
+ for (const fn of _storeWatchers) {
56
+ if (fn._el && !fn._el.isConnected) {
57
+ _storeWatchers.delete(fn);
58
+ continue;
59
+ }
60
+ fn();
61
+ }
47
62
  }
48
63
 
49
64
  export function _watchExpr(expr, ctx, fn) {
@@ -53,6 +68,15 @@ export function _watchExpr(expr, ctx, fn) {
53
68
  }
54
69
  }
55
70
 
71
+ // Register a dispose callback on the element currently being processed.
72
+ // Called from directives to clean up intervals, observers, window listeners.
73
+ export function _onDispose(fn) {
74
+ if (_currentEl) {
75
+ _currentEl.__disposers = _currentEl.__disposers || [];
76
+ _currentEl.__disposers.push(fn);
77
+ }
78
+ }
79
+
56
80
  export function _emitEvent(name, data) {
57
81
  (_eventBus[name] || []).forEach((fn) => fn(data));
58
82
  }
package/src/i18n.js CHANGED
@@ -2,15 +2,83 @@
2
2
  // i18n SYSTEM
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
- import { _config } from "./globals.js";
5
+ import { _config, _warn } from "./globals.js";
6
6
 
7
7
  const _i18nListeners = new Set();
8
+ export { _i18nListeners };
8
9
 
9
10
  export function _watchI18n(fn) {
10
11
  _i18nListeners.add(fn);
11
12
  return () => _i18nListeners.delete(fn);
12
13
  }
13
14
 
15
+ // ─── Notify all i18n listeners (shared by setter + directive) ────────
16
+ export function _notifyI18n() {
17
+ for (const fn of _i18nListeners) {
18
+ if (fn._el && !fn._el.isConnected) { _i18nListeners.delete(fn); continue; }
19
+ fn();
20
+ }
21
+ }
22
+
23
+ // ─── Deep merge (recursive, returns new object) ─────────────────────
24
+ export function _deepMerge(target, source) {
25
+ const out = { ...target };
26
+ for (const key of Object.keys(source)) {
27
+ if (
28
+ source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) &&
29
+ target[key] && typeof target[key] === "object" && !Array.isArray(target[key])
30
+ ) {
31
+ out[key] = _deepMerge(target[key], source[key]);
32
+ } else {
33
+ out[key] = source[key];
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+
39
+ // ─── Locale file cache: Map<string, object> key = "en" or "en:dashboard"
40
+ export const _i18nCache = new Map();
41
+ export const _loadedNs = new Set();
42
+
43
+ // ─── Fetch a single JSON file and merge into _i18n.locales[locale] ──
44
+ export async function _loadLocale(locale, ns) {
45
+ const cacheKey = ns ? `${locale}:${ns}` : locale;
46
+ if (_config.i18n.cache && _i18nCache.has(cacheKey)) return;
47
+
48
+ let url = _config.i18n.loadPath.replace("{locale}", locale);
49
+ if (ns) url = url.replace("{ns}", ns);
50
+
51
+ try {
52
+ const res = await fetch(url);
53
+ if (!res.ok) { _warn(`i18n: failed to load ${url} (${res.status})`); return; }
54
+ const data = await res.json();
55
+ const toMerge = ns ? { [ns]: data } : data;
56
+ _i18n.locales[locale] = _deepMerge(_i18n.locales[locale] || {}, toMerge);
57
+ if (_config.i18n.cache) _i18nCache.set(cacheKey, data);
58
+ } catch (e) {
59
+ _warn(`i18n: error loading ${url}`, e);
60
+ }
61
+ }
62
+
63
+ // ─── Load all configured data for a locale (flat or all namespaces) ──
64
+ export async function _loadI18nForLocale(locale) {
65
+ if (!_config.i18n.loadPath) return;
66
+ const ns = _config.i18n.ns;
67
+ if (!ns.length || !_config.i18n.loadPath.includes("{ns}")) {
68
+ await _loadLocale(locale, null);
69
+ } else {
70
+ await Promise.all(ns.map((n) => _loadLocale(locale, n)));
71
+ }
72
+ }
73
+
74
+ // ─── Load a single namespace for current + fallback locales ──────────
75
+ export async function _loadI18nNamespace(ns) {
76
+ if (!_config.i18n.loadPath) return;
77
+ _loadedNs.add(ns);
78
+ const locales = new Set([_i18n.locale, _config.i18n.fallbackLocale]);
79
+ await Promise.all([...locales].map((l) => _loadLocale(l, ns)));
80
+ }
81
+
14
82
  export const _i18n = {
15
83
  _locale: "en",
16
84
  locales: {},
@@ -20,7 +88,16 @@ export const _i18n = {
20
88
  set locale(v) {
21
89
  if (this._locale !== v) {
22
90
  this._locale = v;
23
- _i18nListeners.forEach((fn) => fn());
91
+ if (_config.i18n.persist && typeof localStorage !== "undefined") {
92
+ try { localStorage.setItem("nojs-locale", v); } catch (_) {}
93
+ }
94
+ if (_config.i18n.loadPath) {
95
+ // Load configured ns + any route-loaded ns for the new locale
96
+ const allNs = new Set([..._config.i18n.ns, ..._loadedNs]);
97
+ Promise.all([...allNs].map((n) => _loadLocale(v, n))).then(() => _notifyI18n());
98
+ } else {
99
+ _notifyI18n();
100
+ }
24
101
  }
25
102
  },
26
103
  t(key, params = {}) {
package/src/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  setRouterInstance,
17
17
  _log,
18
18
  } from "./globals.js";
19
- import { _i18n } from "./i18n.js";
19
+ import { _i18n, _loadI18nForLocale } from "./i18n.js";
20
20
  import { createContext } from "./context.js";
21
21
  import { evaluate, resolve } from "./evaluate.js";
22
22
  import { findContext, _loadRemoteTemplates, _loadRemoteTemplatesPhase1, _loadRemoteTemplatesPhase2, _processTemplateIncludes } from "./dom.js";
@@ -60,6 +60,13 @@ const NoJS = {
60
60
  _config.baseApiUrl = v;
61
61
  },
62
62
 
63
+ get locale() {
64
+ return _i18n.locale;
65
+ },
66
+ set locale(v) {
67
+ _i18n.locale = v;
68
+ },
69
+
63
70
  config(opts = {}) {
64
71
  // Save nested objects before shallow assign overwrites them
65
72
  const prevHeaders = { ..._config.headers };
@@ -85,6 +92,12 @@ const NoJS = {
85
92
  root = root || document.body;
86
93
  _log("Initializing...");
87
94
 
95
+ // Load external locale files (blocking — translations must be available for first paint)
96
+ if (_config.i18n.loadPath) {
97
+ const locales = new Set([_i18n.locale, _config.i18n.fallbackLocale]);
98
+ await Promise.all([...locales].map((l) => _loadI18nForLocale(l)));
99
+ }
100
+
88
101
  // Inline template includes (e.g. skeletons) — synchronous, before any fetch
89
102
  _processTemplateIncludes(root);
90
103
 
@@ -141,13 +154,32 @@ const NoJS = {
141
154
 
142
155
  // i18n
143
156
  i18n(opts) {
157
+ // Set config options BEFORE locale (setter checks loadPath)
158
+ if (opts.loadPath != null) _config.i18n.loadPath = opts.loadPath;
159
+ if (opts.ns) _config.i18n.ns = opts.ns;
160
+ if (opts.cache != null) _config.i18n.cache = opts.cache;
161
+ if (opts.persist != null) _config.i18n.persist = opts.persist;
144
162
  if (opts.locales) _i18n.locales = opts.locales;
145
- if (opts.defaultLocale) _i18n.locale = opts.defaultLocale;
146
163
  if (opts.fallbackLocale) _config.i18n.fallbackLocale = opts.fallbackLocale;
164
+
165
+ // Set defaultLocale WITHOUT the setter (avoids overwriting localStorage)
166
+ if (opts.defaultLocale) _i18n._locale = opts.defaultLocale;
167
+
168
+ // Restore persisted locale (highest priority)
169
+ if (_config.i18n.persist && typeof localStorage !== "undefined") {
170
+ try {
171
+ const saved = localStorage.getItem("nojs-locale");
172
+ if (saved && _i18n.locales[saved]) { _i18n._locale = saved; return; }
173
+ } catch (_) {}
174
+ }
175
+
176
+ // Detect browser language (second priority)
147
177
  if (opts.detectBrowser) {
148
178
  const browserLang =
149
179
  typeof navigator !== "undefined" ? navigator.language : "en";
150
- if (_i18n.locales[browserLang]) _i18n.locale = browserLang;
180
+ const prefix = browserLang.split("-")[0];
181
+ if (_i18n.locales[browserLang]) _i18n._locale = browserLang;
182
+ else if (_i18n.locales[prefix]) _i18n._locale = prefix;
151
183
  }
152
184
  },
153
185
 
@@ -183,7 +215,7 @@ const NoJS = {
183
215
  resolve,
184
216
 
185
217
  // Version
186
- version: "1.0.2",
218
+ version: "1.1.0",
187
219
  };
188
220
 
189
221
  export default NoJS;
package/src/registry.js CHANGED
@@ -2,6 +2,9 @@
2
2
  // DIRECTIVE REGISTRY & DOM PROCESSING
3
3
  // ═══════════════════════════════════════════════════════════════════════
4
4
 
5
+ import { _currentEl, _setCurrentEl, _storeWatchers } from "./globals.js";
6
+ import { _i18nListeners } from "./i18n.js";
7
+
5
8
  const _directives = new Map();
6
9
 
7
10
  export function registerDirective(name, handler) {
@@ -43,9 +46,12 @@ export function processElement(el) {
43
46
  }
44
47
 
45
48
  matched.sort((a, b) => a.priority - b.priority);
49
+ const prev = _currentEl;
46
50
  for (const m of matched) {
51
+ _setCurrentEl(el);
47
52
  m.init(el, m.name, m.value);
48
53
  }
54
+ _setCurrentEl(prev);
49
55
  }
50
56
 
51
57
  export function processTree(root) {
@@ -58,3 +64,27 @@ export function processTree(root) {
58
64
  if (!node.__declared) processElement(node);
59
65
  }
60
66
  }
67
+
68
+ // ─── Disposal: proactive cleanup of watchers/listeners/disposers ────────
69
+
70
+ function _disposeElement(node) {
71
+ if (node.__ctx && node.__ctx.__listeners) {
72
+ for (const fn of node.__ctx.__listeners) {
73
+ _storeWatchers.delete(fn);
74
+ _i18nListeners.delete(fn);
75
+ }
76
+ node.__ctx.__listeners.clear();
77
+ }
78
+ if (node.__disposers) {
79
+ node.__disposers.forEach((fn) => fn());
80
+ node.__disposers = null;
81
+ }
82
+ node.__declared = false;
83
+ }
84
+
85
+ export function _disposeTree(root) {
86
+ if (!root) return;
87
+ _disposeElement(root);
88
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
89
+ while (walker.nextNode()) _disposeElement(walker.currentNode);
90
+ }
package/src/router.js CHANGED
@@ -6,13 +6,14 @@ import { _config, _stores, _log } from "./globals.js";
6
6
  import { createContext } from "./context.js";
7
7
  import { evaluate } from "./evaluate.js";
8
8
  import { findContext, _clearDeclared, _loadTemplateElement, _processTemplateIncludes } from "./dom.js";
9
- import { processTree } from "./registry.js";
9
+ import { processTree, _disposeTree } from "./registry.js";
10
10
  import { _animateIn } from "./animations.js";
11
11
 
12
12
  export function _createRouter() {
13
13
  const routes = [];
14
14
  let current = { path: "", params: {}, query: {}, hash: "" };
15
15
  const listeners = new Set();
16
+ const _autoTemplateCache = new Map();
16
17
 
17
18
  function _getOrCreateEntry(path) {
18
19
  let entry = routes.find((r) => r.path === path);
@@ -118,9 +119,38 @@ export function _createRouter() {
118
119
  const outletName = outletAttr && outletAttr.trim() !== "" ? outletAttr.trim() : "default";
119
120
 
120
121
  // Find the template for this outlet in the matched route
121
- const tpl = matched?.route?.outlets?.[outletName];
122
+ let tpl = matched?.route?.outlets?.[outletName];
123
+
124
+ // ── File-based routing: auto-resolve from route-view[src] or config ──
125
+ const configTemplates = _config.router.templates || "";
126
+ if (!tpl && (outletEl.hasAttribute("src") || configTemplates)) {
127
+ const rawSrc = outletEl.getAttribute("src") || configTemplates;
128
+ const baseSrc = rawSrc.replace(/\/?$/, "/");
129
+ const ext = outletEl.getAttribute("ext") || _config.router.ext || ".html";
130
+ const indexName = outletEl.getAttribute("route-index") || "index";
131
+ const segment = current.path === "/" ? indexName : current.path.replace(/^\//, "");
132
+ const fullSrc = baseSrc + segment + ext;
133
+ const cacheKey = outletName + ":" + fullSrc;
134
+
135
+ if (_autoTemplateCache.has(cacheKey)) {
136
+ tpl = _autoTemplateCache.get(cacheKey);
137
+ } else {
138
+ tpl = document.createElement("template");
139
+ tpl.setAttribute("src", fullSrc);
140
+ tpl.setAttribute("route", current.path);
141
+ document.body.appendChild(tpl);
142
+ _autoTemplateCache.set(cacheKey, tpl);
143
+ _log("[ROUTER] File-based route:", current.path, "→", fullSrc);
144
+ }
145
+
146
+ // Auto i18n namespace (convention: filename = namespace)
147
+ if (outletEl.hasAttribute("i18n-ns") && !tpl.getAttribute("i18n-ns")) {
148
+ tpl.setAttribute("i18n-ns", segment);
149
+ }
150
+ }
122
151
 
123
- // Always clear first
152
+ // Always clear first — dispose watchers/listeners before wiping DOM
153
+ _disposeTree(outletEl);
124
154
  outletEl.innerHTML = "";
125
155
 
126
156
  if (tpl) {
@@ -130,6 +160,13 @@ export function _createRouter() {
130
160
  await _loadTemplateElement(tpl);
131
161
  }
132
162
 
163
+ // i18n namespace loading for route template
164
+ const i18nNs = tpl.getAttribute("i18n-ns");
165
+ if (i18nNs) {
166
+ const { _loadI18nNamespace } = await import("./i18n.js");
167
+ await _loadI18nNamespace(i18nNs);
168
+ }
169
+
133
170
  const clone = tpl.content.cloneNode(true);
134
171
 
135
172
  const routeCtx = createContext(
@@ -176,10 +213,10 @@ export function _createRouter() {
176
213
  if (exactClass) {
177
214
  link.classList.toggle(exactClass, current.path === routePath);
178
215
  } else if (activeClass && !link.hasAttribute("route-active-exact")) {
179
- link.classList.toggle(
180
- activeClass,
181
- current.path.startsWith(routePath),
182
- );
216
+ const isActive = routePath === "/"
217
+ ? current.path === "/"
218
+ : current.path.startsWith(routePath);
219
+ link.classList.toggle(activeClass, isActive);
183
220
  }
184
221
  });
185
222