Files
element-call/src/initializer.tsx
T

261 lines
7.5 KiB
TypeScript
Raw Normal View History

2022-11-03 19:43:41 +01:00
/*
Copyright 2022-2024 New Vector Ltd.
2022-11-03 19:43:41 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
2022-11-03 19:43:41 +01:00
*/
2025-01-06 18:00:20 +01:00
import React from "react";
2024-11-14 11:53:43 +01:00
import i18n, {
type BackendModule,
type ReadCallback,
type ResourceKey,
} from "i18next";
2022-11-03 19:43:41 +01:00
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
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";
2025-01-06 18:00:20 +01:00
import {
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes,
} from "react-router-dom";
2022-11-03 19:43:41 +01:00
import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
2023-03-22 12:41:33 +00:00
import { ElementCallOpenTelemetry } from "./otel/otel";
2023-09-17 12:54:18 -04:00
import { platform } from "./Platform";
2022-11-03 19:43:41 +01:00
2024-11-14 11:53:43 +01:00
// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// {
2024-12-04 14:51:29 +00:00
// "../locales/en/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
2024-11-14 11:53:43 +01:00
// ...
// }
const locales = import.meta.glob<string>("../locales/*/*.json", {
query: "?url",
import: "default",
eager: true,
});
const getLocaleUrl = (
language: string,
namespace: string,
): string | undefined => locales[`../locales/${language}/${namespace}.json`];
const supportedLngs = [
...new Set(
Object.keys(locales).map((url) => {
2024-12-04 14:51:29 +00:00
// The URLs are of the form ../locales/en/app.json
2024-11-14 11:53:43 +01:00
// This extracts the language code from the URL
const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1];
if (!lang) {
throw new Error(`Could not parse locale URL ${url}`);
}
return lang;
}),
),
];
// A backend that fetches the locale files from the URLs generated by the glob above
const Backend = {
type: "backend",
init(): void {},
read(language: string, namespace: string, callback: ReadCallback): void {
(async (): Promise<ResourceKey> => {
const url = getLocaleUrl(language, namespace);
if (!url) {
throw new Error(
`Namespace ${namespace} for locale ${language} not found`,
);
}
const response = await fetch(url, {
credentials: "omit",
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw Error(`Failed to fetch ${url}`);
}
return await response.json();
})().then(
(data) => callback(null, data),
(error) => callback(error, null),
);
},
} satisfies BackendModule;
2022-11-03 19:43:41 +01:00
enum LoadState {
None,
Loading,
Loaded,
}
class DependencyLoadStates {
2023-09-22 18:05:13 -04:00
public config: LoadState = LoadState.None;
public sentry: LoadState = LoadState.None;
public openTelemetry: LoadState = LoadState.None;
2022-11-03 19:43:41 +01:00
2023-09-22 18:05:13 -04:00
public allDepsAreLoaded(): boolean {
2022-11-03 19:43:41 +01:00
return !Object.values(this).some((s) => s !== LoadState.Loaded);
}
}
export class Initializer {
private static internalInstance: Initializer;
2023-06-09 19:18:30 +02:00
private isInitialized = false;
public static isInitialized(): boolean {
2023-06-13 17:08:29 +02:00
return Initializer.internalInstance?.isInitialized;
2023-06-09 19:18:30 +02:00
}
public static async initBeforeReact(): Promise<void> {
const polyfills: Promise<unknown>[] = [];
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();
languageDetector.addDetector({
name: "urlFragment",
// Look for a language code in the URL's fragment
2023-09-18 17:49:10 +01:00
lookup: () => getUrlParams().lang ?? undefined,
});
await i18n
.use(Backend)
.use(languageDetector)
.use(initReactI18next)
.init({
2024-12-04 14:51:29 +00:00
fallbackLng: "en",
defaultNS: "app",
2023-11-20 13:47:36 +00:00
keySeparator: ".",
nsSeparator: false,
2023-11-17 12:08:27 +00:00
pluralSeparator: "_",
contextSeparator: "|",
2024-11-14 11:53:43 +01:00
supportedLngs,
interpolation: {
escapeValue: false, // React has built-in XSS protections
},
detection: {
// No localStorage detectors or caching here, since we don't have any way
// of letting the user manually select a language
order: ["urlFragment", "navigator"],
caches: [],
},
});
// Custom Themeing
2023-08-28 16:45:17 -04:00
if (import.meta.env.VITE_CUSTOM_CSS) {
const style = document.createElement("style");
style.textContent = import.meta.env.VITE_CUSTOM_CSS;
document.head.appendChild(style);
}
2022-12-09 14:25:02 -05:00
// Custom fonts
2023-09-18 17:49:10 +01:00
const { fonts, fontScale } = getUrlParams();
2022-12-09 14:25:02 -05:00
if (fontScale !== null) {
document.documentElement.style.setProperty(
"--font-scale",
2023-10-11 10:42:04 -04:00
fontScale.toString(),
2022-12-09 14:25:02 -05:00
);
}
if (fonts.length > 0) {
document.documentElement.style.setProperty(
"--font-family",
2023-10-11 10:42:04 -04:00
fonts.map((f) => `"${f}"`).join(", "),
2022-12-09 14:25:02 -05:00
);
}
2023-09-17 12:54:18 -04:00
// Add the platform to the DOM, so CSS can query it
document.body.setAttribute("data-platform", platform);
}
2022-11-03 19:43:41 +01:00
public static init(): Promise<void> | null {
if (Initializer?.internalInstance?.initPromise) {
return null;
}
Initializer.internalInstance = new Initializer();
Initializer.internalInstance.initPromise = new Promise<void>((resolve) => {
2024-07-25 14:33:37 +02:00
// initStep calls itself recursively until everything is initialized in the correct order.
2022-11-03 19:43:41 +01:00
// Then the promise gets resolved.
Initializer.internalInstance.initStep(resolve);
});
return Initializer.internalInstance.initPromise;
}
2023-09-22 18:05:13 -04:00
private loadStates = new DependencyLoadStates();
2022-11-03 19:43:41 +01:00
2023-09-22 18:05:13 -04:00
private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
2022-11-03 19:43:41 +01:00
// config
if (this.loadStates.config === LoadState.None) {
this.loadStates.config = LoadState.Loading;
Config.init().then(
() => {
this.loadStates.config = LoadState.Loaded;
this.initStep(resolve);
},
(e) => {
logger.error("Failed to load config", e);
},
);
2022-11-03 19:43:41 +01:00
}
//sentry (only initialize after the config is ready)
if (
this.loadStates.sentry === LoadState.None &&
this.loadStates.config === LoadState.Loaded
) {
2022-12-21 10:17:53 +00:00
if (Config.get().sentry?.DSN && Config.get().sentry?.environment) {
Sentry.init({
2022-12-21 10:17:53 +00:00
dsn: Config.get().sentry?.DSN,
environment: Config.get().sentry?.environment,
integrations: [
Sentry.reactRouterV7BrowserTracingIntegration({
2025-01-06 18:00:20 +01:00
useEffect: React.useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes,
}),
],
tracesSampleRate: 1.0,
});
}
// Sentry is now 'loadeed' (even if we actually skipped starting
// it due to to not being configured)
2022-11-03 19:43:41 +01:00
this.loadStates.sentry = LoadState.Loaded;
}
2023-03-22 12:41:33 +00:00
// OpenTelemetry (also only after config loaded)
if (
this.loadStates.openTelemetry === LoadState.None &&
this.loadStates.config === LoadState.Loaded
) {
ElementCallOpenTelemetry.globalInit();
this.loadStates.openTelemetry = LoadState.Loaded;
}
2022-11-03 19:43:41 +01:00
if (this.loadStates.allDepsAreLoaded()) {
// resolve if there is no dependency that is not loaded
resolve();
2023-06-09 19:18:30 +02:00
this.isInitialized = true;
2022-11-03 19:43:41 +01:00
}
}
2023-06-30 16:43:28 +01:00
private initPromise?: Promise<void>;
2022-11-03 19:43:41 +01:00
}