Files
codecraft/web/src/i18n/index.ts
T
Richard ZinkandClaude Opus 5.5 49262c612c Users, file manager, image generation, budgets and agent workflow
- User management: local accounts (Argon2) and OpenID Connect, admin/user
  roles, per-user folders plus admin-shared folders, read-only session
  sharing, event filtering per user
- File manager with CodeMirror editor and git client (status, diff,
  stage, commit, branches, fetch/pull/push, log, clone/init)
- Image generation via fal.ai: generate_image tool and image page
- Budgets: monthly limits per provider, provider balances/costs
  (OpenRouter, Anthropic admin cost report, fal.ai), price discovery
- Subagents on other models, background tasks, concurrency limits per
  host/model/subagent
- MCP tools loaded on demand (mcp_search/mcp_call), read-only hints
- Permission mode "auto", safe read-only commands always allowed
- Persistent message queue with attachments, deletable
- Live reasoning incl. <think> tags, activity animations, CSS themes,
  mobile layout, statistics per user, upload size handling
- Docker: named volume for state, localhost-bound port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:54:08 +01:00

104 lines
3.4 KiB
TypeScript

// Minimal i18n: dictionaries per language, `{name}` placeholders, persisted choice.
// To add a language: copy en.ts, translate the values, register it in `languages`.
// TypeScript enforces that every language defines every key of `en`.
import { useSyncExternalStore } from "react";
import { en, type Messages } from "./en";
import { de } from "./de";
export type Key = keyof typeof en;
export const languages: Record<string, { name: string; messages: Messages }> = {
en: { name: "English", messages: en },
de: { name: "Deutsch", messages: de },
};
const STORAGE_KEY = "codecraft.lang";
/** English is the default; other languages are chosen in the settings. */
function detect(): string {
const saved = localStorage.getItem(STORAGE_KEY);
return saved && languages[saved] ? saved : "en";
}
let current = detect();
document.documentElement.lang = current;
const listeners = new Set<() => void>();
export function getLang() {
return current;
}
export function setLang(lang: string) {
if (!languages[lang]) return;
current = lang;
localStorage.setItem(STORAGE_KEY, lang);
document.documentElement.lang = lang;
listeners.forEach((l) => l());
}
export function t(key: Key, vars?: Record<string, string | number>): string {
let s: string = languages[current].messages[key] ?? en[key] ?? key;
if (vars) {
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));
}
return s;
}
/** Re-render on language change. Returns `t` for convenience. */
export function useT() {
useSyncExternalStore(
(l) => {
listeners.add(l);
return () => listeners.delete(l);
},
() => current,
);
return t;
}
// --- locale-aware formatting -------------------------------------------------
export function fmtNumber(n: number, digits = 0) {
return new Intl.NumberFormat(current, { maximumFractionDigits: digits }).format(n);
}
export function fmtCompact(n: number) {
return new Intl.NumberFormat(current, { notation: "compact", maximumFractionDigits: 1 }).format(n);
}
export function fmtCost(n: number, currency: string) {
const digits = n !== 0 && Math.abs(n) < 1 ? 4 : 2;
try {
return new Intl.NumberFormat(current, { style: "currency", currency, maximumFractionDigits: digits }).format(n);
} catch {
return `${n.toFixed(digits)} ${currency}`;
}
}
export function fmtPercent(n: number) {
return new Intl.NumberFormat(current, { style: "percent", maximumFractionDigits: 1 }).format(n);
}
export function fmtDate(ms: number, withTime = true) {
return new Intl.DateTimeFormat(current, withTime ? { dateStyle: "medium", timeStyle: "short" } : { dateStyle: "medium" }).format(ms);
}
export function fmtRelative(ms: number) {
const diff = (ms - Date.now()) / 1000;
const rtf = new Intl.RelativeTimeFormat(current, { numeric: "auto" });
const abs = Math.abs(diff);
if (abs < 60) return rtf.format(Math.round(diff), "second");
if (abs < 3600) return rtf.format(Math.round(diff / 60), "minute");
if (abs < 86400) return rtf.format(Math.round(diff / 3600), "hour");
return rtf.format(Math.round(diff / 86400), "day");
}
export function weekdayNames(): string[] {
// Index 0 = Sunday, matching SQLite's %w.
const base = new Date(Date.UTC(2024, 0, 7)); // a Sunday
return Array.from({ length: 7 }, (_, i) =>
new Intl.DateTimeFormat(current, { weekday: "short", timeZone: "UTC" }).format(new Date(base.getTime() + i * 86400000)),
);
}