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 +10 -0
- package/README.md +2 -1
- package/dist/on_the_money.min.js +1 -1
- package/package.json +1 -1
- package/src/core/The.js +26 -4
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
|
|
package/dist/on_the_money.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var w=class{static on(t,o,n
|
|
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
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
|
|
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
|
|
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)
|
|
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
|
}
|