Introduce i18nContext (#31347)
* Introduce i18nContext * Adds a context that holds the module i1n API * Switches shared components to use that instead of importing it directly * Adds the context to MatrixChat and BaseDalog so it should be available most places in EW This is a relatively small PR but does change the way the shared components do i18n so just doing this one by itself (it stands by itself anyway). This will allow shared components to use i18n when used in modules. * Add the file * Fix import lint * Name the translate function _t Then it should continue to get picked up by the script This seems a bit flaky and ew but I'm not sure I want to get into changing this in this PR. * Put humanize back to calling something called _t too * Missed one * Add i18n context wrapper to stories * Unused import * Fix imports * wrap richitem * Wrap other richitem & richlist * One day I will get my head around this syntax * Fix import spacing * Add wrapper to test * unused import * Hack around dependency cycle * Make a moduleapi instance for tests * Add i18n wrapper to jest-matrix-react * Simple test for i18napi * Import type * Move i18n context wrapper to storybook template * Unused imports & fix pill story * Move i18n to its own provider * Add i18ncontext wrapper to jest tests * imports * Bump module api to 1.7.0 * tsdoc
This commit is contained in:
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@element-hq/element-web-module-api": "1.6.0",
|
"@element-hq/element-web-module-api": "1.7.0",
|
||||||
"@element-hq/web-shared-components": "link:packages/shared-components",
|
"@element-hq/web-shared-components": "link:packages/shared-components",
|
||||||
"@fontsource/fira-code": "^5",
|
"@fontsource/fira-code": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useLayoutEffect } from "react";
|
|||||||
import { setLanguage } from "../src/utils/i18n";
|
import { setLanguage } from "../src/utils/i18n";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { StoryContext } from "storybook/internal/csf";
|
import { StoryContext } from "storybook/internal/csf";
|
||||||
|
import { I18nApi, I18nContext } from "../src";
|
||||||
|
|
||||||
export const globalTypes = {
|
export const globalTypes = {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -70,9 +71,17 @@ const withTooltipProvider: Decorator = (Story) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withI18nProvider: Decorator = (Story) => {
|
||||||
|
return (
|
||||||
|
<I18nContext.Provider value={new I18nApi()}>
|
||||||
|
<Story />
|
||||||
|
</I18nContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
decorators: [withThemeProvider, withTooltipProvider],
|
decorators: [withThemeProvider, withTooltipProvider, withI18nProvider],
|
||||||
parameters: {
|
parameters: {
|
||||||
options: {
|
options: {
|
||||||
storySort: {
|
storySort: {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { fireEvent } from "@testing-library/dom";
|
|||||||
import * as stories from "./AudioPlayerView.stories.tsx";
|
import * as stories from "./AudioPlayerView.stories.tsx";
|
||||||
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
|
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
|
||||||
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
|
import { MockViewModel } from "../../viewmodel/MockViewModel.ts";
|
||||||
|
import { I18nContext } from "../../utils/i18nContext.ts";
|
||||||
|
import { I18nApi } from "../../index.ts";
|
||||||
|
|
||||||
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
|
const { Default, NoMediaName, NoSize, HasError } = composeStories(stories);
|
||||||
|
|
||||||
@@ -64,7 +66,9 @@ describe("AudioPlayerView", () => {
|
|||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<AudioPlayerView vm={vm} />);
|
render(<AudioPlayerView vm={vm} />, {
|
||||||
|
wrapper: ({ children }) => <I18nContext.Provider value={new I18nApi()}>{children}</I18nContext.Provider>,
|
||||||
|
});
|
||||||
await user.click(screen.getByRole("button", { name: "Play" }));
|
await user.click(screen.getByRole("button", { name: "Play" }));
|
||||||
expect(togglePlay).toHaveBeenCalled();
|
expect(togglePlay).toHaveBeenCalled();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex";
|
|||||||
import styles from "./AudioPlayerView.module.css";
|
import styles from "./AudioPlayerView.module.css";
|
||||||
import { PlayPauseButton } from "../PlayPauseButton";
|
import { PlayPauseButton } from "../PlayPauseButton";
|
||||||
import { type PlaybackState } from "../playback";
|
import { type PlaybackState } from "../playback";
|
||||||
import { _t } from "../../utils/i18n";
|
import { useI18n } from "../../utils/i18nContext";
|
||||||
import { formatBytes } from "../../utils/FormattingUtils";
|
import { formatBytes } from "../../utils/FormattingUtils";
|
||||||
import { Clock } from "../Clock";
|
import { Clock } from "../Clock";
|
||||||
import { SeekBar } from "../SeekBar";
|
import { SeekBar } from "../SeekBar";
|
||||||
@@ -90,6 +90,8 @@ interface AudioPlayerViewProps {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function AudioPlayerView({ vm }: Readonly<AudioPlayerViewProps>): JSX.Element {
|
export function AudioPlayerView({ vm }: Readonly<AudioPlayerViewProps>): JSX.Element {
|
||||||
|
const { translate: _t } = useI18n();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
playbackState,
|
playbackState,
|
||||||
mediaName = _t("timeline|m.audio|unnamed_audio"),
|
mediaName = _t("timeline|m.audio|unnamed_audio"),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid"
|
|||||||
import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
|
import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid";
|
||||||
|
|
||||||
import styles from "./PlayPauseButton.module.css";
|
import styles from "./PlayPauseButton.module.css";
|
||||||
import { _t } from "../../utils/i18n";
|
import { useI18n } from "../../utils/i18nContext";
|
||||||
|
|
||||||
export interface PlayPauseButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
export interface PlayPauseButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +46,8 @@ export function PlayPauseButton({
|
|||||||
togglePlay,
|
togglePlay,
|
||||||
...rest
|
...rest
|
||||||
}: Readonly<PlayPauseButtonProps>): JSX.Element {
|
}: Readonly<PlayPauseButtonProps>): JSX.Element {
|
||||||
|
const { translate: _t } = useI18n();
|
||||||
|
|
||||||
const label = playing ? _t("action|pause") : _t("action|play");
|
const label = playing ? _t("action|pause") : _t("action|play");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { throttle } from "lodash";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import style from "./SeekBar.module.css";
|
import style from "./SeekBar.module.css";
|
||||||
import { _t } from "../../utils/i18n";
|
import { useI18n } from "../../utils/i18nContext";
|
||||||
|
|
||||||
export interface SeekBarProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
export interface SeekBarProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +33,8 @@ interface ISeekCSS extends CSSProperties {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function SeekBar({ value = 0, className, ...rest }: Readonly<SeekBarProps>): JSX.Element {
|
export function SeekBar({ value = 0, className, ...rest }: Readonly<SeekBarProps>): JSX.Element {
|
||||||
|
const { translate: _t } = useI18n();
|
||||||
|
|
||||||
const [newValue, setNewValue] = useState(value);
|
const [newValue, setNewValue] = useState(value);
|
||||||
// Throttle the value setting to avoid excessive re-renders
|
// Throttle the value setting to avoid excessive re-renders
|
||||||
const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);
|
const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []);
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export * from "./utils/Flex";
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export * from "./utils/i18n";
|
export * from "./utils/i18n";
|
||||||
|
export * from "./utils/i18nContext";
|
||||||
export * from "./utils/humanize";
|
export * from "./utils/humanize";
|
||||||
export * from "./utils/DateUtils";
|
export * from "./utils/DateUtils";
|
||||||
export * from "./utils/numbers";
|
export * from "./utils/numbers";
|
||||||
export * from "./utils/FormattingUtils";
|
export * from "./utils/FormattingUtils";
|
||||||
|
export * from "./utils/I18nApi";
|
||||||
|
|
||||||
// MVVM
|
// MVVM
|
||||||
export * from "./viewmodel";
|
export * from "./viewmodel";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"
|
|||||||
|
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import styles from "./Pill.module.css";
|
import styles from "./Pill.module.css";
|
||||||
import { _t } from "../../utils/i18n";
|
import { useI18n } from "../../utils/i18nContext";
|
||||||
|
|
||||||
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
|
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +39,7 @@ export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick
|
|||||||
*/
|
*/
|
||||||
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
|
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
const { translate: _t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
|
|
||||||
import { RichItem } from "./RichItem";
|
|
||||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||||
|
import { RichItem } from "./RichItem";
|
||||||
|
|
||||||
const currentTimestamp = new Date("2025-03-09T12:00:00Z").getTime();
|
const currentTimestamp = new Date("2025-03-09T12:00:00Z").getTime();
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import React, { type HTMLAttributes, type JSX, memo } from "react";
|
|||||||
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||||
|
|
||||||
import styles from "./RichItem.module.css";
|
import styles from "./RichItem.module.css";
|
||||||
import { humanizeTime } from "../../utils/humanize";
|
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
|
import { useI18n } from "../../utils/i18nContext";
|
||||||
|
|
||||||
export interface RichItemProps extends HTMLAttributes<HTMLLIElement> {
|
export interface RichItemProps extends HTMLAttributes<HTMLLIElement> {
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +63,8 @@ export const RichItem = memo(function RichItem({
|
|||||||
selected,
|
selected,
|
||||||
...props
|
...props
|
||||||
}: RichItemProps): JSX.Element {
|
}: RichItemProps): JSX.Element {
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={styles.richItem}
|
className={styles.richItem}
|
||||||
@@ -77,7 +79,7 @@ export const RichItem = memo(function RichItem({
|
|||||||
<span className={styles.description}>{description}</span>
|
<span className={styles.description}>{description}</span>
|
||||||
{timestamp && (
|
{timestamp && (
|
||||||
<span role="timer" className={styles.timestamp}>
|
<span role="timer" className={styles.timestamp}>
|
||||||
{humanizeTime(timestamp)}
|
{i18n.humanizeTime(timestamp)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -16,16 +16,24 @@ import React, { type ReactElement } from "react";
|
|||||||
import { render, type RenderOptions } from "@testing-library/react";
|
import { render, type RenderOptions } from "@testing-library/react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { I18nApi, I18nContext } from "../..";
|
||||||
|
|
||||||
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
||||||
return ({ children }: { children: React.ReactNode }) => {
|
return ({ children }: { children: React.ReactNode }) => {
|
||||||
if (Wrapper) {
|
if (Wrapper) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<I18nContext.Provider value={new I18nApi()}>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<Wrapper>
|
||||||
</Wrapper>
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
|
</Wrapper>
|
||||||
|
</I18nContext.Provider>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <TooltipProvider>{children}</TooltipProvider>;
|
return (
|
||||||
|
<I18nContext.Provider value={new I18nApi()}>
|
||||||
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
|
</I18nContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
22
packages/shared-components/src/utils/I18nApi.test.ts
Normal file
22
packages/shared-components/src/utils/I18nApi.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type TranslationKey } from "../i18nKeys";
|
||||||
|
import { I18nApi } from "./I18nApi";
|
||||||
|
|
||||||
|
describe("I18nApi", () => {
|
||||||
|
it("can register a translation and use it", () => {
|
||||||
|
const i18n = new I18nApi();
|
||||||
|
i18n.register({
|
||||||
|
"hello.world": {
|
||||||
|
en: "Hello, World!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(i18n.translate("hello.world" as TranslationKey)).toBe("Hello, World!");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,16 +6,17 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api";
|
import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api";
|
||||||
import { registerTranslations } from "@element-hq/web-shared-components";
|
|
||||||
|
|
||||||
import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx";
|
import { humanizeTime } from "./humanize";
|
||||||
|
import { _t, getLocale, registerTranslations } from "./i18n";
|
||||||
|
import { type TranslationKey } from "../i18nKeys";
|
||||||
|
|
||||||
export class I18nApi implements II18nApi {
|
export class I18nApi implements II18nApi {
|
||||||
/**
|
/**
|
||||||
* Read the current language of the user in IETF Language Tag format
|
* Read the current language of the user in IETF Language Tag format
|
||||||
*/
|
*/
|
||||||
public get language(): string {
|
public get language(): string {
|
||||||
return getCurrentLanguage();
|
return getLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,4 +45,8 @@ export class I18nApi implements II18nApi {
|
|||||||
public translate(key: TranslationKey, variables?: Variables): string {
|
public translate(key: TranslationKey, variables?: Variables): string {
|
||||||
return _t(key, variables);
|
return _t(key, variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public humanizeTime(timeMillis: number): string {
|
||||||
|
return humanizeTime(timeMillis, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { _t } from "./i18n";
|
import { type I18nApi } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
|
import { _t as _tFromModule } from "./i18n";
|
||||||
|
|
||||||
// These are the constants we use for when to break the text
|
// These are the constants we use for when to break the text
|
||||||
const MILLISECONDS_RECENT = 15000;
|
const MILLISECONDS_RECENT = 15000;
|
||||||
@@ -21,13 +23,15 @@ const HOURS_1_DAY = 26;
|
|||||||
* @param {number} timeMillis The time in millis to compare against.
|
* @param {number} timeMillis The time in millis to compare against.
|
||||||
* @returns {string} The humanized time.
|
* @returns {string} The humanized time.
|
||||||
*/
|
*/
|
||||||
export function humanizeTime(timeMillis: number): string {
|
export function humanizeTime(timeMillis: number, i18nApi?: I18nApi): string {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let msAgo = now - timeMillis;
|
let msAgo = now - timeMillis;
|
||||||
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
const minutes = Math.abs(Math.ceil(msAgo / 60000));
|
||||||
const hours = Math.ceil(minutes / 60);
|
const hours = Math.ceil(minutes / 60);
|
||||||
const days = Math.ceil(hours / 24);
|
const days = Math.ceil(hours / 24);
|
||||||
|
|
||||||
|
const _t = i18nApi?.translate ?? _tFromModule;
|
||||||
|
|
||||||
if (msAgo >= 0) {
|
if (msAgo >= 0) {
|
||||||
// Past
|
// Past
|
||||||
if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago");
|
if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago");
|
||||||
|
|||||||
27
packages/shared-components/src/utils/i18nContext.ts
Normal file
27
packages/shared-components/src/utils/i18nContext.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import { type I18nApi } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
|
export const I18nContext = createContext<I18nApi | null>(null);
|
||||||
|
I18nContext.displayName = "I18nContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook to get the i18n API from the context. Will throw if no i18n context is found.
|
||||||
|
* @throws If no i18n context is found
|
||||||
|
* @returns The i18n API from the context
|
||||||
|
*/
|
||||||
|
export function useI18n(): I18nApi {
|
||||||
|
const i18n = useContext(I18nContext);
|
||||||
|
|
||||||
|
if (!i18n) {
|
||||||
|
throw new Error("useI18n must be used within an I18nContext.Provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
|
|||||||
// what-input helps improve keyboard accessibility
|
// what-input helps improve keyboard accessibility
|
||||||
import "what-input";
|
import "what-input";
|
||||||
import sanitizeHtml from "sanitize-html";
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
import { I18nContext } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import PosthogTrackers from "../../PosthogTrackers";
|
import PosthogTrackers from "../../PosthogTrackers";
|
||||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||||
@@ -2272,9 +2273,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<SDKContext.Provider value={this.stores}>
|
<I18nContext.Provider value={ModuleApi.instance.i18n}>
|
||||||
<TooltipProvider>{view}</TooltipProvider>
|
<SDKContext.Provider value={this.stores}>
|
||||||
</SDKContext.Provider>
|
<TooltipProvider>{view}</TooltipProvider>
|
||||||
|
</SDKContext.Provider>
|
||||||
|
</I18nContext.Provider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import React, { type JSX } from "react";
|
|||||||
import FocusLock from "react-focus-lock";
|
import FocusLock from "react-focus-lock";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { I18nContext } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
@@ -164,38 +165,42 @@ export default class BaseDialog extends React.Component<IProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this.matrixClient}>
|
// XXX: We can't import ModuleAPI here because it causes a dependency cycle - hack and
|
||||||
{this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
|
// use the copy on the window object :(
|
||||||
<FocusLock
|
<I18nContext.Provider value={window.mxModuleApi.i18n}>
|
||||||
returnFocus={true}
|
<MatrixClientContext.Provider value={this.matrixClient}>
|
||||||
lockProps={lockProps}
|
{this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
|
||||||
className={classNames(this.props.className, {
|
<FocusLock
|
||||||
mx_Dialog_fixedWidth: this.props.fixedWidth,
|
returnFocus={true}
|
||||||
})}
|
lockProps={lockProps}
|
||||||
>
|
className={classNames(this.props.className, {
|
||||||
{this.props.top}
|
mx_Dialog_fixedWidth: this.props.fixedWidth,
|
||||||
<div
|
|
||||||
className={classNames("mx_Dialog_header", {
|
|
||||||
mx_Dialog_headerWithButton: !!this.props.headerButton,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!!(this.props.title || headerImage) && (
|
{this.props.top}
|
||||||
<Heading
|
<div
|
||||||
size="3"
|
className={classNames("mx_Dialog_header", {
|
||||||
as="h1"
|
mx_Dialog_headerWithButton: !!this.props.headerButton,
|
||||||
className={classNames("mx_Dialog_title", this.props.titleClass)}
|
})}
|
||||||
id="mx_BaseDialog_title"
|
>
|
||||||
>
|
{!!(this.props.title || headerImage) && (
|
||||||
{headerImage}
|
<Heading
|
||||||
{this.props.title}
|
size="3"
|
||||||
</Heading>
|
as="h1"
|
||||||
)}
|
className={classNames("mx_Dialog_title", this.props.titleClass)}
|
||||||
{this.props.headerButton}
|
id="mx_BaseDialog_title"
|
||||||
</div>
|
>
|
||||||
{this.props.children}
|
{headerImage}
|
||||||
{cancelButton}
|
{this.props.title}
|
||||||
</FocusLock>
|
</Heading>
|
||||||
</MatrixClientContext.Provider>
|
)}
|
||||||
|
{this.props.headerButton}
|
||||||
|
</div>
|
||||||
|
{this.props.children}
|
||||||
|
{cancelButton}
|
||||||
|
</FocusLock>
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
</I18nContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { createRoot, type Root } from "react-dom/client";
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
import { type Api, type RuntimeModuleConstructor } from "@element-hq/element-web-module-api";
|
import { type Api, type RuntimeModuleConstructor } from "@element-hq/element-web-module-api";
|
||||||
|
import { I18nApi } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
import { ModuleRunner } from "./ModuleRunner.ts";
|
import { ModuleRunner } from "./ModuleRunner.ts";
|
||||||
import AliasCustomisations from "../customisations/Alias.ts";
|
import AliasCustomisations from "../customisations/Alias.ts";
|
||||||
@@ -20,7 +21,6 @@ import UserIdentifierCustomisations from "../customisations/UserIdentifier.ts";
|
|||||||
import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts";
|
import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts";
|
||||||
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
|
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
|
||||||
import { ConfigApi } from "./ConfigApi.ts";
|
import { ConfigApi } from "./ConfigApi.ts";
|
||||||
import { I18nApi } from "./I18nApi.ts";
|
|
||||||
import { CustomComponentsApi } from "./customComponentApi";
|
import { CustomComponentsApi } from "./customComponentApi";
|
||||||
import { WatchableProfile } from "./Profile.ts";
|
import { WatchableProfile } from "./Profile.ts";
|
||||||
import { NavigationApi } from "./Navigation.ts";
|
import { NavigationApi } from "./Navigation.ts";
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
|
import { ModuleLoader } from "@element-hq/element-web-module-api";
|
||||||
|
|
||||||
import * as languageHandler from "../../src/languageHandler";
|
import * as languageHandler from "../../src/languageHandler";
|
||||||
import en from "../../src/i18n/strings/en_EN.json";
|
import en from "../../src/i18n/strings/en_EN.json";
|
||||||
import de from "../../src/i18n/strings/de_DE.json";
|
import de from "../../src/i18n/strings/de_DE.json";
|
||||||
|
import { ModuleApi } from "../../src/modules/Api";
|
||||||
|
|
||||||
const lv = {
|
const lv = {
|
||||||
Save: "Saglabāt",
|
Save: "Saglabāt",
|
||||||
@@ -43,3 +45,7 @@ setupLanguageMock();
|
|||||||
|
|
||||||
languageHandler.setLanguage("en");
|
languageHandler.setLanguage("en");
|
||||||
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
||||||
|
|
||||||
|
// Set up the mdule API (so the i18n API exists)
|
||||||
|
const moduleLoader = new ModuleLoader(ModuleApi.instance);
|
||||||
|
window.mxModuleLoader = moduleLoader;
|
||||||
|
|||||||
@@ -10,17 +10,33 @@ import React, { type ReactElement } from "react";
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { render, type RenderOptions } from "@testing-library/react";
|
import { render, type RenderOptions } from "@testing-library/react";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
import { I18nContext } from "@element-hq/web-shared-components";
|
||||||
|
|
||||||
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
/**
|
||||||
|
* Wraps the provided components in:
|
||||||
|
* * A TooltipProvider
|
||||||
|
* * An I18nContext.Provider
|
||||||
|
*
|
||||||
|
* ...plus any wrapper provided in the options.
|
||||||
|
* @param Wrapper Additional wrapper to include
|
||||||
|
* @returns The wrapped component
|
||||||
|
*/
|
||||||
|
const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => {
|
||||||
return ({ children }: { children: React.ReactNode }) => {
|
return ({ children }: { children: React.ReactNode }) => {
|
||||||
if (Wrapper) {
|
if (Wrapper) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<TooltipProvider>{children}</TooltipProvider>
|
<I18nContext.Provider value={window.mxModuleApi.i18n}>
|
||||||
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
|
</I18nContext.Provider>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <TooltipProvider>{children}</TooltipProvider>;
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<I18nContext.Provider value={window.mxModuleApi.i18n}>{children}</I18nContext.Provider>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -28,7 +44,7 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
|||||||
const customRender = (ui: ReactElement, options: RenderOptions = {}) => {
|
const customRender = (ui: ReactElement, options: RenderOptions = {}) => {
|
||||||
return render(ui, {
|
return render(ui, {
|
||||||
...options,
|
...options,
|
||||||
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"],
|
wrapper: wrapWithStandardContexts(options?.wrapper) as RenderOptions["wrapper"],
|
||||||
}) as ReturnType<typeof render>;
|
}) as ReturnType<typeof render>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@@ -1559,10 +1559,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3"
|
resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3"
|
||||||
integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA==
|
integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA==
|
||||||
|
|
||||||
"@element-hq/element-web-module-api@1.6.0":
|
"@element-hq/element-web-module-api@1.7.0":
|
||||||
version "1.6.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.6.0.tgz#bb109035cd0c82c094e2e83ba66b6c5b9788d58f"
|
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.7.0.tgz#7657df25cc1e7075718af2c6ea8a4ebfaa9cfb2c"
|
||||||
integrity sha512-7xew6AVX4T3J37KyhdgHuiEYdDMMYJC0/aIQOmBvVylWQFnmMmbkmkuqOBqkumcx7q6LgkB0z3cSzdKAKHIw/g==
|
integrity sha512-WhiJTmdETK8vvaYExqyhQ9rtLjxBv9PprWr6dCa1/1VRFSkfFZRlzy2P08nHX2YXpRMTpXb39SLeleR1dgLzow==
|
||||||
|
|
||||||
"@element-hq/element-web-playwright-common@^2.0.0":
|
"@element-hq/element-web-playwright-common@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -4153,13 +4153,14 @@
|
|||||||
|
|
||||||
"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm":
|
"@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@vector-im/matrix-wysiwyg@2.40.0":
|
"@vector-im/matrix-wysiwyg@2.40.0":
|
||||||
version "2.40.0"
|
version "2.40.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.40.0.tgz#53c9ca5ea907d91e4515da64f20a82e5586b882c"
|
resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.40.0.tgz#53c9ca5ea907d91e4515da64f20a82e5586b882c"
|
||||||
integrity sha512-8LRFLs5PEKYs4lOL7aJ4lL/hGCrvEvOYkCR3JggXYXDVMtX4LmfdlKYucSAe98pCmqAAbLRvlRcR1bTOYvM8ug==
|
integrity sha512-8LRFLs5PEKYs4lOL7aJ4lL/hGCrvEvOYkCR3JggXYXDVMtX4LmfdlKYucSAe98pCmqAAbLRvlRcR1bTOYvM8ug==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm"
|
"@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm"
|
||||||
|
|
||||||
"@vitest/expect@3.2.4":
|
"@vitest/expect@3.2.4":
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
@@ -9599,7 +9600,7 @@ matrix-events-sdk@0.0.1:
|
|||||||
jwt-decode "^4.0.0"
|
jwt-decode "^4.0.0"
|
||||||
loglevel "^1.9.2"
|
loglevel "^1.9.2"
|
||||||
matrix-events-sdk "0.0.1"
|
matrix-events-sdk "0.0.1"
|
||||||
matrix-widget-api "^1.14.0"
|
matrix-widget-api "^1.10.0"
|
||||||
oidc-client-ts "^3.0.1"
|
oidc-client-ts "^3.0.1"
|
||||||
p-retry "7"
|
p-retry "7"
|
||||||
sdp-transform "^3.0.0"
|
sdp-transform "^3.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user