on_the_money 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.3] — 2026-05-22
8
+
9
+ ### Added
10
+
11
+ - **`the.boot({ ephemeralKeys })`** — declares state keys that should not persist to `localStorage`. Writes still update DOM and mirror to `[data-text]`, but skip storage; boot replay also skips them. Targets transient signals like `modal`, `loading`, `toast` that shouldn't survive page navigation. (#55)
12
+
13
+ ### Changed
14
+
15
+ - **`_t(key, options)`** now Intl-formats `options.val` even when the dictionary has no entry for `key`, provided `options.type` is `"currency"` or `"date"`. Default-locale apps (those using `defaultLocale` to skip the dictionary fetch) can now use `data-i18n-val` formatting without needing a placeholder dictionary entry. Missing entries with no `type` still return the key, as before. (#53)
16
+
17
+ ## [0.3.2] — 2026-05-20
18
+
19
+ ### Added
20
+
21
+ - **`the.boot({ defaultLocale })`** — when the resolved locale base matches this value, the dictionary fetch and the boot-time `_t()` hydration pass are skipped entirely. No network, no FOUC for default-locale visitors. Auto-detected from `<html lang>` if the option is omitted, so the no-config case Just Works for any page that correctly declares its language. (#52)
22
+ - **localStorage state replay** still runs in the short-circuit path; only the i18n machinery is skipped.
23
+
24
+ ### Changed
25
+
26
+ - **README** now recommends including source-language fallback text inside every `data-i18n` element (e.g. `<h1 data-i18n="title">Welcome</h1>`). Combined with the `defaultLocale` short-circuit, default-locale visitors see correct text immediately with zero hydration pass.
27
+
7
28
  ## [0.3.1] — 2026-05-19
8
29
 
9
30
  ### Fixed
package/README.md CHANGED
@@ -20,13 +20,13 @@ A complete two-file app. `index.html` carries semantic structure, `app.js` carri
20
20
  <head>
21
21
  <meta charset="UTF-8">
22
22
  <meta name="viewport" content="width=device-width, initial-scale=1">
23
- <title data-i18n="app_title"></title>
23
+ <title data-i18n="app_title">My App</title>
24
24
  <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2/css/pico.classless.min.css">
25
25
  <script type="module" src="./app.js"></script>
26
26
  </head>
27
27
  <body>
28
28
  <main>
29
- <h1 data-i18n="app_title"></h1>
29
+ <h1 data-i18n="app_title">My App</h1>
30
30
  <p>Hello, <strong data-text="user">friend</strong>.</p>
31
31
  <button data-action="greet">Greet</button>
32
32
  </main>
@@ -144,7 +144,7 @@ Throws on non-object input.
144
144
  Importing the module does **nothing**. Call `the.boot()` at the consumer's entry point.
145
145
 
146
146
  ```javascript
147
- await the.boot({ signal, locales, dictionary, namespace });
147
+ await the.boot({ signal, locales, dictionary, namespace, defaultLocale });
148
148
  ```
149
149
 
150
150
  | Option | Type | Behavior |
@@ -153,12 +153,27 @@ await the.boot({ signal, locales, dictionary, namespace });
153
153
  | `locales` | `string` | Override the `<meta name="i18n" content>` path. |
154
154
  | `dictionary` | `object` | Inline dictionary; skips the fetch entirely. |
155
155
  | `namespace` | `string` | Sets `localStorage` prefix to `${namespace}:` (default `otm:`). Must be set before any state ops. |
156
+ | `defaultLocale` | `string` | Locale your static HTML is already written in. When the resolved locale base matches this, the dictionary fetch and `_t()` hydration pass are skipped entirely — no network, no FOUC. Auto-detected from `<html lang>` if omitted. |
157
+ | `ephemeralKeys` | `string[]` | Global state keys that **should not** persist to `localStorage`. Writes to these keys still update the body attribute and any `[data-text]` mirrors, but skip the storage write; the boot replay also skips them. Use for transient signals like `modal`, `loading`, `toast`. Scoped state (`the(el, ...)`) never persists regardless. |
156
158
 
157
159
  Boot sequence:
158
160
  1. Resolve locale: `?lang=` query → `localStorage["${prefix}lang"]` → `navigator.language`. Writes `the.locale`.
159
- 2. Resolve dictionary: inline `dictionary` `fetch(${path}/${target}.json)` if `<meta name="i18n">` or `locales` option is present. Falls back through full base → `data-fallback`.
160
- 3. Replay `localStorage` entries matching the prefix (except `${prefix}lang`) back onto body `data-*` and `[data-text]`.
161
- 4. Run `_t()` to hydrate `[data-i18n]`.
161
+ 2. **Short-circuit check.** If resolved locale base matches `defaultLocale` (or `<html lang>` if not provided), skip steps 3 and 5. Static HTML already serves the right text.
162
+ 3. Resolve dictionary: inline `dictionary` `fetch(${path}/${target}.json)` if `<meta name="i18n">` or `locales` option is present. Falls back through full base `data-fallback`.
163
+ 4. Replay `localStorage` entries matching the prefix (except `${prefix}lang`) back onto body `data-*` and `[data-text]`.
164
+ 5. Run `_t()` to hydrate `[data-i18n]`.
165
+
166
+ **Writing `data-i18n` elements:** always include the source-language text inside as a fallback:
167
+
168
+ ```html
169
+ <!-- good — SEO fallback, no-JS fallback, no-flash default -->
170
+ <h1 data-i18n="title">Welcome to the app</h1>
171
+
172
+ <!-- avoid — page is blank until hydration -->
173
+ <h1 data-i18n="title"></h1>
174
+ ```
175
+
176
+ The framework preserves existing `textContent` when a dictionary key is missing, so source-language text inside `data-i18n` elements stays visible if `_t()` runs against an empty dictionary (the short-circuit case above, or any environment without the i18n fetch).
162
177
 
163
178
  ### `_t(key, options)` — `Intl` localization
164
179
 
@@ -173,13 +188,13 @@ _t(node); // hydrate every [data-i18n]
173
188
  _t(); // hydrate document.body
174
189
  ```
175
190
 
176
- Missing dictionary keys preserve existing `textContent` (SEO fallback).
191
+ Missing dictionary keys preserve existing `textContent` (SEO fallback). When `options.type` is `"currency"` or `"date"`, `Intl` formatting of `options.val` runs regardless of whether the dictionary has an entry — useful in default-locale apps that skip the fetch.
177
192
 
178
- `[data-i18n]` element binding:
193
+ `[data-i18n]` element binding (always include source-language fallback text inside):
179
194
 
180
195
  ```html
181
- <span data-i18n="cart_items" data-i18n-qty="3"></span>
182
- <span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency"></span>
196
+ <span data-i18n="cart_items" data-i18n-qty="3">3 items</span>
197
+ <span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency">$9.99</span>
183
198
  ```
184
199
 
185
200
  ### `route(callback)` — pushState router
@@ -1 +1 @@
1
- var b=class{static on(t,n,o,e){let r=typeof t=="string"?document.querySelector(t):t||document.body,a=i=>{let s=i.target.closest(o);s&&r.contains(s)&&e.call(s,i,s)};return r.addEventListener(n,a),()=>r.removeEventListener(n,a)}static emit(t,n,o){let e=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(n,{bubbles:!0,cancelable:!0,detail:o});e.dispatchEvent(r)}};var l=class c{static dictionary={};static locale=typeof navigator<"u"?navigator.language:"en-US";static prefix="otm:";static the(...t){if(t.length===0)throw new TypeError("the(): missing args");return t[0]instanceof Element?c.#e(t[0],t.slice(1)):c.#e(document.body,t)}static#e(t,n){let[o,e]=n,r=t===document.body;if(n.length===1&&typeof o=="string")return c.#o(t,o);if(n.length===2&&typeof o=="string"){if(typeof e>"u")throw new TypeError(`the(${JSON.stringify(o)}, undefined): val is required for set`);return c.#t(t,o,e),r&&localStorage.setItem(`${c.prefix}${o}`,e),t}if(n.length===1&&o?.constructor===Object){for(let[a,i]of Object.entries(o))c.#t(t,a,i),r&&localStorage.setItem(`${c.prefix}${a}`,i);return t}throw new TypeError(`the(): unrecognized call shape (${n.map(a=>typeof a).join(", ")})`)}static flat(t,n="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let o={},e=(r,a)=>{if(r===null||typeof r!="object"){o[a]=r;return}for(let[i,s]of Object.entries(r))e(s,a?`${a}${n}${i}`:i)};return e(t,""),o}static form(t){let n={},o=t.querySelectorAll("input, select, textarea");for(let e of o){let r=e.name;if(!r||e.disabled)continue;let a=(e.type||"text").toLowerCase();if(a==="submit"||a==="button"||a==="reset"||(a==="checkbox"||a==="radio")&&!(e.checked??e.hasAttribute("checked")))continue;let i=e.value??e.getAttribute("value")??"";c.#n(n,r,i)}return n}static#n(t,n,o){let e=n.split(/[\[\]]/).filter(Boolean),r=t;for(let a=0;a<e.length;a++){let i=e[a];a===e.length-1?r[i]!==void 0?(Array.isArray(r[i])||(r[i]=[r[i]]),r[i].push(o)):r[i]=o:(r[i]=r[i]||{},r=r[i])}}static _t(t,n={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let a=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let i of a)if(i.hasAttribute?.("data-i18n")){let s=i.getAttribute("data-i18n"),d={};for(let u of i.attributes)if(u.name.startsWith("data-i18n-")&&u.name!=="data-i18n-type"){let y=u.name.replace("data-i18n-","");d[y]=u.value}let f=i.getAttribute("data-i18n-type");i.textContent=c._t(s,{...d,type:f})}return t}if(!t)return c._t(document.body),"";let e=c.dictionary[t];if(!e)return t;if(typeof e=="object"){let a=n.qty!==void 0?Number(n.qty):0,i=new Intl.PluralRules(c.locale).select(a);e=e[i]||e.other||t}if(typeof e!="string")return t;let r=e;for(let[a,i]of Object.entries(n)){let s=i;if(a==="val"&&n.type){let d=Number(i),f=n.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):n.type==="date"?new Intl.DateTimeFormat(c.locale):new Intl.NumberFormat(c.locale);s=n.type==="date"?f.format(new Date(i)):f.format(d)}r=r.replace(`{${a}}`,s)}return r}static route(t){if(typeof window>"u")return;let n=()=>t(window.location.pathname,window.location.search,window.location.hash);window.addEventListener("popstate",n),window.addEventListener("hashchange",n),document.addEventListener("click",o=>{let e=o.target.closest("a");if(!e||!e.getAttribute("href")||e.hasAttribute("data-external")||e.target==="_blank")return;let r=new URL(e.getAttribute("href"),window.location.href);r.origin===window.location.origin&&(r.pathname===window.location.pathname&&r.search===window.location.search&&r.hash||(o.preventDefault(),window.history.pushState({},"",r.href),n()))}),n()}static#o(t,n){let e={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[n]||`data-${n}`;return t.getAttribute(e)}static#t(t,n,o){let r={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[n]||`data-${n}`,a=typeof o=="boolean"?o?"true":"false":o;if(t.setAttribute(r,a),t.querySelectorAll){let i=t.querySelectorAll(`[data-text="${n}"]`);for(let s of i)s.textContent=a}t.getAttribute?.("data-text")===n&&(t.textContent=a)}static async boot({signal:t,locales:n,dictionary:o,namespace:e}={}){e&&(c.prefix=`${e}:`);let r=typeof window<"u"&&window.location?window.location.search:"",a=new URLSearchParams(r),i=(typeof navigator<"u"?navigator.language:null)||document.documentElement.lang||"en";if(c.locale=a.get("lang")||localStorage.getItem(`${c.prefix}lang`)||i,o)c.dictionary=o;else{let s=document.querySelector('meta[name="i18n"]'),d=n||s?.getAttribute("content");if(d){let f=s?.getAttribute("data-fallback")||"en",u=(s?.getAttribute("data-available")||"").split(",").map(p=>p.trim().toLowerCase()),y=c.locale.toLowerCase(),g=y.split("-")[0],w=f;u.includes(y)?w=y:u.includes(g)&&(w=g);try{let p=await fetch(`${d}/${w}.json`,{signal:t});p.ok&&(c.dictionary=await p.json())}catch(p){if(t?.aborted)throw p;console.warn("otm: i18n fetch failed",p)}}}for(let s=0;s<localStorage.length;s++){let d=localStorage.key(s);if(d.startsWith(c.prefix)){let f=d.slice(c.prefix.length);if(f!=="lang"){let u=localStorage.getItem(d);c.#t(document.body,f,u)}}}c._t()}};var h=class{static $(t,n){let o=t,e=n;return typeof o=="string"&&(e=o,o=document),o.querySelector(e)}static $$(t,n){let o=t,e=n;return typeof o=="string"&&(e=o,o=document),Array.from(o.querySelectorAll(e))}static clone(t,n){let o=typeof t=="string"?document.querySelector(t):t,e=document.querySelector(n);if(!o)throw new Error(`Parent not found: ${t}`);if(!e)throw new Error(`Template not found: ${n}`);let r=e.content.cloneNode(!0).firstElementChild;l._t(r),o.appendChild(r);let a=new CustomEvent("mounted",{bubbles:!0,detail:{parent:o}});return r.dispatchEvent(a),r}};var x=b.on;x.emit=b.emit;var m=l.the,$=l._t,A=l.route;m.t=$;m.route=A;m.form=l.form;m.flat=l.flat;m.boot=l.boot;Object.defineProperty(m,"dictionary",{get:()=>l.dictionary,set:c=>{l.dictionary=c}});Object.defineProperty(m,"locale",{get:()=>l.locale,set:c=>{l.locale=c}});var v=h.$;v.clone=h.clone;var S=h.$$,T={on:x,the:m,$:v,$$:S,_t:$};export{v as $,S as $$,$ as _t,T as default,x as on,A as route,m as the};
1
+ var w=class{static on(t,e,o,n){let r=typeof t=="string"?document.querySelector(t):t||document.body,a=i=>{let s=i.target.closest(o);s&&r.contains(s)&&n.call(s,i,s)};return r.addEventListener(e,a),()=>r.removeEventListener(e,a)}static emit(t,e,o){let n=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:o});n.dispatchEvent(r)}};var l=class c{static dictionary={};static locale=typeof navigator<"u"?navigator.language:"en-US";static prefix="otm:";static ephemeralKeys=new Set;static the(...t){if(t.length===0)throw new TypeError("the(): missing args");return t[0]instanceof Element?c.#e(t[0],t.slice(1)):c.#e(document.body,t)}static#e(t,e){let[o,n]=e,r=t===document.body;if(e.length===1&&typeof o=="string")return c.#o(t,o);if(e.length===2&&typeof o=="string"){if(typeof n>"u")throw new TypeError(`the(${JSON.stringify(o)}, undefined): val is required for set`);return c.#t(t,o,n),r&&!c.ephemeralKeys.has(o)&&localStorage.setItem(`${c.prefix}${o}`,n),t}if(e.length===1&&o?.constructor===Object){for(let[a,i]of Object.entries(o))c.#t(t,a,i),r&&!c.ephemeralKeys.has(a)&&localStorage.setItem(`${c.prefix}${a}`,i);return t}throw new TypeError(`the(): unrecognized call shape (${e.map(a=>typeof a).join(", ")})`)}static flat(t,e="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let o={},n=(r,a)=>{if(r===null||typeof r!="object"){o[a]=r;return}for(let[i,s]of Object.entries(r))n(s,a?`${a}${e}${i}`:i)};return n(t,""),o}static form(t){let e={},o=t.querySelectorAll("input, select, textarea");for(let n of o){let r=n.name;if(!r||n.disabled)continue;let a=(n.type||"text").toLowerCase();if(a==="submit"||a==="button"||a==="reset"||(a==="checkbox"||a==="radio")&&!(n.checked??n.hasAttribute("checked")))continue;let i=n.value??n.getAttribute("value")??"";c.#n(e,r,i)}return e}static#n(t,e,o){let n=e.split(/[\[\]]/).filter(Boolean),r=t;for(let a=0;a<n.length;a++){let i=n[a];a===n.length-1?r[i]!==void 0?(Array.isArray(r[i])||(r[i]=[r[i]]),r[i].push(o)):r[i]=o:(r[i]=r[i]||{},r=r[i])}}static _t(t,e={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let a=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let i of a)if(i.hasAttribute?.("data-i18n")){let s=i.getAttribute("data-i18n"),p={};for(let y of i.attributes)if(y.name.startsWith("data-i18n-")&&y.name!=="data-i18n-type"){let x=y.name.replace("data-i18n-","");p[x]=y.value}let h=i.getAttribute("data-i18n-type");i.textContent=c._t(s,{...p,type:h})}return t}if(!t)return c._t(document.body),"";let n=c.dictionary[t];if(!n){if(e.val!==void 0&&(e.type==="currency"||e.type==="date")){let a=e.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):new Intl.DateTimeFormat(c.locale);return e.type==="date"?a.format(new Date(e.val)):a.format(Number(e.val))}return t}if(typeof n=="object"){let a=e.qty!==void 0?Number(e.qty):0,i=new Intl.PluralRules(c.locale).select(a);n=n[i]||n.other||t}if(typeof n!="string")return t;let r=n;for(let[a,i]of Object.entries(e)){let s=i;if(a==="val"&&e.type){let p=Number(i),h=e.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):e.type==="date"?new Intl.DateTimeFormat(c.locale):new Intl.NumberFormat(c.locale);s=e.type==="date"?h.format(new Date(i)):h.format(p)}r=r.replace(`{${a}}`,s)}return r}static route(t){if(typeof window>"u")return;let e=()=>t(window.location.pathname,window.location.search,window.location.hash);window.addEventListener("popstate",e),window.addEventListener("hashchange",e),document.addEventListener("click",o=>{let n=o.target.closest("a");if(!n||!n.getAttribute("href")||n.hasAttribute("data-external")||n.target==="_blank")return;let r=new URL(n.getAttribute("href"),window.location.href);r.origin===window.location.origin&&(r.pathname===window.location.pathname&&r.search===window.location.search&&r.hash||(o.preventDefault(),window.history.pushState({},"",r.href),e()))}),e()}static#o(t,e){let n={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[e]||`data-${e}`;return t.getAttribute(n)}static#t(t,e,o){let r={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[e]||`data-${e}`,a=typeof o=="boolean"?o?"true":"false":o;if(t.setAttribute(r,a),t.querySelectorAll){let i=t.querySelectorAll(`[data-text="${e}"]`);for(let s of i)s.textContent=a}t.getAttribute?.("data-text")===e&&(t.textContent=a)}static async boot({signal:t,locales:e,dictionary:o,namespace:n,defaultLocale:r,ephemeralKeys:a}={}){n&&(c.prefix=`${n}:`),a&&(c.ephemeralKeys=new Set(a));let i=typeof window<"u"&&window.location?window.location.search:"",s=new URLSearchParams(i),p=(typeof navigator<"u"?navigator.language:null)||document.documentElement.lang||"en";c.locale=s.get("lang")||localStorage.getItem(`${c.prefix}lang`)||p;let h=r||document.documentElement.lang,y=c.locale.toLowerCase().split("-")[0],x=h?.toLowerCase().split("-")[0],A=!!x&&y===x;if(!A)if(o)c.dictionary=o;else{let u=document.querySelector('meta[name="i18n"]'),b=e||u?.getAttribute("content");if(b){let g=u?.getAttribute("data-fallback")||"en",$=(u?.getAttribute("data-available")||"").split(",").map(f=>f.trim().toLowerCase()),v=c.locale.toLowerCase(),E=v.split("-")[0],S=g;$.includes(v)?S=v:$.includes(E)&&(S=E);try{let f=await fetch(`${b}/${S}.json`,{signal:t});f.ok&&(c.dictionary=await f.json())}catch(f){if(t?.aborted)throw f;console.warn("otm: i18n fetch failed",f)}}}for(let u=0;u<localStorage.length;u++){let b=localStorage.key(u);if(b.startsWith(c.prefix)){let g=b.slice(c.prefix.length);if(g!=="lang"&&!c.ephemeralKeys.has(g)){let $=localStorage.getItem(b);c.#t(document.body,g,$)}}}A||c._t()}};var m=class{static $(t,e){let o=t,n=e;return typeof o=="string"&&(n=o,o=document),o.querySelector(n)}static $$(t,e){let o=t,n=e;return typeof o=="string"&&(n=o,o=document),Array.from(o.querySelectorAll(n))}static clone(t,e){let o=typeof t=="string"?document.querySelector(t):t,n=document.querySelector(e);if(!o)throw new Error(`Parent not found: ${t}`);if(!n)throw new Error(`Template not found: ${e}`);let r=n.content.cloneNode(!0).firstElementChild;l._t(r),o.appendChild(r);let a=new CustomEvent("mounted",{bubbles:!0,detail:{parent:o}});return r.dispatchEvent(a),r}};var q=w.on;q.emit=w.emit;var d=l.the,L=l._t,j=l.route;d.t=L;d.route=j;d.form=l.form;d.flat=l.flat;d.boot=l.boot;Object.defineProperty(d,"dictionary",{get:()=>l.dictionary,set:c=>{l.dictionary=c}});Object.defineProperty(d,"locale",{get:()=>l.locale,set:c=>{l.locale=c}});var N=m.$;N.clone=m.clone;var C=m.$$,P={on:q,the:d,$:N,$$:C,_t:L};export{N as $,C as $$,L as _t,P as default,q as on,j as route,d as the};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "on_the_money",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Opinionated, attribute-driven, standards-oriented modern framework. <2KB gzip. Native browser APIs only.",
5
5
  "type": "module",
6
6
  "main": "dist/on_the_money.min.js",
package/src/core/The.js CHANGED
@@ -3,6 +3,7 @@ export default class The {
3
3
  static locale =
4
4
  typeof navigator !== "undefined" ? navigator.language : "en-US";
5
5
  static prefix = "otm:";
6
+ static ephemeralKeys = new Set();
6
7
 
7
8
  static the(...args) {
8
9
  if (args.length === 0) {
@@ -28,13 +29,15 @@ export default class The {
28
29
  );
29
30
  }
30
31
  The.#set(el, a, b);
31
- if (isGlobal) localStorage.setItem(`${The.prefix}${a}`, b);
32
+ if (isGlobal && !The.ephemeralKeys.has(a))
33
+ localStorage.setItem(`${The.prefix}${a}`, b);
32
34
  return el;
33
35
  }
34
36
  if (args.length === 1 && a?.constructor === Object) {
35
37
  for (const [k, v] of Object.entries(a)) {
36
38
  The.#set(el, k, v);
37
- if (isGlobal) localStorage.setItem(`${The.prefix}${k}`, v);
39
+ if (isGlobal && !The.ephemeralKeys.has(k))
40
+ localStorage.setItem(`${The.prefix}${k}`, v);
38
41
  }
39
42
  return el;
40
43
  }
@@ -137,7 +140,24 @@ export default class The {
137
140
  }
138
141
 
139
142
  let entry = The.dictionary[key];
140
- if (!entry) return key;
143
+ if (!entry) {
144
+ if (
145
+ options.val !== undefined &&
146
+ (options.type === "currency" || options.type === "date")
147
+ ) {
148
+ const fmt =
149
+ options.type === "currency"
150
+ ? new Intl.NumberFormat(The.locale, {
151
+ style: "currency",
152
+ currency: "USD",
153
+ })
154
+ : new Intl.DateTimeFormat(The.locale);
155
+ return options.type === "date"
156
+ ? fmt.format(new Date(options.val))
157
+ : fmt.format(Number(options.val));
158
+ }
159
+ return key;
160
+ }
141
161
 
142
162
  if (typeof entry === "object") {
143
163
  const qty = options.qty !== undefined ? Number(options.qty) : 0;
@@ -244,8 +264,16 @@ export default class The {
244
264
  if (el.getAttribute?.("data-text") === key) el.textContent = out;
245
265
  }
246
266
 
247
- static async boot({ signal, locales, dictionary, namespace } = {}) {
267
+ static async boot({
268
+ signal,
269
+ locales,
270
+ dictionary,
271
+ namespace,
272
+ defaultLocale,
273
+ ephemeralKeys,
274
+ } = {}) {
248
275
  if (namespace) The.prefix = `${namespace}:`;
276
+ if (ephemeralKeys) The.ephemeralKeys = new Set(ephemeralKeys);
249
277
  const search =
250
278
  typeof window !== "undefined" && window.location
251
279
  ? window.location.search
@@ -261,30 +289,37 @@ export default class The {
261
289
  localStorage.getItem(`${The.prefix}lang`) ||
262
290
  browserLoc;
263
291
 
264
- if (dictionary) {
265
- The.dictionary = dictionary;
266
- } else {
267
- const meta = document.querySelector('meta[name="i18n"]');
268
- const path = locales || meta?.getAttribute("content");
269
- if (path) {
270
- const fallback = meta?.getAttribute("data-fallback") || "en";
271
- const available = (meta?.getAttribute("data-available") || "")
272
- .split(",")
273
- .map((s) => s.trim().toLowerCase());
274
-
275
- const full = The.locale.toLowerCase();
276
- const base = full.split("-")[0];
277
-
278
- let target = fallback;
279
- if (available.includes(full)) target = full;
280
- else if (available.includes(base)) target = base;
281
-
282
- try {
283
- const res = await fetch(`${path}/${target}.json`, { signal });
284
- if (res.ok) The.dictionary = await res.json();
285
- } catch (e) {
286
- if (signal?.aborted) throw e;
287
- console.warn("otm: i18n fetch failed", e);
292
+ const sourceLang = defaultLocale || document.documentElement.lang;
293
+ const localeBase = The.locale.toLowerCase().split("-")[0];
294
+ const sourceBase = sourceLang?.toLowerCase().split("-")[0];
295
+ const skipI18n = Boolean(sourceBase) && localeBase === sourceBase;
296
+
297
+ if (!skipI18n) {
298
+ if (dictionary) {
299
+ The.dictionary = dictionary;
300
+ } else {
301
+ const meta = document.querySelector('meta[name="i18n"]');
302
+ const path = locales || meta?.getAttribute("content");
303
+ if (path) {
304
+ const fallback = meta?.getAttribute("data-fallback") || "en";
305
+ const available = (meta?.getAttribute("data-available") || "")
306
+ .split(",")
307
+ .map((s) => s.trim().toLowerCase());
308
+
309
+ const full = The.locale.toLowerCase();
310
+ const base = full.split("-")[0];
311
+
312
+ let target = fallback;
313
+ if (available.includes(full)) target = full;
314
+ else if (available.includes(base)) target = base;
315
+
316
+ try {
317
+ const res = await fetch(`${path}/${target}.json`, { signal });
318
+ if (res.ok) The.dictionary = await res.json();
319
+ } catch (e) {
320
+ if (signal?.aborted) throw e;
321
+ console.warn("otm: i18n fetch failed", e);
322
+ }
288
323
  }
289
324
  }
290
325
  }
@@ -293,13 +328,13 @@ export default class The {
293
328
  const fullKey = localStorage.key(i);
294
329
  if (fullKey.startsWith(The.prefix)) {
295
330
  const key = fullKey.slice(The.prefix.length);
296
- if (key !== "lang") {
331
+ if (key !== "lang" && !The.ephemeralKeys.has(key)) {
297
332
  const val = localStorage.getItem(fullKey);
298
333
  The.#set(document.body, key, val);
299
334
  }
300
335
  }
301
336
  }
302
337
 
303
- The._t();
338
+ if (!skipI18n) The._t();
304
339
  }
305
340
  }