@dsi18n/dsi18n
v0.1.3
Published
Unified dsi18n runtime for npm and Deno (npm: specifier).
Downloads
27
Readme
@dsi18n/dsi18n
Runtime i18n for Node, browsers, and Deno (npm: specifier). Load static locale JSON (from Airtable at build time), then call synchronous t() with fallbacks and {placeholder} interpolation.
No Airtable calls at runtime — only manifest.json and per-locale *.json files.
Install
npm install @dsi18n/dsi18nRequires Node.js 18+ for the Node entry (loadLocalesFromDirectory, etc.). Browsers need fetch (built-in in modern browsers).
Entries
| Import | Environment |
|--------|-------------|
| @dsi18n/dsi18n/browser | React, Vite, browser — initDsi18n, t(), setLocale (no fs) |
| @dsi18n/dsi18n or @dsi18n/dsi18n/node | Node — full API including disk + Accept-Language helpers |
Pre-build JSON with @dsi18n/airtable-sync, serve them as static files (e.g. /locales), then load once in the app.
Quick start
Browser (static JSON + t())
Serve pre-built manifest.json and en.json, es.json, … under /locales (from @dsi18n/airtable-sync in CI).
Recommended: load the full bundle once, then t(id) and setLocale without refetching.
import { initDsi18n } from "@dsi18n/dsi18n/browser";
const i18n = await initDsi18n({ baseUrl: "/locales", locale: "es" });
i18n.t("layla_majnun_intro");
i18n.setLocale("en");
i18n.t("layla_majnun_intro"); // same bundle in memory — no second fetchVite/React: put public/locales/ in the app; baseUrl is /locales (or your CDN URL). Keep the i18n object in module state or React context.
Lower-level API (single locale + fallbacks per load): loadI18nFromUrls({ baseUrl, activeLocale }).
Node (files on disk)
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import {
loadLocalesFromDirectory,
createI18nLocaleCache,
} from "@dsi18n/dsi18n/node";
const localesDir = join(
dirname(fileURLToPath(import.meta.url)),
"locales",
);
const cache = createI18nLocaleCache(
await loadLocalesFromDirectory(localesDir),
);
const i18n = cache.getI18n("es");
console.log(i18n.t("user.greeting", { name: "Ana" }));Deno
import { initDsi18n } from "npm:@dsi18n/[email protected]/browser";Locale bundle format
Your build step should emit a directory like:
locales/
manifest.json
en.json
es.jsonmanifest.json—defaultLocale,fallbackChain,locales, metadata.<locale>.json—entriesmap: each key →{ value, placeholders }.
JSON Schemas ship with this package:
import dictSchema from "@dsi18n/dsi18n/dictionary.schema.json" with { type: "json" };
import manifestSchema from "@dsi18n/dsi18n/manifest.schema.json" with { type: "json" };Generate these files from Airtable at build time:
npm install -D @dsi18n/airtable-sync
npx dsi18n-sync # reads .env, writes I18N_OUT_DIR (default ./locales)Commit locales/ or produce them in CI before deploy. If you already have JSON, you only need @dsi18n/dsi18n.
API
createI18n({ manifest, dictionaries, locale })
Builds an I18n instance from data already in memory (SSR, tests, custom loaders).
const i18n = createI18n({ manifest, dictionaries, locale: "es-AR" });
i18n.t("hello", { name: "Juan" }); // interpolates Hello {name}
i18n.t("missing.key"); // returns "missing.key"
i18n.withLocale("fr"); // new instance, same dictionaries| Behavior | Detail |
|----------|--------|
| Lookup | Flat string keys in entries (e.g. "user.greeting") |
| Interpolation | {name} tokens; missing vars stay as {name} |
| Locale order | Active locale, then manifest.fallbackChain |
| Missing key | Returns the key string |
initDsi18n({ baseUrl, locale, bundle?, fetchFn? }) (browser)
Loads all locale files under baseUrl once, wraps createI18nLocaleCache, returns { t, setLocale, getI18n, locale, manifest, cache }. Use after npm run build / dsi18n-sync has produced locales/.
loadI18nBundleFromUrls({ baseUrl, locales?, fetchFn? })
Fetches manifest.json + every manifest.locales entry (or a subset). Pair with createI18nLocaleCache for custom wiring.
loadI18nFromUrls({ baseUrl, activeLocale, fetchFn? })
Fetches only the locale files needed for one active locale and its fallback chain, then returns I18n.
Node helpers
| Export | Role |
|--------|------|
| loadLocalesFromDirectory(path) | Read manifest.json + all locale files from disk |
| createI18nLocaleCache(bundle) | Memoized getI18n(locale) for servers |
| resolveActiveLocale({ manifest, queryLang, acceptLanguage }) | Pick locale from ?lang= or Accept-Language |
| parseAcceptLanguage / pickSupportedLocale | Lower-level HTTP helpers |
| localeLookupOrder / localesToFetch / interpolate | Building custom loaders |
Types
DictionaryEntry, LocaleDictionaryFile, ManifestFile, I18n, TranslationVars, and related types are exported from the main entry.
HTTP example
import {
loadLocalesFromDirectory,
createI18nLocaleCache,
resolveActiveLocale,
} from "@dsi18n/dsi18n";
const cache = createI18nLocaleCache(
await loadLocalesFromDirectory("./locales"),
);
function handler(req) {
const url = new URL(req.url);
const locale = resolveActiveLocale({
manifest: cache.manifest,
queryLang: url.searchParams.get("lang") ?? undefined,
acceptLanguage: req.headers.get("accept-language") ?? undefined,
});
const text = cache.getI18n(locale).t("app.title");
return new Response(text);
}Recommended workflow
Airtable → (build / CI, .env) → locales/*.json → @dsi18n/dsi18n → app- Sync translations in dev or CI (credentials stay out of the published app).
- Ship
locales/with your app or static assets. - Load once at startup; call
t()anywhere (React, Express, Shiny via API, etc.).
License
MIT
