mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Support translations in Storybook: locale toolbar (#6843)
## What Adds a globe toolbar to Storybook so any story can be previewed in all 42 supported languages (the i18n init already on `main` was English-only). ## How Bundles every locale's `translation.toml` via a `?raw` glob into i18next resources, and switches language on toolbar change. RTL locales (`ar`, `fa`) flip `document.dir`. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
co-authored by
James Brunton
parent
ecddce9944
commit
8b179fbc55
@@ -1,3 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
// Storybook compiles .storybook/* with the classic JSX runtime, so the JSX in
|
||||
// the decorators below transpiles to React.createElement and needs React in
|
||||
// scope. (The app + story files use the automatic runtime via the portal vite
|
||||
@@ -22,28 +23,38 @@ import { configureSupabase } from "@proprietary/auth/supabase/supabaseClient";
|
||||
import i18next from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
// Load the real English copy so stories render human text, not raw keys. Bundled
|
||||
// synchronously via ?raw so it's present on the very first render (no async flash).
|
||||
import enTranslationToml from "@public/locales/en-US/translation.toml?raw";
|
||||
import { rtlLanguages, supportedLanguages } from "@core/i18n/languages";
|
||||
|
||||
import "@mantine/core/styles.css";
|
||||
import "@core/tokens/tokens.css";
|
||||
import "@core/theme/index.css";
|
||||
import "@core/tokens/base.css";
|
||||
|
||||
// Storybook-only: init react-i18next with the real English resources parsed from
|
||||
// the app's TOML, so t(key) renders the shipped copy (e.g. "No sources connected
|
||||
// yet") rather than the raw key. Falls back to an empty bundle if parsing ever
|
||||
// fails, so a malformed TOML can't take the whole Storybook down.
|
||||
function parseEnTranslation(): Record<string, unknown> {
|
||||
// Storybook-only: bundle every shipped locale's TOML at build time via a ?raw
|
||||
// glob, so the toolbar language switcher can flip between all languages with no
|
||||
// async fetch (Storybook has no backend to serve /locales/). t(key) then renders
|
||||
// the shipped copy (e.g. "No sources connected yet") rather than the raw key.
|
||||
const localeModules = import.meta.glob<string>(
|
||||
"../editor/public/locales/*/translation.toml",
|
||||
{ query: "?raw", import: "default", eager: true },
|
||||
);
|
||||
|
||||
// Parse each locale into an i18next resources map. A malformed TOML degrades to
|
||||
// an empty bundle for that one locale (its keys fall back to en-US) rather than
|
||||
// taking the whole Storybook down.
|
||||
const resources: Record<string, { translation: Record<string, unknown> }> = {};
|
||||
for (const [path, raw] of Object.entries(localeModules)) {
|
||||
const lng = path.match(/\/locales\/([^/]+)\/translation\.toml$/)?.[1];
|
||||
if (!lng) continue;
|
||||
let translation: Record<string, unknown>;
|
||||
try {
|
||||
return parseToml(enTranslationToml) as Record<string, unknown>;
|
||||
translation = parseToml(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
translation = {};
|
||||
}
|
||||
resources[lng] = { translation };
|
||||
}
|
||||
|
||||
const enTranslationResources = parseEnTranslation();
|
||||
if (!i18next.isInitialized) {
|
||||
// initImmediate: false → initialise synchronously from the inline resources
|
||||
// (there's no async backend here), so i18next is ready before the first story
|
||||
@@ -51,24 +62,25 @@ if (!i18next.isInitialized) {
|
||||
void i18next.use(initReactI18next).init({
|
||||
lng: "en-US",
|
||||
fallbackLng: "en-US",
|
||||
supportedLngs: ["en-US"],
|
||||
resources: { "en-US": { translation: enTranslationResources } },
|
||||
supportedLngs: Object.keys(resources),
|
||||
resources,
|
||||
interpolation: { escapeValue: false },
|
||||
react: { useSuspense: false },
|
||||
initImmediate: false,
|
||||
});
|
||||
} else {
|
||||
// Something initialised i18next first (e.g. the app's async TOML backend):
|
||||
// inject the shipped English copy under the resolved language so t() renders
|
||||
// real copy, not raw keys.
|
||||
const lng = i18next.resolvedLanguage || i18next.language || "en-US";
|
||||
i18next.addResourceBundle(
|
||||
lng,
|
||||
"translation",
|
||||
enTranslationResources,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
// inject every shipped locale's copy so t() renders real copy, not raw keys,
|
||||
// and the toolbar switcher can still change to any of them.
|
||||
for (const [lng, bundle] of Object.entries(resources)) {
|
||||
i18next.addResourceBundle(
|
||||
lng,
|
||||
"translation",
|
||||
bundle.translation,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Start MSW once. Storybook runs in a browser so this uses the service worker.
|
||||
@@ -156,6 +168,19 @@ function SchemeSetup({ scheme }: { scheme: "light" | "dark" }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Switches i18next to the toolbar locale and keeps document dir/lang in sync. */
|
||||
const withLocale: Decorator = (Story, context) => {
|
||||
const locale = (context.globals.locale as string) ?? "en-US";
|
||||
useEffect(() => {
|
||||
void i18next.changeLanguage(locale);
|
||||
document.documentElement.dir = rtlLanguages.includes(locale)
|
||||
? "rtl"
|
||||
: "ltr";
|
||||
document.documentElement.lang = locale;
|
||||
}, [locale]);
|
||||
return <Story />;
|
||||
};
|
||||
|
||||
const withProviders: Decorator = (Story, context) => {
|
||||
const tier = (context.globals.tier as Tier) ?? "pro";
|
||||
const linkState =
|
||||
@@ -243,8 +268,22 @@ const preview: Preview = {
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
locale: {
|
||||
name: "Locale",
|
||||
description: "Active language — drives useTranslation() in all stories",
|
||||
defaultValue: "en-US",
|
||||
toolbar: {
|
||||
icon: "globe",
|
||||
items: Object.entries(supportedLanguages).map(([value, title]) => ({
|
||||
value,
|
||||
title: `${value} - ${title}`,
|
||||
})),
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
withLocale,
|
||||
withProviders,
|
||||
withThemeByDataAttribute({
|
||||
themes: { light: "light", dark: "dark" },
|
||||
|
||||
Reference in New Issue
Block a user