diff --git a/ts/deck-options/SaveButton.svelte b/ts/deck-options/SaveButton.svelte index 5583eeef9..aeaaa1517 100644 --- a/ts/deck-options/SaveButton.svelte +++ b/ts/deck-options/SaveButton.svelte @@ -29,6 +29,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } } + /// Treat text like HTML, merging multiple spaces and converting + /// newlines to spaces. + export function collapseWhitespace(s: string): string { + return s.replace(/\s+/g, " "); + } + function removeConfig(): void { // show pop-up after dropdown has gone away setTimeout(() => { @@ -41,7 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ? tr.deckConfigWillRequireFullSync() + " " : "") + tr.deckConfigConfirmRemoveName({ name: state.getCurrentName() }); - if (confirm(tr.i18n.withCollapsedWhitespace(msg))) { + if (confirm(collapseWhitespace(msg))) { try { state.removeCurrentConfig(); } catch (err) { diff --git a/ts/deck-options/lib.ts b/ts/deck-options/lib.ts index 7a8538f4d..8d10c6bfc 100644 --- a/ts/deck-options/lib.ts +++ b/ts/deck-options/lib.ts @@ -9,7 +9,7 @@ import { DeckConfig } from "../lib/proto"; import { postRequest } from "../lib/postrequest"; import { Writable, writable, get, Readable, readable } from "svelte/store"; import { isEqual, cloneDeep } from "lodash-es"; -import { i18n } from "../lib/i18n"; +import { localeCompare } from "../lib/i18n"; import type { DynamicSvelteComponent } from "../sveltelib/dynamicComponent"; export async function getDeckOptionsInfo( @@ -278,9 +278,7 @@ export class DeckOptionsState { useCount, }; }); - list.sort((a, b) => - a.name.localeCompare(b.name, i18n.langs, { sensitivity: "base" }) - ); + list.sort((a, b) => localeCompare(a.name, b.name, { sensitivity: "base" })); return list; } diff --git a/ts/graphs/TableData.svelte b/ts/graphs/TableData.svelte index 230e2a6aa..838bb5997 100644 --- a/ts/graphs/TableData.svelte +++ b/ts/graphs/TableData.svelte @@ -4,12 +4,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
- +
{#each tableData as { label, value }} diff --git a/ts/graphs/calendar.ts b/ts/graphs/calendar.ts index 26b83d4b9..9d489c00b 100644 --- a/ts/graphs/calendar.ts +++ b/ts/graphs/calendar.ts @@ -29,7 +29,7 @@ import { SearchDispatch, } from "./graph-helpers"; import { clickableClass } from "./graph-styles"; -import { tr, i18n } from "../lib/i18n"; +import { tr, weekdayLabel, toLocaleString } from "../lib/i18n"; export interface GraphData { // indexed by day, where day is relative to today @@ -154,7 +154,7 @@ export function renderCalendar( .interpolator((n) => interpolateBlues(cappedRange(n)!)); function tooltipText(d: DayDatum): string { - const date = d.date.toLocaleString(i18n.langs, { + const date = toLocaleString(d.date, { weekday: "long", year: "numeric", month: "long", @@ -171,7 +171,7 @@ export function renderCalendar( .selectAll("text") .data(sourceData.weekdayLabels) .join("text") - .text((d: number) => i18n.weekdayLabel(d)) + .text((d: number) => weekdayLabel(d)) .attr("width", x(-1)! - 2) .attr("height", height - 2) .attr("x", x(1)! - 3) diff --git a/ts/lib/genfluent.py b/ts/lib/genfluent.py index ca89ef7c2..de8d8e146 100644 --- a/ts/lib/genfluent.py +++ b/ts/lib/genfluent.py @@ -18,7 +18,7 @@ class Variable(TypedDict): def methods() -> str: out = [ - 'import { i18n } from "./i18n";', + 'import { translate } from "./i18n";', ] for module in modules: for translation in module["translations"]: @@ -30,7 +30,7 @@ def methods() -> str: f""" /** {doc} */ export function {key}({arg_types}): string {{ - return i18n.translate("{translation["key"]}"{args}) + return translate("{translation["key"]}"{args}) }} """ ) diff --git a/ts/lib/i18n.ts b/ts/lib/i18n.ts index 5fad548df..bfbd5b44b 100644 --- a/ts/lib/i18n.ts +++ b/ts/lib/i18n.ts @@ -6,13 +6,11 @@ import "intl-pluralrules"; import { FluentBundle, FluentResource, FluentNumber } from "@fluent/bundle"; +import type { ModuleName } from "./i18n-modules"; type RecordVal = number | string | FluentNumber; -function formatNumbers(args?: Record): void { - if (!args) { - return; - } +function formatNumbers(args: Record): void { for (const key of Object.keys(args)) { if (typeof args[key] === "number") { args[key] = new FluentNumber(args[key] as number, { @@ -22,64 +20,70 @@ function formatNumbers(args?: Record): void { } } -export class I18n { - bundles: FluentBundle[] = []; - langs: string[] = []; +let bundles: FluentBundle[] = []; - translate(key: string, args?: Record): string { +export function translate(key: string, args?: Record): string { + if (args) { formatNumbers(args); - for (const bundle of this.bundles) { - const msg = bundle.getMessage(key); - if (msg && msg.value) { - return bundle.formatPattern(msg.value, args); - } - } - return `missing key: ${key}`; } - supportsVerticalText(): boolean { - const firstLang = this.bundles[0].locales[0]; - return ( - firstLang.startsWith("ja") || - firstLang.startsWith("zh") || - firstLang.startsWith("ko") - ); - } - - direction(): string { - const firstLang = this.bundles[0].locales[0]; - if ( - firstLang.startsWith("ar") || - firstLang.startsWith("he") || - firstLang.startsWith("fa") - ) { - return "rtl"; - } else { - return "ltr"; + for (const bundle of bundles) { + const msg = bundle.getMessage(key); + if (msg && msg.value) { + return bundle.formatPattern(msg.value, args); } } + return `missing key: ${key}`; +} - weekdayLabel(n: number): string { - const firstLang = this.bundles[0].locales[0]; - const now = new Date(); - const daysFromToday = -now.getDay() + n; - const desiredDay = new Date(now.getTime() + daysFromToday * 86_400_000); - return desiredDay.toLocaleDateString(firstLang, { - weekday: "narrow", - }); - } +export function supportsVerticalText(): boolean { + const firstLang = bundles[0].locales[0]; + return ( + firstLang.startsWith("ja") || + firstLang.startsWith("zh") || + firstLang.startsWith("ko") + ); +} - /// Treat text like HTML, merging multiple spaces and converting - /// newlines to spaces. - withCollapsedWhitespace(s: string): string { - return s.replace(/\s+/g, " "); +export function direction(): string { + const firstLang = bundles[0].locales[0]; + if ( + firstLang.startsWith("ar") || + firstLang.startsWith("he") || + firstLang.startsWith("fa") + ) { + return "rtl"; + } else { + return "ltr"; } } -// global singleton -export const i18n = new I18n(); +export function weekdayLabel(n: number): string { + const firstLang = bundles[0].locales[0]; + const now = new Date(); + const daysFromToday = -now.getDay() + n; + const desiredDay = new Date(now.getTime() + daysFromToday * 86_400_000); + return desiredDay.toLocaleDateString(firstLang, { + weekday: "narrow", + }); +} -import type { ModuleName } from "./i18n-modules"; +let langs: string[] = []; + +export function toLocaleString( + date: Date, + options?: Intl.DateTimeFormatOptions +): string { + return date.toLocaleDateString(langs, options); +} + +export function localeCompare( + first: string, + second: string, + options?: Intl.CollatorOptions +): number { + return first.localeCompare(second, langs, options); +} export async function setupI18n(args: { modules: ModuleName[] }): Promise { const resp = await fetch("/_anki/i18nResources", { @@ -91,16 +95,17 @@ export async function setupI18n(args: { modules: ModuleName[] }): Promise } const json = await resp.json(); - i18n.bundles = []; + bundles = []; for (const i in json.resources) { const text = json.resources[i]; const lang = json.langs[i]; const bundle = new FluentBundle([lang, "en-US"]); const resource = new FluentResource(text); bundle.addResource(resource); - i18n.bundles.push(bundle); + bundles.push(bundle); } - i18n.langs = json.langs; + + langs = json.langs; } export { ModuleName } from "./i18n-modules";
{label}: