A plain static `import ... from './config.js'` can't gracefully degrade if that file doesn't exist -- a missing module breaks the whole graph before any application code can catch it. So the four consumers (discovery.js, i18n.js, templates.js, router.js) now import BASE_URL/MARKED_URL/ARTICLES_URL from config-default.js instead, which tries a dynamic import() of the optional config.js (catchable, unlike a static import) and merges in whatever it exports, falling back to its own shipped defaults for anything config.js doesn't override -- or doesn't exist at all. Top-level await makes every importing module wait for that resolution first, same as a plain synchronous export. config.js itself is now just an optional, empty-by-default file: only export the specific constant(s) you want to change. Updated the README's "Local configuration" section (and its three cross-references in Running locally / Embedding in an existing page / Pointing articles/ somewhere else) to drop the now-unnecessary "copy this file before first use" step entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
54 lines
2.2 KiB
JavaScript
54 lines
2.2 KiB
JavaScript
// UI string lookup, so every user-facing message lives in a translatable
|
|
// JSON file (strings/{locale}.json) instead of being hardcoded in the
|
|
// JS. English (strings/en.json) is always fetched first as the
|
|
// fallback set; if the page's declared language (<html lang>) names another
|
|
// locale, that file is fetched too and merged on top — a translation file
|
|
// only needs to cover the keys it actually translates, missing ones still
|
|
// render in English rather than breaking.
|
|
import { BASE_URL } from './config-default.js';
|
|
|
|
const FALLBACK_LOCALE = 'en';
|
|
|
|
let strings = null;
|
|
let locale = FALLBACK_LOCALE;
|
|
|
|
async function fetchStrings(code) {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}strings/${code}.json`);
|
|
if (!response.ok) return null;
|
|
return await response.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Must be awaited before the first render — see router.js's `ready` chain —
|
|
// since t()/getLocale() below assume this has already resolved.
|
|
export async function loadStrings() {
|
|
const fallback = await fetchStrings(FALLBACK_LOCALE);
|
|
const requested = (document.documentElement.lang || FALLBACK_LOCALE).trim().toLowerCase().split('-')[0];
|
|
if (requested === FALLBACK_LOCALE || !requested) {
|
|
strings = fallback;
|
|
locale = FALLBACK_LOCALE;
|
|
return;
|
|
}
|
|
const translated = await fetchStrings(requested);
|
|
strings = translated ? { ...fallback, ...translated } : fallback;
|
|
locale = translated ? requested : FALLBACK_LOCALE;
|
|
}
|
|
|
|
// The resolved locale code (e.g. "en", "cs") — used for Intl.* calls
|
|
// elsewhere so dates/sorting/search match whatever language is displayed.
|
|
export function getLocale() {
|
|
return locale;
|
|
}
|
|
|
|
// Looks up `key` and substitutes any `{name}` placeholders from `params`.
|
|
// Falls back to the raw key if strings never loaded (e.g. en.json itself
|
|
// failed to fetch) — a visible-but-not-crashing degradation, same spirit as
|
|
// the rest of this codebase's silent-skip fallbacks.
|
|
export function t(key, params = {}) {
|
|
const template = strings?.[key] ?? key;
|
|
return template.replace(/\{(\w+)\}/g, (match, name) => (params[name] != null ? String(params[name]) : match));
|
|
}
|