Skip to content

Commit ba245e2

Browse files
committed
feat: add i18n support to storybook
1 parent 9ec3a51 commit ba245e2

8 files changed

Lines changed: 1137 additions & 418 deletions

File tree

.storybook/languageAddon.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { Addon, types, useGlobals } from "storybook/manager-api";
9+
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
10+
import React, { useEffect } from "react";
11+
import { GlobeIcon } from "@storybook/icons";
12+
13+
/**
14+
* Returns the title of a language in the user's locale.
15+
*/
16+
function languageTitle(language: string): string {
17+
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
18+
}
19+
20+
export const languageAddon: Addon = {
21+
title: "Language Selector",
22+
type: types.TOOL,
23+
render: ({ active }) => {
24+
const [globals, updateGlobals] = useGlobals();
25+
const selectedLanguage = globals.language || "en";
26+
27+
const [languages, setLanguages] = React.useState<string[]>([]);
28+
useEffect(() => {
29+
const loadLanguages = async () => {
30+
// We can't import `shared/i18n.tsx` directly here.
31+
// The storybook addon doesn't seem to benefit the webpack config of storybook and we can't resolve the alias in i18n.tsx.
32+
const json = await import("../webapp/i18n/languages.json");
33+
const languages = Object.keys(json).filter((lang) => lang !== "default");
34+
setLanguages(languages);
35+
};
36+
37+
loadLanguages();
38+
}, []);
39+
40+
return (
41+
<WithTooltip
42+
placement="top"
43+
trigger="click"
44+
closeOnOutsideClick
45+
tooltip={({ onHide }) => {
46+
return (
47+
<TooltipLinkList
48+
links={languages.map((language) => ({
49+
id: language,
50+
title: languageTitle(language),
51+
active: selectedLanguage === language,
52+
onClick: async () => {
53+
// Update the global state with the selected language
54+
updateGlobals({ language });
55+
onHide();
56+
},
57+
}))}
58+
/>
59+
);
60+
}}
61+
>
62+
<IconButton title="Language">
63+
<GlobeIcon />
64+
{languageTitle(selectedLanguage)}
65+
</IconButton>
66+
</WithTooltip>
67+
);
68+
},
69+
};

.storybook/main.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { StorybookConfig } from "@storybook/react-vite";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { nodePolyfills } from "vite-plugin-node-polyfills";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
28

39
const config: StorybookConfig = {
410
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
11+
staticDirs: ["../webapp"],
512
addons: [
613
"@storybook/addon-docs",
714
"@storybook/addon-designs",
@@ -41,5 +48,20 @@ const config: StorybookConfig = {
4148
typescript: {
4249
reactDocgen: "react-docgen-typescript",
4350
},
51+
async viteFinal(config) {
52+
// Merge custom configuration into the default config
53+
const { mergeConfig } = await import("vite");
54+
55+
return mergeConfig(config, {
56+
resolve: {
57+
alias: {
58+
// Alias used by i18n.tsx
59+
$webapp: path.resolve(__dirname, "../webapp"),
60+
},
61+
},
62+
// Needed for counterpart to work
63+
plugins: [nodePolyfills({ include: ["process", "util"] })],
64+
});
65+
},
4466
};
4567
export default config;

.storybook/manager.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import React from "react";
2+
13
import { addons } from "storybook/manager-api";
24
import ElementTheme from "./ElementTheme";
5+
import { languageAddon } from "./languageAddon";
36

47
addons.setConfig({
58
theme: ElementTheme,
69
});
10+
11+
addons.register("elementhq/language", () => addons.add("language", languageAddon));

.storybook/preview.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
2+
import { addons } from "storybook/preview-api";
23

34
import "../res/css/shared.pcss";
45
import "./preview.css";
56
import React, { useLayoutEffect } from "react";
7+
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
8+
import { setLanguage } from "../src/shared-components/i18n";
69

710
export const globalTypes = {
811
theme: {
912
name: "Theme",
10-
defaultValue: "system",
1113
description: "Global theme for components",
1214
toolbar: {
1315
icon: "circlehollow",
@@ -21,6 +23,14 @@ export const globalTypes = {
2123
],
2224
},
2325
},
26+
language: {
27+
name: "Language",
28+
description: "Global language for components",
29+
},
30+
initialGlobals: {
31+
theme: "system",
32+
language: "en",
33+
},
2434
} satisfies ArgTypes;
2535

2636
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
@@ -48,9 +58,33 @@ const withThemeProvider: Decorator = (Story, context) => {
4858
);
4959
};
5060

61+
const LanguageSwitcher: React.FC<{
62+
language: string;
63+
}> = ({ language }) => {
64+
useLayoutEffect(() => {
65+
const changeLanguage = async (language: string) => {
66+
await setLanguage(language);
67+
// Force the component to re-render to apply the new language
68+
addons.getChannel().emit(FORCE_RE_RENDER);
69+
};
70+
changeLanguage(language);
71+
}, [language]);
72+
73+
return null;
74+
};
75+
76+
export const withLanguageProvider: Decorator = (Story, context) => {
77+
return (
78+
<>
79+
<LanguageSwitcher language={context.globals.language} />
80+
<Story />
81+
</>
82+
);
83+
};
84+
5185
const preview: Preview = {
5286
tags: ["autodocs"],
53-
decorators: [withThemeProvider],
87+
decorators: [withThemeProvider, withLanguageProvider],
5488
};
5589

5690
export default preview;

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
"@storybook/addon-designs": "^10.0.1",
193193
"@storybook/addon-docs": "^9.0.12",
194194
"@storybook/addon-styling-webpack": "^2.0.0",
195+
"@storybook/icons": "^1.4.0",
195196
"@storybook/react-vite": "^9.0.15",
196197
"@stylistic/eslint-plugin": "^4.0.0",
197198
"@svgr/webpack": "^8.0.0",
@@ -303,6 +304,7 @@
303304
"typescript": "5.8.3",
304305
"util": "^0.12.5",
305306
"vite": "^7.0.1",
307+
"vite-plugin-node-polyfills": "^0.24.0",
306308
"web-streams-polyfill": "^4.0.0",
307309
"webpack": "^5.89.0",
308310
"webpack-bundle-analyzer": "^4.8.0",

0 commit comments

Comments
 (0)