on_the_money 0.3.1 → 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 +11 -0
- package/README.md +23 -9
- package/dist/on_the_money.min.js +1 -1
- package/package.json +1 -1
- package/src/core/The.js +39 -26
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ 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
|
+
|
|
7
18
|
## [0.3.1] — 2026-05-19
|
|
8
19
|
|
|
9
20
|
### 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"
|
|
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"
|
|
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.
|
|
160
|
-
3.
|
|
161
|
-
4.
|
|
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"
|
|
182
|
-
<span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency"
|
|
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
|
package/dist/on_the_money.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
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
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({
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
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
|
}
|