summaryrefslogtreecommitdiffstats
path: root/browser/components/storybook/.storybook/addon-fluent
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/storybook/.storybook/addon-fluent')
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs121
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs55
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/constants.mjs32
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/fluent-panel.css83
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/index.js23
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs34
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs27
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs89
8 files changed, 464 insertions, 0 deletions
diff --git a/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs
new file mode 100644
index 0000000000..692ff73737
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/FluentPanel.mjs
@@ -0,0 +1,121 @@
+/* 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";
+// eslint-disable-next-line import/no-unassigned-import
+import "./fluent-panel.css";
+
+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) {
+ let stringValue = e.target.value;
+ if (stringValue.startsWith(".")) {
+ stringValue = "\n" + stringValue;
+ }
+ strings.push([key, stringValue]);
+ } 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;
+ if (strings.length === 0) {
+ return (
+ <AddonPanel active={!!active} api={api}>
+ <div className="addon-panel-body">
+ <div className="addon-panel-message">
+ This story is not configured to use Fluent.
+ </div>
+ </div>
+ </AddonPanel>
+ );
+ }
+
+ return (
+ <AddonPanel active={!!active} api={api}>
+ <div className="addon-panel-body">
+ <table aria-hidden="false" className="addon-panel-table">
+ <thead className="addon-panel-table-head">
+ <tr>
+ <th>
+ <span>Identifier</span>
+ </th>
+ <th>
+ <span>String</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody className="addon-panel-table-body">
+ {strings.map(([identifier, value]) => (
+ <tr key={identifier}>
+ <td>
+ <span>{identifier}</span>
+ </td>
+ <td>
+ <label>
+ <textarea
+ name={identifier}
+ onInput={this.onInput}
+ defaultValue={value
+ .trim()
+ .split("\n")
+ .map(s => s.trim())
+ .join("\n")}
+ ></textarea>
+ </label>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </AddonPanel>
+ );
+ }
+}
diff --git a/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs b/browser/components/storybook/.storybook/addon-fluent/PseudoLocalizationButton.mjs
new file mode 100644
index 0000000000..d60112d224
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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-fluent/constants.mjs b/browser/components/storybook/.storybook/addon-fluent/constants.mjs
new file mode 100644
index 0000000000..3f00b2972a
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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-fluent";
+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-fluent/fluent-panel.css b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css
new file mode 100644
index 0000000000..75f4562820
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/fluent-panel.css
@@ -0,0 +1,83 @@
+/* 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/. */
+
+.addon-panel-body {
+ box-sizing: border-box;
+}
+
+.addon-panel-message {
+ background: #FFF5CF;
+ color: #333333;
+ padding: 10px 15px;
+ line-height: 20px;
+ box-shadow: rgba(0,0,0,.1) 0 -1px 0 0 inset;
+ font-size: 13px;
+}
+
+.addon-panel-table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ color: #333333;
+ font-size: 13px;
+ line-height: 20px;
+ text-align: left;
+ width: 100%;
+ margin: 0;
+}
+
+.addon-panel-table-head {
+ color: rgba(51,51,51,0.75);
+}
+
+.addon-panel-table-head th {
+ padding: 10px 15px;
+ border: none;
+ vertical-align: top;
+}
+
+.addon-panel-table-head th:first-of-type, .addon-panel-table-body td:first-of-type {
+ width: 25%;
+ padding-left: 20px;
+}
+
+.addon-panel-table-head th:last-of-type, .addon-panel-table-body td:last-of-type {
+ padding-right: 20px;
+}
+
+.addon-panel-table-body {
+ border-radius: 4px;
+}
+
+.addon-panel-table-body tr {
+ overflow: hidden;
+ border-top: 1px solid #e6e6e6;
+}
+
+.addon-panel-table-body td {
+ padding: 10px 15px;
+ font-weight: bold;
+}
+
+.addon-panel-table-body label {
+ display: flex;
+}
+
+.addon-panel-table-body textarea {
+ height: fit-content;
+ appearance: none;
+ border: none;
+ box-sizing: inherit;
+ display: block;
+ margin: 0;
+ background-color: rgb(255, 255, 255);
+ padding: 6px 10px;
+ color: #333333;
+ box-shadow: rgba(0,0,0,.1) 0 0 0 1px inset;
+ border-radius: 4px;
+ line-height: 20px;
+ flex: 1;
+ text-align: left;
+ overflow: visible;
+ max-height: 400px;
+}
diff --git a/browser/components/storybook/.storybook/addon-fluent/index.js b/browser/components/storybook/.storybook/addon-fluent/index.js
new file mode 100644
index 0000000000..7f923e2de1
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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-fluent/preset/manager.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs
new file mode 100644
index 0000000000..0f7ff9299b
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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-fluent/preset/preview.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/preview.mjs
new file mode 100644
index 0000000000..cf4f135d40
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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-fluent/withPseudoLocalization.mjs b/browser/components/storybook/.storybook/addon-fluent/withPseudoLocalization.mjs
new file mode 100644
index 0000000000..9d6c62af38
--- /dev/null
+++ b/browser/components/storybook/.storybook/addon-fluent/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();
+};