Merge branch 'develop' into germain-gg/facepile-offset

This commit is contained in:
Germain 2023-09-22 08:46:37 +01:00 committed by GitHub
commit 224f34c211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
163 changed files with 15302 additions and 13240 deletions

View file

@ -119,7 +119,7 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
className="mx_FileDropTarget_image"
alt=""
/>
{_t("Drop file here to upload")}
{_t("room|drop_file_prompt")}
</div>
);
}

View file

@ -231,7 +231,7 @@ class FilePanel extends React.Component<IProps, IState> {
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
<div className="mx_RoomView_empty">
{_t(
"You must <a>register</a> to use this functionality",
"file_panel|guest_note",
{},
{
a: (sub) => (
@ -247,7 +247,7 @@ class FilePanel extends React.Component<IProps, IState> {
} else if (this.noRoom) {
return (
<BaseCard className="mx_FilePanel mx_RoomView_messageListWrapper" onClose={this.props.onClose}>
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
<div className="mx_RoomView_empty">{_t("file_panel|peek_note")}</div>
</BaseCard>
);
}
@ -256,8 +256,8 @@ class FilePanel extends React.Component<IProps, IState> {
const emptyState = (
<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t("No files visible in this room")}</h2>
<p>{_t("Attach files from chat or just drag and drop them anywhere in a room.")}</p>
<h2>{_t("file_panel|empty_heading")}</h2>
<p>{_t("file_panel|empty_description")}</p>
</div>
);

View file

@ -515,12 +515,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const normalFontSize = "15px";
const waitText = _t("Wait!");
const scamText = _t(
"If someone told you to copy/paste something here, there is a high likelihood you're being scammed!",
);
const devText = _t(
"If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!",
);
const scamText = _t("console_scam_warning");
const devText = _t("console_dev_note");
global.mx_rage_logger.bypassRageshake(
"log",
@ -1611,8 +1607,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
Modal.createDialog(ErrorDialog, {
title: _t("Signed Out"),
description: _t("For security, this session has been signed out. Please sign in again."),
title: _t("auth|session_logged_out_title"),
description: _t("auth|session_logged_out_description"),
});
dis.dispatch({
@ -1623,19 +1619,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialog(
QuestionDialog,
{
title: _t("Terms and Conditions"),
title: _t("terms|tac_title"),
description: (
<div>
<p>
{" "}
{_t(
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.",
{ homeserverDomain: cli.getDomain() },
)}
</p>
<p> {_t("terms|tac_description", { homeserverDomain: cli.getDomain() })}</p>
</div>
),
button: _t("Review terms and conditions"),
button: _t("terms|tac_button"),
cancelButton: _t("action|dismiss"),
onFinished: (confirmed) => {
if (confirmed) {
@ -1676,11 +1666,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
switch (type) {
case "CRYPTO_WARNING_OLD_VERSION_DETECTED":
Modal.createDialog(ErrorDialog, {
title: _t("Old cryptography data detected"),
description: _t(
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
{ brand: SdkConfig.get().brand },
),
title: _t("encryption|old_version_detected_title"),
description: _t("encryption|old_version_detected_description", {
brand: SdkConfig.get().brand,
}),
});
break;
}
@ -1736,7 +1725,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({
key: "verifreq_" + request.transactionId,
title: _t("Verification requested"),
title: _t("encryption|verification_requested_toast_title"),
icon: "verification",
props: { request },
component: VerificationRequestToast,

View file

@ -1232,9 +1232,9 @@ class CreationGrouper extends BaseGrouper {
const roomId = ev.getRoomId();
const creator = ev.sender?.name ?? ev.getSender();
if (roomId && DMRoomMap.shared().getUserIdForRoomId(roomId)) {
summaryText = _t("%(creator)s created this DM.", { creator });
summaryText = _t("timeline|creation_summary_dm", { creator });
} else {
summaryText = _t("%(creator)s created and configured the room.", { creator });
summaryText = _t("timeline|creation_summary_room", { creator });
}
ret.push(<NewRoomIntro key="newroomintro" />);

View file

@ -58,8 +58,8 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
public render(): React.ReactNode {
const emptyState = (
<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t("You're all caught up")}</h2>
<p>{_t("You have no visible notifications.")}</p>
<h2>{_t("notif_panel|empty_heading")}</h2>
<p>{_t("notif_panel|empty_description")}</p>
</div>
);

View file

@ -2335,7 +2335,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
className="mx_RoomView_auxPanel_hiddenHighlights"
onClick={this.onHiddenHighlightsClick}
>
{_t("You have %(count)s unread notifications in a prior version of this room.", {
{_t("room|unread_notifications_predecessor", {
count: hiddenHighlightCount,
})}
</AccessibleButton>

View file

@ -245,9 +245,7 @@ const Tile: React.FC<ITileProps> = ({
let suggestedSection: ReactElement | undefined;
if (suggested && (!joinedRoom || hasPermissions)) {
suggestedSection = (
<InfoTooltip tooltip={_t("This room is suggested as a good one to join")}>{_t("Suggested")}</InfoTooltip>
);
suggestedSection = <InfoTooltip tooltip={_t("space|suggested_tooltip")}>{_t("space|suggested")}</InfoTooltip>;
}
const content = (
@ -670,14 +668,14 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
if (!selectedRelations.length) {
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
tooltip: _t("space|select_room_below"),
alignment: Alignment.Top,
};
}
let buttonText = _t("Saving…");
if (!saving) {
buttonText = selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested");
buttonText = selectionAllSuggested ? _t("space|unmark_suggested") : _t("space|mark_suggested");
}
return (
@ -707,7 +705,7 @@ const ManageButtons: React.FC<IManageButtonsProps> = ({ hierarchy, selected, set
hierarchy.removeRelation(parentId, childId);
}
} catch (e) {
setError(_t("Failed to remove some rooms. Try again later"));
setError(_t("space|failed_remove_rooms"));
}
setRemoving(false);
setSelected(new Map());
@ -788,13 +786,13 @@ const SpaceHierarchy: React.FC<IProps> = ({ space, initialText = "", showRoom, a
const [error, setError] = useState("");
let errorText = error;
if (!error && hierarchyError) {
errorText = _t("Failed to load list of rooms.");
errorText = _t("space|failed_load_rooms");
}
const loaderRef = useIntersectionObserver(loadMore);
if (!loading && hierarchy!.noSupport) {
return <p>{_t("Your server does not support showing space hierarchies.")}</p>;
return <p>{_t("space|incompatible_server_hierarchy")}</p>;
}
const onKeyDown = (ev: KeyboardEvent, state: IState): void => {

View file

@ -487,7 +487,7 @@ const validateEmailRules = withValidation({
{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
invalid: () => _t("auth|email_field_label_invalid"),
},
],
});
@ -509,7 +509,7 @@ const SpaceSetupPrivateInvite: React.FC<{
name={name}
type="text"
label={_t("Email address")}
placeholder={_t("Email")}
placeholder={_t("auth|email_field_label")}
value={emailAddresses[i]}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => setEmailAddress(i, ev.target.value)}
ref={fieldRefs[i]}

View file

@ -354,7 +354,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock"
label={_t("Security & Privacy")}
label={_t("room_settings|security|title")}
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
<IconizedContextMenuOption
@ -410,11 +410,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
<RovingAccessibleTooltipButton
className="mx_UserMenu_contextMenu_themeButton"
onClick={this.onSwitchThemeClick}
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
title={
this.state.isDarkTheme
? _t("user_menu|switch_theme_light")
: _t("user_menu|switch_theme_dark")
}
>
<img
src={require("../../../res/img/element-icons/roomlist/dark-light-mode.svg").default}
alt={_t("Switch theme")}
role="presentation"
alt=""
width={16}
/>
</RovingAccessibleTooltipButton>

View file

@ -79,19 +79,21 @@ export default class ViewSource extends React.Component<IProps, IState> {
<>
<details open className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
<span className="mx_ViewSource_heading">
{_t("devtools|view_source_decrypted_event_source")}
</span>
</summary>
{decryptedEventSource ? (
<CopyableText getTextToCopy={copyDecryptedFunc}>
<SyntaxHighlight language="json">{stringify(decryptedEventSource)}</SyntaxHighlight>
</CopyableText>
) : (
<div>{_t("Decrypted source unavailable")}</div>
<div>{_t("devtools|view_source_decrypted_event_source_unavailable")}</div>
)}
</details>
<details className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
<span className="mx_ViewSource_heading">{_t("devtools|original_event_source")}</span>
</summary>
<CopyableText getTextToCopy={copyOriginalFunc}>
<SyntaxHighlight language="json">{stringify(originalEventSource)}</SyntaxHighlight>
@ -102,7 +104,7 @@ export default class ViewSource extends React.Component<IProps, IState> {
} else {
return (
<>
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
<div className="mx_ViewSource_heading">{_t("devtools|original_event_source")}</div>
<CopyableText getTextToCopy={copyOriginalFunc}>
<SyntaxHighlight language="json">{stringify(originalEventSource)}</SyntaxHighlight>
</CopyableText>

View file

@ -396,7 +396,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
<PassphraseField
name="reset_password"
type="password"
label={_td("New Password")}
label={_td("auth|change_password_new_label")}
value={this.state.password}
minScore={PASSWORD_MIN_SCORE}
fieldRef={(field) => (this.fieldPassword = field)}

View file

@ -591,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
const signIn = (
<span className="mx_AuthBody_changeFlow">
{_t(
"auth|sign_in_instead",
"auth|sign_in_instead_prompt",
{},
{
a: (sub) => (
@ -682,7 +682,7 @@ export default class Registration extends React.Component<IProps, IState> {
title={_t("Create account")}
serverPicker={
<ServerPicker
title={_t("auth|server_picker_title")}
title={_t("auth|server_picker_title_registration")}
dialogTitle={_t("auth|server_picker_dialog_title")}
serverConfig={this.props.serverConfig}
onServerConfigChange={

View file

@ -53,7 +53,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
return (
<>
<EMailPromptIcon className="mx_AuthBody_emailPromptIcon--shifted" />
<h1>{_t("Check your email to continue")}</h1>
<h1>{_t("auth|uia|email_auth_header")}</h1>
<div className="mx_AuthBody_text">
<p>{_t("auth|check_email_explainer", { email: email }, { b: (t) => <b>{t}</b> })}</p>
<div className="mx_AuthBody_did-not-receive">

View file

@ -40,9 +40,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
class EmailField extends PureComponent<IProps> {
public static defaultProps = {
label: _td("Email"),
labelRequired: _td("Enter email address"),
labelInvalid: _td("Doesn't look like a valid email address"),
label: _td("auth|email_field_label"),
labelRequired: _td("auth|email_field_label_required"),
labelInvalid: _td("auth|email_field_label_invalid"),
};
public readonly validate = withValidation({

View file

@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswor
return (
<div>
<p>{_t("Confirm your identity by entering your account password below.")}</p>
<p>{_t("auth|uia|password_prompt")}</p>
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field
className={passwordBoxClass}
@ -219,9 +219,7 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
let sitePublicKey: string | undefined;
if (!this.props.stageParams || !this.props.stageParams.public_key) {
errorText = _t(
"Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.",
);
errorText = _t("auth|uia|recaptcha_missing_params");
} else {
sitePublicKey = this.props.stageParams.public_key;
}
@ -352,7 +350,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
if (allChecked) {
this.props.submitAuthDict({ type: AuthType.Terms });
} else {
this.setState({ errorText: _t("Please review and accept all of the homeserver's policies") });
this.setState({ errorText: _t("auth|uia|terms_invalid") });
}
};
@ -403,7 +401,7 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
return (
<div className="mx_InteractiveAuthEntryComponents">
<p>{_t("Please review and accept the policies of this homeserver:")}</p>
<p>{_t("auth|uia|terms")}</p>
{checkboxes}
{errorSection}
{submitButton}
@ -474,19 +472,19 @@ export class EmailIdentityAuthEntry extends React.Component<
return (
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<AuthHeaderModifier
title={_t("Check your email to continue")}
icon={<img src={EmailPromptIcon} alt={_t("Unread email icon")} width={16} />}
title={_t("auth|uia|email_auth_header")}
icon={<img src={EmailPromptIcon} role="presentation" alt="" width={16} />}
hideServerPicker={true}
/>
<p>
{_t("To create your account, open the link in the email we just sent to %(emailAddress)s.", {
{_t("auth|uia|email", {
emailAddress: <b>{this.props.inputs.emailAddress}</b>,
})}
</p>
{this.state.requesting ? (
<p className="secondary">
{_t(
"Did not receive it? <a>Resend it</a>",
"auth|uia|email_resend_prompt",
{},
{
a: (text: string) => (
@ -502,13 +500,15 @@ export class EmailIdentityAuthEntry extends React.Component<
) : (
<p className="secondary">
{_t(
"Did not receive it? <a>Resend it</a>",
"auth|uia|email_resend_prompt",
{},
{
a: (text: string) => (
<AccessibleTooltipButton
kind="link_inline"
title={this.state.requested ? _t("Resent!") : _t("action|resend")}
title={
this.state.requested ? _t("auth|uia|email_resent") : _t("action|resend")
}
alignment={Alignment.Right}
onHideTooltip={
this.state.requested
@ -642,7 +642,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
});
} else {
this.setState({
errorText: _t("Token incorrect"),
errorText: _t("auth|uia|msisdn_token_incorrect"),
});
}
} catch (e) {
@ -670,8 +670,8 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
}
return (
<div>
<p>{_t("A text message has been sent to %(msisdn)s", { msisdn: <i>{this.msisdn}</i> })}</p>
<p>{_t("Please enter the code it contains:")}</p>
<p>{_t("auth|uia|msisdn", { msisdn: <i>{this.msisdn}</i> })}</p>
<p>{_t("auth|uia|msisdn_token_prompt")}</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this.onFormSubmit}>
<input
@ -761,13 +761,13 @@ export class RegistrationTokenAuthEntry extends React.Component<IAuthEntryProps,
return (
<div>
<p>{_t("Enter a registration token provided by the homeserver administrator.")}</p>
<p>{_t("auth|uia|registration_token_prompt")}</p>
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_registrationTokenSection">
<Field
className={registrationTokenBoxClass}
type="text"
name="registrationTokenField"
label={_t("Registration token")}
label={_t("auth|uia|registration_token_label")}
autoFocus={true}
value={this.state.registrationToken}
onChange={this.onRegistrationTokenFieldChange}
@ -895,7 +895,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
} else if (this.state.attemptFailed) {
errorSection = (
<div className="error" role="alert">
{_t("Something went wrong in confirming your identity. Cancel and try again.")}
{_t("auth|uia|sso_failed")}
</div>
);
}
@ -972,7 +972,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
return (
<div>
<AccessibleButton kind="link" inputRef={this.fallbackButton} onClick={this.onShowFallbackClick}>
{_t("Start authentication")}
{_t("auth|uia|fallback_button")}
</AccessibleButton>
{errorSection}
</div>

View file

@ -169,7 +169,7 @@ export default class LoginWithQRFlow extends React.Component<IProps> {
);
break;
case Phase.ShowingQR:
title = _t("Sign in with QR code");
title = _t("settings|sessions|sign_in_with_qr");
if (this.props.code) {
const code = (
<div className="mx_LoginWithQR_qrWrapper">

View file

@ -37,9 +37,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> {
class PassphraseConfirmField extends PureComponent<IProps> {
public static defaultProps = {
label: _td("Confirm password"),
labelRequired: _td("Confirm password"),
labelInvalid: _td("Passwords don't match"),
label: _td("auth|change_password_confirm_label"),
labelRequired: _td("auth|change_password_confirm_label"),
labelInvalid: _td("auth|change_password_confirm_invalid"),
};
private validate = withValidation({

View file

@ -46,9 +46,9 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
class PassphraseField extends PureComponent<IProps> {
public static defaultProps = {
label: _td("common|password"),
labelEnterPassword: _td("Enter password"),
labelStrongPassword: _td("Nice, strong password!"),
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
labelEnterPassword: _td("auth|password_field_label"),
labelStrongPassword: _td("auth|password_field_strong_label"),
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
};
public readonly validate = withValidation<this, ZxcvbnResult | null>({
@ -91,7 +91,7 @@ class PassphraseField extends PureComponent<IProps> {
return null;
}
const { feedback } = complexity;
return feedback.warning || feedback.suggestions[0] || _t("Keep going…");
return feedback.warning || feedback.suggestions[0] || _t("auth|password_field_keep_going_prompt");
},
},
],

View file

@ -214,7 +214,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
test({ value, allowEmpty }) {
return allowEmpty || !!value;
},
invalid: () => _t("Enter username"),
invalid: () => _t("auth|username_field_required_invalid"),
},
],
});
@ -236,12 +236,12 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
test({ value, allowEmpty }): boolean {
return allowEmpty || !!value;
},
invalid: (): string => _t("Enter phone number"),
invalid: (): string => _t("auth|msisdn_field_required_invalid"),
},
{
key: "number",
test: ({ value }): boolean => !value || PHONE_NUMBER_REGEX.test(value),
invalid: (): string => _t("That phone number doesn't look quite right, please check and try again"),
invalid: (): string => _t("auth|msisdn_field_number_invalid"),
},
],
});
@ -259,7 +259,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
test({ value, allowEmpty }): boolean {
return allowEmpty || !!value;
},
invalid: (): string => _t("Enter password"),
invalid: (): string => _t("auth|password_field_label"),
},
],
});
@ -341,7 +341,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
autoComplete="tel-national"
key="phone_input"
type="text"
label={_t("Phone")}
label={_t("auth|msisdn_field_label")}
value={this.props.phoneNumber}
prefixComponent={phoneCountry}
onChange={this.onPhoneNumberChanged}
@ -378,7 +378,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
kind="link"
onClick={this.onForgotPasswordClick}
>
{_t("Forgot password?")}
{_t("auth|reset_password_button")}
</AccessibleButton>
);
}
@ -396,7 +396,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
if (!SdkConfig.get().disable_3pid_login) {
loginType = (
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{_t("Sign in with")}</label>
<label className="mx_Login_type_label">{_t("auth|identifier_label")}</label>
<Field
element="select"
value={this.state.loginType}
@ -410,7 +410,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
{_t("Email address")}
</option>
<option key={LoginField.Password} value={LoginField.Password}>
{_t("Phone")}
{_t("auth|msisdn_field_label")}
</option>
</Field>
</div>

View file

@ -267,7 +267,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
};
private validateEmailRules = withValidation({
description: () => _t("Use an email address to recover your account"),
description: () => _t("auth|reset_password_email_field_description"),
hideDescriptionIfValid: true,
rules: [
{
@ -275,12 +275,12 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
test(this: RegistrationForm, { value, allowEmpty }) {
return allowEmpty || !this.authStepIsRequired("m.login.email.identity") || !!value;
},
invalid: () => _t("Enter email address (required on this homeserver)"),
invalid: () => _t("auth|reset_password_email_field_required_invalid"),
},
{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
invalid: () => _t("auth|email_field_label_invalid"),
},
],
});
@ -324,7 +324,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
};
private validatePhoneNumberRules = withValidation({
description: () => _t("Other users can invite you to rooms using your contact details"),
description: () => _t("auth|msisdn_field_description"),
hideDescriptionIfValid: true,
rules: [
{
@ -332,12 +332,12 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
test(this: RegistrationForm, { value, allowEmpty }) {
return allowEmpty || !this.authStepIsRequired("m.login.msisdn") || !!value;
},
invalid: () => _t("Enter phone number (required on this homeserver)"),
invalid: () => _t("auth|registration_msisdn_field_required_invalid"),
},
{
key: "email",
test: ({ value }) => !value || phoneNumberLooksValid(value),
invalid: () => _t("That phone number doesn't look quite right, please check and try again"),
invalid: () => _t("auth|msisdn_field_number_invalid"),
},
],
});
@ -358,7 +358,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
description: (_, results) => {
// omit the description if the only failing result is the `available` one as it makes no sense for it.
if (results.every(({ key, valid }) => key === "available" || valid)) return null;
return _t("Use lowercase letters, numbers, dashes and underscores only");
return _t("auth|registration_username_validation");
},
hideDescriptionIfValid: true,
async deriveData(this: RegistrationForm, { value }) {
@ -380,7 +380,7 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter username"),
invalid: () => _t("auth|username_field_required_invalid"),
},
{
key: "safeLocalpart",
@ -401,8 +401,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
},
invalid: (usernameAvailable) =>
usernameAvailable === UsernameAvailableStatus.Error
? _t("Unable to check if username has been taken. Try again later.")
: _t("Someone already has that username. Try another or if it is you, sign in below."),
? _t("auth|registration_username_unable_check")
: _t("auth|registration_username_in_use"),
},
],
});
@ -451,7 +451,9 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (!this.showEmail()) {
return null;
}
const emailLabel = this.authStepIsRequired("m.login.email.identity") ? _td("Email") : _td("Email (optional)");
const emailLabel = this.authStepIsRequired("m.login.email.identity")
? _td("auth|email_field_label")
: _td("Email (optional)");
return (
<EmailField
fieldRef={(field) => (this[RegistrationField.Email] = field)}
@ -496,7 +498,9 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (!this.showPhoneNumber()) {
return null;
}
const phoneLabel = this.authStepIsRequired("m.login.msisdn") ? _t("Phone") : _t("Phone (optional)");
const phoneLabel = this.authStepIsRequired("m.login.msisdn")
? _t("auth|phone_label")
: _t("auth|phone_optional_label");
const phoneCountry = (
<CountryDropdown
value={this.state.phoneCountry}
@ -549,15 +553,13 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (this.showPhoneNumber()) {
emailHelperText = (
<div>
{_t("Add an email to be able to reset your password.")}{" "}
{_t("Use email or phone to optionally be discoverable by existing contacts.")}
{_t("auth|email_help_text")} {_t("auth|email_phone_discovery_text")}
</div>
);
} else {
emailHelperText = (
<div>
{_t("Add an email to be able to reset your password.")}{" "}
{_t("Use email to optionally be discoverable by existing contacts.")}
{_t("auth|email_help_text")} {_t("auth|email_discovery_text")}
</div>
);
}

View file

@ -132,7 +132,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
devtoolsOption = (
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconSettings"
label={_t("See room timeline (devtools)")}
label={_t("space|context_menu|devtools_open_timeline")}
onClick={onViewTimelineClick}
/>
);
@ -243,13 +243,13 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label={_t("Space home")}
label={_t("space|context_menu|home")}
onClick={onHomeClick}
/>
{inviteOption}
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
label={canAddRooms ? _t("space|context_menu|manage_and_explore") : _t("space|context_menu|explore")}
onClick={onExploreRoomsClick}
/>
<IconizedContextMenuOption

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from "react";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { logger } from "matrix-js-sdk/src/logger";
import type CreateKeyBackupDialog from "../../../async-components/views/dialogs/security/CreateKeyBackupDialog";
@ -35,10 +34,28 @@ interface IProps {
onFinished: (success: boolean) => void;
}
enum BackupStatus {
/** we're trying to figure out if there is an active backup */
LOADING,
/** crypto is disabled in this client (so no need to back up) */
NO_CRYPTO,
/** Key backup is active and working */
BACKUP_ACTIVE,
/** there is a backup on the server but we are not backing up to it */
SERVER_BACKUP_BUT_DISABLED,
/** backup is not set up locally and there is no backup on the server */
NO_BACKUP,
/** there was an error fetching the state */
ERROR,
}
interface IState {
shouldLoadBackupStatus: boolean;
loading: boolean;
backupInfo: IKeyBackupInfo | null;
backupStatus: BackupStatus;
}
export default class LogoutDialog extends React.Component<IProps, IState> {
@ -49,33 +66,40 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
const cli = MatrixClientPeg.safeGet();
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
this.state = {
shouldLoadBackupStatus: shouldLoadBackupStatus,
loading: shouldLoadBackupStatus,
backupInfo: null,
backupStatus: BackupStatus.LOADING,
};
if (shouldLoadBackupStatus) {
this.loadBackupStatus();
}
// we can't call setState() immediately, so wait a beat
window.setTimeout(() => this.startLoadBackupStatus(), 0);
}
/** kick off the asynchronous calls to populate `state.backupStatus` in the background */
private startLoadBackupStatus(): void {
this.loadBackupStatus().catch((e) => {
logger.log("Unable to fetch key backup status", e);
this.setState({
backupStatus: BackupStatus.ERROR,
});
});
}
private async loadBackupStatus(): Promise<void> {
try {
const backupInfo = await MatrixClientPeg.safeGet().getKeyBackupVersion();
this.setState({
loading: false,
backupInfo,
});
} catch (e) {
logger.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
});
const client = MatrixClientPeg.safeGet();
const crypto = client.getCrypto();
if (!crypto) {
this.setState({ backupStatus: BackupStatus.NO_CRYPTO });
return;
}
if ((await crypto.getActiveSessionBackupVersion()) !== null) {
this.setState({ backupStatus: BackupStatus.BACKUP_ACTIVE });
return;
}
// backup is not active. see if there is a backup version on the server we ought to back up to.
const backupInfo = await client.getKeyBackupVersion();
this.setState({ backupStatus: backupInfo ? BackupStatus.SERVER_BACKUP_BUT_DISABLED : BackupStatus.NO_BACKUP });
}
private onExportE2eKeysClicked = (): void => {
@ -98,7 +122,7 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
};
private onSetRecoveryMethodClick = (): void => {
if (this.state.backupInfo) {
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
@ -132,82 +156,106 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
this.props.onFinished(true);
};
public render(): React.ReactNode {
if (this.state.shouldLoadBackupStatus) {
const description = (
<div>
<p>
{_t(
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
)}
</p>
<p>
{_t(
"When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.",
)}
</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>
);
/**
* Show a dialog prompting the user to set up key backup.
*
* Either there is no backup at all ({@link BackupStatus.NO_BACKUP}), there is a backup on the server but
* we are not connected to it ({@link BackupStatus.SERVER_BACKUP_BUT_DISABLED}), or we were unable to pull the
* backup data ({@link BackupStatus.ERROR}). In all three cases, we should prompt the user to set up key backup.
*/
private renderSetupBackupDialog(): React.ReactNode {
const description = (
<div>
<p>
{_t(
"Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
)}
</p>
<p>
{_t(
"When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.",
)}
</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</div>
);
let dialogContent;
if (this.state.loading) {
dialogContent = <Spinner />;
} else {
let setupButtonCaption;
if (this.state.backupInfo) {
setupButtonCaption = _t("Connect this session to Key Backup");
} else {
// if there's an error fetching the backup info, we'll just assume there's
// no backup for the purpose of the button caption
setupButtonCaption = _t("Start using Key Backup");
}
dialogContent = (
<div>
<div className="mx_Dialog_content" id="mx_Dialog_content">
{description}
</div>
<DialogButtons
primaryButton={setupButtonCaption}
hasCancel={false}
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this.onLogoutConfirm}>{_t("I don't want my encrypted messages")}</button>
</DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<p>
<button onClick={this.onExportE2eKeysClicked}>{_t("Manually export keys")}</button>
</p>
</details>
</div>
);
}
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (
<BaseDialog
title={_t("You'll lose access to your encrypted messages")}
contentId="mx_Dialog_content"
hasCancel={true}
onFinished={this.onFinished}
>
{dialogContent}
</BaseDialog>
);
let setupButtonCaption;
if (this.state.backupStatus === BackupStatus.SERVER_BACKUP_BUT_DISABLED) {
setupButtonCaption = _t("Connect this session to Key Backup");
} else {
return (
<QuestionDialog
hasCancelButton={true}
title={_t("action|sign_out")}
description={_t("Are you sure you want to sign out?")}
button={_t("action|sign_out")}
onFinished={this.onFinished}
/>
);
// if there's an error fetching the backup info, we'll just assume there's
// no backup for the purpose of the button caption
setupButtonCaption = _t("Start using Key Backup");
}
const dialogContent = (
<div>
<div className="mx_Dialog_content" id="mx_Dialog_content">
{description}
</div>
<DialogButtons
primaryButton={setupButtonCaption}
hasCancel={false}
onPrimaryButtonClick={this.onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this.onLogoutConfirm}>{_t("I don't want my encrypted messages")}</button>
</DialogButtons>
<details>
<summary>{_t("Advanced")}</summary>
<p>
<button onClick={this.onExportE2eKeysClicked}>{_t("Manually export keys")}</button>
</p>
</details>
</div>
);
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (
<BaseDialog
title={_t("You'll lose access to your encrypted messages")}
contentId="mx_Dialog_content"
hasCancel={true}
onFinished={this.onFinished}
>
{dialogContent}
</BaseDialog>
);
}
public render(): React.ReactNode {
switch (this.state.backupStatus) {
case BackupStatus.LOADING:
// while we're deciding if we have backups, show a spinner
return (
<BaseDialog
title={_t("action|sign_out")}
contentId="mx_Dialog_content"
hasCancel={true}
onFinished={this.onFinished}
>
<Spinner />;
</BaseDialog>
);
case BackupStatus.NO_CRYPTO:
case BackupStatus.BACKUP_ACTIVE:
return (
<QuestionDialog
hasCancelButton={true}
title={_t("action|sign_out")}
description={_t("Are you sure you want to sign out?")}
button={_t("action|sign_out")}
onFinished={this.onFinished}
/>
);
case BackupStatus.NO_BACKUP:
case BackupStatus.SERVER_BACKUP_BUT_DISABLED:
case BackupStatus.ERROR:
return this.renderSetupBackupDialog();
}
}
}

View file

@ -88,9 +88,9 @@ export function ManualDeviceKeyVerificationDialog({
return (
<QuestionDialog
title={_t("Verify session")}
title={_t("settings|sessions|verify_session")}
description={body}
button={_t("Verify session")}
button={_t("settings|sessions|verify_session")}
onFinished={onLegacyFinished}
/>
);

View file

@ -136,6 +136,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
clientId: ELEMENT_CLIENT_ID,
clientTheme: SettingsStore.getValue("theme"),
clientLanguage: getUserLanguage(),
baseUrl: MatrixClientPeg.safeGet().baseUrl,
});
const parsed = new URL(templated);

View file

@ -163,7 +163,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
tabs.push(
new Tab(
RoomSettingsTab.Security,
_td("Security & Privacy"),
_td("room_settings|security|title"),
"mx_RoomSettingsDialog_securityIcon",
<SecurityRoomSettingsTab room={this.state.room} closeSettingsFn={() => this.props.onFinished(true)} />,
"RoomSettingsSecurityPrivacy",
@ -172,7 +172,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
tabs.push(
new Tab(
RoomSettingsTab.Roles,
_td("Roles & Permissions"),
_td("room_settings|permissions|title"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab room={this.state.room} />,
"RoomSettingsRolesPermissions",

View file

@ -80,8 +80,8 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
nativeSupport = _t("Checking…");
} else {
nativeSupport = hasNativeSupport
? _t("Your server has native support")
: _t("Your server lacks native support");
? _t("labs|sliding_sync_server_support")
: _t("labs|sliding_sync_server_no_support");
}
const validProxy = withValidation<undefined, { error?: unknown }>({
@ -97,7 +97,7 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
{
key: "required",
test: async ({ value }) => !!value || !!hasNativeSupport,
invalid: () => _t("Your server lacks native support, you must specify a proxy"),
invalid: () => _t("labs|sliding_sync_server_specify_proxy"),
},
{
key: "working",
@ -111,16 +111,20 @@ export const SlidingSyncOptionsDialog: React.FC<{ onFinished(enabled: boolean):
return (
<TextInputDialog
title={_t("Sliding Sync configuration")}
title={_t("labs|sliding_sync_configuration")}
description={
<div>
<div>
<b>{_t("To disable you will need to log out and back in, use with caution!")}</b>
<b>{_t("labs|sliding_sync_disable_warning")}</b>
</div>
{nativeSupport}
</div>
}
placeholder={hasNativeSupport ? _t("Proxy URL (optional)") : _t("Proxy URL")}
placeholder={
hasNativeSupport
? _t("labs|sliding_sync_proxy_url_optional_label")
: _t("labs|sliding_sync_proxy_url_label")
}
value={currentProxy}
button={_t("action|enable")}
validator={validProxy}

View file

@ -67,7 +67,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
),
new Tab(
SpaceSettingsTab.Roles,
_td("Roles & Permissions"),
_td("room_settings|permissions|title"),
"mx_RoomSettingsDialog_rolesIcon",
<RolesRoomSettingsTab room={space} />,
),
@ -84,7 +84,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
return (
<BaseDialog
title={_t("Settings - %(spaceName)s", { spaceName: space.name || _t("common|unnamed_space") })}
title={_t("space_settings|title", { spaceName: space.name || _t("common|unnamed_space") })}
className="mx_SpaceSettingsDialog"
contentId="mx_SpaceSettingsDialog"
onFinished={onFinished}

View file

@ -117,7 +117,7 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
</div>
);
case SERVICE_TYPES.IM:
return <div>{_t("Use bots, bridges, widgets and sticker packs")}</div>;
return <div>{_t("terms|integration_manager")}</div>;
}
}
@ -192,19 +192,19 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
<BaseDialog
fixedWidth={false}
onFinished={this.onCancelClick}
title={_t("Terms of Service")}
title={_t("terms|tos")}
contentId="mx_Dialog_content"
hasCancel={false}
>
<div id="mx_Dialog_content">
<p>{_t("To continue you need to accept the terms of this service.")}</p>
<p>{_t("terms|intro")}</p>
<table className="mx_TermsDialog_termsTable">
<tbody>
<tr className="mx_TermsDialog_termsTableHeader">
<th>{_t("Service")}</th>
<th>{_t("Summary")}</th>
<th>{_t("Document")}</th>
<th>{_t("terms|column_service")}</th>
<th>{_t("terms|column_summary")}</th>
<th>{_t("terms|column_document")}</th>
<th>{_t("action|accept")}</th>
</tr>
{rows}

View file

@ -144,7 +144,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
tabs.push(
new Tab(
UserTab.Security,
_td("Security & Privacy"),
_td("room_settings|security|title"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
"UserSettingsSecurityPrivacy",
@ -179,7 +179,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
tabs.push(
new Tab(
UserTab.Mjolnir,
_td("Ignored users"),
_td("labs_mjolnir|title"),
"mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />,
"UserSettingMjolnir",

View file

@ -479,6 +479,7 @@ export default class ImageView extends React.Component<IProps, IState> {
fallbackUserId={mxEvent.getSender()}
size="32px"
viewUserOnClick={true}
className="mx_Dialog_nonDialogButton"
/>
);

View file

@ -103,7 +103,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
const classes = classNames("mx_Pill", {
mx_AtRoomPill: type === PillType.AtRoomMention,
mx_RoomPill: type === PillType.RoomMention,
mx_SpacePill: type === "space",
mx_SpacePill: type === "space" || targetRoom?.isSpaceRoom(),
mx_UserPill: type === PillType.UserMention,
mx_UserPill_me: resourceId === MatrixClientPeg.safeGet().getUserId(),
mx_EventPill: type === PillType.EventInOtherRoom || type === PillType.EventInSameRoom,

View file

@ -63,8 +63,8 @@ const MAX_OPTION_LENGTH = 340;
function creatingInitialState(): IState {
return {
title: _t("Create poll"),
actionLabel: _t("Create Poll"),
title: _t("poll|create_poll_title"),
actionLabel: _t("poll|create_poll_action"),
canSubmit: false, // need to add a question and at least one option first
question: "",
options: arraySeed("", DEFAULT_NUM_OPTIONS),
@ -79,7 +79,7 @@ function editingInitialState(editingMxEvent: MatrixEvent): IState {
if (!poll?.isEquivalentTo(M_POLL_START)) return creatingInitialState();
return {
title: _t("Edit poll"),
title: _t("poll|edit_poll_title"),
actionLabel: _t("action|done"),
canSubmit: true,
question: poll.question.text,
@ -175,8 +175,8 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
.catch((e) => {
console.error("Failed to post poll:", e);
Modal.createDialog(QuestionDialog, {
title: _t("Failed to post poll"),
description: _t("Sorry, the poll you tried to create was not posted."),
title: _t("poll|failed_send_poll_title"),
description: _t("poll|failed_send_poll_description"),
button: _t("action|try_again"),
cancelButton: _t("action|cancel"),
onFinished: (tryAgain: boolean) => {
@ -197,37 +197,37 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
protected renderContent(): React.ReactNode {
return (
<div className="mx_PollCreateDialog">
<h2>{_t("Poll type")}</h2>
<h2>{_t("poll|type_heading")}</h2>
<Field element="select" value={this.state.kind.name} onChange={this.onPollTypeChange}>
<option key={M_POLL_KIND_DISCLOSED.name} value={M_POLL_KIND_DISCLOSED.name}>
{_t("Open poll")}
{_t("poll|type_open")}
</option>
<option key={M_POLL_KIND_UNDISCLOSED.name} value={M_POLL_KIND_UNDISCLOSED.name}>
{_t("Closed poll")}
{_t("poll|type_closed")}
</option>
</Field>
<p>{pollTypeNotes(this.state.kind)}</p>
<h2>{_t("What is your poll question or topic?")}</h2>
<h2>{_t("poll|topic_heading")}</h2>
<Field
id="poll-topic-input"
value={this.state.question}
maxLength={MAX_QUESTION_LENGTH}
label={_t("Question or topic")}
placeholder={_t("Write something…")}
label={_t("poll|topic_label")}
placeholder={_t("poll|topic_placeholder")}
onChange={this.onQuestionChange}
usePlaceholderAsHint={true}
disabled={this.state.busy}
autoFocus={this.state.autoFocusTarget === FocusTarget.Topic}
/>
<h2>{_t("Create options")}</h2>
<h2>{_t("poll|options_heading")}</h2>
{this.state.options.map((op, i) => (
<div key={`option_${i}`} className="mx_PollCreateDialog_option">
<Field
id={`pollcreate_option_${i}`}
value={op}
maxLength={MAX_OPTION_LENGTH}
label={_t("Option %(number)s", { number: i + 1 })}
placeholder={_t("Write an option")}
label={_t("poll|options_label", { number: i + 1 })}
placeholder={_t("poll|options_placeholder")}
onChange={(e: ChangeEvent<HTMLInputElement>) => this.onOptionChange(i, e)}
usePlaceholderAsHint={true}
disabled={this.state.busy}
@ -250,7 +250,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
className="mx_PollCreateDialog_addOption"
inputRef={this.addOptionRef}
>
{_t("Add option")}
{_t("poll|options_add_button")}
</AccessibleButton>
{this.state.busy && (
<div className="mx_PollCreateDialog_busy">
@ -270,8 +270,8 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
function pollTypeNotes(kind: KnownPollKind): string {
if (M_POLL_KIND_DISCLOSED.matches(kind.name)) {
return _t("Voters see results as soon as they have voted");
return _t("poll|disclosed_notes");
} else {
return _t("Results are only revealed when you end the poll");
return _t("poll|notes");
}
}

View file

@ -99,7 +99,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
dis.dispatch({ action: "open_room_settings" });
}}
>
{_t("Edit topic")}
{_t("room|edit_topic")}
</AccessibleButton>
)}
</div>
@ -119,7 +119,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
onClick={onClick}
dir="auto"
tooltipTargetClassName={className}
label={_t("Click to read topic")}
label={_t("room|read_topic")}
alignment={Alignment.Bottom}
ignoreHover={ignoreHover}
>

View file

@ -106,7 +106,7 @@ const EncryptionInfo: React.FC<IProps> = ({
return (
<React.Fragment>
<div data-testid="encryption-info-description" className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>
<h3>{_t("settings|security|encryption_section")}</h3>
{description}
</div>
<div className="mx_UserInfo_container">

View file

@ -78,7 +78,7 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
value={this.state.isRoomPublished}
onChange={this.onRoomPublishChange}
disabled={!enabled}
label={_t("Publish this room to the public in %(domain)s's room directory?", {
label={_t("room_settings|general|publish_toggle", {
domain: client.getDomain(),
})}
/>

View file

@ -53,7 +53,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
if (accountEnabled) {
previewsForAccount = _t(
"You have <a>enabled</a> URL previews by default.",
"room_settings|general|user_url_previews_default_on",
{},
{
a: (sub) => (
@ -65,7 +65,7 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
);
} else {
previewsForAccount = _t(
"You have <a>disabled</a> URL previews by default.",
"room_settings|general|user_url_previews_default_off",
{},
{
a: (sub) => (
@ -87,16 +87,14 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
/>
);
} else {
let str = _td("URL previews are enabled by default for participants in this room.");
let str = _td("room_settings|general|default_url_previews_on");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) {
str = _td("URL previews are disabled by default for participants in this room.");
str = _td("room_settings|general|default_url_previews_off");
}
previewsForRoom = <div>{_t(str)}</div>;
}
} else {
previewsForAccount = _t(
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
);
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
}
const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in
@ -110,17 +108,13 @@ export default class UrlPreviewSettings extends React.Component<IProps> {
const description = (
<>
<p>
{_t(
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
)}
</p>
<p>{_t("room_settings|general|url_preview_explainer")}</p>
<p>{previewsForAccount}</p>
</>
);
return (
<SettingsFieldset legend={_t("URL Previews")} description={description}>
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}>
{previewsForRoom}
{previewsForRoomAccount}
</SettingsFieldset>

View file

@ -813,7 +813,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
initialTabId: UserTab.Labs,
});
const betaPill = isVideoRoom ? (
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
<BetaPill onClick={viewLabs} tooltipTitle={_t("labs|video_rooms_beta")} />
) : null;
return (

View file

@ -46,14 +46,14 @@ function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room):
const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolean): TranslationKey => {
if (room instanceof LocalRoom) {
return _td("Send your first message to invite <displayName/> to chat");
return _td("room|intro|send_message_start_dm");
}
if (encryptedSingle3rdPartyInvite) {
return _td("Once everyone has joined, youll be able to chat");
return _td("room|intro|encrypted_3pid_dm_pending_join");
}
return _td("This is the beginning of your direct message history with <displayName/>.");
return _td("room|intro|start_of_dm_history");
};
const NewRoomIntro: React.FC = () => {
@ -78,7 +78,7 @@ const NewRoomIntro: React.FC = () => {
!encryptedSingle3rdPartyInvite &&
room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2
) {
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
caption = _t("room|intro|dm_caption");
}
const member = room?.getMember(dmPartner);
@ -133,7 +133,7 @@ const NewRoomIntro: React.FC = () => {
let topicText;
if (canAddTopic && topic) {
topicText = _t(
"Topic: %(topic)s (<a>edit</a>)",
"room|intro|topic_edit",
{ topic },
{
a: (sub) => (
@ -144,10 +144,10 @@ const NewRoomIntro: React.FC = () => {
},
);
} else if (topic) {
topicText = _t("Topic: %(topic)s ", { topic });
topicText = _t("room|intro|topic", { topic });
} else if (canAddTopic) {
topicText = _t(
"<a>Add a topic</a> to help people know what it is about.",
"room|intro|no_topic",
{},
{
a: (sub) => (
@ -164,9 +164,9 @@ const NewRoomIntro: React.FC = () => {
let createdText: string;
if (creator === cli.getUserId()) {
createdText = _t("You created this room.");
createdText = _t("room|intro|you_created");
} else {
createdText = _t("%(displayName)s created this room.", {
createdText = _t("room|intro|user_created", {
displayName: creatorName,
});
}
@ -200,7 +200,7 @@ const NewRoomIntro: React.FC = () => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
{_t("room|intro|room_invite")}
</AccessibleButton>
)}
</div>
@ -228,7 +228,7 @@ const NewRoomIntro: React.FC = () => {
avatar = (
<MiniAvatarUploader
hasAvatar={false}
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
noAvatarLabel={_t("room|intro|no_avatar_label")}
setAvatarUrl={(url) => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, "")}
>
{avatar}
@ -245,7 +245,7 @@ const NewRoomIntro: React.FC = () => {
<p>
{createdText}{" "}
{_t(
"This is the start of <roomName/>.",
"room|intro|start_of_room",
{},
{
roomName: () => <b>{room.name}</b>,
@ -266,9 +266,7 @@ const NewRoomIntro: React.FC = () => {
});
}
const subText = _t(
"Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.",
);
const subText = _t("room|intro|private_unencrypted_warning");
let subButton: JSX.Element | undefined;
if (
@ -277,7 +275,7 @@ const NewRoomIntro: React.FC = () => {
) {
subButton = (
<AccessibleButton kind="link_inline" onClick={openRoomSettings}>
{_t("Enable encryption in settings.")}
{_t("room|intro|enable_encryption_prompt")}
</AccessibleButton>
);
}
@ -294,7 +292,7 @@ const NewRoomIntro: React.FC = () => {
{!hasExpectedEncryptionSettings(cli, room) && (
<EventTileBubble
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
title={_t("End-to-end encryption isn't enabled")}
title={_t("room|intro|unencrypted_warning")}
subtitle={subtitle}
/>
)}

View file

@ -47,6 +47,7 @@ import { useRoomState } from "../../../hooks/useRoomState";
import RoomAvatar from "../avatars/RoomAvatar";
import { formatCount } from "../../../utils/FormattingUtils";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { Linkify, topicToHtml } from "../../../HtmlUtils";
/**
* A helper to transform a notification color to the what the Compound Icon Button
@ -100,6 +101,11 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
const notificationsEnabled = useFeatureEnabled("feature_notifications");
const roomTopicBody = useMemo(
() => topicToHtml(roomTopic?.text, roomTopic?.html),
[roomTopic?.html, roomTopic?.text],
);
return (
<Flex
as="header"
@ -159,7 +165,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
</BodyText>
{roomTopic && (
<BodyText as="div" size="sm" className="mx_RoomHeader_topic">
{roomTopic.text}
<Linkify>{roomTopicBody}</Linkify>
</BodyText>
)}
</Box>

View file

@ -162,7 +162,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
<>
<RoomAvatar room={room} size="50px" viewAvatarOnClick />
<div className="mx_RoomPreviewCard_video" />
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
<BetaPill onClick={viewLabs} tooltipTitle={_t("labs|video_rooms_beta")} />
</>
);
} else if (room.isSpaceRoom()) {

View file

@ -78,7 +78,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
</p>
<p>
{_t(
"<b>Warning</b>: upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.",
"room_settings|advanced|room_upgrade_warning",
{},
{
b: (sub) => <b>{sub}</b>,
@ -89,7 +89,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
</div>
<p className="mx_RoomUpgradeWarningBar_upgradelink">
<AccessibleButton onClick={this.onUpgradeClick}>
{_t("Upgrade this room to the recommended room version")}
{_t("room_settings|advanced|room_upgrade_button")}
</AccessibleButton>
</p>
</div>

View file

@ -89,7 +89,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
creator = (
<li>
{_t(
"This bridge was provisioned by <user />.",
"labs|bridge_state_creator",
{},
{
user: () => (
@ -109,7 +109,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
const bot = (
<li>
{_t(
"This bridge is managed by <user />.",
"labs|bridge_state_manager",
{},
{
user: () => (
@ -154,7 +154,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
);
}
networkItem = _t(
"Workspace: <networkLink/>",
"labs|bridge_state_workspace",
{},
{
networkLink: () => networkLink,
@ -181,7 +181,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
{networkItem}
<span className="mx_RoomSettingsDialog_channel">
{_t(
"Channel: <channelLink/>",
"labs|bridge_state_channel",
{},
{
channelLink: () => channelLink,

View file

@ -127,7 +127,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
this.props.onError(err);
} else {
this.props.onError(
new UserFriendlyError("Error while changing password: %(error)s", {
new UserFriendlyError("auth|change_password_error", {
error: String(err),
cause: undefined,
}),
@ -155,16 +155,16 @@ export default class ChangePassword extends React.Component<IProps, IState> {
*/
private checkPassword(oldPass: string, newPass: string, confirmPass: string): void {
if (newPass !== confirmPass) {
throw new UserFriendlyError("New passwords don't match");
throw new UserFriendlyError("auth|change_password_mismatch");
} else if (!newPass || newPass.length === 0) {
throw new UserFriendlyError("Passwords can't be empty");
throw new UserFriendlyError("auth|change_password_empty");
}
}
private optionallySetEmail(): Promise<boolean> {
// Ask for an email otherwise the user has no way to reset their password
const modal = Modal.createDialog(SetEmailDialog, {
title: _t("Do you want to set an email address?"),
title: _t("auth|set_email_prompt"),
});
return modal.finished.then(([confirmed]) => !!confirmed);
}
@ -194,7 +194,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Passwords can't be empty"),
invalid: () => _t("auth|change_password_empty"),
},
],
});
@ -226,14 +226,14 @@ export default class ChangePassword extends React.Component<IProps, IState> {
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Confirm password"),
invalid: () => _t("auth|change_password_confirm_label"),
},
{
key: "match",
test({ value }) {
return !value || value === this.state.newPassword;
},
invalid: () => _t("Passwords don't match"),
invalid: () => _t("auth|change_password_confirm_invalid"),
},
],
});
@ -261,7 +261,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
this.props.onError(err);
} else {
this.props.onError(
new UserFriendlyError("Error while changing password: %(error)s", {
new UserFriendlyError("auth|change_password_error", {
error: String(err),
cause: undefined,
}),
@ -344,7 +344,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
<Field
ref={(field) => (this[FIELD_OLD_PASSWORD] = field)}
type="password"
label={_t("Current password")}
label={_t("auth|change_password_current_label")}
value={this.state.oldPassword}
onChange={this.onChangeOldPassword}
onValidate={this.onOldPasswordValidate}
@ -354,7 +354,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
<PassphraseField
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
type="password"
label={_td("New Password")}
label={_td("auth|change_password_new_label")}
minScore={PASSWORD_MIN_SCORE}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}
@ -367,7 +367,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
<Field
ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)}
type="password"
label={_t("Confirm password")}
label={_t("auth|change_password_confirm_label")}
value={this.state.newPasswordConfirm}
onChange={this.onChangeNewPasswordConfirm}
onValidate={this.onNewPasswordConfirmValidate}
@ -379,7 +379,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
kind={this.props.buttonKind}
onClick={this.onClickChange}
>
{this.props.buttonLabel || _t("Change Password")}
{this.props.buttonLabel || _t("auth|change_password_action")}
</AccessibleButton>
</form>
);

View file

@ -263,32 +263,52 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
<summary>{_t("Advanced")}</summary>
<table className="mx_CrossSigningPanel_statusList">
<tr>
<th scope="row">{_t("Cross-signing public keys:")}</th>
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
</tr>
<tr>
<th scope="row">{_t("Cross-signing private keys:")}</th>
<th scope="row">{_t("settings|security|cross_signing_public_keys")}</th>
<td>
{crossSigningPrivateKeysInStorage
? _t("in secret storage")
: _t("not found in storage")}
{crossSigningPublicKeysOnDevice
? _t("settings|security|cross_signing_in_memory")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
<tr>
<th scope="row">{_t("Master private key:")}</th>
<td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<th scope="row">{_t("settings|security|cross_signing_private_keys")}</th>
<td>
{crossSigningPrivateKeysInStorage
? _t("settings|security|cross_signing_in_4s")
: _t("settings|security|cross_signing_not_in_4s")}
</td>
</tr>
<tr>
<th scope="row">{_t("Self signing private key:")}</th>
<td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<th scope="row">{_t("settings|security|cross_signing_master_private_Key")}</th>
<td>
{masterPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("User signing private key:")}</th>
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<th scope="row">{_t("settings|security|cross_signing_self_signing_private_key")}</th>
<td>
{selfSigningPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("Homeserver feature support:")}</th>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
<th scope="row">{_t("settings|security|cross_signing_user_signing_private_key")}</th>
<td>
{userSigningPrivateKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
</td>
</tr>
<tr>
<th scope="row">{_t("settings|security|cross_signing_homeserver_support")}</th>
<td>
{homeserverSupportsCrossSigning
? _t("settings|security|cross_signing_homeserver_support_exists")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
</table>
</details>

View file

@ -52,10 +52,10 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
importExportButtons = (
<div className="mx_CryptographyPanel_importExportButtons">
<AccessibleButton kind="primary" onClick={this.onExportE2eKeysClicked}>
{_t("Export E2E room keys")}
{_t("settings|security|export_megolm_keys")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onImportE2eKeysClicked}>
{_t("Import E2E room keys")}
{_t("settings|security|import_megolm_keys")}
</AccessibleButton>
</div>
);
@ -73,17 +73,17 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
}
return (
<SettingsSubsection heading={_t("Cryptography")}>
<SettingsSubsection heading={_t("settings|security|cryptography_section")}>
<SettingsSubsectionText>
<table className="mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("Session ID:")}</th>
<th scope="row">{_t("settings|security|session_id")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("Session key:")}</th>
<th scope="row">{_t("settings|security|session_key")}</th>
<td>
<code>
<b>{identityKey}</b>

View file

@ -26,7 +26,7 @@ const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel: React.FC = () => {
return (
<SettingsSubsection heading={_t("Encryption")}>
<SettingsSubsection heading={_t("settings|security|encryption_section")}>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
<SettingsSubsectionText>
{_t(

View file

@ -67,7 +67,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
}
public componentDidMount(): void {
this.checkKeyBackupStatus();
this.loadBackupStatus();
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
MatrixClientPeg.safeGet().on(CryptoEvent.KeyBackupSessionsRemaining, this.onKeyBackupSessionsRemaining);
@ -97,28 +97,6 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
this.loadBackupStatus();
};
private async checkKeyBackupStatus(): Promise<void> {
this.getUpdatedDiagnostics();
try {
const keyBackupResult = await MatrixClientPeg.safeGet().checkKeyBackup();
this.setState({
loading: false,
error: false,
backupInfo: keyBackupResult?.backupInfo ?? null,
backupSigStatus: keyBackupResult?.trustInfo ?? null,
});
} catch (e) {
logger.log("Unable to fetch check backup status", e);
if (this.unmounted) return;
this.setState({
loading: false,
error: true,
backupInfo: null,
backupSigStatus: null,
});
}
}
private async loadBackupStatus(): Promise<void> {
this.setState({ loading: true });
this.getUpdatedDiagnostics();
@ -388,18 +366,28 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
<table className="mx_SecureBackupPanel_statusList">
<tr>
<th scope="row">{_t("Backup key stored:")}</th>
<td>{backupKeyStored === true ? _t("in secret storage") : _t("not stored")}</td>
<td>
{backupKeyStored === true
? _t("settings|security|cross_signing_in_4s")
: _t("not stored")}
</td>
</tr>
<tr>
<th scope="row">{_t("Backup key cached:")}</th>
<td>
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
{backupKeyCached
? _t("settings|security|cross_signing_cached")
: _t("settings|security|cross_signing_not_cached")}
{backupKeyWellFormedText}
</td>
</tr>
<tr>
<th scope="row">{_t("Secret storage public key:")}</th>
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
<td>
{secretStorageKeyInAccount
? _t("in account data")
: _t("settings|security|cross_signing_not_found")}
</td>
</tr>
<tr>
<th scope="row">{_t("Secret storage:")}</th>

View file

@ -33,16 +33,16 @@ function installUpdate(): void {
function getStatusText(status: UpdateCheckStatus, errorDetail?: string): ReactNode {
switch (status) {
case UpdateCheckStatus.Error:
return _t("Error encountered (%(errorDetail)s).", { errorDetail });
return _t("update|error_encountered", { errorDetail });
case UpdateCheckStatus.Checking:
return _t("Checking for an update…");
return _t("update|checking");
case UpdateCheckStatus.NotAvailable:
return _t("No update available.");
return _t("update|no_update");
case UpdateCheckStatus.Downloading:
return _t("Downloading update…");
return _t("update|downloading");
case UpdateCheckStatus.Ready:
return _t(
"New version available. <a>Update now.</a>",
"update|new_version_available",
{},
{
a: (sub) => (
@ -86,7 +86,7 @@ const UpdateCheckButton: React.FC = () => {
return (
<React.Fragment>
<AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
{_t("Check for update")}
{_t("update|check_action")}
</AccessibleButton>
{suffix}
</React.Fragment>

View file

@ -68,7 +68,7 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
? [
<IconizedContextMenuOption
key="sign-out-all-others"
label={_t("Sign out of all other sessions (%(otherSessionsCount)s)", { otherSessionsCount })}
label={_t("settings|sessions|sign_out_all_other_sessions", { otherSessionsCount })}
onClick={signOutAllOtherSessions}
isDestructive
/>,
@ -76,7 +76,7 @@ const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> =
: []),
];
return (
<SettingsSubsectionHeading heading={_t("Current session")}>
<SettingsSubsectionHeading heading={_t("settings|sessions|current_session")}>
<KebabContextMenu
disabled={disabled}
title={_t("common|options")}

View file

@ -60,7 +60,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
return (
<form aria-disabled={isLoading} className="mx_DeviceDetailHeading_renameForm" onSubmit={onSubmit} method="post">
<p id={headingId} className="mx_DeviceDetailHeading_renameFormHeading">
{_t("Rename session")}
{_t("settings|sessions|rename_form_heading")}
</p>
<div>
<Field
@ -77,21 +77,13 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
maxLength={100}
/>
<Caption id={descriptionId}>
{_t("Please be aware that session names are also visible to people you communicate with.")}
{_t("settings|sessions|rename_form_caption")}
<LearnMore
title={_t("Renaming sessions")}
title={_t("settings|sessions|rename_form_learn_more")}
description={
<>
<p>
{_t(
"Other users in direct messages and rooms that you join are able to view a full list of your sessions.",
)}
</p>
<p>
{_t(
"This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.",
)}
</p>
<p>{_t("settings|sessions|rename_form_learn_more_description_1")}</p>
<p>{_t("settings|sessions|rename_form_learn_more_description_2")}</p>
</>
}
/>

View file

@ -64,9 +64,9 @@ const DeviceDetails: React.FC<Props> = ({
{
id: "session",
values: [
{ label: _t("Session ID"), value: device.device_id },
{ label: _t("settings|sessions|session_id"), value: device.device_id },
{
label: _t("Last activity"),
label: _t("settings|sessions|last_activity"),
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
},
],
@ -77,7 +77,7 @@ const DeviceDetails: React.FC<Props> = ({
values: [
{ label: _t("common|name"), value: device.appName },
{ label: _t("common|version"), value: device.appVersion },
{ label: _t("URL"), value: device.url },
{ label: _t("settings|sessions|url"), value: device.url },
],
},
{
@ -85,9 +85,9 @@ const DeviceDetails: React.FC<Props> = ({
heading: _t("common|device"),
values: [
{ label: _t("common|model"), value: device.deviceModel },
{ label: _t("Operating system"), value: device.deviceOperatingSystem },
{ label: _t("Browser"), value: device.client },
{ label: _t("IP address"), value: device.last_seen_ip },
{ label: _t("settings|sessions|os"), value: device.deviceOperatingSystem },
{ label: _t("settings|sessions|browser"), value: device.client },
{ label: _t("settings|sessions|ip"), value: device.last_seen_ip },
],
},
]
@ -122,7 +122,7 @@ const DeviceDetails: React.FC<Props> = ({
<DeviceVerificationStatusCard device={device} onVerifyDevice={onVerifyDevice} isCurrentDevice />
</section>
<section className="mx_DeviceDetails_section">
<p className="mx_DeviceDetails_sectionHeading">{_t("Session details")}</p>
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
{metadata.map(({ heading, values, id }, index) => (
<table
className="mx_DeviceDetails_metadataTable"
@ -158,13 +158,13 @@ const DeviceDetails: React.FC<Props> = ({
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
onChange={(checked) => setPushNotifications?.(device.device_id, checked)}
title={_t("Toggle push notifications on this session.")}
title={_t("settings|sessions|push_toggle")}
data-testid="device-detail-push-notification-checkbox"
/>
<p className="mx_DeviceDetails_sectionHeading">
{_t("Push notifications")}
{_t("settings|sessions|push_heading")}
<small className="mx_DeviceDetails_sectionSubheading">
{_t("Receive push notifications on this session.")}
{_t("settings|sessions|push_subheading")}
</small>
</p>
</section>
@ -177,7 +177,7 @@ const DeviceDetails: React.FC<Props> = ({
data-testid="device-detail-sign-out-cta"
>
<span className="mx_DeviceDetails_signOutButtonContent">
{_t("Sign out of this session")}
{_t("settings|sessions|sign_out")}
{isSigningOut && <Spinner w={16} h={16} />}
</span>
</AccessibleButton>

View file

@ -27,7 +27,7 @@ interface Props extends React.ComponentProps<typeof AccessibleTooltipButton> {
}
export const DeviceExpandDetailsButton: React.FC<Props> = ({ isExpanded, onClick, ...rest }) => {
const label = isExpanded ? _t("Hide details") : _t("Show details");
const label = isExpanded ? _t("settings|sessions|hide_details") : _t("settings|sessions|show_details");
return (
<AccessibleTooltipButton
{...rest}

View file

@ -50,7 +50,7 @@ const getInactiveMetadata = (device: ExtendedDevice): { id: string; value: React
value: (
<>
<InactiveIcon className="mx_DeviceTile_inactiveIcon" />
{_t("Inactive for %(inactiveAgeDays)s+ days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
{_t("settings|sessions|inactive_days", { inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS }) +
` (${formatLastActivity(device.last_seen_ts)})`}
</>
),
@ -62,7 +62,8 @@ const DeviceMetaDatum: React.FC<{ value: string | React.ReactNode; id: string }>
export const DeviceMetaData: React.FC<Props> = ({ device }) => {
const inactive = getInactiveMetadata(device);
const lastActivity = device.last_seen_ts && `${_t("Last activity")} ${formatLastActivity(device.last_seen_ts)}`;
const lastActivity =
device.last_seen_ts && `${_t("settings|sessions|last_activity")} ${formatLastActivity(device.last_seen_ts)}`;
const verificationStatus = device.isVerified ? _t("common|verified") : _t("common|unverified");
// if device is inactive, don't display last activity or verificationStatus
const metadata = inactive

View file

@ -32,73 +32,41 @@ const securityCardContent: Record<
}
> = {
[DeviceSecurityVariation.Verified]: {
title: _t("Verified sessions"),
title: _t("settings|sessions|verified_sessions"),
description: (
<>
<p>
{_t(
"Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.",
)}
</p>
<p>
{_t(
"This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.",
)}
</p>
<p>{_t("settings|sessions|verified_sessions_explainer_1")}</p>
<p>{_t("settings|sessions|verified_sessions_explainer_2")}</p>
</>
),
},
[DeviceSecurityVariation.Unverified]: {
title: _t("Unverified sessions"),
title: _t("settings|sessions|unverified_sessions"),
description: (
<>
<p>
{_t(
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
)}
</p>
<p>
{_t(
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
)}
</p>
<p>{_t("settings|sessions|unverified_sessions_explainer_1")}</p>
<p>{_t("settings|sessions|unverified_sessions_explainer_2")}</p>
</>
),
},
// unverifiable uses single-session case
// because it is only ever displayed on a single session detail
[DeviceSecurityVariation.Unverifiable]: {
title: _t("Unverified session"),
title: _t("settings|sessions|unverified_session"),
description: (
<>
<p>{_t(`This session doesn't support encryption and thus can't be verified.`)}</p>
<p>
{_t(
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
)}
</p>
<p>
{_t(
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
)}
</p>
<p>{_t("settings|sessions|unverified_session_explainer_1")}</p>
<p>{_t("settings|sessions|unverified_session_explainer_2")}</p>
<p>{_t("settings|sessions|unverified_session_explainer_3")}</p>
</>
),
},
[DeviceSecurityVariation.Inactive]: {
title: _t("Inactive sessions"),
title: _t("settings|sessions|inactive_sessions"),
description: (
<>
<p>
{_t(
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
)}
</p>
<p>
{_t(
"Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.",
)}
</p>
<p>{_t("settings|sessions|inactive_sessions_explainer_1")}</p>
<p>{_t("settings|sessions|inactive_sessions_explainer_2")}</p>
</>
),
},

View file

@ -23,7 +23,7 @@ import { Icon as WebIcon } from "../../../../../res/img/element-icons/settings/w
import { Icon as MobileIcon } from "../../../../../res/img/element-icons/settings/mobile.svg";
import { Icon as VerifiedIcon } from "../../../../../res/img/e2e/verified.svg";
import { Icon as UnverifiedIcon } from "../../../../../res/img/e2e/warning.svg";
import { _t } from "../../../../languageHandler";
import { _t, _td, TranslationKey } from "../../../../languageHandler";
import { ExtendedDevice } from "./types";
import { DeviceType } from "../../../../utils/device/parseUserAgent";
@ -39,16 +39,16 @@ const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>
[DeviceType.Web]: WebIcon,
[DeviceType.Unknown]: UnknownDeviceIcon,
};
const deviceTypeLabel: Record<DeviceType, string> = {
[DeviceType.Desktop]: _t("Desktop session"),
[DeviceType.Mobile]: _t("Mobile session"),
[DeviceType.Web]: _t("Web session"),
[DeviceType.Unknown]: _t("Unknown session type"),
const deviceTypeLabel: Record<DeviceType, TranslationKey> = {
[DeviceType.Desktop]: _td("settings|sessions|desktop_session"),
[DeviceType.Mobile]: _td("settings|sessions|mobile_session"),
[DeviceType.Web]: _td("settings|sessions|web_session"),
[DeviceType.Unknown]: _td("settings|sessions|unknown_session"),
};
export const DeviceTypeIcon: React.FC<Props> = ({ isVerified, isSelected, deviceType }) => {
const Icon = deviceTypeIcon[deviceType!] || deviceTypeIcon[DeviceType.Unknown];
const label = deviceTypeLabel[deviceType!] || deviceTypeLabel[DeviceType.Unknown];
const label = _t(deviceTypeLabel[deviceType!] || deviceTypeLabel[DeviceType.Unknown]);
return (
<div
className={classNames("mx_DeviceTypeIcon", {

View file

@ -38,11 +38,11 @@ const getCardProps = (
} => {
if (device.isVerified) {
const descriptionText = isCurrentDevice
? _t("Your current session is ready for secure messaging.")
: _t("This session is ready for secure messaging.");
? _t("settings|sessions|device_verified_description_current")
: _t("settings|sessions|device_verified_description");
return {
variation: DeviceSecurityVariation.Verified,
heading: _t("Verified session"),
heading: _t("settings|sessions|verified_session"),
description: (
<>
{descriptionText}
@ -54,10 +54,10 @@ const getCardProps = (
if (device.isVerified === null) {
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t("Unverified session"),
heading: _t("settings|sessions|unverified_session"),
description: (
<>
{_t(`This session doesn't support encryption and thus can't be verified.`)}
{_t("settings|sessions|unverified_session_explainer_1")}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
</>
),
@ -65,11 +65,11 @@ const getCardProps = (
}
const descriptionText = isCurrentDevice
? _t("Verify your current session for enhanced secure messaging.")
: _t("Verify or sign out from this session for best security and reliability.");
? _t("settings|sessions|device_unverified_description_current")
: _t("settings|sessions|device_unverified_description");
return {
variation: DeviceSecurityVariation.Unverified,
heading: _t("Unverified session"),
heading: _t("settings|sessions|unverified_session"),
description: (
<>
{descriptionText}
@ -95,7 +95,7 @@ export const DeviceVerificationStatusCard: React.FC<DeviceVerificationStatusCard
onClick={onVerifyDevice}
data-testid={`verification-status-button-${device.device_id}`}
>
{_t("Verify session")}
{_t("settings|sessions|verify_session")}
</AccessibleButton>
)}
</DeviceSecurityCard>

View file

@ -73,36 +73,6 @@ const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVar
const ALL_FILTER_ID = "ALL";
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
const securityCardContent: Record<
DeviceSecurityVariation,
{
title: string;
description: string;
}
> = {
[DeviceSecurityVariation.Verified]: {
title: _t("Verified sessions"),
description: _t("For best security, sign out from any session that you don't recognize or use anymore."),
},
[DeviceSecurityVariation.Unverified]: {
title: _t("Unverified sessions"),
description: _t(
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
),
},
[DeviceSecurityVariation.Unverifiable]: {
title: _t("Unverified session"),
description: _t(`This session doesn't support encryption and thus can't be verified.`),
},
[DeviceSecurityVariation.Inactive]: {
title: _t("Inactive sessions"),
description: _t(
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
),
},
};
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
!!filter &&
(
@ -115,6 +85,33 @@ const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariatio
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
if (isSecurityVariation(filter)) {
const securityCardContent: Record<
DeviceSecurityVariation,
{
title: string;
description: string;
}
> = {
[DeviceSecurityVariation.Verified]: {
title: _t("settings|sessions|verified_sessions"),
description: _t("settings|sessions|verified_sessions_list_description"),
},
[DeviceSecurityVariation.Unverified]: {
title: _t("settings|sessions|unverified_sessions"),
description: _t("settings|sessions|unverified_sessions_list_description"),
},
[DeviceSecurityVariation.Unverifiable]: {
title: _t("settings|sessions|unverified_session"),
description: _t("settings|sessions|unverified_session_explainer_1"),
},
[DeviceSecurityVariation.Inactive]: {
title: _t("settings|sessions|inactive_sessions"),
description: _t("settings|sessions|inactive_sessions_list_description", {
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
}),
},
};
const { title, description } = securityCardContent[filter];
return (
<div className="mx_FilteredDeviceList_securityCard">
@ -138,13 +135,13 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
const getNoResultsMessage = (filter?: FilterVariation): string => {
switch (filter) {
case DeviceSecurityVariation.Verified:
return _t("No verified sessions found.");
return _t("settings|sessions|no_verified_sessions");
case DeviceSecurityVariation.Unverified:
return _t("No unverified sessions found.");
return _t("settings|sessions|no_unverified_sessions");
case DeviceSecurityVariation.Inactive:
return _t("No inactive sessions found.");
return _t("settings|sessions|no_inactive_sessions");
default:
return _t("No sessions found.");
return _t("settings|sessions|no_sessions");
}
};
interface NoResultsProps {
@ -281,21 +278,21 @@ export const FilteredDeviceList = forwardRef(
};
const options: FilterDropdownOption<DeviceFilterKey>[] = [
{ id: ALL_FILTER_ID, label: _t("All") },
{ id: ALL_FILTER_ID, label: _t("settings|sessions|filter_all") },
{
id: DeviceSecurityVariation.Verified,
label: _t("common|verified"),
description: _t("Ready for secure messaging"),
description: _t("settings|sessions|filter_verified_description"),
},
{
id: DeviceSecurityVariation.Unverified,
label: _t("common|unverified"),
description: _t("Not ready for secure messaging"),
description: _t("settings|sessions|filter_unverified_description"),
},
{
id: DeviceSecurityVariation.Inactive,
label: _t("Inactive"),
description: _t("Inactive for %(inactiveAgeDays)s days or longer", {
label: _t("settings|sessions|filter_inactive"),
description: _t("settings|sessions|filter_inactive_description", {
inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS,
}),
},
@ -349,7 +346,7 @@ export const FilteredDeviceList = forwardRef(
) : (
<FilterDropdown<DeviceFilterKey>
id="device-list-filter"
label={_t("Filter devices")}
label={_t("settings|sessions|filter_label")}
value={filter || ALL_FILTER_ID}
onOptionChange={onFilterOptionChange}
options={options}

View file

@ -37,7 +37,7 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
children,
...rest
}) => {
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
const checkboxLabel = isAllSelected ? _t("common|deselect_all") : _t("common|select_all");
return (
<div className="mx_FilteredDeviceListHeader" {...rest}>
{!isSelectDisabled && (
@ -54,7 +54,7 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
)}
<span className="mx_FilteredDeviceListHeader_label">
{selectedDeviceCount > 0
? _t("%(count)s sessions selected", { count: selectedDeviceCount })
? _t("settings|sessions|n_sessions_selected", { count: selectedDeviceCount })
: _t("Sessions")}
</span>
{children}

View file

@ -52,15 +52,13 @@ export default class LoginWithQRSection extends React.Component<IProps> {
}
return (
<SettingsSubsection heading={_t("Sign in with QR code")}>
<SettingsSubsection heading={_t("settings|sessions|sign_in_with_qr")}>
<div className="mx_LoginWithQRSection">
<p className="mx_SettingsTab_subsectionText">
{_t(
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.",
)}
{_t("settings|sessions|sign_in_with_qr_description")}
</p>
<AccessibleButton onClick={this.props.onShowQr} kind="primary">
{_t("Show QR code")}
{_t("settings|sessions|sign_in_with_qr_button")}
</AccessibleButton>
</div>
</SettingsSubsection>

View file

@ -41,14 +41,14 @@ export const OtherSessionsSectionHeading: React.FC<Props> = ({
signOutAllOtherSessions ? (
<IconizedContextMenuOption
key="sign-out-all-others"
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
label={_t("settings|sessions|sign_out_n_sessions", { count: otherSessionsCount })}
onClick={signOutAllOtherSessions}
isDestructive
/>
) : null,
]);
return (
<SettingsSubsectionHeading heading={_t("Other sessions")}>
<SettingsSubsectionHeading heading={_t("settings|sessions|other_sessions_heading")}>
{!!menuOptions.length && (
<KebabContextMenu
disabled={disabled}

View file

@ -52,19 +52,17 @@ const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, go
return (
<SettingsSubsection
heading={_t("Security recommendations")}
description={_t("Improve your account security by following these recommendations.")}
heading={_t("settings|sessions|security_recommendations")}
description={_t("settings|sessions|security_recommendations_description")}
data-testid="security-recommendations-section"
>
{!!unverifiedDevicesCount && (
<DeviceSecurityCard
variation={DeviceSecurityVariation.Unverified}
heading={_t("Unverified sessions")}
heading={_t("settings|sessions|unverified_sessions")}
description={
<>
{_t(
"Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.",
)}
{_t("settings|sessions|unverified_sessions_list_description")}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
</>
}
@ -83,13 +81,10 @@ const SecurityRecommendations: React.FC<Props> = ({ devices, currentDeviceId, go
{!!unverifiedDevicesCount && <div className="mx_SecurityRecommendations_spacing" />}
<DeviceSecurityCard
variation={DeviceSecurityVariation.Inactive}
heading={_t("Inactive sessions")}
heading={_t("settings|sessions|inactive_sessions")}
description={
<>
{_t(
"Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.",
{ inactiveAgeDays },
)}
{_t("settings|sessions|inactive_sessions_list_description", { inactiveAgeDays })}
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Inactive} />
</>
}

View file

@ -53,20 +53,20 @@ export const deleteDevicesWithInteractiveAuth = async (
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm logging out these devices by using Single Sign On to prove your identity.", {
body: _t("settings|sessions|confirm_sign_out_sso", {
count: numDevices,
}),
continueText: _t("auth|sso"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm signing out these devices", {
title: _t("settings|sessions|confirm_sign_out", {
count: numDevices,
}),
body: _t("Click the button below to confirm signing out these devices.", {
body: _t("settings|sessions|confirm_sign_out_body", {
count: numDevices,
}),
continueText: _t("Sign out devices", { count: numDevices }),
continueText: _t("settings|sessions|confirm_sign_out_continue", { count: numDevices }),
continueKind: "danger",
},
};

View file

@ -111,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
let unfederatableSection: JSX.Element | undefined;
if (room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"] === false) {
unfederatableSection = <div>{_t("This room is not accessible by remote Matrix servers")}</div>;
unfederatableSection = <div>{_t("room_settings|advanced|unfederated")}</div>;
}
let roomUpgradeButton;
@ -120,7 +120,7 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
<div>
<p className="mx_SettingsTab_warningText">
{_t(
"<b>Warning</b>: upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.",
"room_settings|advanced|room_upgrade_warning",
{},
{
b: (sub) => <b>{sub}</b>,
@ -130,8 +130,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
</p>
<AccessibleButton onClick={this.upgradeRoom} kind="primary">
{isSpace
? _t("Upgrade this space to the recommended room version")
: _t("Upgrade this room to the recommended room version")}
? _t("room_settings|advanced|space_upgrade_button")
: _t("room_settings|advanced|room_upgrade_button")}
</AccessibleButton>
</div>
);
@ -141,9 +141,9 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
if (this.state.oldRoomId) {
let copy: string;
if (isSpace) {
copy = _t("View older version of %(spaceName)s.", { spaceName: room.name ?? this.state.oldRoomId });
copy = _t("room_settings|advanced|space_predecessor", { spaceName: room.name ?? this.state.oldRoomId });
} else {
copy = _t("View older messages in %(roomName)s.", { roomName: room.name ?? this.state.oldRoomId });
copy = _t("room_settings|advanced|room_predecessor", { roomName: room.name ?? this.state.oldRoomId });
}
oldRoomLink = (
@ -158,16 +158,16 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
<SettingsSection heading={_t("Advanced")}>
<SettingsSubsection heading={room.isSpaceRoom() ? _t("Space information") : _t("Room information")}>
<div>
<span>{_t("Internal room ID")}</span>
<span>{_t("room_settings|advanced|room_id")}</span>
<CopyableText getTextToCopy={() => this.props.room.roomId}>
{this.props.room.roomId}
</CopyableText>
</div>
{unfederatableSection}
</SettingsSubsection>
<SettingsSubsection heading={_t("Room version")}>
<SettingsSubsection heading={_t("room_settings|advanced|room_version_section")}>
<div>
<span>{_t("Room version:")}</span>&nbsp;
<span>{_t("room_settings|advanced|room_version")}</span>&nbsp;
{room.getVersion()}
</div>
{oldRoomLink}

View file

@ -341,7 +341,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
);
let privilegedUsersSection = <div>{_t("No users have specific privileges in this room")}</div>;
let privilegedUsersSection = <div>{_t("room_settings|permissions|no_privileged_users")}</div>;
let mutedUsersSection;
if (Object.keys(userLevels).length) {
const privilegedUsers: JSX.Element[] = [];
@ -391,11 +391,17 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (privilegedUsers.length) {
privilegedUsersSection = (
<SettingsFieldset legend={_t("Privileged Users")}>{privilegedUsers}</SettingsFieldset>
<SettingsFieldset legend={_t("room_settings|permissions|privileged_users_section")}>
{privilegedUsers}
</SettingsFieldset>
);
}
if (mutedUsers.length) {
mutedUsersSection = <SettingsFieldset legend={_t("Muted Users")}>{mutedUsers}</SettingsFieldset>;
mutedUsersSection = (
<SettingsFieldset legend={_t("room_settings|permissions|muted_users_section")}>
{mutedUsers}
</SettingsFieldset>
);
}
}
@ -404,7 +410,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
if (banned?.length) {
const canBanUsers = currentUserLevel >= banLevel;
bannedUsersSection = (
<SettingsFieldset legend={_t("Banned users")}>
<SettingsFieldset legend={_t("room_settings|permissions|banned_users_section")}>
<ul className="mx_RolesRoomSettingsTab_bannedList">
{banned.map((member) => {
const banEvent = member.events.member?.getContent();
@ -468,7 +474,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
label = _t(translationKeyForEvent, { brand });
} else {
label = _t("Send %(eventType)s events", { eventType });
label = _t("room_settings|permissions|send_event_type", { eventType });
}
return (
<div key={eventType}>
@ -487,17 +493,17 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
return (
<SettingsTab>
<SettingsSection heading={_t("Roles & Permissions")}>
<SettingsSection heading={_t("room_settings|permissions|title")}>
{privilegedUsersSection}
{canChangeLevels && <AddPrivilegedUsers room={room} defaultUserLevel={defaultUserLevel} />}
{mutedUsersSection}
{bannedUsersSection}
<SettingsFieldset
legend={_t("Permissions")}
legend={_t("room_settings|permissions|permissions_section")}
description={
isSpaceRoom
? _t("Select the roles required to change various parts of the space")
: _t("Select the roles required to change various parts of the room")
? _t("room_settings|permissions|permissions_section_description_space")
: _t("room_settings|permissions|permissions_section_description_room")
}
>
{powerSelectors}

View file

@ -116,13 +116,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onEncryptionChange = async (): Promise<void> => {
if (this.props.room.getJoinRule() === JoinRule.Public) {
const dialog = Modal.createDialog(QuestionDialog, {
title: _t("Are you sure you want to add encryption to this public room?"),
title: _t("room_settings|security|enable_encryption_public_room_confirm_title"),
description: (
<div>
<p>
{" "}
{_t(
"<b>It's not recommended to add encryption to public rooms.</b> Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.",
"room_settings|security|enable_encryption_public_room_confirm_description_1",
undefined,
{ b: (sub) => <b>{sub}</b> },
)}{" "}
@ -130,7 +130,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
<p>
{" "}
{_t(
"To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.",
"room_settings|security|enable_encryption_public_room_confirm_description_2",
undefined,
{
a: (sub) => (
@ -158,9 +158,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
}
Modal.createDialog(QuestionDialog, {
title: _t("Enable encryption?"),
title: _t("room_settings|security|enable_encryption_confirm_title"),
description: _t(
"Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>",
"room_settings|security|enable_encryption_confirm_description",
{},
{
a: (sub) => <ExternalLink href={SdkConfig.get("help_encryption_url")}>{sub}</ExternalLink>,
@ -259,11 +259,11 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
aliasWarning = (
<div className="mx_SecurityRoomSettingsTab_warning">
<WarningIcon width={15} height={15} />
<span>{_t("To link to this room, please add an address.")}</span>
<span>{_t("room_settings|security|public_without_alias_warning")}</span>
</div>
);
}
const description = _t("Decide who can join %(roomName)s.", {
const description = _t("room_settings|security|join_rule_description", {
roomName: room.name,
});
@ -309,37 +309,31 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onBeforeJoinRuleChange = async (joinRule: JoinRule): Promise<boolean> => {
if (this.state.encrypted && joinRule === JoinRule.Public) {
const dialog = Modal.createDialog(QuestionDialog, {
title: _t("Are you sure you want to make this encrypted room public?"),
title: _t("room_settings|security|encrypted_room_public_confirm_title"),
description: (
<div>
<p>
{" "}
{_t(
"<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.",
undefined,
{ b: (sub) => <b>{sub}</b> },
)}{" "}
{_t("room_settings|security|encrypted_room_public_confirm_description_1", undefined, {
b: (sub) => <b>{sub}</b>,
})}{" "}
</p>
<p>
{" "}
{_t(
"To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.",
undefined,
{
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={(): void => {
dialog.close();
this.createNewRoom(true, false);
}}
>
{" "}
{sub}{" "}
</AccessibleButton>
),
},
)}{" "}
{_t("room_settings|security|encrypted_room_public_confirm_description_2", undefined, {
a: (sub) => (
<AccessibleButton
kind="link_inline"
onClick={(): void => {
dialog.close();
this.createNewRoom(true, false);
}}
>
{" "}
{sub}{" "}
</AccessibleButton>
),
})}{" "}
</p>
</div>
),
@ -366,15 +360,15 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
const options = [
{
value: HistoryVisibility.Shared,
label: _t("Members only (since the point in time of selecting this option)"),
label: _t("room_settings|security|history_visibility_shared"),
},
{
value: HistoryVisibility.Invited,
label: _t("Members only (since they were invited)"),
label: _t("room_settings|security|history_visibility_invited"),
},
{
value: HistoryVisibility.Joined,
label: _t("Members only (since they joined)"),
label: _t("room_settings|security|history_visibility_joined"),
},
];
@ -382,16 +376,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
if (!this.state.encrypted || history === HistoryVisibility.WorldReadable) {
options.unshift({
value: HistoryVisibility.WorldReadable,
label: _t("Anyone"),
label: _t("room_settings|security|history_visibility_world_readable"),
});
}
const description = _t(
"Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.",
);
const description = _t("room_settings|security|history_visibility_warning");
return (
<SettingsFieldset legend={_t("Who can read history?")} description={description}>
<SettingsFieldset legend={_t("room_settings|security|history_visibility_legend")} description={description}>
<StyledRadioGroup
name="historyVis"
value={history}
@ -421,11 +413,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
disabled={!canSetGuestAccess}
label={_t("Enable guest access")}
/>
<p>
{_t(
"People with supported clients will be able to join the room without having a registered account.",
)}
</p>
<p>{_t("room_settings|security|guest_access_warning")}</p>
</div>
);
}
@ -454,13 +442,13 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
return (
<SettingsTab>
<SettingsSection heading={_t("Security & Privacy")}>
<SettingsSection heading={_t("room_settings|security|title")}>
<SettingsFieldset
legend={_t("Encryption")}
legend={_t("settings|security|encryption_section")}
description={
isEncryptionForceDisabled && !isEncrypted
? undefined
: _t("Once enabled, encryption cannot be disabled.")
: _t("room_settings|security|encryption_permanent")
}
>
<LabelledToggleSwitch
@ -470,7 +458,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
disabled={!canEnableEncryption}
/>
{isEncryptionForceDisabled && !isEncrypted && (
<Caption>{_t("Your server requires encryption to be disabled.")}</Caption>
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
)}
{encryptionSettings}
</SettingsFieldset>

View file

@ -402,14 +402,18 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
href={this.state.externalAccountManagementUrl}
data-testid="external-account-management-link"
>
{_t("Manage account")}
{_t("settings|general|oidc_manage_button")}
</AccessibleButton>
</>
);
}
return (
<>
<SettingsSubsection heading={_t("Account")} stretchContent data-testid="accountSection">
<SettingsSubsection
heading={_t("settings|general|account_section")}
stretchContent
data-testid="accountSection"
>
{externalAccountManagement}
{passwordChangeSection}
</SettingsSubsection>
@ -421,7 +425,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
private renderLanguageSection(): JSX.Element {
// TODO: Convert to new-styled Field
return (
<SettingsSubsection heading={_t("Language and region")} stretchContent>
<SettingsSubsection heading={_t("settings|general|language_section")} stretchContent>
<LanguageDropdown
className="mx_GeneralUserSettingsTab_section_languageInput"
onOptionChange={this.onLanguageChange}
@ -433,7 +437,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
private renderSpellCheckSection(): JSX.Element {
const heading = (
<SettingsSubsectionHeading heading={_t("Spell check")}>
<SettingsSubsectionHeading heading={_t("settings|general|spell_check_section")}>
<ToggleSwitch checked={!!this.state.spellCheckEnabled} onChange={this.onSpellCheckEnabledChange} />
</SettingsSubsectionHeading>
);

View file

@ -110,21 +110,18 @@ export default class LabsUserSettingsTab extends React.Component<{}> {
return (
<SettingsTab>
<SettingsSection heading={_t("Upcoming features")}>
<SettingsSection heading={_t("labs|beta_section")}>
<SettingsSubsectionText>
{_t(
"What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.",
{ brand: SdkConfig.get("brand") },
)}
{_t("labs|beta_description", { brand: SdkConfig.get("brand") })}
</SettingsSubsectionText>
{betaSection}
</SettingsSection>
{labsSections && (
<SettingsSection heading={_t("Early previews")}>
<SettingsSection heading={_t("labs|experimental_section")}>
<SettingsSubsectionText>
{_t(
"Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
"labs|experimental_description",
{},
{
a: (sub) => {

View file

@ -69,14 +69,14 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
this.setState({ busy: true });
try {
const list = await Mjolnir.sharedInstance().getOrCreatePersonalList();
await list.banEntity(kind, this.state.newPersonalRule, _t("Ignored/Blocked"));
await list.banEntity(kind, this.state.newPersonalRule, _t("labs_mjolnir|ban_reason"));
this.setState({ newPersonalRule: "" }); // this will also cause the new rule to be rendered
} catch (e) {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error adding ignored user/server"),
description: _t("Something went wrong. Please try again or view your console for hints."),
title: _t("labs_mjolnir|error_adding_ignore"),
description: _t("labs_mjolnir|something_went_wrong"),
});
} finally {
this.setState({ busy: false });
@ -96,8 +96,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error subscribing to list"),
description: _t("Please verify the room ID or address and try again."),
title: _t("labs_mjolnir|error_adding_list_title"),
description: _t("labs_mjolnir|error_adding_list_description"),
});
} finally {
this.setState({ busy: false });
@ -113,8 +113,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error removing ignored user/server"),
description: _t("Something went wrong. Please try again or view your console for hints."),
title: _t("labs_mjolnir|error_removing_ignore"),
description: _t("labs_mjolnir|something_went_wrong"),
});
} finally {
this.setState({ busy: false });
@ -130,8 +130,8 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error unsubscribing from list"),
description: _t("Please try again or view your console for hints."),
title: _t("labs_mjolnir|error_removing_list_title"),
description: _t("labs_mjolnir|error_removing_list_description"),
});
} finally {
this.setState({ busy: false });
@ -157,12 +157,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
};
Modal.createDialog(QuestionDialog, {
title: _t("Ban list rules - %(roomName)s", { roomName: name }),
title: _t("labs_mjolnir|rules_title", { roomName: name }),
description: (
<div>
<h3>{_t("Server rules")}</h3>
<h3>{_t("labs_mjolnir|rules_server")}</h3>
{renderRules(list.serverRules)}
<h3>{_t("User rules")}</h3>
<h3>{_t("labs_mjolnir|rules_user")}</h3>
{renderRules(list.userRules)}
</div>
),
@ -174,7 +174,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
private renderPersonalBanListRules(): JSX.Element {
const list = Mjolnir.sharedInstance().getPersonalList();
const rules = list ? [...list.userRules, ...list.serverRules] : [];
if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
if (!list || rules.length <= 0) return <i>{_t("labs_mjolnir|personal_empty")}</i>;
const tiles: JSX.Element[] = [];
for (const rule of rules) {
@ -195,7 +195,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
return (
<div>
<p>{_t("You are currently ignoring:")}</p>
<p>{_t("labs_mjolnir|personal_section")}</p>
<ul>{tiles}</ul>
</div>
);
@ -206,7 +206,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
const lists = Mjolnir.sharedInstance().lists.filter((b) => {
return personalList ? personalList.roomId !== b.roomId : true;
});
if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
if (!lists || lists.length <= 0) return <i>{_t("labs_mjolnir|no_lists")}</i>;
const tiles: JSX.Element[] = [];
for (const list of lists) {
@ -233,7 +233,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
onClick={() => this.viewListRules(list)}
disabled={this.state.busy}
>
{_t("View rules")}
{_t("labs_mjolnir|view_rules")}
</AccessibleButton>
&nbsp;
{name}
@ -243,7 +243,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
return (
<div>
<p>{_t("You are currently subscribed to:")}</p>
<p>{_t("labs_mjolnir|lists")}</p>
<ul>{tiles}</ul>
</div>
);
@ -254,37 +254,24 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
return (
<SettingsTab>
<SettingsSection heading={_t("Ignored users")}>
<SettingsSection heading={_t("labs_mjolnir|title")}>
<SettingsSubsectionText>
<span className="warning">{_t("⚠ These settings are meant for advanced users.")}</span>
<p>
{_t(
"Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.",
{ brand },
{ code: (s) => <code>{s}</code> },
)}
</p>
<p>
{_t(
"Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.",
)}
</p>
<span className="warning">{_t("labs_mjolnir|advanced_warning")}</span>
<p>{_t("labs_mjolnir|explainer_1", { brand }, { code: (s) => <code>{s}</code> })}</p>
<p>{_t("labs_mjolnir|explainer_2")}</p>
</SettingsSubsectionText>
<SettingsSubsection
heading={_t("Personal ban list")}
description={_t(
"Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.",
{
myBanList: _t("labs_mjolnir|room_name"),
},
)}
heading={_t("labs_mjolnir|personal_heading")}
description={_t("labs_mjolnir|personal_description", {
myBanList: _t("labs_mjolnir|room_name"),
})}
>
{this.renderPersonalBanListRules()}
<form onSubmit={this.onAddPersonalRule} autoComplete="off">
<Field
type="text"
label={_t("Server or user ID to ignore")}
placeholder={_t("eg: @bot:* or example.org")}
label={_t("labs_mjolnir|personal_new_label")}
placeholder={_t("labs_mjolnir|personal_new_placeholder")}
value={this.state.newPersonalRule}
onChange={this.onPersonalRuleChanged}
/>
@ -299,16 +286,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
</form>
</SettingsSubsection>
<SettingsSubsection
heading={_t("Subscribed lists")}
heading={_t("labs_mjolnir|lists_heading")}
description={
<>
<span className="warning">
{_t("Subscribing to a ban list will cause you to join it!")}
</span>
<span className="warning">{_t("labs_mjolnir|lists_description_1")}</span>
&nbsp;
<span>
{_t("If this isn't what you want, please use a different tool to ignore users.")}
</span>
<span>{_t("labs_mjolnir|lists_description_2")}</span>
</>
}
>
@ -316,7 +299,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
<form onSubmit={this.onSubscribeList} autoComplete="off">
<Field
type="text"
label={_t("Room ID or address of ban list")}
label={_t("labs_mjolnir|lists_new_label")}
value={this.state.newList}
onChange={this.onNewListChanged}
/>

View file

@ -358,7 +358,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
return (
<SettingsTab>
{warning}
<SettingsSection heading={_t("Encryption")}>
<SettingsSection heading={_t("settings|security|encryption_section")}>
{secureBackup}
{eventIndex}
{crossSigning}

View file

@ -104,6 +104,7 @@ export const HomeButtonContextMenu: React.FC<ComponentProps<typeof SpaceContextM
label={_t("Show all rooms")}
active={allRoomsInHome}
onClick={() => {
onFinished();
SettingsStore.setValue("Spaces.allRoomsInHome", null, SettingLevel.ACCOUNT, !allRoomsInHome);
}}
/>

View file

@ -29,14 +29,10 @@ export default class VerificationComplete extends React.Component<IProps> {
<div>
<h2>{_t("encryption|verification|complete_title")}</h2>
<p>{_t("encryption|verification|complete_description")}</p>
<p>
{_t(
"Secure messages with this user are end-to-end encrypted and not able to be read by third parties.",
)}
</p>
<p>{_t("encryption|verification|explainer")}</p>
<DialogButtons
onPrimaryButtonClick={this.props.onDone}
primaryButton={_t("Got It")}
primaryButton={_t("encryption|verification|complete_action")}
hasCancel={false}
/>
</div>

View file

@ -133,18 +133,18 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
</div>
);
sasCaption = this.props.isSelf
? _t("Confirm the emoji below are displayed on both devices, in the same order:")
: _t("Verify this user by confirming the following emoji appear on their screen.");
? _t("encryption|verification|sas_emoji_caption_self")
: _t("encryption|verification|sas_emoji_caption_user");
} else if (this.props.sas.decimal) {
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>{num}</span>);
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">{numberBlocks}</div>;
sasCaption = this.props.isSelf
? _t("Verify this device by confirming the following number appears on its screen.")
: _t("Verify this user by confirming the following number appears on their screen.");
? _t("encryption|verification|sas_caption_self")
: _t("encryption|verification|sas_caption_user");
} else {
return (
<div>
{_t("Unable to find a supported verification method.")}
{_t("encryption|verification|unsupported_method")}
<AccessibleButton kind="primary" onClick={this.props.onCancel}>
{_t("action|cancel")}
</AccessibleButton>
@ -159,21 +159,21 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
// logged out during verification
const otherDevice = this.props.otherDeviceDetails;
if (otherDevice) {
text = _t("Waiting for you to verify on your other device, %(deviceName)s (%(deviceId)s)…", {
text = _t("encryption|verification|waiting_other_device_details", {
deviceName: otherDevice.displayName,
deviceId: otherDevice.deviceId,
});
} else {
text = _t("Waiting for you to verify on your other device…");
text = _t("encryption|verification|waiting_other_device");
}
confirm = <p>{text}</p>;
} else if (this.state.pending || this.state.cancelling) {
let text;
if (this.state.pending) {
const { displayName } = this.props;
text = _t("Waiting for %(displayName)s to verify…", { displayName });
text = _t("encryption|verification|waiting_other_user", { displayName });
} else {
text = _t("Cancelling…");
text = _t("encryption|verification|cancelling");
}
confirm = <PendingActionSpinner text={text} />;
} else {