Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { createContext, useCallback, useContext, useEffect, useMemo, type ReactNode } from 'react';
import { resolveLocale, type Locale } from '@wellformed/shared';
import { useSession } from '../state/session';
import { messages, type MessageArgs, type MessageKey } from './messages';
/**
* Which language the page speaks, and the `t` that speaks it.
*
* The choice lives on the workspace, beside its currency — see
* shared/locale.ts. Inside a workspace everyone reads the same page in the
* same language; changing it is a workspace setting, saved in the workspace
* dialog like a rename. Outside a workspace — the login page, the marketing
* page, the picker — there is nothing to ask, and the browser decides.
*/
export type TranslateFn = <K extends MessageKey>(key: K, ...args: MessageArgs<K>) => string;
interface I18nApi {
/** The language everything renders in right now. */
locale: Locale;
t: TranslateFn;
}
const I18nContext = createContext<I18nApi | null>(null);
export function I18nProvider({
children,
locale: forced,
}: {
children: ReactNode;
/**
* A locale the URL or the server chose — /nl and /en are real addresses so
* search engines have a stable page per language to index, and the SSR
* entry renders outside a browser where there is no navigator to ask.
*/
locale?: Locale;
}): ReactNode {
const session = useSession();
const locale =
forced ??
session.workspace?.locale ??
resolveLocale(typeof navigator === 'undefined' ? [] : (navigator.languages ?? [navigator.language]));
// Screen readers and translation tools read the document, not our context.
useEffect(() => {
document.documentElement.lang = locale;
}, [locale]);
const t = useCallback<TranslateFn>(
(key, ...args) => {
const text = messages[key][locale] as string | ((...a: unknown[]) => string);
return typeof text === 'function' ? text(...args) : text;
},
[locale],
);
const value = useMemo<I18nApi>(() => ({ locale, t }), [locale, t]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
export function useI18n(): I18nApi {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error('useI18n must be used inside an I18nProvider.');
return ctx;
}
/**
* Just the locale, for the many places that only format a number or a date.
* Pass it to `formatCell` and `formatCount` from @wellformed/shared.
*/
export function useLocale(): Locale {
return useI18n().locale;
}