105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
// IMPORTANT: OTel MUST initialise BEFORE Sentry. The Sentry browser SDK
|
|
// plugs into @opentelemetry/api's global TracerProvider; initialising the
|
|
// WebTracerProvider first installs that provider so the two stacks share
|
|
// the trace_id. No-op when VITE_OTEL_EXPORTER_OTLP_ENDPOINT is unset.
|
|
const publicEnv = ((import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}) as Record<string, string | undefined>;
|
|
|
|
function hasRuntimeSentryDsn() {
|
|
return typeof globalThis !== "undefined" && typeof (globalThis as { __SENTRY_WEB_DSN__?: unknown }).__SENTRY_WEB_DSN__ === "string";
|
|
}
|
|
|
|
async function initClientObservability() {
|
|
if (publicEnv.VITE_OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
const { initClientOtel } = await import("./otel.client");
|
|
initClientOtel();
|
|
}
|
|
|
|
if (publicEnv.VITE_SENTRY_DSN_WEB || hasRuntimeSentryDsn()) {
|
|
const { initSentryClient } = await import("./sentry.client");
|
|
initSentryClient();
|
|
|
|
// Core Web Vitals → Sentry (#70). Fire-and-forget; the helper is SSR-safe
|
|
// and silently no-ops when Sentry/web-vitals aren't available. Keep it out
|
|
// of observabilityReady so hydration never waits for the web-vitals chunk.
|
|
void import("./lib/observability/web-vitals").then(({ reportWebVitals }) => reportWebVitals()).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// Start configured observability immediately, but keep it off hydration's
|
|
// critical path. A failed SDK import must degrade to browser/React defaults
|
|
// instead of delaying or blanking the app.
|
|
void initClientObservability().catch(() => {});
|
|
|
|
import { HydratedRouter } from "react-router/dom";
|
|
import { StrictMode } from "react";
|
|
import { hydrateRoot } from "react-dom/client";
|
|
|
|
import { sentryHydrationErrorHandler } from "./lib/sentry/routeTracing";
|
|
|
|
import { getInitialNamespaces } from "remix-i18next/client";
|
|
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
|
|
import i18next from "i18next";
|
|
import Backend from "i18next-http-backend";
|
|
import LanguageDetector from "i18next-browser-languagedetector";
|
|
import { formalLngs, i18nConfig } from "./locale/i18n.js";
|
|
|
|
function hydrate() {
|
|
const localeAssetVersion = publicEnv.VITE_APP_REVISION || publicEnv.VITE_SENTRY_RELEASE;
|
|
const localeLoadPath = localeAssetVersion ? `/locales/{{lng}}/{{ns}}.json?v=${encodeURIComponent(localeAssetVersion)}` : "/locales/{{lng}}/{{ns}}.json";
|
|
|
|
i18next
|
|
// Use the react-i18next plugin.
|
|
.use(initReactI18next)
|
|
// Setup client-side language detector.
|
|
.use(LanguageDetector)
|
|
// Setup backend.
|
|
.use(Backend)
|
|
.init({
|
|
// Spread configuration.
|
|
...i18nConfig,
|
|
// Permit the formal variants as languages so a post-hydration
|
|
// `changeLanguage("<locale>-formal")` (driven by the Anrede toggle in
|
|
// root.tsx) actually loads "/locales/<locale>-formal/*.json" instead of
|
|
// being filtered out. Purely additive: with the default "du" the language
|
|
// stays the base locale detected from `<html lang>`, so existing
|
|
// behaviour is unchanged. Missing formal files fall back to the base
|
|
// locale via `fallbackLng`.
|
|
supportedLngs: [...i18nConfig.supportedLngs, ...formalLngs],
|
|
// Detects the namespaces your routes rendered while SSR use
|
|
// and pass them here to load the translations.
|
|
ns: getInitialNamespaces(),
|
|
backend: {
|
|
loadPath: localeLoadPath,
|
|
},
|
|
detection: {
|
|
// We'll detect the language only server-side with remix-i18next.
|
|
// By using `<html lang>` attribute we communicate to the Client.
|
|
order: ["htmlTag"],
|
|
},
|
|
})
|
|
.then(() => {
|
|
hydrateRoot(
|
|
document,
|
|
<I18nextProvider i18n={i18next}>
|
|
<StrictMode>
|
|
<HydratedRouter onError={sentryHydrationErrorHandler} />
|
|
</StrictMode>
|
|
</I18nextProvider>
|
|
);
|
|
// Signal hydration completion so end-to-end tests can wait for the app to
|
|
// be interactive before driving JS-only controls (sidebar drawer, command
|
|
// palette, dropdowns). React 18 attaches event handlers during the
|
|
// hydrateRoot commit; flag after two animation frames so the marker only
|
|
// appears once that commit has painted. Progressive-enhanced <Form>s work
|
|
// before this, but pure-JS buttons do not — hence the explicit signal.
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
document.documentElement.setAttribute("data-hydrated", "true");
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
hydrate();
|