on_the_money 0.3.0 → 0.3.2

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.2] — 2026-05-20
8
+
9
+ ### Added
10
+
11
+ - **`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)
12
+ - **localStorage state replay** still runs in the short-circuit path; only the i18n machinery is skipped.
13
+
14
+ ### Changed
15
+
16
+ - **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.
17
+
18
+ ## [0.3.1] — 2026-05-19
19
+
20
+ ### Fixed
21
+
22
+ - **README lint-stack tables** no longer refer to `eslint-plugin-otm` and `stylelint-plugin-otm` as if they were standalone packages. They ship bundled inside `on_the_money` via subpath exports; the rule-source columns now read "bundled in `on_the_money`". Anyone following the README literally was tempted to `npm install -D eslint-plugin-otm`, which 404s. (#49)
23
+
24
+ ### Changed
25
+
26
+ - **HTML-004 diagnostic** now hints toward `<dl>/<dt>/<dd>` for label/value patterns. The full message reads: "Naked strings in HTML are forbidden. Use data-i18n or wrap in a semantic tag (for label/value pairs, prefer `<dl>`/`<dt>`/`<dd>`)." Pushes consumers toward the semantically meaningful restructure rather than a cosmetic `<span>` wrap. (#50)
27
+
7
28
  ## [0.3.0] — 2026-05-18
8
29
 
9
30
  ### Breaking
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,26 @@ 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. |
156
157
 
157
158
  Boot sequence:
158
159
  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]`.
160
+ 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.
161
+ 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`.
162
+ 4. Replay `localStorage` entries matching the prefix (except `${prefix}lang`) back onto body `data-*` and `[data-text]`.
163
+ 5. Run `_t()` to hydrate `[data-i18n]`.
164
+
165
+ **Writing `data-i18n` elements:** always include the source-language text inside as a fallback:
166
+
167
+ ```html
168
+ <!-- good — SEO fallback, no-JS fallback, no-flash default -->
169
+ <h1 data-i18n="title">Welcome to the app</h1>
170
+
171
+ <!-- avoid — page is blank until hydration -->
172
+ <h1 data-i18n="title"></h1>
173
+ ```
174
+
175
+ 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
176
 
163
177
  ### `_t(key, options)` — `Intl` localization
164
178
 
@@ -175,11 +189,11 @@ _t(); // hydrate document.body
175
189
 
176
190
  Missing dictionary keys preserve existing `textContent` (SEO fallback).
177
191
 
178
- `[data-i18n]` element binding:
192
+ `[data-i18n]` element binding (always include source-language fallback text inside):
179
193
 
180
194
  ```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>
195
+ <span data-i18n="cart_items" data-i18n-qty="3">3 items</span>
196
+ <span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency">$9.99</span>
183
197
  ```
184
198
 
185
199
  ### `route(callback)` — pushState router
@@ -306,10 +320,12 @@ The framework reads existing attributes via `the(key)` without modification, so
306
320
 
307
321
  on_the_money ships a three-tool stack. Each layer covers what the others can't.
308
322
 
309
- ### 1. JavaScript — ESLint + `eslint-plugin-otm`
323
+ ### 1. JavaScript — ESLint + bundled plugin
324
+
325
+ The ESLint plugin and config ship inside `on_the_money` itself. No separate `eslint-plugin-otm` package — import via subpath.
310
326
 
311
327
  ```bash
312
- npm install -D eslint
328
+ npm install -D eslint eslint-plugin-no-unsanitized
313
329
  ```
314
330
 
315
331
  ```javascript
@@ -325,15 +341,17 @@ export default [
325
341
 
326
342
  | Rule | Source | Behavior |
327
343
  | --- | --- | --- |
328
- | `otm/prefer-on` | eslint-plugin-otm | Ban `addEventListener`; use `on()`. |
329
- | `otm/prefer-the-set` | eslint-plugin-otm | Ban `textContent`/`innerText`/`nodeValue` assignment. |
330
- | `otm/flat-state` | eslint-plugin-otm | Ban nested objects/arrays in `the()` calls. |
331
- | `otm/prefer-submit` | eslint-plugin-otm | Warn on `on(btn, "click", ...)` for form data. |
332
- | `otm/no-style-mutation` | eslint-plugin-otm | Ban `el.style.* = ...`. |
333
- | `no-unsanitized/no-inner-html` | external | Ban `innerHTML`/`outerHTML`. |
334
- | `no-unsanitized/method` | external | Ban `document.write`, `insertAdjacentHTML`. |
344
+ | `otm/prefer-on` | bundled in `on_the_money` | Ban `addEventListener`; use `on()`. |
345
+ | `otm/prefer-the-set` | bundled in `on_the_money` | Ban `textContent`/`innerText`/`nodeValue` assignment. |
346
+ | `otm/flat-state` | bundled in `on_the_money` | Ban nested objects/arrays in `the()` calls. |
347
+ | `otm/prefer-submit` | bundled in `on_the_money` | Warn on `on(btn, "click", ...)` for form data. |
348
+ | `otm/no-style-mutation` | bundled in `on_the_money` | Ban `el.style.* = ...`. |
349
+ | `no-unsanitized/no-inner-html` | `eslint-plugin-no-unsanitized` | Ban `innerHTML`/`outerHTML`. |
350
+ | `no-unsanitized/method` | `eslint-plugin-no-unsanitized` | Ban `document.write`, `insertAdjacentHTML`. |
351
+
352
+ ### 2. CSS — Stylelint + bundled plugin
335
353
 
336
- ### 2. CSS Stylelint + `stylelint-plugin-otm`
354
+ Same pattern: the Stylelint plugin and config ship inside `on_the_money`. No separate `stylelint-plugin-otm` package.
337
355
 
338
356
  ```bash
339
357
  npm install -D stylelint stylelint-config-standard
@@ -351,7 +369,7 @@ export default {
351
369
 
352
370
  | Rule | Source | Behavior |
353
371
  | --- | --- | --- |
354
- | `otm/prefer-attribute-selector` | stylelint-plugin-otm | Ban `.class` selectors; use `[data-state="..."]`. |
372
+ | `otm/prefer-attribute-selector` | bundled in `on_the_money` | Ban `.class` selectors; use `[data-state="..."]`. |
355
373
  | `declaration-no-important` | stylelint built-in | Ban `!important`. |
356
374
 
357
375
  ### 3. HTML / cross-file — `otm-lint`
@@ -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,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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "on_the_money",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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
@@ -244,7 +244,13 @@ export default class The {
244
244
  if (el.getAttribute?.("data-text") === key) el.textContent = out;
245
245
  }
246
246
 
247
- static async boot({ signal, locales, dictionary, namespace } = {}) {
247
+ static async boot({
248
+ signal,
249
+ locales,
250
+ dictionary,
251
+ namespace,
252
+ defaultLocale,
253
+ } = {}) {
248
254
  if (namespace) The.prefix = `${namespace}:`;
249
255
  const search =
250
256
  typeof window !== "undefined" && window.location
@@ -261,30 +267,37 @@ export default class The {
261
267
  localStorage.getItem(`${The.prefix}lang`) ||
262
268
  browserLoc;
263
269
 
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);
270
+ const sourceLang = defaultLocale || document.documentElement.lang;
271
+ const localeBase = The.locale.toLowerCase().split("-")[0];
272
+ const sourceBase = sourceLang?.toLowerCase().split("-")[0];
273
+ const skipI18n = Boolean(sourceBase) && localeBase === sourceBase;
274
+
275
+ if (!skipI18n) {
276
+ if (dictionary) {
277
+ The.dictionary = dictionary;
278
+ } else {
279
+ const meta = document.querySelector('meta[name="i18n"]');
280
+ const path = locales || meta?.getAttribute("content");
281
+ if (path) {
282
+ const fallback = meta?.getAttribute("data-fallback") || "en";
283
+ const available = (meta?.getAttribute("data-available") || "")
284
+ .split(",")
285
+ .map((s) => s.trim().toLowerCase());
286
+
287
+ const full = The.locale.toLowerCase();
288
+ const base = full.split("-")[0];
289
+
290
+ let target = fallback;
291
+ if (available.includes(full)) target = full;
292
+ else if (available.includes(base)) target = base;
293
+
294
+ try {
295
+ const res = await fetch(`${path}/${target}.json`, { signal });
296
+ if (res.ok) The.dictionary = await res.json();
297
+ } catch (e) {
298
+ if (signal?.aborted) throw e;
299
+ console.warn("otm: i18n fetch failed", e);
300
+ }
288
301
  }
289
302
  }
290
303
  }
@@ -300,6 +313,6 @@ export default class The {
300
313
  }
301
314
  }
302
315
 
303
- The._t();
316
+ if (!skipI18n) The._t();
304
317
  }
305
318
  }
@@ -92,7 +92,7 @@ export default class Linter {
92
92
  file,
93
93
  { line: loc.startLine, column: loc.startCol },
94
94
  "HTML-004",
95
- "Naked strings in HTML are forbidden. Use data-i18n or wrap in a semantic tag.",
95
+ "Naked strings in HTML are forbidden. Use data-i18n or wrap in a semantic tag (for label/value pairs, prefer <dl>/<dt>/<dd>).",
96
96
  );
97
97
  }
98
98
  }