summaryrefslogtreecommitdiffstats
path: root/browser/components/storybook/.storybook/addon-pseudo-localization
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/storybook/.storybook/addon-pseudo-localization')
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/FluentPanel.mjs79
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/PseudoLocalizationButton.mjs55
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/constants.mjs32
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/index.js23
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/preset/manager.mjs34
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/preset/preview.mjs27
-rw-r--r--browser/components/storybook/.storybook/addon-pseudo-localization/withPseudoLocalization.mjs89
7 files changed, 339 insertions, 0 deletions
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/FluentPanel.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/FluentPanel.mjs
new file mode 100644
index 0000000000..5718ec5601
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/FluentPanel.mjs
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import React from "react";
+import { addons } from "@storybook/addons";
+// eslint-disable-next-line no-unused-vars
+import { AddonPanel } from "@storybook/components";
+import { FLUENT_CHANGED, FLUENT_SET_STRINGS } from "./constants.mjs";
+
+export class FluentPanel extends React.Component {
+ constructor(props) {
+ super(props);
+ this.channel = addons.getChannel();
+ this.state = {
+ name: null,
+ strings: [],
+ };
+ }
+
+ componentDidMount() {
+ const { api } = this.props;
+ api.on(FLUENT_CHANGED, this.handleFluentChanged);
+ }
+
+ componentWillUnmount() {
+ const { api } = this.props;
+ api.off(FLUENT_CHANGED, this.handleFluentChanged);
+ }
+
+ handleFluentChanged = strings => {
+ let storyData = this.props.api.getCurrentStoryData();
+ let fileName = `${storyData.component}.ftl`;
+ this.setState(state => ({ ...state, strings, fileName }));
+ };
+
+ onInput = e => {
+ this.setState(state => {
+ let strings = [];
+ for (let [key, value] of state.strings) {
+ if (key == e.target.name) {
+ strings.push([key, e.target.value]);
+ } else {
+ strings.push([key, value]);
+ }
+ }
+ let stringified = strings
+ .map(([key, value]) => `${key} = ${value}`)
+ .join("\n");
+ this.channel.emit(FLUENT_SET_STRINGS, stringified);
+ const { fluentStrings } = this.props.api.getGlobals();
+ this.props.api.updateGlobals({
+ fluentStrings: { ...fluentStrings, [state.fileName]: strings },
+ });
+ return { ...state, strings };
+ });
+ };
+
+ render() {
+ const { api, active } = this.props;
+ const { strings } = this.state;
+ return (
+ <AddonPanel active={!!active} api={api}>
+ {strings.map(([identifier, value]) => (
+ <div key={identifier}>
+ <label>
+ {identifier} =
+ <textarea
+ name={identifier}
+ onInput={this.onInput}
+ defaultValue={value}
+ ></textarea>
+ </label>
+ </div>
+ ))}
+ </AddonPanel>
+ );
+ }
+}
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/PseudoLocalizationButton.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/PseudoLocalizationButton.mjs
new file mode 100644
index 0000000000..d60112d224
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/PseudoLocalizationButton.mjs
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// eslint-disable-next-line no-unused-vars
+import React from "react";
+import { useGlobals } from "@storybook/api";
+import {
+ // eslint-disable-next-line no-unused-vars
+ Icons,
+ // eslint-disable-next-line no-unused-vars
+ IconButton,
+ // eslint-disable-next-line no-unused-vars
+ WithTooltip,
+ // eslint-disable-next-line no-unused-vars
+ TooltipLinkList,
+} from "@storybook/components";
+import { TOOL_ID, STRATEGY_DEFAULT, PSEUDO_STRATEGIES } from "./constants.mjs";
+
+// React component for a button + tooltip that gets added to the Storybook toolbar.
+export const PseudoLocalizationButton = () => {
+ const [{ pseudoStrategy = STRATEGY_DEFAULT }, updateGlobals] = useGlobals();
+
+ const updatePseudoStrategy = strategy => {
+ updateGlobals({ pseudoStrategy: strategy });
+ };
+
+ const getTooltipLinks = ({ onHide }) => {
+ return PSEUDO_STRATEGIES.map(strategy => ({
+ id: strategy,
+ title: strategy.charAt(0).toUpperCase() + strategy.slice(1),
+ onClick: () => {
+ updatePseudoStrategy(strategy);
+ onHide();
+ },
+ active: pseudoStrategy === strategy,
+ }));
+ };
+
+ return (
+ <WithTooltip
+ placement="top"
+ trigger="click"
+ tooltip={props => <TooltipLinkList links={getTooltipLinks(props)} />}
+ >
+ <IconButton
+ key={TOOL_ID}
+ active={pseudoStrategy && pseudoStrategy !== STRATEGY_DEFAULT}
+ title="Apply pseudo localization"
+ >
+ <Icons icon="transfer" />
+ </IconButton>
+ </WithTooltip>
+ );
+};
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/constants.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/constants.mjs
new file mode 100644
index 0000000000..7d059593dd
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/constants.mjs
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export const ADDON_ID = "addon-pseudo-localization";
+export const PANEL_ID = `${ADDON_ID}/fluentPanel`;
+export const TOOL_ID = `${ADDON_ID}/toolbarButton`;
+
+export const STRATEGY_DEFAULT = "default";
+export const STRATEGY_ACCENTED = "accented";
+export const STRATEGY_BIDI = "bidi";
+
+export const PSEUDO_STRATEGIES = [
+ STRATEGY_DEFAULT,
+ STRATEGY_ACCENTED,
+ STRATEGY_BIDI,
+];
+
+export const DIRECTIONS = {
+ ltr: "ltr",
+ rtl: "rtl",
+};
+
+export const DIRECTION_BY_STRATEGY = {
+ [STRATEGY_DEFAULT]: DIRECTIONS.ltr,
+ [STRATEGY_ACCENTED]: DIRECTIONS.ltr,
+ [STRATEGY_BIDI]: DIRECTIONS.rtl,
+};
+
+export const UPDATE_STRATEGY_EVENT = "update-strategy";
+export const FLUENT_SET_STRINGS = "fluent-set-strings";
+export const FLUENT_CHANGED = "fluent-changed";
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/index.js b/browser/components/storybook/.storybook/addon-pseudo-localization/index.js
new file mode 100644
index 0000000000..7f923e2de1
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/index.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint-env node */
+
+/**
+ * This file hooks our addon into Storybook. Having a root-level file like this
+ * is a Storybook requirement. It handles registering the addon without any
+ * additional user configuration.
+ */
+
+function config(entry = []) {
+ return [...entry, require.resolve("./preset/preview.mjs")];
+}
+
+function managerEntries(entry = []) {
+ return [...entry, require.resolve("./preset/manager.mjs")];
+}
+
+module.exports = {
+ managerEntries,
+ config,
+};
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/preset/manager.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/preset/manager.mjs
new file mode 100644
index 0000000000..0f7ff9299b
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/preset/manager.mjs
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** This file handles registering the Storybook addon */
+
+// eslint-disable-next-line no-unused-vars
+import React from "react";
+import { addons, types } from "@storybook/addons";
+import { ADDON_ID, PANEL_ID, TOOL_ID } from "../constants.mjs";
+import { PseudoLocalizationButton } from "../PseudoLocalizationButton.mjs";
+// eslint-disable-next-line no-unused-vars
+import { FluentPanel } from "../FluentPanel.mjs";
+
+// Register the addon.
+addons.register(ADDON_ID, api => {
+ // Register the tool.
+ addons.add(TOOL_ID, {
+ type: types.TOOL,
+ title: "Pseudo Localization",
+ // Toolbar button doesn't show on the "Docs" tab.
+ match: ({ viewMode }) => !!(viewMode && viewMode.match(/^story$/)),
+ render: PseudoLocalizationButton,
+ });
+
+ addons.add(PANEL_ID, {
+ title: "Fluent",
+ //👇 Sets the type of UI element in Storybook
+ type: types.PANEL,
+ render: ({ active, key }) => (
+ <FluentPanel active={active} api={api} key={key}></FluentPanel>
+ ),
+ });
+});
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/preset/preview.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/preset/preview.mjs
new file mode 100644
index 0000000000..cf4f135d40
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/preset/preview.mjs
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file provides global decorators for the Storybook addon. In theory we
+ * could combine multiple decorators, but for now we only need one.
+ */
+
+import {
+ withPseudoLocalization,
+ withFluentStrings,
+} from "../withPseudoLocalization.mjs";
+
+export const decorators = [withPseudoLocalization, withFluentStrings];
+export const globalTypes = {
+ pseudoStrategy: {
+ name: "Pseudo l10n strategy",
+ description: "Provides text variants for testing different locales.",
+ defaultValue: "default",
+ },
+ fluentStrings: {
+ name: "Fluent string map for components",
+ description: "Mapping of component to fluent strings.",
+ defaultValue: {},
+ },
+};
diff --git a/browser/components/storybook/.storybook/addon-pseudo-localization/withPseudoLocalization.mjs b/browser/components/storybook/.storybook/addon-pseudo-localization/withPseudoLocalization.mjs
new file mode 100644
index 0000000000..9d6c62af38
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-pseudo-localization/withPseudoLocalization.mjs
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { useEffect, useGlobals, addons } from "@storybook/addons";
+import {
+ DIRECTIONS,
+ DIRECTION_BY_STRATEGY,
+ UPDATE_STRATEGY_EVENT,
+ FLUENT_CHANGED,
+} from "./constants.mjs";
+import { provideFluent } from "../fluent-utils.mjs";
+
+/**
+ * withPseudoLocalization is a Storybook decorator that handles emitting an
+ * event to update translations when a new pseudo localization strategy is
+ * applied. It also handles setting a "dir" attribute on the root element in the
+ * Storybook iframe.
+ *
+ * @param {Function} StoryFn - Provided by Storybook, used to render the story.
+ * @param {Object} context - Provided by Storybook, data about the story.
+ * @returns {Function} StoryFn with a modified "dir" attr set.
+ */
+export const withPseudoLocalization = (StoryFn, context) => {
+ const [{ pseudoStrategy }] = useGlobals();
+ const direction = DIRECTION_BY_STRATEGY[pseudoStrategy] || DIRECTIONS.ltr;
+ const isInDocs = context.viewMode === "docs";
+ const channel = addons.getChannel();
+
+ useEffect(() => {
+ if (pseudoStrategy) {
+ channel.emit(UPDATE_STRATEGY_EVENT, pseudoStrategy);
+ }
+ }, [pseudoStrategy]);
+
+ useEffect(() => {
+ if (isInDocs) {
+ document.documentElement.setAttribute("dir", DIRECTIONS.ltr);
+ let storyElements = document.querySelectorAll(".docs-story");
+ storyElements.forEach(element => element.setAttribute("dir", direction));
+ } else {
+ document.documentElement.setAttribute("dir", direction);
+ }
+ }, [direction, isInDocs]);
+
+ return StoryFn();
+};
+
+/**
+ * withFluentStrings is a Storybook decorator that handles emitting an
+ * event to update the Fluent strings shown in the Fluent panel.
+ *
+ * @param {Function} StoryFn - Provided by Storybook, used to render the story.
+ * @param {Object} context - Provided by Storybook, data about the story.
+ * @returns {Function} StoryFn unmodified.
+ */
+export const withFluentStrings = (StoryFn, context) => {
+ const [{ fluentStrings }, updateGlobals] = useGlobals();
+ const channel = addons.getChannel();
+
+ const fileName = context.component + ".ftl";
+ let strings = [];
+
+ if (context.parameters?.fluent && fileName) {
+ if (fluentStrings.hasOwnProperty(fileName)) {
+ strings = fluentStrings[fileName];
+ } else {
+ let resource = provideFluent(context.parameters.fluent, fileName);
+ for (let message of resource.body) {
+ strings.push([
+ message.id,
+ [
+ message.value,
+ ...Object.entries(message.attributes).map(
+ ([key, value]) => ` .${key} = ${value}`
+ ),
+ ].join("\n"),
+ ]);
+ }
+ updateGlobals({
+ fluentStrings: { ...fluentStrings, [fileName]: strings },
+ });
+ }
+ }
+
+ channel.emit(FLUENT_CHANGED, strings);
+
+ return StoryFn();
+};