diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/storybook/.storybook/addon-pseudo-localization | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/storybook/.storybook/addon-pseudo-localization')
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(); +}; |