Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

2.5 kB Ruw bestand
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;
}