From fdd000a9f9f6bd4d56b0d5db20225f7fea747730 Mon Sep 17 00:00:00 2001 From: Claas Date: Sun, 28 Jun 2026 16:47:41 +0000 Subject: [PATCH] Reuse selected locale for intl API --- client/src/i18n.tsx | 29 ++++++++++++------- client/src/routes/_app/timer.tsx | 48 ++++++++++++++++++------------- client/src/routes/_app/times.tsx | 49 +++++++++++++++----------------- 3 files changed, 69 insertions(+), 57 deletions(-) diff --git a/client/src/i18n.tsx b/client/src/i18n.tsx index 230cff1..9353905 100644 --- a/client/src/i18n.tsx +++ b/client/src/i18n.tsx @@ -21,15 +21,20 @@ function createBundle(locale: string): FluentBundle { return bundle; } -export type TranslateArgs = Record; +export type TranslateArguments = Record; const I18nContext = createContext<{ - t: (id: string, args?: TranslateArgs) => string; + /** + * Translate a message by its id, optionally providing arguments for placeholders. This function + * uses an acronym/abbreviation for "translate" and one of the few exceptions for convenience in + * this repository. + */ + t: (id: string, args?: TranslateArguments) => string; locale: () => string; setLocale: (locale: string) => void; }>(); -export function I18nProvider(props: ParentProps) { +export function I18nProvider(properties: ParentProps) { const userLocales = Array.from( navigator.languages?.length ? navigator.languages : [navigator.language ?? DEFAULT_LOCALE], ); @@ -40,21 +45,23 @@ export function I18nProvider(props: ParentProps) { const bundle = createMemo(() => createBundle(locale())); - function t(id: string, args?: TranslateArgs): string { - const b = bundle(); - const message = b.getMessage(id); + function t(id: string, translationArguments?: TranslateArguments): string { + const currentBundle = bundle(); + const message = currentBundle.getMessage(id); if (!message?.value) return id; const errors: Error[] = []; - return b.formatPattern(message.value, args, errors); + return currentBundle.formatPattern(message.value, translationArguments, errors); } return ( - {props.children} + + {properties.children} + ); } export function useI18n() { - const ctx = useContext(I18nContext); - if (!ctx) throw new Error("useI18n must be used within I18nProvider"); - return ctx; + const context = useContext(I18nContext); + if (!context) throw new Error("useI18n must be used within I18nProvider"); + return context; } diff --git a/client/src/routes/_app/timer.tsx b/client/src/routes/_app/timer.tsx index cb634e6..17fef32 100644 --- a/client/src/routes/_app/timer.tsx +++ b/client/src/routes/_app/timer.tsx @@ -33,26 +33,25 @@ export const Route = createFileRoute("/_app/timer")({ }, }); -const timeFormatter = new Intl.DateTimeFormat(undefined, { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", -}); - -const durationFormatter = new Intl.DurationFormat(undefined, { - style: "digital", - hours: "2-digit", - minutes: "2-digit", - seconds: "2-digit", -}); - function EntryRow(properties: { entry: TimerEntry; tick: number; onDelete: () => void; isDeleting: boolean; }) { - const { t } = useI18n(); + const { t, locale } = useI18n(); + const timeFormatter = createMemo( + () => new Intl.DateTimeFormat(locale(), { hour: "2-digit", minute: "2-digit", second: "2-digit" }), + ); + const durationFormatter = createMemo( + () => + new Intl.DurationFormat(locale(), { + style: "digital", + hours: "2-digit", + minutes: "2-digit", + seconds: "2-digit", + }), + ); const startedAt = () => Temporal.Instant.from(properties.entry.startedAt); const pausedAt = () => @@ -68,7 +67,7 @@ function EntryRow(properties: { datetime={properties.entry.startedAt} class="bg-surface-container text-on-surface rounded-full px-3 py-1.5 text-center" > - {timeFormatter.format(startedAt())} + {timeFormatter().format(startedAt())} - {timeFormatter.format(pausedAtInstant())} + {timeFormatter().format(pausedAtInstant())} )} @@ -91,7 +90,7 @@ function EntryRow(properties: { datetime={duration().toString()} class="bg-surface-container text-on-surface rounded-full px-3 py-1.5 text-center" > - {durationFormatter.format(duration())} + {durationFormatter().format(duration())}