on_the_money 0.5.2 → 0.5.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 +1 -1
- package/dist/on_the_money.min.js +1 -1
- package/package.json +1 -1
- package/src/core/The.js +6 -1
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.5.3] — 2026-05-28
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`the.boot()` locale resolution now prefers `<html lang>` over `navigator.language`.** When a server deliberately renders `<html lang="es">`, that's an explicit signal about the document's language — the server may have already considered the visitor's cookies, URL path, or preferences when choosing it. `navigator.language` was previously winning and overriding the server's render with the browser's passive default, causing OTM to flash server-rendered Spanish text back to English on every navigation. Resolution chain is now: `?lang=` → `localStorage["${prefix}lang"]` → `document.documentElement.lang` → `navigator.language` → `"en"`. (#82)
|
|
12
|
+
|
|
13
|
+
### Behavior change worth noting
|
|
14
|
+
|
|
15
|
+
Static SPAs that hardcoded `<html lang="en">` and relied on the framework to detect the visitor's browser language are affected. The framework now treats the `lang` attribute as authoritative. Consumers wanting navigator-driven detection should leave `<html lang>` empty or omit the attribute — then the chain falls through to navigator. This was always the more honest setup; the previous behavior papered over the SPA author's hardcoded `lang` claim.
|
|
16
|
+
|
|
7
17
|
## [0.5.2] — 2026-05-23
|
|
8
18
|
|
|
9
19
|
### Documentation
|
package/README.md
CHANGED
|
@@ -163,7 +163,7 @@ await the.boot({ signal, locales, dictionary, namespace, defaultLocale, persistK
|
|
|
163
163
|
| `persistKeys` | `string[]` | Global state keys that **should** persist to `localStorage`. Default: empty — nothing persists. Writes still update the body attribute and any `[data-text]` mirrors regardless. Boot replay rehydrates only these keys. Use for stable preferences: `["theme", "lang"]`. Scoped state (`the(el, ...)`) never persists regardless. |
|
|
164
164
|
|
|
165
165
|
Boot sequence:
|
|
166
|
-
1. Resolve locale: `?lang=` query → `localStorage["${prefix}lang"]` → `navigator.language`. Writes `the.locale`.
|
|
166
|
+
1. Resolve locale: `?lang=` query → `localStorage["${prefix}lang"]` → `document.documentElement.lang` → `navigator.language` → `"en"`. Writes `the.locale`. **`<html lang>` outranks `navigator.language`** — the server's deliberate language declaration wins over the browser's passive preference. If you want navigator-driven detection (static SPA with no server-side i18n), leave `<html lang>` empty or omit the attribute and the chain falls through to navigator.
|
|
167
167
|
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.
|
|
168
168
|
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`.
|
|
169
169
|
4. Replay `localStorage` entries matching the prefix (except `${prefix}lang`) back onto body `data-*` and `[data-text]`.
|
package/dist/on_the_money.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var b=class{static on(t,e,a,n){let r=typeof t=="string"?document.querySelector(t):t||document.body,o=i=>{let s=i.target.closest(a);s&&r.contains(s)&&n.call(s,i,s)};return r.addEventListener(e,o),()=>r.removeEventListener(e,o)}static emit(t,e,a){let n=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:a});n.dispatchEvent(r)}};var l=class c{static dictionary={};static locale=typeof navigator<"u"?navigator.language:"en-US";static prefix="otm:";static persistKeys=new Set;static#t(t){return t.replace(/_/g,"-").replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}static the(...t){if(t.length===0)throw new TypeError("the(): missing args");return t[0]instanceof Element?c.#n(t[0],t.slice(1)):c.#n(document.body,t)}static#n(t,e){let[a,n]=e,r=t===document.body;if(e.length===1&&typeof a=="string")return c.#a(t,c.#t(a));if(e.length===2&&typeof a=="string"){if(typeof n>"u")throw new TypeError(`the(${JSON.stringify(a)}, undefined): val is required for set`);let o=c.#t(a);return c.#e(t,o,n),r&&c.persistKeys.has(o)&&localStorage.setItem(`${c.prefix}${o}`,n),t}if(e.length===1&&a?.constructor===Object){for(let[o,i]of Object.entries(a)){let s=c.#t(o);c.#e(t,s,i),r&&c.persistKeys.has(s)&&localStorage.setItem(`${c.prefix}${s}`,i)}return t}throw new TypeError(`the(): unrecognized call shape (${e.map(o=>typeof o).join(", ")})`)}static match(t,e){let a=e??(typeof window<"u"&&window.location?window.location.pathname:""),n=[],o=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(d,u)=>(n.push(u),"([^/]+)")),i=a.match(new RegExp(`^${o}$`));if(!i)return null;let s={};return n.forEach((d,u)=>{s[d]=decodeURIComponent(i[u+1])}),s}static flat(t,e="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let a={},n=(r,o)=>{if(r===null||typeof r!="object"){a[o]=r;return}for(let[i,s]of Object.entries(r))n(s,o?`${o}${e}${i}`:i)};return n(t,""),a}static form(t){let e={},a=t.querySelectorAll("input, select, textarea");for(let n of a){let r=n.name;if(!r||n.disabled)continue;let o=(n.type||"text").toLowerCase();if(o==="submit"||o==="button"||o==="reset"||(o==="checkbox"||o==="radio")&&!(n.checked??n.hasAttribute("checked")))continue;let i=n.value??n.getAttribute("value")??"";c.#r(e,r,i)}return e}static#r(t,e,a){let n=e.split(/[\[\]]/).filter(Boolean),r=t;for(let o=0;o<n.length;o++){let i=n[o];o===n.length-1?r[i]!==void 0?(Array.isArray(r[i])||(r[i]=[r[i]]),r[i].push(a)):r[i]=a:(r[i]=r[i]||{},r=r[i])}}static _t(t,e={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let o=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let i of o)if(i.hasAttribute?.("data-i18n")){let s=i.getAttribute("data-i18n"),d={};for(let y of i.attributes)if(y.name.startsWith("data-i18n-")&&y.name!=="data-i18n-type"){let $=y.name.replace("data-i18n-","");d[$]=y.value}let u=i.getAttribute("data-i18n-type");i.textContent=c._t(s,{...d,type:u})}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 o=e.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):new Intl.DateTimeFormat(c.locale);return e.type==="date"?o.format(new Date(e.val)):o.format(Number(e.val))}return t}if(typeof n=="object"){let o=e.qty!==void 0?Number(e.qty):0,i=new Intl.PluralRules(c.locale).select(o);n=n[i]||n.other||t}if(typeof n!="string")return t;let r=n;for(let[o,i]of Object.entries(e)){let s=i;if(o==="val"&&e.type){let d=Number(i),u=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"?u.format(new Date(i)):u.format(d)}r=r.replace(`{${o}}`,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",a=>{let n=a.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||(a.preventDefault(),window.history.pushState({},"",r.href),e()))}),e()}static#o={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"};static#a(t,e){let a=c.#o[e]||`data-${e}`;return t.getAttribute(a)}static#e(t,e,a){let n=c.#o[e]||`data-${e}`,r=typeof a=="boolean"?a?"true":"false":a;t.setAttribute(n,r);let o=t===document.body&&typeof document<"u"?document.documentElement||document:t;if(o.querySelectorAll){let i=o.querySelectorAll(`[data-text="${e}"]`);for(let s of i)s.textContent=r}t.getAttribute?.("data-text")===e&&(t.textContent=r)}static async boot({signal:t,locales:e,dictionary:a,namespace:n,defaultLocale:r,persistKeys:o}={}){n&&(c.prefix=`${n}:`),o&&(c.persistKeys=new Set(o.map(f=>c.#t(f))));let i=typeof window<"u"&&window.location?window.location.search:"",s=new URLSearchParams(i),d=(typeof navigator<"u"?navigator.language:null)||
|
|
1
|
+
var b=class{static on(t,e,a,n){let r=typeof t=="string"?document.querySelector(t):t||document.body,o=i=>{let s=i.target.closest(a);s&&r.contains(s)&&n.call(s,i,s)};return r.addEventListener(e,o),()=>r.removeEventListener(e,o)}static emit(t,e,a){let n=typeof t=="string"?document.querySelector(t):t,r=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:a});n.dispatchEvent(r)}};var l=class c{static dictionary={};static locale=typeof navigator<"u"?navigator.language:"en-US";static prefix="otm:";static persistKeys=new Set;static#t(t){return t.replace(/_/g,"-").replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}static the(...t){if(t.length===0)throw new TypeError("the(): missing args");return t[0]instanceof Element?c.#n(t[0],t.slice(1)):c.#n(document.body,t)}static#n(t,e){let[a,n]=e,r=t===document.body;if(e.length===1&&typeof a=="string")return c.#a(t,c.#t(a));if(e.length===2&&typeof a=="string"){if(typeof n>"u")throw new TypeError(`the(${JSON.stringify(a)}, undefined): val is required for set`);let o=c.#t(a);return c.#e(t,o,n),r&&c.persistKeys.has(o)&&localStorage.setItem(`${c.prefix}${o}`,n),t}if(e.length===1&&a?.constructor===Object){for(let[o,i]of Object.entries(a)){let s=c.#t(o);c.#e(t,s,i),r&&c.persistKeys.has(s)&&localStorage.setItem(`${c.prefix}${s}`,i)}return t}throw new TypeError(`the(): unrecognized call shape (${e.map(o=>typeof o).join(", ")})`)}static match(t,e){let a=e??(typeof window<"u"&&window.location?window.location.pathname:""),n=[],o=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g,(d,u)=>(n.push(u),"([^/]+)")),i=a.match(new RegExp(`^${o}$`));if(!i)return null;let s={};return n.forEach((d,u)=>{s[d]=decodeURIComponent(i[u+1])}),s}static flat(t,e="_"){if(t===null||typeof t!="object")throw new TypeError("the.flat: input must be an object");let a={},n=(r,o)=>{if(r===null||typeof r!="object"){a[o]=r;return}for(let[i,s]of Object.entries(r))n(s,o?`${o}${e}${i}`:i)};return n(t,""),a}static form(t){let e={},a=t.querySelectorAll("input, select, textarea");for(let n of a){let r=n.name;if(!r||n.disabled)continue;let o=(n.type||"text").toLowerCase();if(o==="submit"||o==="button"||o==="reset"||(o==="checkbox"||o==="radio")&&!(n.checked??n.hasAttribute("checked")))continue;let i=n.value??n.getAttribute("value")??"";c.#r(e,r,i)}return e}static#r(t,e,a){let n=e.split(/[\[\]]/).filter(Boolean),r=t;for(let o=0;o<n.length;o++){let i=n[o];o===n.length-1?r[i]!==void 0?(Array.isArray(r[i])||(r[i]=[r[i]]),r[i].push(a)):r[i]=a:(r[i]=r[i]||{},r=r[i])}}static _t(t,e={}){if(typeof Node<"u"?t instanceof Node:t?.nodeType){let o=t.querySelectorAll?[t,...t.querySelectorAll("[data-i18n]")]:[t];for(let i of o)if(i.hasAttribute?.("data-i18n")){let s=i.getAttribute("data-i18n"),d={};for(let y of i.attributes)if(y.name.startsWith("data-i18n-")&&y.name!=="data-i18n-type"){let $=y.name.replace("data-i18n-","");d[$]=y.value}let u=i.getAttribute("data-i18n-type");i.textContent=c._t(s,{...d,type:u})}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 o=e.type==="currency"?new Intl.NumberFormat(c.locale,{style:"currency",currency:"USD"}):new Intl.DateTimeFormat(c.locale);return e.type==="date"?o.format(new Date(e.val)):o.format(Number(e.val))}return t}if(typeof n=="object"){let o=e.qty!==void 0?Number(e.qty):0,i=new Intl.PluralRules(c.locale).select(o);n=n[i]||n.other||t}if(typeof n!="string")return t;let r=n;for(let[o,i]of Object.entries(e)){let s=i;if(o==="val"&&e.type){let d=Number(i),u=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"?u.format(new Date(i)):u.format(d)}r=r.replace(`{${o}}`,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",a=>{let n=a.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||(a.preventDefault(),window.history.pushState({},"",r.href),e()))}),e()}static#o={expanded:"aria-expanded",selected:"aria-selected",hidden:"aria-hidden",checked:"aria-checked",disabled:"aria-disabled"};static#a(t,e){let a=c.#o[e]||`data-${e}`;return t.getAttribute(a)}static#e(t,e,a){let n=c.#o[e]||`data-${e}`,r=typeof a=="boolean"?a?"true":"false":a;t.setAttribute(n,r);let o=t===document.body&&typeof document<"u"?document.documentElement||document:t;if(o.querySelectorAll){let i=o.querySelectorAll(`[data-text="${e}"]`);for(let s of i)s.textContent=r}t.getAttribute?.("data-text")===e&&(t.textContent=r)}static async boot({signal:t,locales:e,dictionary:a,namespace:n,defaultLocale:r,persistKeys:o}={}){n&&(c.prefix=`${n}:`),o&&(c.persistKeys=new Set(o.map(f=>c.#t(f))));let i=typeof window<"u"&&window.location?window.location.search:"",s=new URLSearchParams(i),d=document.documentElement.lang||(typeof navigator<"u"?navigator.language:null)||"en";c.locale=s.get("lang")||localStorage.getItem(`${c.prefix}lang`)||d;let u=r||document.documentElement.lang,y=c.locale.toLowerCase().split("-")[0],$=u?.toLowerCase().split("-")[0],S=!!$&&y===$;if(!S)if(a)c.dictionary=a;else{let f=document.querySelector('meta[name="i18n"]'),w=e||f?.getAttribute("content");if(w){let g=f?.getAttribute("data-fallback")||"en",x=(f?.getAttribute("data-available")||"").split(",").map(p=>p.trim().toLowerCase()),v=c.locale.toLowerCase(),E=v.split("-")[0],A=g;x.includes(v)?A=v:x.includes(E)&&(A=E);try{let p=await fetch(`${w}/${A}.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 f=0;f<localStorage.length;f++){let w=localStorage.key(f);if(w.startsWith(c.prefix)){let g=w.slice(c.prefix.length);if(g!=="lang"&&c.persistKeys.has(g)){let x=localStorage.getItem(w);c.#e(document.body,g,x)}}}S||c._t()}};var h=class{static $(t,e){let a=t,n=e;return typeof a=="string"&&(n=a,a=document),a.querySelector(n)}static $$(t,e){let a=t,n=e;return typeof a=="string"&&(n=a,a=document),Array.from(a.querySelectorAll(n))}static clone(t,e,{position:a="beforeend"}={}){let n=typeof t=="string"?document.querySelector(t):t,r=document.querySelector(e);if(!n)throw new Error(`Parent not found: ${t}`);if(!r)throw new Error(`Template not found: ${e}`);let o=r.content.cloneNode(!0).firstElementChild;l._t(o),n.insertAdjacentElement(a,o);let i=new CustomEvent("mounted",{bubbles:!0,detail:{parent:n}});return o.dispatchEvent(i),o}};var L=b.on;L.emit=b.emit;var m=l.the,q=l._t,N=l.route;m.t=q;m.route=N;m.form=l.form;m.flat=l.flat;m.match=l.match;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 j=h.$;j.clone=h.clone;var C=h.$$,F={on:L,the:m,$:j,$$:C,_t:q};export{j as $,C as $$,q as _t,F as default,L as on,N as route,m as the};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "on_the_money",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Opinionated, attribute-driven, standards-oriented modern framework. <2KB gzip. Native browser APIs only. See README.md for the authoring context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/on_the_money.min.js",
|
package/src/core/The.js
CHANGED
|
@@ -321,9 +321,14 @@ export default class The {
|
|
|
321
321
|
? window.location.search
|
|
322
322
|
: "";
|
|
323
323
|
const params = new URLSearchParams(search);
|
|
324
|
+
// <html lang> is the server's deliberate signal about the rendered document's
|
|
325
|
+
// language. It outranks navigator.language because the server may have
|
|
326
|
+
// already considered the visitor's preference when choosing it. Static SPAs
|
|
327
|
+
// that want navigator-driven locale detection should leave <html lang> empty
|
|
328
|
+
// or omit the attribute — then the chain falls through to navigator.
|
|
324
329
|
const browserLoc =
|
|
325
|
-
(typeof navigator !== "undefined" ? navigator.language : null) ||
|
|
326
330
|
document.documentElement.lang ||
|
|
331
|
+
(typeof navigator !== "undefined" ? navigator.language : null) ||
|
|
327
332
|
"en";
|
|
328
333
|
|
|
329
334
|
The.locale =
|