on_the_money 0.3.2 → 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,16 @@ 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
+
7
17
  ## [0.3.2] — 2026-05-20
8
18
 
9
19
  ### Added
package/README.md CHANGED
@@ -154,6 +154,7 @@ await the.boot({ signal, locales, dictionary, namespace, defaultLocale });
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
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. |
157
158
 
158
159
  Boot sequence:
159
160
  1. Resolve locale: `?lang=` query → `localStorage["${prefix}lang"]` → `navigator.language`. Writes `the.locale`.
@@ -187,7 +188,7 @@ _t(node); // hydrate every [data-i18n]
187
188
  _t(); // hydrate document.body
188
189
  ```
189
190
 
190
- 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.
191
192
 
192
193
  `[data-i18n]` element binding (always include source-language fallback text inside):
193
194
 
@@ -1 +1 @@
1
- var w=class{static on(t,o,n,e){let r=typeof t=="string"?document.querySelector(t):t||document.body,a=c=>{let s=c.target.closest(n);s&&r.contains(s)&&e.call(s,c,s)};return r.addEventListener(o,a),()=>r.removeEventListener(o,a)}static emit(t,o,n){let e=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(o,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(r)}};var l=class i{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?i.#e(t[0],t.slice(1)):i.#e(document.body,t)}static#e(t,o){let[n,e]=o,r=t===document.body;if(o.length===1&&typeof n=="string")return i.#n(t,n);if(o.length===2&&typeof n=="string"){if(typeof e>"u")throw new TypeError(`the(${JSON.stringify(n)}, undefined): val is required for set`);return i.#t(t,n,e),r&&localStorage.setItem(`${i.prefix}${n}`,e),t}if(o.length===1&&n?.constructor===Object){for(let[a,c]of Object.entries(n))i.#t(t,a,c),r&&localStorage.setItem(`${i.prefix}${a}`,c);return t}throw new TypeError(`the(): unrecognized call shape (${o.map(a=>typeof a).join(", ")})`)}static flat(t,o="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let n={},e=(r,a)=>{if(r===null||typeof r!="object"){n[a]=r;return}for(let[c,s]of Object.entries(r))e(s,a?`${a}${o}${c}`:c)};return e(t,""),n}static form(t){let o={},n=t.querySelectorAll("input, select, textarea");for(let e of n){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 c=e.value??e.getAttribute("value")??"";i.#o(o,r,c)}return o}static#o(t,o,n){let e=o.split(/[\[\]]/).filter(Boolean),r=t;for(let a=0;a<e.length;a++){let c=e[a];a===e.length-1?r[c]!==void 0?(Array.isArray(r[c])||(r[c]=[r[c]]),r[c].push(n)):r[c]=n:(r[c]=r[c]||{},r=r[c])}}static _t(t,o={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let a=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let c of a)if(c.hasAttribute?.("data-i18n")){let s=c.getAttribute("data-i18n"),h={};for(let u of c.attributes)if(u.name.startsWith("data-i18n-")&&u.name!=="data-i18n-type"){let g=u.name.replace("data-i18n-","");h[g]=u.value}let b=c.getAttribute("data-i18n-type");c.textContent=i._t(s,{...h,type:b})}return t}if(!t)return i._t(document.body),"";let e=i.dictionary[t];if(!e)return t;if(typeof e=="object"){let a=o.qty!==void 0?Number(o.qty):0,c=new Intl.PluralRules(i.locale).select(a);e=e[c]||e.other||t}if(typeof e!="string")return t;let r=e;for(let[a,c]of Object.entries(o)){let s=c;if(a==="val"&&o.type){let h=Number(c),b=o.type==="currency"?new Intl.NumberFormat(i.locale,{style:"currency",currency:"USD"}):o.type==="date"?new Intl.DateTimeFormat(i.locale):new Intl.NumberFormat(i.locale);s=o.type==="date"?b.format(new Date(c)):b.format(h)}r=r.replace(`{${a}}`,s)}return r}static route(t){if(typeof window>"u")return;let o=()=>t(window.location.pathname,window.location.search,window.location.hash);window.addEventListener("popstate",o),window.addEventListener("hashchange",o),document.addEventListener("click",n=>{let e=n.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||(n.preventDefault(),window.history.pushState({},"",r.href),o()))}),o()}static#n(t,o){let e={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[o]||`data-${o}`;return t.getAttribute(e)}static#t(t,o,n){let r={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"}[o]||`data-${o}`,a=typeof n=="boolean"?n?"true":"false":n;if(t.setAttribute(r,a),t.querySelectorAll){let c=t.querySelectorAll(`[data-text="${o}"]`);for(let s of c)s.textContent=a}t.getAttribute?.("data-text")===o&&(t.textContent=a)}static async boot({signal:t,locales:o,dictionary:n,namespace:e,defaultLocale:r}={}){e&&(i.prefix=`${e}:`);let a=typeof window<"u"&&window.location?window.location.search:"",c=new URLSearchParams(a),s=(typeof navigator<"u"?navigator.language:null)||document.documentElement.lang||"en";i.locale=c.get("lang")||localStorage.getItem(`${i.prefix}lang`)||s;let h=r||document.documentElement.lang,b=i.locale.toLowerCase().split("-")[0],u=h?.toLowerCase().split("-")[0],g=!!u&&b===u;if(!g)if(n)i.dictionary=n;else{let f=document.querySelector('meta[name="i18n"]'),y=o||f?.getAttribute("content");if(y){let x=f?.getAttribute("data-fallback")||"en",$=(f?.getAttribute("data-available")||"").split(",").map(m=>m.trim().toLowerCase()),v=i.locale.toLowerCase(),S=v.split("-")[0],A=x;$.includes(v)?A=v:$.includes(S)&&(A=S);try{let m=await fetch(`${y}/${A}.json`,{signal:t});m.ok&&(i.dictionary=await m.json())}catch(m){if(t?.aborted)throw m;console.warn("otm: i18n fetch failed",m)}}}for(let f=0;f<localStorage.length;f++){let y=localStorage.key(f);if(y.startsWith(i.prefix)){let x=y.slice(i.prefix.length);if(x!=="lang"){let $=localStorage.getItem(y);i.#t(document.body,x,$)}}}g||i._t()}};var p=class{static $(t,o){let n=t,e=o;return typeof n=="string"&&(e=n,n=document),n.querySelector(e)}static $$(t,o){let n=t,e=o;return typeof n=="string"&&(e=n,n=document),Array.from(n.querySelectorAll(e))}static clone(t,o){let n=typeof t=="string"?document.querySelector(t):t,e=document.querySelector(o);if(!n)throw new Error(`Parent not found: ${t}`);if(!e)throw new Error(`Template not found: ${o}`);let r=e.content.cloneNode(!0).firstElementChild;l._t(r),n.appendChild(r);let a=new CustomEvent("mounted",{bubbles:!0,detail:{parent:n}});return r.dispatchEvent(a),r}};var E=w.on;E.emit=w.emit;var d=l.the,q=l._t,j=l.route;d.t=q;d.route=j;d.form=l.form;d.flat=l.flat;d.boot=l.boot;Object.defineProperty(d,"dictionary",{get:()=>l.dictionary,set:i=>{l.dictionary=i}});Object.defineProperty(d,"locale",{get:()=>l.locale,set:i=>{l.locale=i}});var L=p.$;L.clone=p.clone;var C=p.$$,D={on:E,the:d,$:L,$$:C,_t:q};export{L as $,C as $$,q as _t,D as default,E as on,j as route,d 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.2",
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;
@@ -250,8 +270,10 @@ export default class The {
250
270
  dictionary,
251
271
  namespace,
252
272
  defaultLocale,
273
+ ephemeralKeys,
253
274
  } = {}) {
254
275
  if (namespace) The.prefix = `${namespace}:`;
276
+ if (ephemeralKeys) The.ephemeralKeys = new Set(ephemeralKeys);
255
277
  const search =
256
278
  typeof window !== "undefined" && window.location
257
279
  ? window.location.search
@@ -306,7 +328,7 @@ export default class The {
306
328
  const fullKey = localStorage.key(i);
307
329
  if (fullKey.startsWith(The.prefix)) {
308
330
  const key = fullKey.slice(The.prefix.length);
309
- if (key !== "lang") {
331
+ if (key !== "lang" && !The.ephemeralKeys.has(key)) {
310
332
  const val = localStorage.getItem(fullKey);
311
333
  The.#set(document.body, key, val);
312
334
  }