diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index de8bb788..94a4e379 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details. */ import "matrix-js-sdk/src/@types/global"; +import type { DurationFormat as PolyfillDurationFormat } from "@formatjs/intl-durationformat"; import { Controls } from "../controls"; declare global { @@ -23,4 +24,9 @@ declare global { // Safari only supports this prefixed, so tell the type system about it webkitRequestFullscreen: () => void; } + + namespace Intl { + // Add DurationFormat as part of the Intl namespace because we polyfill it + const DurationFormat: typeof PolyfillDurationFormat; + } } diff --git a/src/initializer.test.ts b/src/initializer.test.ts index dff1651e..19f52b69 100644 --- a/src/initializer.test.ts +++ b/src/initializer.test.ts @@ -9,9 +9,9 @@ import { expect, test } from "vitest"; import { Initializer } from "../src/initializer"; -test("initBeforeReact sets font family from URL param", () => { +test("initBeforeReact sets font family from URL param", async () => { window.location.hash = "#?font=DejaVu Sans"; - Initializer.initBeforeReact(); + await Initializer.initBeforeReact(); expect( getComputedStyle(document.documentElement).getPropertyValue( "--font-family", @@ -19,9 +19,9 @@ test("initBeforeReact sets font family from URL param", () => { ).toBe('"DejaVu Sans"'); }); -test("initBeforeReact sets font scale from URL param", () => { +test("initBeforeReact sets font scale from URL param", async () => { window.location.hash = "#?fontScale=1.2"; - Initializer.initBeforeReact(); + await Initializer.initBeforeReact(); expect( getComputedStyle(document.documentElement).getPropertyValue("--font-scale"), ).toBe("1.2"); diff --git a/src/initializer.tsx b/src/initializer.tsx index 4bc1dc9f..0ac8a88a 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -11,6 +11,8 @@ import LanguageDetector from "i18next-browser-languagedetector"; import Backend from "i18next-http-backend"; import * as Sentry from "@sentry/react"; import { logger } from "matrix-js-sdk/src/logger"; +import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill"; +import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill"; import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; @@ -41,10 +43,17 @@ export class Initializer { return Initializer.internalInstance?.isInitialized; } - public static initBeforeReact(): void { - // this maybe also needs to return a promise in the future, - // if we have to do async inits before showing the loading screen - // but this should be avoided if possible + public static async initBeforeReact(): Promise { + const polyfills: Promise[] = []; + if (shouldPolyfillSegmenter()) { + polyfills.push(import("@formatjs/intl-segmenter/polyfill-force")); + } + + if (shouldPolyfillDurationFormat()) { + polyfills.push(import("@formatjs/intl-durationformat/polyfill-force")); + } + + await Promise.all(polyfills); //i18n const languageDetector = new LanguageDetector(); @@ -54,7 +63,7 @@ export class Initializer { lookup: () => getUrlParams().lang ?? undefined, }); - i18n + await i18n .use(Backend) .use(languageDetector) .use(initReactI18next) @@ -74,9 +83,6 @@ export class Initializer { order: ["urlFragment", "navigator"], caches: [], }, - }) - .catch((e) => { - logger.error("Failed to initialize i18n", e); }); // Custom Themeing diff --git a/src/main.tsx b/src/main.tsx index d4a4539b..ac0440b7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,8 +20,6 @@ import { setLogExtension as setLKLogExtension, setLogLevel as setLKLogLevel, } from "livekit-client"; -import "@formatjs/intl-segmenter/polyfill"; -import "@formatjs/intl-durationformat/polyfill"; import { App } from "./App"; import { init as initRageshake } from "./settings/rageshake"; @@ -57,12 +55,17 @@ if (fatalError !== null) { throw fatalError; // Stop the app early } -Initializer.initBeforeReact(); +Initializer.initBeforeReact() + .then(() => { + const history = createBrowserHistory(); -const history = createBrowserHistory(); - -root.render( - - - , -); + root.render( + + + , + ); + }) + .catch((e) => { + logger.error("Failed to initialize app", e); + root.render(e.message); + }); diff --git a/src/reactions/RaisedHandIndicator.tsx b/src/reactions/RaisedHandIndicator.tsx index cfc83ab8..8c4747b3 100644 --- a/src/reactions/RaisedHandIndicator.tsx +++ b/src/reactions/RaisedHandIndicator.tsx @@ -11,19 +11,12 @@ import { useCallback, useEffect, useState, + useMemo, } from "react"; -import { DurationFormat } from "@formatjs/intl-durationformat"; import { useTranslation } from "react-i18next"; import { ReactionIndicator } from "./ReactionIndicator"; -const durationFormatter = new DurationFormat(undefined, { - minutesDisplay: "always", - secondsDisplay: "always", - hoursDisplay: "auto", - style: "digital", -}); - export function RaisedHandIndicator({ raisedHandTime, miniature, @@ -38,6 +31,17 @@ export function RaisedHandIndicator({ const { t } = useTranslation(); const [raisedHandDuration, setRaisedHandDuration] = useState(""); + const durationFormatter = useMemo( + () => + new Intl.DurationFormat(undefined, { + minutesDisplay: "always", + secondsDisplay: "always", + hoursDisplay: "auto", + style: "digital", + }), + [], + ); + const clickCallback = useCallback>( (event) => { if (!onClick) { @@ -69,7 +73,7 @@ export function RaisedHandIndicator({ calculateTime(); const to = setInterval(calculateTime, 1000); return (): void => clearInterval(to); - }, [setRaisedHandDuration, raisedHandTime, showTimer]); + }, [setRaisedHandDuration, raisedHandTime, showTimer, durationFormatter]); if (!raisedHandTime) { return; diff --git a/src/vitest.setup.ts b/src/vitest.setup.ts index 596453ed..776a13b0 100644 --- a/src/vitest.setup.ts +++ b/src/vitest.setup.ts @@ -6,6 +6,8 @@ Please see LICENSE in the repository root for full details. */ import "global-jsdom/register"; +import "@formatjs/intl-durationformat/polyfill"; +import "@formatjs/intl-segmenter/polyfill"; import i18n from "i18next"; import posthog from "posthog-js"; import { initReactI18next } from "react-i18next"; @@ -14,6 +16,7 @@ import { cleanup } from "@testing-library/react"; import "vitest-axe/extend-expect"; import { logger } from "matrix-js-sdk/src/logger"; +import EN_GB from "../public/locales/en-GB/app.json"; import { Config } from "./config/Config"; // Bare-minimum i18n config @@ -22,6 +25,13 @@ i18n .init({ lng: "en-GB", fallbackLng: "en-GB", + supportedLngs: ["en-GB"], + // We embed the translations, so that it never needs to fetch + resources: { + "en-GB": { + app: EN_GB, + }, + }, interpolation: { escapeValue: false, // React has built-in XSS protections },