Merge branch 'develop' into andybalaam/fix-receipt-flakes

This commit is contained in:
Andy Balaam 2023-09-28 14:23:45 +01:00 committed by GitHub
commit d7d0f54e9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
184 changed files with 14091 additions and 12814 deletions

View file

@ -293,7 +293,9 @@ function findRoomByName(room: string): Chainable<Room> {
export function openThread(rootMessage: string) {
cy.log("Open thread", rootMessage);
cy.get(".mx_RoomView_body", { log: false }).within(() => {
cy.contains(".mx_EventTile[data-scroll-tokens]", rootMessage, { log: false })
cy.findAllByText(rootMessage)
.filter(".mx_EventTile_body")
.parents(".mx_EventTile[data-scroll-tokens]")
.realHover()
.findByRole("button", { name: "Reply in thread", log: false })
.click();

View file

@ -84,7 +84,6 @@
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.0.12",
"focus-visible": "^5.2.0",
"gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1",
"graphemer": "^1.4.0",

View file

@ -230,7 +230,7 @@ textarea:focus {
/* accessible (focusable) components. Not intended for buttons, but */
/* should be used on things like focusable containers where the outline */
/* is usually not helping anyone. */
*:focus:not(.focus-visible) {
*:focus:not(:focus-visible) {
outline: none;
}
@ -585,7 +585,7 @@ legend {
cursor: pointer;
display: inline-block;
&:not(.focus-visible) {
&:not(:focus-visible) {
outline: none;
}
}

View file

@ -28,7 +28,7 @@ limitations under the License.
display: none;
}
&:not(.focus-visible) {
&:not(:focus-visible) {
outline: none;
}
}

View file

@ -35,7 +35,7 @@ limitations under the License.
--active-color: $slider-background-color;
}
&:focus:not(.focus-visible) {
&:focus:not(:focus-visible) {
outline: none;
}

View file

@ -70,7 +70,7 @@ limitations under the License.
cursor: not-allowed;
}
&.focus-visible {
&:focus-visible {
& + label .mx_Checkbox_background {
@mixin unreal-focus;
}

View file

@ -78,7 +78,7 @@ limitations under the License.
}
}
&.focus-visible {
&:focus-visible {
& + div {
@mixin unreal-focus;
}

View file

@ -224,7 +224,7 @@ $left-gutter: 64px;
}
}
&.focus-visible:focus-within,
&:focus-visible:focus-within,
&.mx_EventTile_actionBarFocused,
&.mx_EventTile_isEditing,
&.mx_EventTile_selected {
@ -870,7 +870,7 @@ $left-gutter: 64px;
border: 1px solid transparent;
.mx_EventTile:hover &,
.mx_EventTile.focus-visible:focus-within & {
.mx_EventTile:focus-visible:focus-within & {
border: 1px solid $tertiary-content;
}
}
@ -993,7 +993,7 @@ $left-gutter: 64px;
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput="keyboard"] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
.mx_EventTile:focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
@ -1002,7 +1002,7 @@ $left-gutter: 64px;
/* animation for when it's shown which means duplicating the style definition in */
/* multiple places. */
.mx_EventTile:not(:hover):not(.mx_EventTile_actionBarFocused):not([data-whatinput="keyboard"] :focus-within) {
&:not(.focus-visible:focus-within) .mx_MessageActionBar .mx_Indicator {
&:not(:focus-visible:focus-within) .mx_MessageActionBar .mx_Indicator {
animation: none;
}
}

View file

@ -27,7 +27,7 @@ limitations under the License.
}
&:hover .mx_LinkPreviewGroup_hide img,
.mx_LinkPreviewGroup_hide.focus-visible:focus img {
.mx_LinkPreviewGroup_hide:focus-visible:focus img {
visibility: visible;
}

View file

@ -28,7 +28,7 @@ limitations under the License.
content: "";
position: absolute;
top: -8px;
left: 10.5px;
left: 11px;
width: 4px;
height: 4px;
border-radius: 16px;

View file

@ -21,6 +21,7 @@ limitations under the License.
flex-wrap: wrap;
> .mx_StyledRadioButton {
align-items: center;
padding: $font-16px;
box-sizing: border-box;
border-radius: 10px;

View file

@ -501,11 +501,41 @@ export interface IStoredSession {
isUrl: string;
hasAccessToken: boolean;
accessToken: string | IEncryptedPayload;
hasRefreshToken: boolean;
refreshToken?: string | IEncryptedPayload;
userId: string;
deviceId: string;
isGuest: boolean;
}
/**
* Retrieve a token, as stored by `persistCredentials`
* Attempts to migrate token from localStorage to idb
* @param storageKey key used to store the token, eg ACCESS_TOKEN_STORAGE_KEY
* @returns Promise that resolves to token or undefined
*/
async function getStoredToken(storageKey: string): Promise<string | undefined> {
let token: string | undefined;
try {
token = await StorageManager.idbLoad("account", storageKey);
} catch (e) {
logger.error(`StorageManager.idbLoad failed for account:${storageKey}`, e);
}
if (!token) {
token = localStorage.getItem(storageKey) ?? undefined;
if (token) {
try {
// try to migrate access token to IndexedDB if we can
await StorageManager.idbSave("account", storageKey, token);
localStorage.removeItem(storageKey);
} catch (e) {
logger.error(`migration of token ${storageKey} to IndexedDB failed`, e);
}
}
}
return token;
}
/**
* Retrieves information about the stored session from the browser's storage. The session
* may not be valid, as it is not tested for consistency here.
@ -514,27 +544,14 @@ export interface IStoredSession {
export async function getStoredSessionVars(): Promise<Partial<IStoredSession>> {
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY) ?? undefined;
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY) ?? undefined;
let accessToken: string | undefined;
try {
accessToken = await StorageManager.idbLoad("account", ACCESS_TOKEN_STORAGE_KEY);
} catch (e) {
logger.error("StorageManager.idbLoad failed for account:mx_access_token", e);
}
if (!accessToken) {
accessToken = localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY) ?? undefined;
if (accessToken) {
try {
// try to migrate access token to IndexedDB if we can
await StorageManager.idbSave("account", ACCESS_TOKEN_STORAGE_KEY, accessToken);
localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
} catch (e) {
logger.error("migration of access token to IndexedDB failed", e);
}
}
}
const accessToken = await getStoredToken(ACCESS_TOKEN_STORAGE_KEY);
const refreshToken = await getStoredToken(REFRESH_TOKEN_STORAGE_KEY);
// if we pre-date storing "mx_has_access_token", but we retrieved an access
// token, then we should say we have an access token
const hasAccessToken = localStorage.getItem(HAS_ACCESS_TOKEN_STORAGE_KEY) === "true" || !!accessToken;
const hasRefreshToken = localStorage.getItem(HAS_REFRESH_TOKEN_STORAGE_KEY) === "true" || !!refreshToken;
const userId = localStorage.getItem("mx_user_id") ?? undefined;
const deviceId = localStorage.getItem("mx_device_id") ?? undefined;
@ -546,7 +563,7 @@ export async function getStoredSessionVars(): Promise<Partial<IStoredSession>> {
isGuest = localStorage.getItem("matrix-is-guest") === "true";
}
return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest };
return { hsUrl, isUrl, hasAccessToken, accessToken, refreshToken, hasRefreshToken, userId, deviceId, isGuest };
}
// The pickle key is a string of unspecified length and format. For AES, we
@ -585,6 +602,36 @@ async function abortLogin(): Promise<void> {
}
}
const isEncryptedPayload = (token?: IEncryptedPayload | string | undefined): token is IEncryptedPayload => {
return !!token && typeof token !== "string";
};
/**
* Try to decrypt a token retrieved from storage
* Where token is not encrypted (plain text) returns the plain text token
* Where token is encrypted, attempts decryption. Returns successfully decrypted token, else undefined.
* @param pickleKey pickle key used during encryption of token, or undefined
* @param token
* @param tokenIv initialization vector used during encryption of token eg ACCESS_TOKEN_IV
* @returns the decrypted token, or the plain text token. Returns undefined when token cannot be decrypted
*/
async function tryDecryptToken(
pickleKey: string | undefined,
token: IEncryptedPayload | string | undefined,
tokenIv: string,
): Promise<string | undefined> {
if (pickleKey && isEncryptedPayload(token)) {
const encrKey = await pickleKeyToAesKey(pickleKey);
const decryptedToken = await decryptAES(token, encrKey, tokenIv);
encrKey.fill(0);
return decryptedToken;
}
// if the token wasn't encrypted (plain string) just return it back
if (typeof token === "string") {
return token;
}
// otherwise return undefined
}
// returns a promise which resolves to true if a session is found in
// localstorage
//
@ -602,7 +649,8 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
return false;
}
const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars();
const { hsUrl, isUrl, hasAccessToken, accessToken, refreshToken, userId, deviceId, isGuest } =
await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
await abortLogin();
@ -614,18 +662,14 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
return false;
}
let decryptedAccessToken = accessToken;
const pickleKey = await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "");
const pickleKey = (await PlatformPeg.get()?.getPickleKey(userId, deviceId ?? "")) ?? undefined;
if (pickleKey) {
logger.log("Got pickle key");
if (typeof accessToken !== "string") {
const encrKey = await pickleKeyToAesKey(pickleKey);
decryptedAccessToken = await decryptAES(accessToken, encrKey, ACCESS_TOKEN_IV);
encrKey.fill(0);
}
} else {
logger.log("No pickle key available");
}
const decryptedAccessToken = await tryDecryptToken(pickleKey, accessToken, ACCESS_TOKEN_IV);
const decryptedRefreshToken = await tryDecryptToken(pickleKey, refreshToken, REFRESH_TOKEN_IV);
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
sessionStorage.removeItem("mx_fresh_login");
@ -635,7 +679,8 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
{
userId: userId,
deviceId: deviceId,
accessToken: decryptedAccessToken as string,
accessToken: decryptedAccessToken!,
refreshToken: decryptedRefreshToken,
homeserverUrl: hsUrl,
identityServerUrl: isUrl,
guest: isGuest,

View file

@ -130,7 +130,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
private renderPhaseDone(): JSX.Element {
return (
<div>
<p>{_t("Your keys are being backed up (the first backup could take a few minutes).")}</p>
<p>{_t("settings|key_backup|backup_in_progress")}</p>
<DialogButtons primaryButton={_t("action|ok")} onPrimaryButtonClick={this.onDone} hasCancel={false} />
</div>
);
@ -139,11 +139,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
private titleForPhase(phase: Phase): string {
switch (phase) {
case Phase.BackingUp:
return _t("Starting backup…");
return _t("settings|key_backup|backup_starting");
case Phase.Done:
return _t("Success!");
return _t("settings|key_backup|backup_success");
default:
return _t("Create key backup");
return _t("settings|key_backup|create_title");
}
}
@ -152,7 +152,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
if (this.state.error) {
content = (
<div>
<p>{_t("Unable to create key backup")}</p>
<p>{_t("settings|key_backup|cannot_create_backup")}</p>
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={this.createBackup}

View file

@ -541,13 +541,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{_t("Generate a Security Key")}
</div>
<div>
{_t(
"We'll generate a Security Key for you to store somewhere safe, like a password manager or a safe.",
)}
{_t("settings|key_backup|setup_secure_backup|generate_security_key_title")}
</div>
<div>{_t("settings|key_backup|setup_secure_backup|generate_security_key_description")}</div>
</StyledRadioButton>
);
}
@ -564,11 +560,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
{_t("Enter a Security Phrase")}
</div>
<div>
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
{_t("settings|key_backup|setup_secure_backup|enter_phrase_title")}
</div>
<div>{_t("settings|key_backup|setup_secure_backup|use_phrase_only_you_know")}</div>
</StyledRadioButton>
);
}
@ -583,9 +577,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
return (
<form onSubmit={this.onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">
{_t(
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.",
)}
{_t("settings|key_backup|setup_secure_backup|description")}
</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey}
@ -607,7 +599,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = (
<div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
<div>{_t("settings|key_backup|setup_secure_backup|requires_password_confirmation")}</div>
<div>
<Field
id="mx_CreateSecretStorageDialog_password"
@ -624,21 +616,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
} else if (!this.state.backupTrustInfo?.trusted) {
authPrompt = (
<div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
<div>{_t("settings|key_backup|setup_secure_backup|requires_key_restore")}</div>
</div>
);
nextCaption = _t("action|restore");
} else {
authPrompt = <p>{_t("You'll need to authenticate with the server to confirm the upgrade.")}</p>;
authPrompt = <p>{_t("settings|key_backup|setup_secure_backup|requires_server_authentication")}</p>;
}
return (
<form onSubmit={this.onMigrateFormSubmit}>
<p>
{_t(
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
)}
</p>
<p>{_t("settings|key_backup|setup_secure_backup|session_upgrade_description")}</p>
<div>{authPrompt}</div>
<DialogButtons
primaryButton={nextCaption}
@ -657,11 +645,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private renderPhasePassPhrase(): JSX.Element {
return (
<form onSubmit={this.onPassPhraseNextClick}>
<p>
{_t(
"Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.",
)}
</p>
<p>{_t("settings|key_backup|setup_secure_backup|enter_phrase_description")}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
@ -672,10 +656,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
onValidate={this.onPassPhraseValidate}
fieldRef={this.passphraseField}
autoFocus={true}
label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
label={_td("settings|key_backup|setup_secure_backup|enter_phrase_title")}
labelEnterPassword={_td("settings|key_backup|setup_secure_backup|enter_phrase_title")}
labelStrongPassword={_td("settings|key_backup|setup_secure_backup|phrase_strong_enough")}
labelAllowedButUnsafe={_td("settings|key_backup|setup_secure_backup|phrase_strong_enough")}
/>
</div>
@ -697,8 +681,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
let matchText;
let changeText;
if (this.state.passPhraseConfirm === this.state.passPhrase) {
matchText = _t("That matches!");
changeText = _t("Use a different passphrase?");
matchText = _t("settings|key_backup|setup_secure_backup|pass_phrase_match_success");
changeText = _t("settings|key_backup|setup_secure_backup|use_different_passphrase");
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
// only tell them they're wrong if they've actually gone wrong.
// Security conscious readers will note that if you left element-web unattended
@ -707,8 +691,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
// just opening the browser's developer tools and reading it.
// Note that not having typed anything at all will not hit this clause and
// fall through so empty box === no hint.
matchText = _t("That doesn't match.");
changeText = _t("Go back to set it again.");
matchText = _t("settings|key_backup|setup_secure_backup|pass_phrase_match_failed");
changeText = _t("settings|key_backup|setup_secure_backup|set_phrase_again");
}
let passPhraseMatch: JSX.Element | undefined;
@ -724,14 +708,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
}
return (
<form onSubmit={this.onPassPhraseConfirmNextClick}>
<p>{_t("Enter your Security Phrase a second time to confirm it.")}</p>
<p>{_t("settings|key_backup|setup_secure_backup|enter_phrase_to_confirm")}</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
onChange={this.onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm}
className="mx_CreateSecretStorageDialog_passPhraseField"
label={_t("Confirm your Security Phrase")}
label={_t("settings|key_backup|setup_secure_backup|confirm_security_phrase")}
autoFocus={true}
autoComplete="new-password"
/>
@ -772,11 +756,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
return (
<div>
<p>
{_t(
"Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.",
)}
</p>
<p>{_t("settings|key_backup|setup_secure_backup|security_key_safety_reminder")}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer mx_CreateSecretStorageDialog_recoveryKeyPrimarycontainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
@ -792,7 +772,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
{_t("action|download")}
</AccessibleButton>
<span>
{_t("%(downloadButton)s or %(copyButton)s", {
{_t("settings|key_backup|setup_secure_backup|download_or_copy", {
downloadButton: "",
copyButton: "",
})}
@ -824,7 +804,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private renderStoredPhase(): JSX.Element {
return (
<>
<p className="mx_Dialog_content">{_t("Your keys are now being backed up from this device.")}</p>
<p className="mx_Dialog_content">
{_t("settings|key_backup|setup_secure_backup|backup_setup_success_description")}
</p>
<DialogButtons
primaryButton={_t("action|done")}
onPrimaryButtonClick={() => this.props.onFinished(true)}
@ -837,7 +819,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private renderPhaseLoadError(): JSX.Element {
return (
<div>
<p>{_t("Unable to query secret storage status")}</p>
<p>{_t("settings|key_backup|setup_secure_backup|secret_storage_query_failure")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("action|retry")}
@ -853,10 +835,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private renderPhaseSkipConfirm(): JSX.Element {
return (
<div>
<p>
{_t("If you cancel now, you may lose encrypted messages & data if you lose access to your logins.")}
</p>
<p>{_t("You can also set up Secure Backup & manage your keys in Settings.")}</p>
<p>{_t("settings|key_backup|setup_secure_backup|cancel_warning")}</p>
<p>{_t("settings|key_backup|setup_secure_backup|settings_reminder")}</p>
<DialogButtons
primaryButton={_t("action|go_back")}
onPrimaryButtonClick={this.onGoBackClick}
@ -875,19 +855,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
case Phase.ChooseKeyPassphrase:
return _t("encryption|set_up_toast_title");
case Phase.Migrate:
return _t("Upgrade your encryption");
return _t("settings|key_backup|setup_secure_backup|title_upgrade_encryption");
case Phase.Passphrase:
return _t("Set a Security Phrase");
return _t("settings|key_backup|setup_secure_backup|title_set_phrase");
case Phase.PassphraseConfirm:
return _t("Confirm Security Phrase");
return _t("settings|key_backup|setup_secure_backup|title_confirm_phrase");
case Phase.ConfirmSkip:
return _t("Are you sure?");
case Phase.ShowKey:
return _t("Save your Security Key");
return _t("settings|key_backup|setup_secure_backup|title_save_key");
case Phase.Storing:
return _t("encryption|bootstrap_title");
case Phase.Stored:
return _t("Secure Backup successful");
return _t("settings|key_backup|setup_secure_backup|backup_setup_success_title");
default:
return "";
}
@ -912,7 +892,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
if (this.state.error) {
content = (
<div>
<p>{_t("Unable to set up secret storage")}</p>
<p>{_t("settings|key_backup|setup_secure_backup|unable_to_setup")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("action|retry")}

View file

@ -158,29 +158,21 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
<BaseDialog
className="mx_exportE2eKeysDialog"
onFinished={this.props.onFinished}
title={_t("Export room keys")}
title={_t("settings|key_export_import|export_title")}
>
<form onSubmit={this.onPassphraseFormSubmit}>
<div className="mx_Dialog_content">
<p>
{_t(
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.",
)}
</p>
<p>
{_t(
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.",
)}
</p>
<p>{_t("settings|key_export_import|export_description_1")}</p>
<p>{_t("settings|key_export_import|export_description_2")}</p>
<div className="error">{this.state.errStr}</div>
<div className="mx_E2eKeysDialog_inputTable">
<div className="mx_E2eKeysDialog_inputRow">
<PassphraseField
minScore={3}
label={_td("Enter passphrase")}
labelEnterPassword={_td("Enter passphrase")}
labelStrongPassword={_td("Great! This passphrase looks strong enough")}
labelAllowedButUnsafe={_td("Great! This passphrase looks strong enough")}
label={_td("settings|key_export_import|enter_passphrase")}
labelEnterPassword={_td("settings|key_export_import|enter_passphrase")}
labelStrongPassword={_td("settings|key_export_import|phrase_strong_enough")}
labelAllowedButUnsafe={_td("settings|key_export_import|phrase_strong_enough")}
value={this.state.passphrase1}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
this.onPassphraseChange(e, "passphrase1")
@ -196,9 +188,9 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
<div className="mx_E2eKeysDialog_inputRow">
<PassphraseConfirmField
password={this.state.passphrase1}
label={_td("Confirm passphrase")}
labelRequired={_td("Passphrase must not be empty")}
labelInvalid={_td("Passphrases must match")}
label={_td("settings|key_export_import|confirm_passphrase")}
labelRequired={_td("settings|key_export_import|phrase_cannot_be_empty")}
labelInvalid={_td("settings|key_export_import|phrase_must_match")}
value={this.state.passphrase2}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
this.onPassphraseChange(e, "passphrase2")

View file

@ -140,25 +140,19 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
<BaseDialog
className="mx_importE2eKeysDialog"
onFinished={this.props.onFinished}
title={_t("Import room keys")}
title={_t("settings|key_export_import|import_title")}
>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<p>
{_t(
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.",
)}
</p>
<p>
{_t(
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
)}
</p>
<p>{_t("settings|key_export_import|import_description_1")}</p>
<p>{_t("settings|key_export_import|import_description_2")}</p>
<div className="error">{this.state.errStr}</div>
<div className="mx_E2eKeysDialog_inputTable">
<div className="mx_E2eKeysDialog_inputRow">
<div className="mx_E2eKeysDialog_inputLabel">
<label htmlFor="importFile">{_t("File to import")}</label>
<label htmlFor="importFile">
{_t("settings|key_export_import|file_to_import")}
</label>
</div>
<div className="mx_E2eKeysDialog_inputCell">
<input
@ -173,7 +167,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
</div>
<div className="mx_E2eKeysDialog_inputRow">
<Field
label={_t("Enter passphrase")}
label={_t("settings|key_export_import|enter_passphrase")}
value={this.state.passphrase}
onChange={this.onPassphraseChange}
size={64}

View file

@ -55,29 +55,27 @@ export default class NewRecoveryMethodDialog extends React.PureComponent<IProps>
};
public render(): React.ReactNode {
const title = <span className="mx_KeyBackupFailedDialog_title">{_t("New Recovery Method")}</span>;
const newMethodDetected = <p>{_t("A new Security Phrase and key for Secure Messages have been detected.")}</p>;
const hackWarning = (
<p className="warning">
{_t(
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
)}
</p>
const title = (
<span className="mx_KeyBackupFailedDialog_title">
{_t("encryption|new_recovery_method_detected|title")}
</span>
);
const newMethodDetected = <p>{_t("encryption|new_recovery_method_detected|description_1")}</p>;
const hackWarning = <p className="warning">{_t("encryption|new_recovery_method_detected|warning")}</p>;
let content: JSX.Element | undefined;
if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) {
content = (
<div>
{newMethodDetected}
<p>{_t("This session is encrypting history using the new recovery method.")}</p>
<p>{_t("encryption|new_recovery_method_detected|description_2")}</p>
{hackWarning}
<DialogButtons
primaryButton={_t("action|ok")}
onPrimaryButtonClick={this.onOkClick}
cancelButton={_t("Go to Settings")}
cancelButton={_t("common|go_to_settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
@ -88,9 +86,9 @@ export default class NewRecoveryMethodDialog extends React.PureComponent<IProps>
{newMethodDetected}
{hackWarning}
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
primaryButton={_t("common|setup_secure_messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Go to Settings")}
cancelButton={_t("common|go_to_settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>

View file

@ -46,30 +46,20 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent<IPr
};
public render(): React.ReactNode {
const title = <span className="mx_KeyBackupFailedDialog_title">{_t("Recovery Method Removed")}</span>;
const title = (
<span className="mx_KeyBackupFailedDialog_title">{_t("encryption|recovery_method_removed|title")}</span>
);
return (
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
<div>
<p>
{_t(
"This session has detected that your Security Phrase and key for Secure Messages have been removed.",
)}
</p>
<p>
{_t(
"If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.",
)}
</p>
<p className="warning">
{_t(
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
)}
</p>
<p>{_t("encryption|recovery_method_removed|description_1")}</p>
<p>{_t("encryption|recovery_method_removed|description_2")}</p>
<p className="warning">{_t("encryption|recovery_method_removed|warning")}</p>
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
primaryButton={_t("common|setup_secure_messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("Go to Settings")}
cancelButton={_t("common|go_to_settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>

View file

@ -134,7 +134,7 @@ export default class RoomProvider extends AutocompleteProvider {
}
public getName(): string {
return _t("Rooms");
return _t("common|rooms");
}
public renderCompletions(completions: React.ReactNode[]): React.ReactNode {

View file

@ -345,7 +345,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_LeftPanel_exploreButton"
onClick={this.onExplore}
title={_t("Explore rooms")}
title={_t("action|explore_rooms")}
/>
);
}

View file

@ -35,8 +35,6 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { DecryptionError } from "matrix-js-sdk/src/crypto/algorithms";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
import "focus-visible";
// what-input helps improve keyboard accessibility
import "what-input";

View file

@ -384,7 +384,7 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
oob_data: {
avatarUrl: room?.avatar_url,
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
name: room?.name || roomAlias || _t("Unnamed room"),
name: room?.name || roomAlias || _t("common|unnamed_room"),
roomType,
} as IOOBData,
metricsTrigger: "RoomDirectory",

View file

@ -124,7 +124,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => {
{canCreateRoom && (
<>
<IconizedContextMenuOption
label={_t("New room")}
label={_t("action|new_room")}
iconClassName="mx_RoomList_iconNewRoom"
onClick={async (e): Promise<void> => {
e.preventDefault();
@ -139,7 +139,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => {
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
label={_t("action|new_video_room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={async (e): Promise<void> => {
e.preventDefault();
@ -164,7 +164,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => {
</>
)}
<IconizedContextMenuOption
label={_t("Add existing room")}
label={_t("action|add_existing_room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
@ -175,7 +175,7 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => {
/>
{canCreateSpace && (
<IconizedContextMenuOption
label={_t("Add space")}
label={_t("room_list|add_space_label")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();

View file

@ -29,7 +29,7 @@ export function SessionLockStolenView(): JSX.Element {
return (
<SplashPage className="mx_SessionLockStolenView">
<h1>{_t("common|error")}</h1>
<h2>{_t("%(brand)s has been opened in another tab.", { brand })}</h2>
<h2>{_t("error_app_open_in_another_tab", { brand })}</h2>
</SplashPage>
);
}

View file

@ -159,15 +159,11 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
if (lostKeys) {
return (
<div>
<p>
{_t(
"It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
)}
</p>
<p>{_t("encryption|verification|no_key_or_device")}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="primary" onClick={this.onResetConfirmClick}>
{_t("Proceed with reset")}
{_t("encryption|verification|reset_proceed_prompt")}
</AccessibleButton>
</div>
</div>
@ -176,9 +172,9 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
recoveryKeyPrompt = _t("Verify with Security Key or Phrase");
recoveryKeyPrompt = _t("encryption|verification|verify_using_key_or_phrase");
} else if (store.keyInfo) {
recoveryKeyPrompt = _t("Verify with Security Key");
recoveryKeyPrompt = _t("encryption|verification|verify_using_key");
}
let useRecoveryKeyButton;
@ -194,16 +190,14 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
if (store.hasDevicesToVerifyAgainst) {
verifyButton = (
<AccessibleButton kind="primary" onClick={this.onVerifyClick}>
{_t("Verify with another device")}
{_t("encryption|verification|verify_using_device")}
</AccessibleButton>
);
}
return (
<div>
<p>
{_t("Verify your identity to access encrypted messages and prove your identity to others.")}
</p>
<p>{_t("encryption|verification|verification_description")}</p>
<div className="mx_CompleteSecurity_actionRow">
{verifyButton}
@ -228,15 +222,9 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
} else if (phase === Phase.Done) {
let message: JSX.Element;
if (this.state.backupInfo) {
message = (
<p>
{_t(
"Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
)}
</p>
);
message = <p>{_t("encryption|verification|verification_success_with_backup")}</p>;
} else {
message = <p>{_t("Your new device is now verified. Other users will see it as trusted.")}</p>;
message = <p>{_t("encryption|verification|verification_success_without_backup")}</p>;
}
return (
<div>
@ -252,14 +240,10 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
} else if (phase === Phase.ConfirmSkip) {
return (
<div>
<p>
{_t(
"Without verifying, you won't have access to all your messages and may appear as untrusted to others.",
)}
</p>
<p>{_t("encryption|verification|verification_skip_warning")}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="danger_outline" onClick={this.onSkipConfirmClick}>
{_t("I'll verify later")}
{_t("encryption|verification|verify_later")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onSkipBackClick}>
{_t("action|go_back")}
@ -270,20 +254,12 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
} else if (phase === Phase.ConfirmReset) {
return (
<div>
<p>
{_t(
"Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.",
)}
</p>
<p>
{_t(
"Please only proceed if you're sure you've lost all of your other devices and your Security Key.",
)}
</p>
<p>{_t("encryption|verification|verify_reset_warning_1")}</p>
<p>{_t("encryption|verification|verify_reset_warning_2")}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="danger_outline" onClick={this.onResetConfirmClick}>
{_t("Proceed with reset")}
{_t("encryption|verification|reset_proceed_prompt")}
</AccessibleButton>
<AccessibleButton kind="primary" onClick={this.onResetBackClick}>
{_t("action|go_back")}

View file

@ -155,7 +155,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
try {
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
} catch (e) {
let errorText = _t("Failed to re-authenticate due to a homeserver problem");
let errorText = _t("auth|failed_soft_logout_homeserver");
if (
e instanceof MatrixError &&
e.errcode === "M_FORBIDDEN" &&
@ -311,12 +311,8 @@ export default class SoftLogout extends React.Component<IProps, IState> {
<h2>{_t("action|sign_in")}</h2>
<div>{this.renderSignInSection()}</div>
<h2>{_t("Clear personal data")}</h2>
<p>
{_t(
"Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.",
)}
</p>
<h2>{_t("auth|soft_logout_subheading")}</h2>
<p>{_t("auth|soft_logout_warning")}</p>
<div>
<AccessibleButton onClick={this.onClearAll} kind="danger">
{_t("Clear all data")}

View file

@ -46,7 +46,7 @@ export const EnterEmail: React.FC<EnterEmailProps> = ({
onLoginClick,
onSubmitForm,
}) => {
const submitButtonChild = loading ? <Spinner w={16} h={16} /> : _t("Send email");
const submitButtonChild = loading ? <Spinner w={16} h={16} /> : _t("auth|forgot_password_send_email");
const emailFieldRef = useRef<Field>(null);

View file

@ -153,9 +153,11 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
private generateCode = async (): Promise<void> => {
let rendezvous: MSC3906Rendezvous;
try {
const fallbackRzServer = this.props.client.getClientWellKnown()?.["io.element.rendezvous"]?.server;
const transport = new MSC3886SimpleHttpRendezvousTransport<MSC3903ECDHPayload>({
onFailure: this.onFailure,
client: this.props.client,
fallbackRzServer,
});
const channel = new MSC3903ECDHv2RendezvousChannel<MSC3906RendezvousPayload>(

View file

@ -164,7 +164,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
<IconizedContextMenuCheckbox
onClick={(e) => onTagRoom(e, DefaultTagID.LowPriority)}
active={isLowPriority}
label={_t("Low priority")}
label={_t("common|low_priority")}
iconClassName="mx_RoomTile_iconArrowDown"
/>
);
@ -174,11 +174,11 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
let iconClassName: string | undefined;
switch (echoChamber.notificationVolume) {
case RoomNotifState.AllMessages:
notificationLabel = _t("Default");
notificationLabel = _t("notifications|default");
iconClassName = "mx_RoomTile_iconNotificationsDefault";
break;
case RoomNotifState.AllMessagesLoud:
notificationLabel = _t("All messages");
notificationLabel = _t("notifications|all_messages");
iconClassName = "mx_RoomTile_iconNotificationsAllMessages";
break;
case RoomNotifState.MentionsOnly:

View file

@ -61,7 +61,7 @@ export const RoomNotificationContextMenu: React.FC<IProps> = ({ room, onFinished
const allMessagesOption: JSX.Element = (
<IconizedContextMenuRadio
label={_t("All messages")}
label={_t("notifications|all_messages")}
active={notificationState === RoomNotifState.AllMessagesLoud}
iconClassName="mx_RoomNotificationContextMenu_iconBellDot"
onClick={wrapHandler(() => setNotificationState(RoomNotifState.AllMessagesLoud))}

View file

@ -187,7 +187,7 @@ const SpaceContextMenu: React.FC<IProps> = ({ space, hideHeader, onFinished, ...
<IconizedContextMenuOption
data-testid="new-video-room-option"
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Video room")}
label={_t("common|video_room")}
onClick={onNewVideoRoomClick}
>
<BetaPill />

View file

@ -387,7 +387,7 @@ const defaultRendererFactory =
</div>
);
export const defaultRoomsRenderer = defaultRendererFactory(_td("Rooms"));
export const defaultRoomsRenderer = defaultRendererFactory(_td("common|rooms"));
export const defaultSpacesRenderer = defaultRendererFactory(_td("common|spaces"));
export const defaultDmsRenderer = defaultRendererFactory(_td("Direct Messages"));

View file

@ -95,7 +95,7 @@ export default class ConfirmUserActionDialog extends React.Component<IProps, ISt
onChange={this.onReasonChange}
value={this.state.reason}
className="mx_ConfirmUserActionDialog_reasonField"
label={_t("Reason")}
label={_t("room_settings|permissions|ban_reason")}
autoFocus={true}
/>
</form>

View file

@ -353,9 +353,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
microcopy = _t("create_room|encryption_forced");
}
} else {
microcopy = _t(
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
);
microcopy = _t("settings|security|e2ee_default_disabled_warning");
}
e2eeSection = (
<React.Fragment>
@ -420,7 +418,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
labelKnock={
this.askToJoinEnabled ? _t("room_settings|security|join_rule_knock") : undefined
}
labelPublic={_t("Public room")}
labelPublic={_t("common|public_room")}
labelRestricted={
this.supportsRestricted ? _t("create_room|join_rule_restricted") : undefined
}

View file

@ -163,7 +163,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
<JoinRuleDropdown
label={_t("Space visibility")}
labelInvite={_t("Private space (invite only)")}
labelPublic={_t("Public space")}
labelPublic={_t("common|public_space")}
labelRestricted={_t("create_room|join_rule_restricted")}
width={478}
value={joinRule}

View file

@ -214,7 +214,7 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
titleClass="danger"
title={_t("Deactivate Account")}
title={_t("settings|general|deactivate_section")}
screenName="DeactivateAccount"
>
<div className="mx_Dialog_content">

View file

@ -47,7 +47,7 @@ export default class IntegrationsDisabledDialog extends React.Component<IProps>
<div className="mx_IntegrationsDisabledDialog_content">
<p>
{_t("Enable '%(manageIntegrations)s' in Settings to do this.", {
manageIntegrations: _t("Manage integrations"),
manageIntegrations: _t("integration_manager|manage_title"),
})}
</p>
</div>

View file

@ -143,7 +143,7 @@ export default class InteractiveAuthDialog<T> extends React.Component<Interactiv
// Let's pick a title, body, and other params text that we'll show to the user. The order
// is most specific first, so stagePhase > our props > defaults.
let title = this.state.authError ? "Error" : this.props.title || _t("Authentication");
let title = this.state.authError ? "Error" : this.props.title || _t("common|authentication");
let body = this.state.authError ? null : this.props.body;
let continueText: string | undefined;
let continueKind: string | undefined;

View file

@ -417,7 +417,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
label={_t("room_settings|permissions|ban_reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
@ -456,7 +456,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
label={_t("room_settings|permissions|ban_reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}

View file

@ -154,7 +154,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
tabs.push(
new Tab(
RoomSettingsTab.Voip,
_td("Voice & Video"),
_td("settings|voip|title"),
"mx_RoomSettingsDialog_voiceIcon",
<VoipRoomSettingsTab room={this.state.room} />,
),
@ -197,7 +197,7 @@ class RoomSettingsDialog extends React.Component<IProps, IState> {
tabs.push(
new Tab(
RoomSettingsTab.Bridges,
_td("Bridges"),
_td("room_settings|bridges|title"),
"mx_RoomSettingsDialog_bridgesIcon",
<BridgeSettingsTab room={this.state.room} />,
"RoomSettingsBridges",

View file

@ -67,8 +67,8 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
const emailAddress = this.state.emailAddress;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
title: _t("settings|general|error_invalid_email"),
description: _t("settings|general|error_invalid_email_detail"),
});
return;
}
@ -88,7 +88,7 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
this.setState({ emailBusy: false });
logger.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
title: _t("settings|general|error_add_email"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
},
@ -123,7 +123,7 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
const message =
_t("Unable to verify email address.") +
_t("settings|general|error_email_verification") +
" " +
_t(
"Please check your email and click on the link it contains. Once this is done, click continue.",
@ -137,7 +137,7 @@ export default class SetEmailDialog extends React.Component<IProps, IState> {
} else {
logger.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
title: _t("settings|general|error_email_verification"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
}

View file

@ -133,7 +133,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
tabs.push(
new Tab(
UserTab.Voice,
_td("Voice & Video"),
_td("settings|voip|title"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
"UserSettingsVoiceVideo",

View file

@ -33,7 +33,9 @@ interface Props {
export function PublicRoomResultDetails({ room, labelId, descriptionId, detailsId }: Props): JSX.Element {
let name =
room.name || getDisplayAliasForAliasSet(room.canonical_alias ?? "", room.aliases ?? []) || _t("Unnamed room");
room.name ||
getDisplayAliasForAliasSet(room.canonical_alias ?? "", room.aliases ?? []) ||
_t("common|unnamed_room");
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}

View file

@ -92,7 +92,7 @@ export function RoomResultContextMenus({ room }: Props): JSX.Element {
const target = ev.target as HTMLElement;
setGeneralMenuPosition(target.getBoundingClientRect());
}}
title={room.isSpaceRoom() ? _t("space|context_menu|options") : _t("Room options")}
title={room.isSpaceRoom() ? _t("space|context_menu|options") : _t("room|context_menu|title")}
isExpanded={generalMenuPosition !== null}
/>
)}

View file

@ -788,7 +788,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
role="group"
aria-labelledby="mx_SpotlightDialog_section_rooms"
>
<h4 id="mx_SpotlightDialog_section_rooms">{_t("Rooms")}</h4>
<h4 id="mx_SpotlightDialog_section_rooms">{_t("common|rooms")}</h4>
<div>{results[Section.Rooms].slice(0, SECTION_LIMIT).map(resultMapper)}</div>
</div>
);

View file

@ -409,7 +409,7 @@ export const UserOptionsSection: React.FC<{
kind="link"
className={classNames("mx_UserInfo_field", { mx_UserInfo_destructive: !isIgnored })}
>
{isIgnored ? _t("Unignore") : _t("action|ignore")}
{isIgnored ? _t("action|unignore") : _t("action|ignore")}
</AccessibleButton>
);

View file

@ -212,9 +212,12 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
menu = (
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
<IconizedContextMenuOption
label={_t("Video call (%(brand)s)", { brand })}
label={_t("room|header|video_call_button_jitsi")}
onClick={onJitsiClick}
/>
<IconizedContextMenuOption
label={_t("room|header|video_call_button_ec", { brand })}
onClick={onElementClick}
/>
</IconizedContextMenuOptionList>
@ -412,13 +415,13 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
<IconizedContextMenuOptionList>
<IconizedContextMenuRadio
iconClassName="mx_LegacyRoomHeader_freedomIcon"
label={_t("Freedom")}
label={_t("room|header|video_call_ec_layout_freedom")}
active={layout === Layout.Tile}
onClick={onFreedomClick}
/>
<IconizedContextMenuRadio
iconClassName="mx_LegacyRoomHeader_spotlightIcon"
label={_t("Spotlight")}
label={_t("room|header|video_call_ec_layout_spotlight")}
active={layout === Layout.Spotlight}
onClick={onSpotlightClick}
/>
@ -436,7 +439,7 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
"mx_LegacyRoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("Change layout")}
title={_t("room|header|video_call_ec_change_layout")}
alignment={Alignment.Bottom}
key="layout"
/>
@ -589,7 +592,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("Forget room")}
title={_t("room|header|forget_room_button")}
alignment={Alignment.Bottom}
key="forget"
/>,
@ -603,7 +606,11 @@ export default class RoomHeader extends React.Component<IProps, IState> {
mx_LegacyRoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
title={
this.props.appsShown
? _t("room|header|hide_widgets_button")
: _t("room|header|show_widgets_button")
}
aria-checked={this.props.appsShown}
alignment={Alignment.Bottom}
key="apps"
@ -643,7 +650,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_closeButton"
onClick={this.onHideCallClick}
title={_t("Close call")}
title={_t("room|header|close_call_button")}
key="close"
/>,
);
@ -652,7 +659,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
<AccessibleTooltipButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_minimiseButton"
onClick={this.onHideCallClick}
title={_t("View chat timeline")}
title={_t("room|header|video_room_view_chat_button")}
alignment={Alignment.Bottom}
key="minimise"
/>,
@ -718,7 +725,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
className="mx_LegacyRoomHeader_name"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("Room options")}
title={_t("room|context_menu|title")}
alignment={Alignment.Bottom}
>
{roomName}
@ -762,7 +769,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
let oobName = _t("Unnamed room");
let oobName = _t("common|unnamed_room");
if (this.props.oobData && this.props.oobData.name) {
oobName = this.props.oobData.name;
}

View file

@ -59,7 +59,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
<AccessibleButton onClick={toggleExpanded}>
{expanded
? _t("action|collapse")
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length })}
: _t("timeline|url_preview|show_n_more", { count: previews.length - showPreviews.length })}
</AccessibleButton>
);
}
@ -72,7 +72,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
<AccessibleButton
className="mx_LinkPreviewGroup_hide"
onClick={onCancelClick}
aria-label={_t("Close preview")}
aria-label={_t("timeline|url_preview|close")}
>
<img
className="mx_filterFlipColor"

View file

@ -354,9 +354,9 @@ export default class MemberList extends React.Component<IProps, IState> {
let inviteButton: JSX.Element | undefined;
if (room?.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers)) {
let inviteButtonText = _t("Invite to this room");
let inviteButtonText = _t("room|invite_this_room");
if (room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
inviteButtonText = _t("space|invite_this_space");
}
if (this.state.canInvite) {
@ -371,7 +371,7 @@ export default class MemberList extends React.Component<IProps, IState> {
className="mx_MemberList_invite"
onClick={null}
disabled
tooltip={_t("You do not have permission to invite users")}
tooltip={_t("member_list|invite_button_no_perms_tooltip")}
>
<span>{inviteButtonText}</span>
</AccessibleTooltipButton>
@ -382,7 +382,7 @@ export default class MemberList extends React.Component<IProps, IState> {
let invitedHeader;
let invitedSection;
if (this.getChildCountInvited() > 0) {
invitedHeader = <h2>{_t("Invited")}</h2>;
invitedHeader = <h2>{_t("member_list|invited_list_heading")}</h2>;
invitedSection = (
<TruncatedList
className="mx_MemberList_section mx_MemberList_invited"
@ -397,7 +397,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const footer = (
<SearchBox
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={_t("Filter room members")}
placeholder={_t("member_list|filter_placeholder")}
onSearch={this.onSearchQueryChanged}
initialValue={this.props.searchQuery}
/>

View file

@ -173,7 +173,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
}
private getPowerLabel(): string {
return _t("%(userName)s (power %(powerLevelNumber)s)", {
return _t("member_list|power_label", {
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
roomId: this.props.member.roomId,
}),

View file

@ -535,7 +535,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
className="mx_MessageComposer_roomReplaced_link"
onClick={this.onTombstoneClick}
>
{_t("The conversation continues here.")}
{_t("composer|room_upgraded_link")}
</a>
) : (
""
@ -551,7 +551,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
src={require("../../../../res/img/room_replaced.svg").default}
/>
<span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")}
{_t("composer|room_upgraded_notice")}
</span>
<br />
{continuesLink}
@ -561,7 +561,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
{_t("You do not have permission to post to this room")}
{_t("composer|no_perms_notice")}
</div>,
);
}
@ -649,7 +649,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
title={
this.state.haveRecording ? _t("composer|send_button_voice_message") : undefined
}
/>
)}
</div>

View file

@ -258,7 +258,7 @@ function showStickersButton(props: IProps): ReactElement | null {
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("common|sticker")}
title={props.isStickerPickerOpen ? _t("composer|close_sticker_picker") : _t("common|sticker")}
/>
) : null;
}
@ -283,7 +283,7 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | nu
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick}
title={_t("Voice Message")}
title={_t("composer|voice_message_button")}
/>
);
}
@ -309,8 +309,8 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
);
if (!canSend) {
Modal.createDialog(ErrorDialog, {
title: _t("Permission Required"),
description: _t("You do not have permission to start polls in this room."),
title: _t("composer|poll_button_no_perms_title"),
description: _t("composer|poll_button_no_perms_description"),
});
} else {
const threadId =
@ -338,7 +338,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_poll"
onClick={this.onCreateClick}
title={_t("Poll")}
title={_t("composer|poll_button")}
/>
);
}
@ -364,7 +364,7 @@ interface WysiwygToggleButtonProps {
}
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps): JSX.Element {
const title = isRichTextEnabled ? _t("Hide formatting") : _t("Show formatting");
const title = isRichTextEnabled ? _t("composer|mode_plain") : _t("composer|mode_rich_text");
return (
<CollapsibleButton

View file

@ -52,7 +52,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
mx_MessageComposerFormatBar_shown: this.state.visible,
});
return (
<Toolbar className={classes} ref={this.formatBarRef} aria-label={_t("Formatting")}>
<Toolbar className={classes} ref={this.formatBarRef} aria-label={_t("composer|formatting_toolbar_label")}>
<FormatButton
label={_t("composer|format_bold")}
onClick={() => this.props.onAction(Formatting.Bold)}
@ -61,7 +61,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
visible={this.state.visible}
/>
<FormatButton
label={_t("Italics")}
label={_t("composer|format_italics")}
onClick={() => this.props.onAction(Formatting.Italics)}
icon="Italic"
shortcut={this.props.shortcuts.italics}
@ -88,7 +88,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
visible={this.state.visible}
/>
<FormatButton
label={_t("Insert link")}
label={_t("composer|format_insert_link")}
onClick={() => this.props.onAction(Formatting.InsertLink)}
icon="InsertLink"
shortcut={this.props.shortcuts.insert_link}

View file

@ -215,7 +215,7 @@ const NewRoomIntro: React.FC = () => {
defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")}
{_t("room|invite_this_room")}
</AccessibleButton>
</div>
);

View file

@ -122,7 +122,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
let label: string | undefined;
let tooltip: JSX.Element | undefined;
if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
label = _t("Message didn't send. Click for info.");
label = _t("notifications|message_didnt_send");
tooltip = <Tooltip className="mx_NotificationBadge_tooltip" label={label} />;
}

View file

@ -100,7 +100,9 @@ export function ReadReceiptGroup({
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
label: (
<>
<div className="mx_Tooltip_title">{_t("Seen by %(count)s people", { count: readReceipts.length })}</div>
<div className="mx_Tooltip_title">
{_t("timeline|read_receipt_title", { count: readReceipts.length })}
</div>
<div className="mx_Tooltip_sub">{tooltipText}</div>
</>
),
@ -176,7 +178,7 @@ export function ReadReceiptGroup({
<ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}>
<AutoHideScrollbar>
<SectionHeader className="mx_ReadReceiptGroup_title">
{_t("Seen by %(count)s people", { count: readReceipts.length })}
{_t("timeline|read_receipt_title", { count: readReceipts.length })}
</SectionHeader>
{readReceipts.map((receipt) => (
<ReadReceiptPerson
@ -193,7 +195,7 @@ export function ReadReceiptGroup({
return (
<div className="mx_EventTile_msgOption">
<div className="mx_ReadReceiptGroup" role="group" aria-label={_t("Read receipts")}>
<div className="mx_ReadReceiptGroup" role="group" aria-label={_t("timeline|read_receipts_label")}>
<AccessibleButton
className="mx_ReadReceiptGroup_button"
inputRef={button}

View file

@ -47,7 +47,7 @@ export default class ReplyPreview extends React.Component<IProps> {
<div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header">
<span>{_t("Replying")}</span>
<span>{_t("composer|replying_title")}</span>
<AccessibleButton
className="mx_ReplyPreview_header_cancel"
onClick={() => cancelQuoting(this.context.timelineRenderingType)}

View file

@ -50,7 +50,7 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v
<AccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb"
onClick={onClick}
aria-label={_t("Room %(name)s", { name: room.name })}
aria-label={_t("a11y|room_name", { name: room.name })}
title={room.name}
tooltipClassName="mx_RoomBreadcrumbs_Tooltip"
onFocus={onFocus}
@ -123,7 +123,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition appear={true} in={this.state.doAnimation} timeout={640} classNames="mx_RoomBreadcrumbs">
<Toolbar className="mx_RoomBreadcrumbs" aria-label={_t("Recently visited rooms")}>
<Toolbar className="mx_RoomBreadcrumbs" aria-label={_t("room_list|breadcrumbs_label")}>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
</Toolbar>
</CSSTransition>
@ -131,7 +131,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
} else {
return (
<div className="mx_RoomBreadcrumbs">
<div className="mx_RoomBreadcrumbs_placeholder">{_t("No recently visited rooms")}</div>
<div className="mx_RoomBreadcrumbs_placeholder">{_t("room_list|breadcrumbs_empty")}</div>
</div>
);
}

View file

@ -131,12 +131,12 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
{roomName}
{!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && (
<Tooltip label={_t("Public room")}>
<Tooltip label={_t("common|public_room")}>
<PublicIcon
width="16px"
height="16px"
className="text-secondary"
aria-label={_t("Public room")}
aria-label={_t("common|public_room")}
/>
</Tooltip>
)}
@ -153,12 +153,12 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
)}
{isDirectMessage && e2eStatus === E2EStatus.Warning && (
<Tooltip label={_t("Untrusted")}>
<Tooltip label={_t("room|header_untrusted_label")}>
<ErrorIcon
width="16px"
height="16px"
className="mx_Untrusted"
aria-label={_t("Untrusted")}
aria-label={_t("room|header_untrusted_label")}
/>
</Tooltip>
)}

View file

@ -51,13 +51,13 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
let roomType: string;
if (isVideoRoom) {
iconClass = "mx_RoomInfoLine_video";
roomType = _t("Video room");
roomType = _t("common|video_room");
} else if (joinRule === JoinRule.Public) {
iconClass = "mx_RoomInfoLine_public";
roomType = room.isSpaceRoom() ? _t("Public space") : _t("Public room");
roomType = room.isSpaceRoom() ? _t("common|public_space") : _t("common|public_room");
} else {
iconClass = "mx_RoomInfoLine_private";
roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
roomType = room.isSpaceRoom() ? _t("common|private_space") : _t("common|private_room");
}
let members: JSX.Element | undefined;

View file

@ -81,7 +81,6 @@ interface IState {
export const TAG_ORDER: TagID[] = [
DefaultTagID.Invite,
DefaultTagID.SavedItems,
DefaultTagID.Favourite,
DefaultTagID.DM,
DefaultTagID.Untagged,
@ -132,7 +131,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
<IconizedContextMenuOptionList first>
{showCreateRooms && (
<IconizedContextMenuOption
label={_t("Start new chat")}
label={_t("action|start_new_chat")}
iconClassName="mx_RoomList_iconStartChat"
onClick={(e) => {
e.preventDefault();
@ -148,7 +147,7 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
)}
{showInviteUsers && (
<IconizedContextMenuOption
label={_t("Invite to space")}
label={_t("action|invite_to_space")}
iconClassName="mx_RoomList_iconInvite"
onClick={(e) => {
e.preventDefault();
@ -172,8 +171,8 @@ const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = default
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add people")}
title={_t("Add people")}
aria-label={_t("action|add_people")}
title={_t("action|add_people")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
@ -222,7 +221,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
contextMenuContent = (
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Explore rooms")}
label={_t("action|explore_rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -239,7 +238,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
{showCreateRoom ? (
<>
<IconizedContextMenuOption
label={_t("New room")}
label={_t("action|new_room")}
iconClassName="mx_RoomList_iconNewRoom"
onClick={(e) => {
e.preventDefault();
@ -253,7 +252,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
label={_t("action|new_video_room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
@ -271,7 +270,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
</IconizedContextMenuOption>
)}
<IconizedContextMenuOption
label={_t("Add existing room")}
label={_t("action|add_existing_room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
onClick={(e) => {
e.preventDefault();
@ -292,7 +291,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
{showCreateRoom && (
<>
<IconizedContextMenuOption
label={_t("New room")}
label={_t("action|new_room")}
iconClassName="mx_RoomList_iconNewRoom"
onClick={(e) => {
e.preventDefault();
@ -304,7 +303,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
label={_t("action|new_video_room")}
iconClassName="mx_RoomList_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
@ -325,7 +324,7 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
)}
{showExploreRooms ? (
<IconizedContextMenuOption
label={_t("Explore public rooms")}
label={_t("action|explore_public_rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -357,8 +356,8 @@ const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
onClick={openMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={_t("Add room")}
title={_t("Add room")}
aria-label={_t("room_list|add_room_label")}
title={_t("room_list|add_room_label")}
isExpanded={menuDisplayed}
inputRef={handle}
/>
@ -382,11 +381,6 @@ const TAG_AESTHETICS: TagAestheticsMap = {
isInvite: false,
defaultHidden: false,
},
[DefaultTagID.SavedItems]: {
sectionLabel: _td("Saved Items"),
isInvite: false,
defaultHidden: false,
},
[DefaultTagID.DM]: {
sectionLabel: _td("common|people"),
isInvite: false,
@ -394,13 +388,13 @@ const TAG_AESTHETICS: TagAestheticsMap = {
AuxButtonComponent: DmAuxButton,
},
[DefaultTagID.Untagged]: {
sectionLabel: _td("Rooms"),
sectionLabel: _td("common|rooms"),
isInvite: false,
defaultHidden: false,
AuxButtonComponent: UntaggedAuxButton,
},
[DefaultTagID.LowPriority]: {
sectionLabel: _td("Low priority"),
sectionLabel: _td("common|low_priority"),
isInvite: false,
defaultHidden: false,
},
@ -412,13 +406,13 @@ const TAG_AESTHETICS: TagAestheticsMap = {
// TODO: Replace with archived view: https://github.com/vector-im/element-web/issues/14038
[DefaultTagID.Archived]: {
sectionLabel: _td("Historical"),
sectionLabel: _td("common|historical"),
isInvite: false,
defaultHidden: true,
},
[DefaultTagID.Suggested]: {
sectionLabel: _td("Suggested Rooms"),
sectionLabel: _td("room_list|suggested_rooms_heading"),
isInvite: false,
defaultHidden: false,
},
@ -654,7 +648,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
onKeyDown={onKeyDownHandler}
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
aria-label={_t("common|rooms")}
ref={this.treeRef}
>
{sublists}

View file

@ -203,7 +203,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
<>
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewRoom"
label={_t("New room")}
label={_t("action|new_room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -215,7 +215,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
{videoRoomsEnabled && (
<IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
label={_t("New video room")}
label={_t("action|new_video_room")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -243,7 +243,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
{inviteOption}
{newRoomOptions}
<IconizedContextMenuOption
label={_t("Explore rooms")}
label={_t("action|explore_rooms")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -258,7 +258,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
}}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
label={_t("action|add_existing_room")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
@ -271,7 +271,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
/>
{canCreateSpaces && (
<IconizedContextMenuOption
label={_t("Add space")}
label={_t("room_list|add_space_label")}
iconClassName="mx_RoomListHeader_iconPlus"
onClick={(e) => {
e.preventDefault();
@ -296,7 +296,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
newRoomOpts = (
<>
<IconizedContextMenuOption
label={_t("Start new chat")}
label={_t("action|start_new_chat")}
iconClassName="mx_RoomListHeader_iconStartChat"
onClick={(e) => {
e.preventDefault();
@ -307,7 +307,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
}}
/>
<IconizedContextMenuOption
label={_t("New room")}
label={_t("action|new_room")}
iconClassName="mx_RoomListHeader_iconNewRoom"
onClick={(e) => {
e.preventDefault();
@ -319,7 +319,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
/>
{videoRoomsEnabled && (
<IconizedContextMenuOption
label={_t("New video room")}
label={_t("action|new_video_room")}
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
onClick={(e) => {
e.preventDefault();
@ -340,7 +340,7 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
if (canExploreRooms) {
joinRoomOpt = (
<IconizedContextMenuOption
label={_t("Join public room")}
label={_t("room_list|join_public_room_label")}
iconClassName="mx_RoomListHeader_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -379,9 +379,9 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
.map(([type, keys]) => {
switch (type) {
case PendingActionType.JoinRoom:
return _t("Currently joining %(count)s rooms", { count: keys.size });
return _t("room_list|joining_rooms_status", { count: keys.size });
case PendingActionType.BulkRedact:
return _t("Currently removing messages in %(count)s rooms", { count: keys.size });
return _t("room_list|redacting_messages_status", { count: keys.size });
}
})
.join("\n");
@ -400,11 +400,11 @@ const RoomListHeader: React.FC<IProps> = ({ onVisibilityChange }) => {
contextMenuButton = (
<ContextMenuButton
{...commonProps}
label={_t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })}
label={_t("room_list|space_menu_label", { spaceName: spaceName ?? activeSpace.name })}
/>
);
} else {
contextMenuButton = <ContextMenuTooltipButton {...commonProps} title={_t("Home options")} />;
contextMenuButton = <ContextMenuTooltipButton {...commonProps} title={_t("room_list|home_menu_label")} />;
}
}

View file

@ -169,7 +169,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
identityAccessToken!,
);
if (!("mxid" in result)) {
throw new UserFriendlyError("Unable to find user by email");
throw new UserFriendlyError("room|error_3pid_invite_email_lookup");
}
this.setState({ invitedEmailMxid: result.mxid });
} catch (err) {
@ -329,9 +329,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
switch (messageCase) {
case MessageCase.Joining: {
if (this.props.oobData?.roomType || isSpace) {
title = isSpace ? _t("Joining space…") : _t("Joining room…");
title = isSpace ? _t("room|joining_space") : _t("room|joining_room");
} else {
title = _t("Joining…");
title = _t("room|joining");
}
showSpinner = true;
@ -343,7 +343,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
break;
}
case MessageCase.Rejecting: {
title = _t("Rejecting invite…");
title = _t("room|rejecting");
showSpinner = true;
break;
}
@ -353,15 +353,15 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
ModuleRunner.instance.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.roomId);
}
if (opts.canJoin) {
title = _t("Join the room to participate");
title = _t("room|join_title");
primaryActionLabel = _t("action|join");
primaryActionHandler = () => {
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.roomId);
};
} else {
title = _t("Join the conversation with an account");
title = _t("room|join_title_account");
if (SettingsStore.getValue(UIFeature.Registration)) {
primaryActionLabel = _t("Sign Up");
primaryActionLabel = _t("room|join_button_account");
primaryActionHandler = this.onRegisterClick;
}
secondaryActionLabel = _t("action|sign_in");
@ -371,7 +371,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
footer = (
<div>
<Spinner w={20} h={20} />
{_t("Loading preview")}
{_t("room|loading_preview")}
</div>
);
}
@ -380,16 +380,16 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
case MessageCase.Kicked: {
const { memberName, reason } = this.getKickOrBanInfo();
if (roomName) {
title = _t("You were removed from %(roomName)s by %(memberName)s", { memberName, roomName });
title = _t("room|kicked_from_room_by", { memberName, roomName });
} else {
title = _t("You were removed by %(memberName)s", { memberName });
title = _t("room|kicked_by", { memberName });
}
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : undefined;
subTitle = reason ? _t("room|kick_reason", { reason }) : undefined;
if (isSpace) {
primaryActionLabel = _t("Forget this space");
primaryActionLabel = _t("room|forget_space");
} else {
primaryActionLabel = _t("Forget this room");
primaryActionLabel = _t("room|forget_room");
}
primaryActionHandler = this.props.onForgetClick;
@ -397,22 +397,20 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
secondaryActionLabel = primaryActionLabel;
secondaryActionHandler = primaryActionHandler;
primaryActionLabel = _t("Re-join");
primaryActionLabel = _t("room|rejoin_button");
primaryActionHandler = this.props.onJoinClick;
}
break;
}
case MessageCase.RequestDenied: {
title = _t("You have been denied access");
title = _t("room|knock_denied_title");
subTitle = _t(
"As you have been denied access, you cannot rejoin unless you are invited by the admin or moderator of the group.",
);
subTitle = _t("room|knock_denied_subtitle");
if (isSpace) {
primaryActionLabel = _t("Forget this space");
primaryActionLabel = _t("room|forget_space");
} else {
primaryActionLabel = _t("Forget this room");
primaryActionLabel = _t("room|forget_room");
}
primaryActionHandler = this.props.onForgetClick;
break;
@ -420,44 +418,43 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
case MessageCase.Banned: {
const { memberName, reason } = this.getKickOrBanInfo();
if (roomName) {
title = _t("You were banned from %(roomName)s by %(memberName)s", { memberName, roomName });
title = _t("room|banned_from_room_by", { memberName, roomName });
} else {
title = _t("You were banned by %(memberName)s", { memberName });
title = _t("room|banned_by", { memberName });
}
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : undefined;
subTitle = reason ? _t("room|kick_reason", { reason }) : undefined;
if (isSpace) {
primaryActionLabel = _t("Forget this space");
primaryActionLabel = _t("room|forget_space");
} else {
primaryActionLabel = _t("Forget this room");
primaryActionLabel = _t("room|forget_room");
}
primaryActionHandler = this.props.onForgetClick;
break;
}
case MessageCase.OtherThreePIDError: {
if (roomName) {
title = _t("Something went wrong with your invite to %(roomName)s", { roomName });
title = _t("room|3pid_invite_error_title_room", { roomName });
} else {
title = _t("Something went wrong with your invite.");
title = _t("room|3pid_invite_error_title");
}
const joinRule = this.joinRule();
const errCodeMessage = _t(
"An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.",
{ errcode: this.state.threePidFetchError?.errcode || _t("unknown error code") },
);
const errCodeMessage = _t("room|3pid_invite_error_description", {
errcode: this.state.threePidFetchError?.errcode || _t("unknown error code"),
});
switch (joinRule) {
case "invite":
subTitle = [_t("You can only join it with a working invite."), errCodeMessage];
primaryActionLabel = _t("Try to join anyway");
subTitle = [_t("room|3pid_invite_error_invite_subtitle"), errCodeMessage];
primaryActionLabel = _t("room|3pid_invite_error_invite_action");
primaryActionHandler = this.props.onJoinClick;
break;
case "public":
subTitle = _t("You can still join here.");
primaryActionLabel = _t("Join the discussion");
subTitle = _t("room|3pid_invite_error_public_subtitle");
primaryActionLabel = _t("room|join_the_discussion");
primaryActionHandler = this.props.onJoinClick;
break;
default:
subTitle = errCodeMessage;
primaryActionLabel = _t("Try to join anyway");
primaryActionLabel = _t("room|3pid_invite_error_invite_action");
primaryActionHandler = this.props.onJoinClick;
break;
}
@ -465,56 +462,50 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
case MessageCase.InvitedEmailNotFoundInAccount: {
if (roomName) {
title = _t(
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account",
{
title = _t("room|3pid_invite_email_not_found_account_room", {
roomName,
email: this.props.invitedEmail,
},
);
});
} else {
title = _t("This invite was sent to %(email)s which is not associated with your account", {
title = _t("room|3pid_invite_email_not_found_account", {
email: this.props.invitedEmail,
});
}
subTitle = _t(
"Link this email with your account in Settings to receive invites directly in %(brand)s.",
{ brand },
);
primaryActionLabel = _t("Join the discussion");
subTitle = _t("room|link_email_to_receive_3pid_invite", { brand });
primaryActionLabel = _t("room|join_the_discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
case MessageCase.InvitedEmailNoIdentityServer: {
if (roomName) {
title = _t("This invite to %(roomName)s was sent to %(email)s", {
title = _t("room|invite_sent_to_email_room", {
roomName,
email: this.props.invitedEmail,
});
} else {
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
title = _t("room|invite_sent_to_email", { email: this.props.invitedEmail });
}
subTitle = _t("Use an identity server in Settings to receive invites directly in %(brand)s.", {
subTitle = _t("room|3pid_invite_no_is_subtitle", {
brand,
});
primaryActionLabel = _t("Join the discussion");
primaryActionLabel = _t("room|join_the_discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
case MessageCase.InvitedEmailMismatch: {
if (roomName) {
title = _t("This invite to %(roomName)s was sent to %(email)s", {
title = _t("room|invite_sent_to_email_room", {
roomName,
email: this.props.invitedEmail,
});
} else {
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
title = _t("room|invite_sent_to_email", { email: this.props.invitedEmail });
}
subTitle = _t("Share this email in Settings to receive invites directly in %(brand)s.", { brand });
primaryActionLabel = _t("Join the discussion");
subTitle = _t("room|invite_email_mismatch_suggestion", { brand });
primaryActionLabel = _t("room|join_the_discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
@ -536,14 +527,14 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const isDM = this.isDMInvite();
if (isDM) {
title = _t("Do you want to chat with %(user)s?", {
title = _t("room|dm_invite_title", {
user: inviteMember?.name ?? this.props.inviterName,
});
subTitle = [avatar, _t("<userName/> wants to chat", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("Start chatting");
subTitle = [avatar, _t("room|dm_invite_subtitle", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("room|dm_invite_action");
} else {
title = _t("Do you want to join %(roomName)s?", { roomName });
subTitle = [avatar, _t("<userName/> invited you", {}, { userName: () => inviterElement })];
title = _t("room|invite_title", { roomName });
subTitle = [avatar, _t("room|invite_subtitle", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("action|accept");
}
@ -567,7 +558,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
if (this.props.onRejectAndIgnoreClick) {
extraComponents.push(
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
{_t("Reject & Ignore user")}
{_t("room|invite_reject_ignore")}
</AccessibleButton>,
);
}
@ -575,35 +566,35 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
case MessageCase.ViewingRoom: {
if (this.props.canPreview) {
title = _t("You're previewing %(roomName)s. Want to join it?", { roomName });
title = _t("room|peek_join_prompt", { roomName });
} else if (roomName) {
title = _t("%(roomName)s can't be previewed. Do you want to join it?", { roomName });
title = _t("room|no_peek_join_prompt", { roomName });
} else {
title = _t("There's no preview, would you like to join?");
title = _t("room|no_peek_no_name_join_prompt");
}
primaryActionLabel = _t("Join the discussion");
primaryActionLabel = _t("room|join_the_discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
case MessageCase.RoomNotFound: {
if (roomName) {
title = _t("%(roomName)s does not exist.", { roomName });
title = _t("room|not_found_title_name", { roomName });
} else {
title = _t("This room or space does not exist.");
title = _t("room|not_found_title");
}
subTitle = _t("Are you sure you're at the right place?");
subTitle = _t("room|not_found_subtitle");
break;
}
case MessageCase.OtherError: {
if (roomName) {
title = _t("%(roomName)s is not accessible at this time.", { roomName });
title = _t("room|inaccessible_name", { roomName });
} else {
title = _t("This room or space is not accessible at this time.");
title = _t("room|inaccessible");
}
subTitle = [
_t("Try again later, or ask a room or space admin to check if you have access."),
_t("room|inaccessible_subtitle_1"),
_t(
"%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
"room|inaccessible_subtitle_2",
{ errcode: String(this.props.error?.errcode) },
{
issueLink: (label) => (
@ -622,18 +613,13 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
case MessageCase.PromptAskToJoin: {
if (roomName) {
title = _t("Ask to join %(roomName)s?", { roomName });
title = _t("room|knock_prompt_name", { roomName });
} else {
title = _t("Ask to join?");
title = _t("room|knock_prompt");
}
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
subTitle = [
avatar,
_t(
"You need to be granted access to this room in order to view or participate in the conversation. You can send a request to join below.",
),
];
subTitle = [avatar, _t("room|knock_subtitle")];
reasonElement = (
<Field
@ -641,7 +627,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
className="mx_RoomPreviewBar_fullWidth"
element="textarea"
onChange={this.onChangeReason}
placeholder={_t("Message (optional)")}
placeholder={_t("room|knock_message_field_placeholder")}
type="text"
value={this.state.reason ?? ""}
/>
@ -649,22 +635,22 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
primaryActionHandler = () =>
this.props.onSubmitAskToJoin && this.props.onSubmitAskToJoin(this.state.reason);
primaryActionLabel = _t("Request access");
primaryActionLabel = _t("room|knock_send_action");
break;
}
case MessageCase.Knocked: {
title = _t("Request to join sent");
title = _t("room|knock_sent");
subTitle = [
<>
<AskToJoinIcon className="mx_Icon mx_Icon_16 mx_RoomPreviewBar_icon" />
{_t("Your request to join is pending.")}
{_t("room|knock_sent_subtitle")}
</>,
];
secondaryActionHandler = this.props.onCancelAskToJoin;
secondaryActionLabel = _t("Cancel request");
secondaryActionLabel = _t("room|knock_cancel_action");
break;
}

View file

@ -173,14 +173,14 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
let notice: string | null = null;
if (cannotJoin) {
notice = _t("To view %(roomName)s, you need an invite", {
notice = _t("room|join_failed_needs_invite", {
roomName: room.name,
});
} else if (isVideoRoom && !videoRoomsEnabled) {
notice =
myMembership === "join"
? _t("To view, please enable video rooms in Labs first")
: _t("To join, please enable video rooms in Labs first");
? _t("room|view_failed_enable_video_rooms")
: _t("room|join_failed_enable_video_rooms");
joinButtons = (
<AccessibleButton kind="primary" onClick={viewLabs}>

View file

@ -559,7 +559,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}
private renderMenu(): ReactNode {
if (this.props.tagId === DefaultTagID.Suggested || this.props.tagId === DefaultTagID.SavedItems) return null; // not sortable
if (this.props.tagId === DefaultTagID.Suggested) return null; // not sortable
let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuPosition) {

View file

@ -342,7 +342,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
<ContextMenuTooltipButton
className="mx_RoomTile_menuButton"
onClick={this.onGeneralMenuOpenClick}
title={_t("Room options")}
title={_t("room|context_menu|title")}
isExpanded={!!this.state.generalMenuPosition}
/>
{this.state.generalMenuPosition && (

View file

@ -36,7 +36,7 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
active = false;
break;
case ConnectionState.Connecting:
text = _t("Joining…");
text = _t("room|joining");
active = true;
break;
case ConnectionState.Connected:

View file

@ -311,7 +311,11 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
</tr>
<tr>
<th scope="row">{_t("settings|security|key_backup_active_version")}</th>
<td>{this.state.activeBackupVersion === null ? _t("None") : this.state.activeBackupVersion}</td>
<td>
{this.state.activeBackupVersion === null
? _t("settings|security|key_backup_active_version_none")
: this.state.activeBackupVersion}
</td>
</tr>
</>
);

View file

@ -47,7 +47,7 @@ const REACHABILITY_TIMEOUT = 10000; // ms
async function checkIdentityServerUrl(u: string): Promise<string | null> {
const parsedUrl = parseUrl(u);
if (parsedUrl.protocol !== "https:") return _t("Identity server URL must be HTTPS");
if (parsedUrl.protocol !== "https:") return _t("identity_server|url_not_https");
// XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the
// js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it
@ -56,12 +56,12 @@ async function checkIdentityServerUrl(u: string): Promise<string | null> {
if (response.ok) {
return null;
} else if (response.status < 200 || response.status >= 300) {
return _t("Not a valid identity server (status code %(code)s)", { code: response.status });
return _t("identity_server|error_invalid", { code: response.status });
} else {
return _t("Could not connect to identity server");
return _t("identity_server|error_connection");
}
} catch (e) {
return _t("Could not connect to identity server");
return _t("identity_server|error_connection");
}
}
@ -133,7 +133,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
return (
<div>
<InlineSpinner />
{_t("Checking server")}
{_t("identity_server|checking")}
</div>
);
} else if (this.state.error) {
@ -191,9 +191,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
// 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
const [confirmed] = await this.showServerChangeWarning({
title: _t("Change identity server"),
title: _t("identity_server|change"),
unboundMessage: _t(
"Disconnect from the identity server <current /> and connect to <new /> instead?",
"identity_server|change_prompt",
{},
{
current: (sub) => <b>{abbreviateUrl(currentClientIdServer)}</b>,
@ -210,7 +210,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
}
} catch (e) {
logger.error(e);
errStr = _t("Terms of service not accepted or the identity server is invalid.");
errStr = _t("identity_server|error_invalid_or_terms");
}
}
this.setState({
@ -226,9 +226,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
title: _t("terms|identity_server_no_terms_title"),
description: (
<div>
<span className="warning">
{_t("The identity server you have chosen does not have any terms of service.")}
</span>
<span className="warning">{_t("identity_server|no_terms")}</span>
<span>&nbsp;{_t("terms|identity_server_no_terms_description_2")}</span>
</div>
),
@ -241,9 +239,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
this.setState({ disconnectBusy: true });
try {
const [confirmed] = await this.showServerChangeWarning({
title: _t("Disconnect identity server"),
title: _t("identity_server|disconnect"),
unboundMessage: _t(
"Disconnect from the identity server <idserver />?",
"identity_server|disconnect_server",
{},
{ idserver: (sub) => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b> },
),
@ -294,54 +292,34 @@ export default class SetIdServer extends React.Component<IProps, IState> {
if (!currentServerReachable) {
message = (
<div>
<p>
{_t(
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.",
{},
messageElements,
)}
</p>
<p>{_t("You should:")}</p>
<p>{_t("identity_server|disconnect_offline_warning", {}, messageElements)}</p>
<p>{_t("identity_server|suggestions")}</p>
<ul>
<li>{_t("identity_server|suggestions_1")}</li>
<li>
{_t(
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)",
)}
</li>
<li>
{_t(
"contact the administrators of identity server <idserver />",
"identity_server|suggestions_2",
{},
{
idserver: messageElements.idserver,
},
)}
</li>
<li>{_t("wait and try again later")}</li>
<li>{_t("identity_server|suggestions_3")}</li>
</ul>
</div>
);
danger = true;
button = _t("Disconnect anyway");
button = _t("identity_server|disconnect_anyway");
} else if (boundThreepids.length) {
message = (
<div>
<p>
{_t(
"You are still <b>sharing your personal data</b> on the identity server <idserver />.",
{},
messageElements,
)}
</p>
<p>
{_t(
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.",
)}
</p>
<p>{_t("identity_server|disconnect_personal_data_warning_1", {}, messageElements)}</p>
<p>{_t("identity_server|disconnect_personal_data_warning_2")}</p>
</div>
);
danger = true;
button = _t("Disconnect anyway");
button = _t("identity_server|disconnect_anyway");
} else {
message = unboundMessage;
}
@ -382,37 +360,31 @@ export default class SetIdServer extends React.Component<IProps, IState> {
let sectionTitle;
let bodyText;
if (idServerUrl) {
sectionTitle = _t("Identity server (%(server)s)", { server: abbreviateUrl(idServerUrl) });
sectionTitle = _t("identity_server|url", { server: abbreviateUrl(idServerUrl) });
bodyText = _t(
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.",
"identity_server|description_connected",
{},
{ server: (sub) => <b>{abbreviateUrl(idServerUrl)}</b> },
);
if (this.props.missingTerms) {
bodyText = _t(
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.",
"identity_server|change_server_prompt",
{},
{ server: (sub) => <b>{abbreviateUrl(idServerUrl)}</b> },
);
}
} else {
sectionTitle = _t("common|identity_server");
bodyText = _t(
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.",
);
bodyText = _t("identity_server|description_disconnected");
}
let discoSection;
if (idServerUrl) {
let discoButtonContent: React.ReactNode = _t("action|disconnect");
let discoBodyText = _t(
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.",
);
let discoBodyText = _t("identity_server|disconnect_warning");
if (this.props.missingTerms) {
discoBodyText = _t(
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.",
);
discoButtonContent = _t("Do not use an identity server");
discoBodyText = _t("identity_server|description_optional");
discoButtonContent = _t("identity_server|do_not_use");
}
if (this.state.disconnectBusy) {
discoButtonContent = <InlineSpinner />;
@ -431,7 +403,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
<SettingsFieldset legend={sectionTitle} description={bodyText}>
<form className="mx_SetIdServer" onSubmit={this.checkIdServer}>
<Field
label={_t("Enter a new identity server")}
label={_t("identity_server|url_field_label")}
type="text"
autoComplete="off"
placeholder={this.state.defaultIdServer}

View file

@ -63,12 +63,12 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
if (currentManager) {
managerName = `(${currentManager.name})`;
bodyText = _t(
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.",
"integration_manager|use_im_default",
{ serverName: currentManager.name },
{ b: (sub) => <b>{sub}</b> },
);
} else {
bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
bodyText = _t("integration_manager|use_im");
}
return (
@ -79,7 +79,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
>
<div className="mx_SettingsFlag">
<div className="mx_SetIntegrationManager_heading_manager">
<Heading size="2">{_t("Manage integrations")}</Heading>
<Heading size="2">{_t("integration_manager|manage_title")}</Heading>
<Heading size="3">{managerName}</Heading>
</div>
<ToggleSwitch
@ -90,11 +90,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
/>
</div>
<SettingsSubsectionText>{bodyText}</SettingsSubsectionText>
<SettingsSubsectionText>
{_t(
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
)}
</SettingsSubsectionText>
<SettingsSubsectionText>{_t("integration_manager|explainer")}</SettingsSubsectionText>
</label>
);
}

View file

@ -88,7 +88,7 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
.catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
title: _t("settings|general|error_remove_3pid"),
description: err && err.message ? err.message : _t("invite|failed_generic"),
});
});
@ -99,7 +99,7 @@ export class ExistingEmailAddress extends React.Component<IExistingEmailAddressP
return (
<div className="mx_GeneralUserSettingsTab_section--discovery_existing">
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_promptText">
{_t("Remove %(email)s?", { email: this.props.email.address })}
{_t("settings|general|remove_email_prompt", { email: this.props.email.address })}
</span>
<AccessibleButton
onClick={this.onActuallyRemove}
@ -182,8 +182,8 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
// TODO: Inline field validation
if (!Email.looksValid(email)) {
Modal.createDialog(ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
title: _t("settings|general|error_invalid_email"),
description: _t("settings|general|error_invalid_email_detail"),
});
return;
}
@ -199,7 +199,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
logger.error("Unable to add email address " + email + " " + err);
this.setState({ verifying: false, continueDisabled: false, addTask: null });
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
title: _t("settings|general|error_add_email"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
});
@ -239,14 +239,12 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t(
"Click the link in the email you received to verify and then click continue again.",
),
title: _t("settings|general|email_not_verified"),
description: _t("settings|general|email_verification_instructions"),
});
} else {
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
title: _t("settings|general|error_email_verification"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
}
@ -273,11 +271,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
if (this.state.verifying) {
addButton = (
<div>
<div>
{_t(
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
)}
</div>
<div>{_t("settings|general|add_email_instructions")}</div>
<AccessibleButton
onClick={this.onContinueClick}
kind="primary"
@ -295,7 +289,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
<form onSubmit={this.onAddClick} autoComplete="off" noValidate={true}>
<Field
type="text"
label={_t("Email Address")}
label={_t("settings|general|email_address_label")}
autoComplete="email"
disabled={this.props.disabled || this.state.verifying}
value={this.state.newEmailAddress}

View file

@ -84,7 +84,7 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
.catch((err) => {
logger.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to remove contact information"),
title: _t("settings|general|error_remove_3pid"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
});
@ -95,7 +95,7 @@ export class ExistingPhoneNumber extends React.Component<IExistingPhoneNumberPro
return (
<div className="mx_GeneralUserSettingsTab_section--discovery_existing">
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_promptText">
{_t("Remove %(phone)s?", { phone: this.props.msisdn.address })}
{_t("settings|general|remove_msisdn_prompt", { phone: this.props.msisdn.address })}
</span>
<AccessibleButton
onClick={this.onActuallyRemove}
@ -244,11 +244,11 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
if (underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify phone number."),
title: _t("settings|general|error_msisdn_verification"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
} else {
this.setState({ verifyError: _t("Incorrect verification code") });
this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") });
}
});
};
@ -279,17 +279,14 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
addVerifySection = (
<div>
<div>
{_t(
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
{ msisdn: msisdn },
)}
{_t("settings|general|add_msisdn_instructions", { msisdn: msisdn })}
<br />
{this.state.verifyError}
</div>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
type="text"
label={_t("Verification code")}
label={_t("settings|general|msisdn_verification_field_label")}
autoComplete="off"
disabled={this.props.disabled || this.state.continueDisabled}
value={this.state.newPhoneNumberCode}
@ -329,7 +326,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
<div className="mx_PhoneNumbers_input">
<Field
type="text"
label={_t("Phone Number")}
label={_t("settings|general|msisdn_label")}
autoComplete="tel-national"
disabled={this.props.disabled || this.state.verifying}
prefixComponent={phoneCountry}

View file

@ -49,7 +49,7 @@ const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({ devic
await saveDeviceName(deviceName);
stopEditing();
} catch (error) {
setError(_t("Failed to set display name"));
setError(_t("settings|sessions|error_set_name"));
setIsLoading(false);
}
};

View file

@ -20,6 +20,7 @@ import {
IServerVersions,
UNSTABLE_MSC3882_CAPABILITY,
Capabilities,
IClientWellKnown,
} from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler";
@ -30,6 +31,7 @@ interface IProps {
onShowQr: () => void;
versions?: IServerVersions;
capabilities?: Capabilities;
wellKnown?: IClientWellKnown;
}
export default class LoginWithQRSection extends React.Component<IProps> {
@ -43,7 +45,9 @@ export default class LoginWithQRSection extends React.Component<IProps> {
const capability = UNSTABLE_MSC3882_CAPABILITY.findIn<IMSC3882GetLoginTokenCapability>(this.props.capabilities);
const msc3882Supported =
!!this.props.versions?.unstable_features?.["org.matrix.msc3882"] || !!capability?.enabled;
const msc3886Supported = !!this.props.versions?.unstable_features?.["org.matrix.msc3886"];
const msc3886Supported =
!!this.props.versions?.unstable_features?.["org.matrix.msc3886"] ||
this.props.wellKnown?.["io.element.rendezvous"]?.server;
const offerShowQr = msc3882Supported && msc3886Supported;
// don't show anything if no method is available

View file

@ -71,7 +71,7 @@ export const deleteDevicesWithInteractiveAuth = async (
},
};
Modal.createDialog(InteractiveAuthDialog, {
title: _t("Authentication"),
title: _t("common|authentication"),
matrixClient: matrixClient,
authData: error.data as IAuthData,
onFinished,

View file

@ -194,8 +194,8 @@ export const useOwnDevices = (): DevicesState => {
await matrixClient.setDeviceDetails(deviceId, { display_name: deviceName });
await refreshDevices();
} catch (error) {
logger.error("Error setting session display name", error);
throw new Error(_t("Failed to set display name"));
logger.error("Error setting device name", error);
throw new Error(_t("settings|sessions|error_set_name"));
}
},
[matrixClient, devices, refreshDevices],
@ -217,7 +217,7 @@ export const useOwnDevices = (): DevicesState => {
}
} catch (error) {
logger.error("Error setting pusher state", error);
throw new Error(_t("Failed to set pusher state"));
throw new Error(_t("settings|sessions|error_pusher_state"));
} finally {
await refreshDevices();
}

View file

@ -116,7 +116,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
this.changeBinding({
bind: false,
label: "revoke",
errorTitle: _t("Unable to revoke sharing for email address"),
errorTitle: _t("settings|general|error_revoke_email_discovery"),
});
};
@ -126,7 +126,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
this.changeBinding({
bind: true,
label: "share",
errorTitle: _t("Unable to share email address"),
errorTitle: _t("settings|general|error_share_email_discovery"),
});
};
@ -152,15 +152,13 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
if (underlyingError instanceof MatrixError && underlyingError.errcode === "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Your email address hasn't been verified yet"),
description: _t(
"Click the link in the email you received to verify and then click continue again.",
),
title: _t("settings|general|email_not_verified"),
description: _t("settings|general|email_verification_instructions"),
});
} else {
logger.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
title: _t("settings|general|error_email_verification"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
}
@ -178,7 +176,7 @@ export class EmailAddress extends React.Component<IEmailAddressProps, IEmailAddr
if (verifying) {
status = (
<span>
{_t("Verify the link in your inbox")}
{_t("settings|general|discovery_email_verification_instructions")}
<AccessibleButton
className="mx_GeneralUserSettingsTab_section--discovery_existing_button"
kind="primary_sm"
@ -242,10 +240,8 @@ export default class EmailAddresses extends React.Component<IProps> {
return (
<SettingsSubsection
heading={_t("Email addresses")}
description={
(!hasEmails && _t("Discovery options will appear once you have added an email above.")) || undefined
}
heading={_t("settings|general|emails_heading")}
description={(!hasEmails && _t("settings|general|discovery_email_empty")) || undefined}
stretchContent
>
{content}

View file

@ -117,7 +117,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
this.changeBinding({
bind: false,
label: "revoke",
errorTitle: _t("Unable to revoke sharing for phone number"),
errorTitle: _t("settings|general|error_revoke_msisdn_discovery"),
});
};
@ -127,7 +127,7 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
this.changeBinding({
bind: true,
label: "share",
errorTitle: _t("Unable to share phone number"),
errorTitle: _t("settings|general|error_share_msisdn_discovery"),
});
};
@ -163,11 +163,11 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
this.setState({ continueDisabled: false });
if (underlyingError instanceof MatrixError && underlyingError.errcode !== "M_THREEPID_AUTH_FAILED") {
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify phone number."),
title: _t("settings|general|error_msisdn_verification"),
description: extractErrorMessageFromError(err, _t("invite|failed_generic")),
});
} else {
this.setState({ verifyError: _t("Incorrect verification code") });
this.setState({ verifyError: _t("settings|general|incorrect_msisdn_verification") });
}
}
};
@ -181,14 +181,14 @@ export class PhoneNumber extends React.Component<IPhoneNumberProps, IPhoneNumber
status = (
<span className="mx_GeneralUserSettingsTab_section--discovery_existing_verification">
<span>
{_t("Please enter verification code sent via text.")}
{_t("settings|general|msisdn_verification_instructions")}
<br />
{this.state.verifyError}
</span>
<form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
<Field
type="text"
label={_t("Verification code")}
label={_t("settings|general|msisdn_verification_field_label")}
autoComplete="off"
disabled={this.state.continueDisabled}
value={this.state.verificationCode}
@ -247,13 +247,12 @@ export default class PhoneNumbers extends React.Component<IProps> {
});
}
const description =
(!content && _t("Discovery options will appear once you have added a phone number above.")) || undefined;
const description = (!content && _t("settings|general|discovery_msisdn_empty")) || undefined;
return (
<SettingsSubsection
data-testid="mx_DiscoveryPhoneNumbers"
heading={_t("Phone numbers")}
heading={_t("settings|general|msisdns_heading")}
description={description}
stretchContent
>

View file

@ -51,7 +51,7 @@ export function NotificationPusherSettings(): JSX.Element {
() => ({
kind: "email",
app_id: "m.email",
app_display_name: _t("Email Notifications"),
app_display_name: _t("notifications|email_pusher_app_display_name"),
lang: navigator.language,
data: {
brand: SdkConfig.get().brand,
@ -91,17 +91,16 @@ export function NotificationPusherSettings(): JSX.Element {
return (
<>
<SettingsSubsection className="mx_NotificationPusherSettings" heading={_t("Email summary")}>
<SettingsSubsection
className="mx_NotificationPusherSettings"
heading={_t("settings|notifications|email_section")}
>
<SettingsSubsectionText className="mx_NotificationPusherSettings_description">
{_t("Receive an email summary of missed notifications")}
{_t("settings|notifications|email_description")}
</SettingsSubsectionText>
<div className="mx_SettingsSubsection_description mx_NotificationPusherSettings_detail">
<SettingsSubsectionText>
{_t(
"Select which emails you want to send summaries to. Manage your emails in <button>General</button>.",
{},
{ button: generalTabButton },
)}
{_t("settings|notifications|email_select", {}, { button: generalTabButton })}
</SettingsSubsectionText>
</div>
<SettingsIndent>

View file

@ -58,21 +58,6 @@ function toDefaultLevels(levels: NotificationSettings["defaultLevels"]): Notific
}
}
const NotificationOptions = [
{
value: NotificationDefaultLevels.AllMessages,
label: _t("All messages"),
},
{
value: NotificationDefaultLevels.PeopleMentionsKeywords,
label: _t("People, Mentions and Keywords"),
},
{
value: NotificationDefaultLevels.MentionsKeywords,
label: _t("Mentions and Keywords only"),
},
];
function boldText(text: string): JSX.Element {
return <strong>{text}</strong>;
}
@ -101,6 +86,21 @@ export default function NotificationSettings2(): JSX.Element {
const [updatingUnread, setUpdatingUnread] = useState<boolean>(false);
const hasUnreadNotifications = useHasUnreadNotifications();
const NotificationOptions = [
{
value: NotificationDefaultLevels.AllMessages,
label: _t("notifications|all_messages"),
},
{
value: NotificationDefaultLevels.PeopleMentionsKeywords,
label: _t("settings|notifications|people_mentions_keywords"),
},
{
value: NotificationDefaultLevels.MentionsKeywords,
label: _t("settings|notifications|mentions_keywords_only"),
},
];
return (
<div className="mx_NotificationSettings2">
{hasPendingChanges && model !== null && (
@ -110,7 +110,7 @@ export default function NotificationSettings2(): JSX.Element {
onAction={() => reconcile(model!)}
>
{_t(
"<strong>Update:</strong>Weve simplified Notifications Settings to make options easier to find. Some custom settings youve chosen in the past are not shown here, but theyre still active. If you proceed, some of your settings may change. <a>Learn more</a>",
"settings|notifications|labs_notice_prompt",
{},
{
strong: boldText,
@ -140,7 +140,7 @@ export default function NotificationSettings2(): JSX.Element {
}
/>
<LabelledToggleSwitch
label={_t("Show message preview in desktop notification")}
label={_t("settings|notifications|desktop_notification_message_preview")}
value={desktopShowBody}
onChange={(value) =>
SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, value)
@ -155,8 +155,8 @@ export default function NotificationSettings2(): JSX.Element {
/>
</div>
<SettingsSubsection
heading={_t("I want to be notified for (Default Setting)")}
description={_t("This setting will be applied by default to all your rooms.")}
heading={_t("settings|notifications|default_setting_section")}
description={_t("settings|notifications|default_setting_description")}
>
<StyledRadioGroup
name="defaultNotificationLevel"
@ -182,8 +182,8 @@ export default function NotificationSettings2(): JSX.Element {
/>
</SettingsSubsection>
<SettingsSubsection
heading={_t("Play a sound for")}
description={_t("Applied by default to all rooms on all devices.")}
heading={_t("settings|notifications|play_sound_for_section")}
description={_t("settings|notifications|play_sound_for_description")}
>
<LabelledCheckbox
label="People"
@ -200,7 +200,7 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("Mentions and Keywords")}
label={_t("settings|notifications|mentions_keywords")}
value={settings.sound.mentions !== undefined}
disabled={disabled}
onChange={(value) => {
@ -214,7 +214,7 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("Audio and Video calls")}
label={_t("settings|notifications|voip")}
value={settings.sound.calls !== undefined}
disabled={disabled}
onChange={(value) => {
@ -228,9 +228,9 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("Other things we think you might be interested in:")}>
<SettingsSubsection heading={_t("settings|notifications|other_section")}>
<LabelledCheckbox
label={_t("Invited to a room")}
label={_t("settings|notifications|invites")}
value={settings.activity.invite}
disabled={disabled}
onChange={(value) => {
@ -244,7 +244,7 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("New room activity, upgrades and status messages occur")}
label={_t("settings|notifications|room_activity")}
value={settings.activity.status_event}
disabled={disabled}
onChange={(value) => {
@ -258,7 +258,7 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("Messages sent by bots")}
label={_t("settings|notifications|notices")}
value={settings.activity.bot_notices}
disabled={disabled}
onChange={(value) => {
@ -273,9 +273,9 @@ export default function NotificationSettings2(): JSX.Element {
/>
</SettingsSubsection>
<SettingsSubsection
heading={_t("Mentions and Keywords")}
heading={_t("settings|notifications|mentions_keywords")}
description={_t(
"Show a badge <badge/> when keywords are used in a room.",
"settings|notifications|keywords",
{},
{
badge: <StatelessNotificationBadge symbol="1" count={1} color={NotificationColor.Grey} />,
@ -283,7 +283,7 @@ export default function NotificationSettings2(): JSX.Element {
)}
>
<LabelledCheckbox
label={_t("Notify when someone mentions using @room")}
label={_t("settings|notifications|notify_at_room")}
value={settings.mentions.room}
disabled={disabled}
onChange={(value) => {
@ -297,7 +297,7 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("Notify when someone mentions using @displayname or %(mxid)s", {
label={_t("settings|notifications|notify_mention", {
mxid: cli.getUserId()!,
})}
value={settings.mentions.user}
@ -313,8 +313,8 @@ export default function NotificationSettings2(): JSX.Element {
}}
/>
<LabelledCheckbox
label={_t("Notify when someone uses a keyword")}
byline={_t("Enter keywords here, or use for spelling variations or nicknames")}
label={_t("settings|notifications|notify_keyword")}
byline={_t("settings|notifications|keywords_prompt")}
value={settings.mentions.keywords}
disabled={disabled}
onChange={(value) => {
@ -348,7 +348,7 @@ export default function NotificationSettings2(): JSX.Element {
/>
</SettingsSubsection>
<NotificationPusherSettings />
<SettingsSubsection heading={_t("Quick Actions")}>
<SettingsSubsection heading={_t("settings|notifications|quick_actions_section")}>
{hasUnreadNotifications && (
<AccessibleButton
kind="primary_outline"
@ -359,7 +359,7 @@ export default function NotificationSettings2(): JSX.Element {
setUpdatingUnread(false);
}}
>
{_t("Mark all messages as read")}
{_t("settings|notifications|quick_actions_mark_all_read")}
</AccessibleButton>
)}
<AccessibleButton
@ -369,7 +369,7 @@ export default function NotificationSettings2(): JSX.Element {
reconcile(DefaultNotificationSettings);
}}
>
{_t("Reset to default settings")}
{_t("settings|notifications|quick_actions_reset")}
</AccessibleButton>
</SettingsSubsection>
</SettingsSection>

View file

@ -156,7 +156,13 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
return (
<SettingsTab>
<SettingsSection heading={_t("common|advanced")}>
<SettingsSubsection heading={room.isSpaceRoom() ? _t("Space information") : _t("Room information")}>
<SettingsSubsection
heading={
room.isSpaceRoom()
? _t("room_settings|advanced|information_section_space")
: _t("room_settings|advanced|information_section_room")
}
>
<div>
<span>{_t("room_settings|advanced|room_id")}</span>
<CopyableText getTextToCopy={() => this.props.room.roomId}>

View file

@ -63,7 +63,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
<div>
<p>
{_t(
"This room is bridging messages to the following platforms. <a>Learn more.</a>",
"room_settings|bridges|description",
{},
{
// TODO: We don't have this link yet: this will prevent the translators
@ -85,7 +85,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
content = (
<p>
{_t(
"This room isn't bridging messages to any platforms. <a>Learn more.</a>",
"room_settings|bridges|empty",
{},
{
// TODO: We don't have this link yet: this will prevent the translators
@ -103,7 +103,7 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
return (
<SettingsTab>
<SettingsSection heading={_t("Bridges")}>{content}</SettingsSection>
<SettingsSection heading={_t("room_settings|bridges|title")}>{content}</SettingsSection>
</SettingsTab>
);
}

View file

@ -89,7 +89,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
<RoomProfileSettings roomId={room.roomId} />
</SettingsSection>
<SettingsSection heading={_t("Room Addresses")}>
<SettingsSection heading={_t("room_settings|general|aliases_section")}>
<AliasSettings
roomId={room.roomId}
canSetCanonicalAlias={canSetCanonical}
@ -98,7 +98,7 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
/>
</SettingsSection>
<SettingsSection heading={_t("Other")}>
<SettingsSection heading={_t("room_settings|general|other_section")}>
{urlPreviewSettings}
{leaveSection}
</SettingsSection>

View file

@ -163,7 +163,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
currentUploadedFile = (
<div>
<span>
{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code>
{_t("room_settings|notifications|uploaded_sound")}: <code>{this.state.uploadedFile.name}</code>
</span>
</div>
);
@ -181,10 +181,10 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
className: "mx_NotificationSettingsTab_defaultEntry",
label: (
<>
{_t("Default")}
{_t("notifications|default")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t(
"Get notifications as set up in your <a>settings</a>",
"room_settings|notifications|settings_link",
{},
{
a: (sub) => (
@ -206,9 +206,9 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
className: "mx_NotificationSettingsTab_allMessagesEntry",
label: (
<>
{_t("All messages")}
{_t("notifications|all_messages")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t("Get notified for every message")}
{_t("notifications|all_messages_description")}
</div>
</>
),
@ -218,10 +218,10 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
className: "mx_NotificationSettingsTab_mentionsKeywordsEntry",
label: (
<>
{_t("@mentions & keywords")}
{_t("notifications|mentions_and_keywords")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t(
"Get notified only with mentions and keywords as set up in your <a>settings</a>",
"notifications|mentions_and_keywords_description",
{},
{
a: (sub) => (
@ -245,7 +245,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
<>
{_t("common|off")}
<div className="mx_NotificationSettingsTab_microCopy">
{_t("You won't get any notifications")}
{_t("notifications|mute_description")}
</div>
</>
),
@ -256,11 +256,12 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
/>
</div>
<SettingsSubsection heading={_t("Sounds")}>
<SettingsSubsection heading={_t("room_settings|notifications|sounds_section")}>
<div>
<div className="mx_SettingsTab_subsectionText">
<span>
{_t("Notification sound")}: <code>{this.state.currentSound}</code>
{_t("room_settings|notifications|notification_sound")}:{" "}
<code>{this.state.currentSound}</code>
</span>
</div>
<AccessibleButton
@ -273,7 +274,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
</AccessibleButton>
</div>
<div>
<h4 className="mx_Heading_h4">{_t("Set a new custom sound")}</h4>
<h4 className="mx_Heading_h4">{_t("room_settings|notifications|custom_sound_prompt")}</h4>
<div className="mx_SettingsFlag">
<form autoComplete="off" noValidate={true}>
<input
@ -283,7 +284,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
onClick={chromeFileInputFix}
onChange={this.onSoundUploadChanged}
accept="audio/*"
aria-label={_t("Upload custom sound")}
aria-label={_t("room_settings|notifications|upload_sound_label")}
/>
</form>
@ -295,7 +296,7 @@ export default class NotificationsSettingsTab extends React.Component<IProps, IS
onClick={this.triggerUploader}
kind="primary"
>
{_t("Browse")}
{_t("room_settings|notifications|browse_button")}
</AccessibleButton>
<AccessibleButton

View file

@ -52,7 +52,7 @@ const SeeMoreOrLess: VFC<{ roomMember: RoomMember }> = ({ roomMember }) => {
</p>
{shouldTruncate && (
<AccessibleButton kind="link" onClick={() => setSeeMore(!seeMore)}>
{seeMore ? _t("See less") : _t("See more")}
{seeMore ? _t("room_settings|people|see_less") : _t("room_settings|people|see_more")}
</AccessibleButton>
)}
</>
@ -151,7 +151,7 @@ export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => {
return (
<SettingsTab>
<SettingsSection heading={_t("common|people")}>
<SettingsFieldset legend={_t("Asking to join")}>
<SettingsFieldset legend={_t("room_settings|people|knock_section")}>
{knockMembers.length ? (
knockMembers.map((knockMember) => (
<Knock
@ -164,7 +164,7 @@ export const PeopleRoomSettingsTab: VFC<{ room: Room }> = ({ room }) => {
/>
))
) : (
<p className="mx_PeopleRoomSettingsTab_paragraph">{_t("No requests")}</p>
<p className="mx_PeopleRoomSettingsTab_paragraph">{_t("room_settings|people|knock_empty")}</p>
)}
</SettingsFieldset>
</SettingsSection>

View file

@ -95,7 +95,7 @@ export class BannedUser extends React.Component<IBannedUserProps> {
logger.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("common|error"),
description: _t("Failed to unban"),
description: _t("room_settings|permissions|error_unbanning"),
});
});
};
@ -119,9 +119,11 @@ export class BannedUser extends React.Component<IBannedUserProps> {
return (
<li>
{unbanButton}
<span title={_t("Banned by %(displayName)s", { displayName: this.props.by })}>
<span title={_t("room_settings|permissions|banned_by", { displayName: this.props.by })}>
<strong>{this.props.member.name}</strong> {userId}
{this.props.reason ? " " + _t("Reason") + ": " + this.props.reason : ""}
{this.props.reason
? " " + _t("room_settings|permissions|ban_reason") + ": " + this.props.reason
: ""}
</span>
</li>
);
@ -205,10 +207,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error changing power level requirement"),
description: _t(
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
),
title: _t("room_settings|permissions|error_changing_pl_reqs_title"),
description: _t("room_settings|permissions|error_changing_pl_reqs_description"),
});
});
};
@ -230,10 +230,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
logger.error(e);
Modal.createDialog(ErrorDialog, {
title: _t("Error changing power level"),
description: _t(
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.",
),
title: _t("room_settings|permissions|error_changing_pl_title"),
description: _t("room_settings|permissions|error_changing_pl_description"),
});
});
};

View file

@ -301,8 +301,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
private onJoinRuleChangeError = (error: Error): void => {
Modal.createDialog(ErrorDialog, {
title: _t("Failed to update the join rules"),
description: error.message ?? _t("Unknown failure"),
title: _t("room_settings|security|error_join_rule_change_title"),
description: error.message ?? _t("room_settings|security|error_join_rule_change_unknown"),
});
};

View file

@ -81,14 +81,14 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
return (
<LabelledToggleSwitch
data-testid="element-call-switch"
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
caption={_t("%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", {
label={_t("room_settings|voip|enable_element_call_label", { brand })}
caption={_t("room_settings|voip|enable_element_call_caption", {
brand,
})}
value={elementCallEnabled}
onChange={onChange}
disabled={!maySend}
tooltip={_t("You do not have sufficient permissions to change this.")}
tooltip={_t("room_settings|voip|enable_element_call_no_permissions_tooltip")}
/>
);
};
@ -100,8 +100,8 @@ interface Props {
export const VoipRoomSettingsTab: React.FC<Props> = ({ room }) => {
return (
<SettingsTab>
<SettingsSection heading={_t("Voice & Video")}>
<SettingsSubsection heading={_t("Call type")}>
<SettingsSection heading={_t("settings|voip|title")}>
<SettingsSubsection heading={_t("room_settings|voip|call_type_section")}>
<ElementCallSwitch room={room} />
</SettingsSubsection>
</SettingsSection>

View file

@ -282,16 +282,16 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const errorMessage = extractErrorMessageFromError(
err,
_t("Unknown password change error (%(stringifiedError)s)", {
_t("settings|general|error_password_change_unknown", {
stringifiedError: String(err),
}),
);
let errorMessageToDisplay = errorMessage;
if (underlyingError instanceof HTTPError && underlyingError.httpStatus === 403) {
errorMessageToDisplay = _t("Failed to change password. Is your password correct?");
errorMessageToDisplay = _t("settings|general|error_password_change_403");
} else if (underlyingError instanceof HTTPError) {
errorMessageToDisplay = _t("%(errorMessage)s (HTTP status %(httpStatus)s)", {
errorMessageToDisplay = _t("settings|general|error_password_change_http", {
errorMessage,
httpStatus: underlyingError.httpStatus,
});
@ -299,13 +299,13 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
// TODO: Figure out a design that doesn't involve replacing the current dialog
Modal.createDialog(ErrorDialog, {
title: _t("Error changing password"),
title: _t("settings|general|error_password_change_title"),
description: errorMessageToDisplay,
});
};
private onPasswordChanged = (): void => {
const description = _t("Your password was successfully changed.");
const description = _t("settings|general|password_change_success");
// TODO: Figure out a design that doesn't involve replacing the current dialog
Modal.createDialog(ErrorDialog, {
title: _t("common|success"),
@ -346,7 +346,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
threepidSection = (
<>
<SettingsSubsection
heading={_t("Email addresses")}
heading={_t("settings|general|emails_heading")}
stretchContent
data-testid="mx_AccountEmailAddresses"
>
@ -354,7 +354,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
</SettingsSubsection>
<SettingsSubsection
heading={_t("Phone numbers")}
heading={_t("settings|general|msisdns_heading")}
stretchContent
data-testid="mx_AccountPhoneNumbers"
>
@ -368,7 +368,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
if (this.state.canChangePassword) {
passwordChangeSection = (
<>
<SettingsSubsectionText>{_t("Set a new account password…")}</SettingsSubsectionText>
<SettingsSubsectionText>{_t("settings|general|password_change_section")}</SettingsSubsectionText>
<ChangePassword
className="mx_GeneralUserSettingsTab_section--account_changePassword"
rowClassName=""
@ -388,7 +388,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
<>
<SettingsSubsectionText data-testid="external-account-management-outer">
{_t(
"Your account details are managed separately at <code>%(hostname)s</code>.",
"settings|general|external_account_management",
{ hostname },
{ code: (sub) => <code>{sub}</code> },
)}
@ -457,10 +457,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
if (this.state.requiredPolicyInfo.hasTerms) {
const intro = (
<SettingsSubsectionText>
{_t(
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
{ serverName: this.state.idServerName },
)}
{_t("settings|general|discovery_needs_terms", { serverName: this.state.idServerName })}
</SettingsSubsectionText>
);
return (
@ -504,14 +501,14 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
private renderManagementSection(): JSX.Element {
// TODO: Improve warning text for account deactivation
return (
<SettingsSection heading={_t("Deactivate account")}>
<SettingsSection heading={_t("settings|general|deactivate_section")}>
<SettingsSubsection
heading={_t("Account management")}
heading={_t("settings|general|account_management_section")}
data-testid="account-management-section"
description={_t("Deactivating your account is a permanent action — be careful!")}
description={_t("settings|general|deactivate_warning")}
>
<AccessibleButton onClick={this.onDeactivateClicked} kind="danger">
{_t("Deactivate Account")}
{_t("settings|general|deactivate_section")}
</AccessibleButton>
</SettingsSubsection>
</SettingsSection>
@ -549,7 +546,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
const heading = (
<Heading size="2">
{discoWarning}
{_t("Discovery")}
{_t("settings|general|discovery_section")}
</Heading>
);
discoverySection = (

View file

@ -143,7 +143,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
const name = room ? room.name : list.roomId;
const renderRules = (rules: ListRule[]): JSX.Element => {
if (rules.length === 0) return <i>{_t("None")}</i>;
if (rules.length === 0) return <i>{_t("labs_mjolnir|rules_empty")}</i>;
const tiles: JSX.Element[] = [];
for (const rule of rules) {

View file

@ -63,7 +63,7 @@ export class IgnoredUser extends React.Component<IIgnoredUserProps> {
aria-describedby={id}
disabled={this.props.inProgress}
>
{_t("Unignore")}
{_t("action|unignore")}
</AccessibleButton>
<span id={id}>{this.props.userId}</span>
</div>
@ -225,7 +225,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
const { waitingUnignored, ignoredUserIds } = this.state;
const userIds = !ignoredUserIds?.length
? _t("You have no ignored users.")
? _t("settings|security|ignore_users_empty")
: ignoredUserIds.map((u) => {
return (
<IgnoredUser
@ -238,7 +238,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
});
return (
<SettingsSubsection heading={_t("Ignored users")}>
<SettingsSubsection heading={_t("settings|security|ignore_users_section")}>
<SettingsSubsectionText>{userIds}</SettingsSubsectionText>
</SettingsSubsection>
);
@ -301,9 +301,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
if (!privateShouldBeEncrypted(MatrixClientPeg.safeGet())) {
warning = (
<div className="mx_SecurityUserSettingsTab_warning">
{_t(
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.",
)}
{_t("settings|security|e2ee_default_disabled_warning")}
</div>
);
}
@ -320,9 +318,7 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
<SettingsSection heading={_t("common|privacy")}>
<SettingsSubsection
heading={_t("common|analytics")}
description={_t(
"Share anonymous data to help us identify issues. Nothing personal. No third parties.",
)}
description={_t("settings|security|analytics_description")}
>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("action|learn_more")}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
@ -180,6 +180,7 @@ const SessionManagerTab: React.FC = () => {
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
const capabilities = useAsyncMemo(async () => matrixClient?.getCapabilities(), [matrixClient]);
const wellKnown = useMemo(() => matrixClient?.getClientWellKnown(), [matrixClient]);
const onDeviceExpandToggle = (deviceId: ExtendedDevice["device_id"]): void => {
if (expandedDeviceIds.includes(deviceId)) {
@ -302,9 +303,7 @@ const SessionManagerTab: React.FC = () => {
disabled={!!signingOutDeviceIds.length}
/>
}
description={_t(
"For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.",
)}
description={_t("settings|sessions|best_security_note")}
data-testid="other-sessions-section"
stretchContent
>
@ -331,7 +330,12 @@ const SessionManagerTab: React.FC = () => {
/>
</SettingsSubsection>
)}
<LoginWithQRSection onShowQr={onShowQrClicked} versions={clientVersions} capabilities={capabilities} />
<LoginWithQRSection
onShowQr={onShowQrClicked}
versions={clientVersions}
capabilities={capabilities}
wellKnown={wellKnown}
/>
</SettingsSection>
</SettingsTab>
);

View file

@ -67,9 +67,7 @@ const SidebarUserSettingsTab: React.FC = () => {
<SettingsSection heading={_t("settings|sidebar|title")}>
<SettingsSubsection
heading={_t("settings|sidebar|metaspaces_subsection")}
description={_t(
"Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.",
)}
description={_t("settings|sidebar|spaces_explainer")}
>
<StyledCheckbox
checked={!!homeEnabled}

View file

@ -159,29 +159,30 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
if (!this.state.mediaDevices) {
requestButton = (
<div>
<p>{_t("Missing media permissions, click the button below to request.")}</p>
<p>{_t("settings|voip|missing_permissions_prompt")}</p>
<AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
{_t("Request media permissions")}
{_t("settings|voip|request_permissions")}
</AccessibleButton>
</div>
);
} else if (this.state.mediaDevices) {
speakerDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioOutput, _t("Audio Output")) || (
<p>{_t("No Audio Outputs detected")}</p>
);
speakerDropdown = this.renderDropdown(
MediaDeviceKindEnum.AudioOutput,
_t("settings|voip|audio_output"),
) || <p>{_t("settings|voip|audio_output_empty")}</p>;
microphoneDropdown = this.renderDropdown(MediaDeviceKindEnum.AudioInput, _t("common|microphone")) || (
<p>{_t("No Microphones detected")}</p>
<p>{_t("settings|voip|audio_input_empty")}</p>
);
webcamDropdown = this.renderDropdown(MediaDeviceKindEnum.VideoInput, _t("common|camera")) || (
<p>{_t("No Webcams detected")}</p>
<p>{_t("settings|voip|video_input_empty")}</p>
);
}
return (
<SettingsTab>
<SettingsSection heading={_t("Voice & Video")}>
<SettingsSection heading={_t("settings|voip|title")}>
{requestButton}
<SettingsSubsection heading={_t("Voice settings")} stretchContent>
<SettingsSubsection heading={_t("settings|voip|voice_section")} stretchContent>
{speakerDropdown}
{microphoneDropdown}
<LabelledToggleSwitch
@ -190,18 +191,18 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
await MediaDeviceHandler.setAudioAutoGainControl(v);
this.setState({ audioAutoGainControl: MediaDeviceHandler.getAudioAutoGainControl() });
}}
label={_t("Automatically adjust the microphone volume")}
label={_t("settings|voip|voice_agc")}
data-testid="voice-auto-gain"
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("Video settings")} stretchContent>
<SettingsSubsection heading={_t("settings|voip|video_section")} stretchContent>
{webcamDropdown}
<SettingsFlag name="VideoView.flipVideoHorizontally" level={SettingLevel.ACCOUNT} />
</SettingsSubsection>
</SettingsSection>
<SettingsSection heading={_t("common|advanced")}>
<SettingsSubsection heading={_t("Voice processing")}>
<SettingsSubsection heading={_t("settings|voip|voice_processing")}>
<LabelledToggleSwitch
value={this.state.audioNoiseSuppression}
onChange={async (v): Promise<void> => {
@ -221,7 +222,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
data-testid="voice-echo-cancellation"
/>
</SettingsSubsection>
<SettingsSubsection heading={_t("Connection")}>
<SettingsSubsection heading={_t("settings|voip|connection_section")}>
<SettingsFlag
name="webRtcAllowPeerToPeer"
level={SettingLevel.DEVICE}

View file

@ -125,7 +125,7 @@ const SpaceSettingsVisibilityTab: React.FC<IProps> = ({ matrixClient: cli, space
let addressesSection: JSX.Element | undefined;
if (space.getJoinRule() === JoinRule.Public) {
addressesSection = (
<SettingsSection heading={_t("Address")}>
<SettingsSection heading={_t("room_settings|visibility|alias_section")}>
<AliasSettings
roomId={space.roomId}
canSetCanonicalAlias={canSetCanonical}

View file

@ -31,7 +31,7 @@ const getRoomName = (room?: Room, oobName = ""): string => room?.name || oobName
* @returns {string} the room name
*/
export function useRoomName(room?: Room, oobData?: IOOBData): string {
let oobName = _t("Unnamed room");
let oobName = _t("common|unnamed_room");
if (oobData && oobData.name) {
oobName = oobData.name;
}

View file

@ -1,13 +1,10 @@
{
"Create new room": "إنشاء غرفة جديدة",
"Failed to change password. Is your password correct?": "فشلت عملية تعديل الكلمة السرية. هل كلمتك السرية صحيحة ؟",
"Send": "إرسال",
"Unavailable": "غير متوفر",
"All Rooms": "كل الغُرف",
"All messages": "كل الرسائل",
"Changelog": "سِجل التغييرات",
"Thank you!": "شكرًا !",
"Permission Required": "التصريح مطلوب",
"Sun": "الأحد",
"Mon": "الإثنين",
"Tue": "الثلاثاء",
@ -33,11 +30,9 @@
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s، %(day)s %(monthName)s %(fullYear)s %(time)s",
"Default": "المبدئي",
"Restricted": "مقيد",
"Moderator": "مشرف",
"Logs sent": "تم ارسال سجل الاحداث",
"Reason": "السبب",
"You signed in to a new session without verifying it:": "قمت بتسجيل الدخول لجلسة جديدة من غير التحقق منها:",
"Verify your other session using one of the options below.": "أكِّد جلستك الأخرى باستخدام أحد الخيارات أدناه.",
"%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s تم تسجيل الدخول لجلسة جديدة من غير التحقق منها:",
@ -137,105 +132,28 @@
"This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "هذه الغرفة تشغل إصدار الغرفة <roomVersion /> ، والذي عده الخادم الوسيط هذا بأنه<i> غير مستقر </i>.",
"This room has already been upgraded.": "سبق وأن تمت ترقية هذه الغرفة.",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "ستؤدي ترقية هذه الغرفة إلى إغلاق النسخة الحالية للغرفة وإنشاء غرفة تمت ترقيتها بنفس الاسم.",
"Room options": "خيارات الغرفة",
"%(roomName)s is not accessible at this time.": "لا يمكن الوصول إلى %(roomName)s في الوقت الحالي.",
"%(roomName)s does not exist.": "الغرفة %(roomName)s ليست موجودة.",
"%(roomName)s can't be previewed. Do you want to join it?": "لا يمكن معاينة %(roomName)s. هل تريد الانضمام إليها؟",
"You're previewing %(roomName)s. Want to join it?": "أنت تعاين %(roomName)s. تريد الانضمام إليها؟",
"Reject & Ignore user": "رفض الدعوة وتجاهل الداعي",
"<userName/> invited you": "<userName/> دعاك",
"Do you want to join %(roomName)s?": "هل تريد أن تنضم إلى %(roomName)s؟",
"Start chatting": "ابدأ المحادثة",
"<userName/> wants to chat": "<userName/> يريد محادثتك",
"Do you want to chat with %(user)s?": "هل تريد محادثة %(user)s؟",
"Share this email in Settings to receive invites directly in %(brand)s.": "شارك هذا البريد الإلكتروني في الإعدادات لتلقي الدعوات مباشرةً في %(brand)s.",
"Use an identity server in Settings to receive invites directly in %(brand)s.": "استخدم خادم هوية في الإعدادات لتلقي الدعوات مباشرة في %(brand)s.",
"This invite to %(roomName)s was sent to %(email)s": "الدعوة إلى %(roomName)s أرسلت إلى %(email)s",
"Link this email with your account in Settings to receive invites directly in %(brand)s.": "اربط هذا البريد الإلكتروني بحسابك في الإعدادات لتلقي الدعوات مباشرةً في%(brand)s.",
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "تم إرسال الدعوة إلى %(roomName)s إلى %(email)s الذي لا يرتبط بحسابك",
"Join the discussion": "انضم للنقاش",
"Try to join anyway": "حاول الانضمام على أي حال",
"You can only join it with a working invite.": "لا يمكن الإنضمام إليها إلا بدعوة صالحة.",
"unknown error code": "رمز خطأٍ غير معروف",
"Something went wrong with your invite to %(roomName)s": "حدث خطأ في دعوتك إلى %(roomName)s",
"You were banned from %(roomName)s by %(memberName)s": "لقد حُظِرت من غرفة %(roomName)s من قِبَل %(memberName)s",
"Re-join": "أعِد الانضمام",
"Forget this room": "انسَ هذه الغرفة",
"Reason: %(reason)s": "السبب: %(reason)s",
"Sign Up": "سجل",
"Join the conversation with an account": "انضم للمحادثة بحساب",
"Historical": "تاريخي",
"Low priority": "أولوية منخفضة",
"Explore public rooms": "استكشف الغرف العامة",
"Add room": "أضف غرفة",
"Rooms": "الغرف",
"Show Widgets": "إظهار عناصر الواجهة",
"Hide Widgets": "إخفاء عناصر الواجهة",
"Forget room": "انسَ الغرفة",
"Join Room": "انضم للغرفة",
"(~%(count)s results)": {
"one": "(~%(count)s نتيجة)",
"other": "(~%(count)s نتائج)"
},
"Unnamed room": "غرفة بلا اسم",
"No recently visited rooms": "لا توجد غرف تمت زيارتها مؤخرًا",
"Room %(name)s": "الغرفة %(name)s",
"Replying": "الرد",
"%(duration)sd": "%(duration)sي",
"%(duration)sh": "%(duration)sس",
"%(duration)sm": "%(duration)sد",
"%(duration)ss": "%(duration)sث",
"Italics": "مائل",
"You do not have permission to post to this room": "ليس لديك إذن للنشر في هذه الغرفة",
"This room has been replaced and is no longer active.": "تم استبدال هذه الغرفة ولم تعد نشطة.",
"The conversation continues here.": "تستمر المحادثة هنا.",
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (قوة %(powerLevelNumber)s)",
"Filter room members": "تصفية أعضاء الغرفة",
"Invited": "مدعو",
"Invite to this room": "ادع لهذه الغرفة",
"and %(count)s others...": {
"one": "وواحدة أخرى...",
"other": "و %(count)s أخر..."
},
"Close preview": "إغلاق المعاينة",
"Scroll to most recent messages": "انتقل إلى أحدث الرسائل",
"The authenticity of this encrypted message can't be guaranteed on this device.": "لا يمكن ضمان موثوقية هذه الرسالة المشفرة على هذا الجهاز.",
"Encrypted by a deleted session": "مشفرة باتصال محذوف",
"Unencrypted": "غير مشفر",
"Encrypted by an unverified session": "مشفرة باتصال لم يتم التحقق منه",
"Ignored users": "المستخدمون المتجاهَلون",
"None": "لا شيء",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "للإبلاغ عن مشكلة أمنية متعلقة بMatrix ، يرجى قراءة <a>سياسة الإفصاح الأمني</a> في Matrix.org.",
"Discovery": "الاكتشاف",
"Deactivate account": "تعطيل الحساب",
"Deactivate Account": "تعطيل الحساب",
"Account management": "إدارة الحساب",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "وافق على شروط خدمة خادم الهوية %(serverName)s لتكون قابلاً للاكتشاف عن طريق عنوان البريد الإلكتروني أو رقم الهاتف.",
"Phone numbers": "أرقام الهواتف",
"Email addresses": "عنوان البريد الإلكتروني",
"Manage integrations": "إدارة التكاملات",
"Enter a new identity server": "أدخل خادم هوية جديدًا",
"Do not use an identity server": "لا تستخدم خادم هوية",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "استخدام خادم الهوية اختياري. إذا اخترت عدم استخدام خادم هوية ، فلن يتمكن المستخدمون الآخرون من اكتشافك ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "قطع الاتصال بخادم الهوية الخاص بك يعني أنك لن تكون قابلاً للاكتشاف من قبل المستخدمين الآخرين ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "أنت لا تستخدم حاليًا خادم هوية. لاكتشاف جهات الاتصال الحالية التي تعرفها وتكون قابلاً للاكتشاف ، أضف واحداً أدناه.",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "إذا كنت لا تريد استخدام <server /> لاكتشاف جهات الاتصال الموجودة التي تعرفها وتكون قابلاً للاكتشاف ، فأدخل خادم هوية آخر أدناه.",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "أنت تستخدم حاليًا <server> </server> لاكتشاف جهات الاتصال الحالية التي تعرفها وتجعل نفسك قابلاً للاكتشاف. يمكنك تغيير خادم الهوية الخاص بك أدناه.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "نوصي بإزالة عناوين البريد الإلكتروني وأرقام الهواتف من خادم الهوية قبل قطع الاتصال.",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "لا زالت <b>بياناتك الشخصية مشاعة</b> على خادم الهوية <idserver />.",
"Disconnect anyway": "افصل على أي حال",
"wait and try again later": "انتظر وعاوِد لاحقًا",
"contact the administrators of identity server <idserver />": "اتصل بمديري خادم الهوية <idserver />",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "تحقق من المكونات الإضافية للمتصفح الخاص بك بحثًا عن أي شيء قد يحظر خادم الهوية (مثل Privacy Badger)",
"You should:": "يجب عليك:",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "لابد من محو <b>بيانات الشخصية</b> من خادم الهوية <idserver /> قبل الانفصال. لسوء الحظ ، خادم الهوية <idserver /> حاليًّا خارج الشبكة أو لا يمكن الوصول إليه.",
"Disconnect from the identity server <idserver />?": "انفصل عن خادم الهوية <idserver />؟",
"Disconnect identity server": "افصل خادم الهوية",
"The identity server you have chosen does not have any terms of service.": "خادم الهوية الذي اخترت ليس له شروط خدمة.",
"Terms of service not accepted or the identity server is invalid.": "شروط الخدمة لم تُقبل أو أن خادم الهوية مردود.",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "انفصل عن خادم الهوية <current /> واتصل بآخر <new /> بدلاً منه؟",
"Change identity server": "تغيير خادم الهوية",
"Checking server": "فحص خادم",
"Back up your keys before signing out to avoid losing them.": "أضف مفاتيحك للاحتياطي قبل تسجيل الخروج لتتجنب ضياعها.",
"Backup version:": "نسخة الاحتياطي:",
"This backup is trusted because it has been restored on this session": "هذا الاحتياطي موثوق به لأنه تمت استعادته في هذا الاتصال",
@ -304,8 +222,6 @@
"Room avatar": "صورة الغرفة",
"Room Topic": "موضوع الغرفة",
"Room Name": "اسم الغرفة",
"Failed to set display name": "تعذر تعيين الاسم الظاهر",
"Authentication": "المصادقة",
"Set up": "تأسيس",
"Warning!": "إنذار!",
"Show more": "أظهر أكثر",
@ -319,53 +235,6 @@
"You have verified this user. This user has verified all of their sessions.": "لقد تحققت من هذا المستخدم. لقد تحقق هذا المستخدم من جميع اتصالاته.",
"You have not verified this user.": "أنت لم تتحقق من هذا المستخدم.",
"This user has not verified all of their sessions.": "هذا المستخدم لم يتحقق من جميع اتصالاته.",
"Phone Number": "رقم الهاتف",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "تم إرسال رسالة نصية إلى +%(msisdn)s. الرجاء إدخال رمز التحقق الذي فيها.",
"Remove %(phone)s?": "حذف %(phone)s؟",
"Email Address": "عنوان بريد الكتروني",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "لقد أرسلنا إليك بريدًا إلكترونيًا للتحقق من عنوانك. يرجى اتباع التعليمات الموجودة هناك ثم نقر الزر أدناه.",
"Unable to add email address": "تعذرت إضافة عنوان البريد الإلكتروني",
"This doesn't appear to be a valid email address": "لا يبدو عنوان البريد الإلكتروني هذا صالحاً",
"Invalid Email Address": "عنوان البريد الإلكتروني غير صالح",
"Remove %(email)s?": "حذف %(email)s؟",
"Unable to remove contact information": "غير قادر على إزالة معلومات التواصل",
"Discovery options will appear once you have added a phone number above.": "ستظهر خيارات الاكتشاف بمجرد إضافة رقم هاتف أعلاه.",
"Verification code": "رمز التحقق",
"Please enter verification code sent via text.": "الرجاء إدخال رمز التحقق المرسل عبر النص.",
"Incorrect verification code": "رمز التحقق غير صحيح",
"Unable to verify phone number.": "تعذر التحقق من رقم الهاتف.",
"Unable to share phone number": "تعذرت مشاركة رقم الهاتف",
"Unable to revoke sharing for phone number": "تعذر إبطال مشاركة رقم الهاتف",
"Discovery options will appear once you have added an email above.": "ستظهر خيارات الاكتشاف بمجرد إضافة بريد إلكتروني أعلاه.",
"Verify the link in your inbox": "تحقق من الرابط في بريدك الوارد",
"Unable to verify email address.": "تعذر التحقق من عنوان البريد الإلكتروني.",
"Click the link in the email you received to verify and then click continue again.": "انقر الرابط الواصل لبريدك الإلكتروني للتحقق ثم انقر \"متابعة\" مرة أخرى.",
"Your email address hasn't been verified yet": "لم يتم التحقق من عنوان بريدك الإلكتروني حتى الآن",
"Unable to share email address": "تعذرت مشاركة البريد الإلتكروني",
"Unable to revoke sharing for email address": "تعذر إبطال مشاركة عنوان البريد الإلكتروني",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "تعذر تغيير مستوى قوة المستخدم. تأكد من أن لديك صلاحيات كافية وحاول مرة أخرى.",
"Error changing power level": "تعذر تغيير مستوى القوة",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "تعذر تغيير مستوى قوة الغرفة. تأكد من أن لديك صلاحيات كافية وحاول مرة أخرى.",
"Error changing power level requirement": "تعذر تغيير متطلبات مستوى القوة",
"Banned by %(displayName)s": "حظره %(displayName)s",
"Failed to unban": "تعذر فك الحظر",
"Browse": "تصفح",
"Set a new custom sound": "تعيين صوت مخصص جديد",
"Notification sound": "صوت الإشعار",
"Sounds": "الأصوات",
"Uploaded sound": "صوت تمام الرفع",
"Room Addresses": "عناوين الغرف",
"Bridges": "الجسور",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "تعمل هذه الغرفة على توصيل الرسائل بالمنصات التالية. <a> اعرف المزيد. </a>",
"Room information": "معلومات الغرفة",
"Voice & Video": "الصوت والفيديو",
"Audio Output": "مخرج الصوت",
"No Webcams detected": "لم يتم الكشف عن كاميرات الويب",
"No Microphones detected": "لم يتم الكشف عن أجهزة ميكروفون",
"No Audio Outputs detected": "لم يتم الكشف عن مخرجات الصوت",
"Request media permissions": "اطلب الإذن للوسائط",
"Missing media permissions, click the button below to request.": "إذن الوسائط مفقود ، انقر الزر أدناه لطلب الإذن.",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "قام مسؤول الخادم بتعطيل التشفير من طرف إلى طرف أصلاً في الغرف الخاصة والرسائل الخاصّة.",
"Ok": "حسنا",
"Your homeserver has exceeded one of its resource limits.": "لقد تجاوز خادمك أحد حدود موارده.",
"Your homeserver has exceeded its user limit.": "لقد تجاوز خادمك حد عدد المستخدمين.",
@ -428,14 +297,6 @@
"American Samoa": "ساموا الأمريكية",
"Algeria": "الجزائر",
"Åland Islands": "جزر آلاند",
"Explore rooms": "استكشِف الغرف",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط، ويمكنهم تعديل عناصر واجهة المستخدم، وإرسال دعوات الغرف، وتعيين مستويات القوة نيابة عنك.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"Identity server (%(server)s)": "خادوم الهوية (%(server)s)",
"Could not connect to identity server": "تعذر الاتصال بخادوم الهوية",
"Not a valid identity server (status code %(code)s)": "ليس خادوم هوية صالح (رمز الحالة %(code)s)",
"Identity server URL must be HTTPS": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS",
"Paraguay": "باراغواي",
"Netherlands": "هولندا",
"Greece": "اليونان",
@ -585,7 +446,11 @@
"general": "عام",
"profile": "الملف الشخصي",
"display_name": "الاسم الظاهر",
"user_avatar": "الصورة الشخصية"
"user_avatar": "الصورة الشخصية",
"authentication": "المصادقة",
"rooms": "الغرف",
"low_priority": "أولوية منخفضة",
"historical": "تاريخي"
},
"action": {
"continue": "واصِل",
@ -640,7 +505,9 @@
"review": "مراجعة",
"manage": "إدارة",
"mention": "إشارة",
"unban": "فك الحظر"
"unban": "فك الحظر",
"explore_rooms": "استكشِف الغرف",
"explore_public_rooms": "استكشف الغرف العامة"
},
"labs": {
"pinning": "تثبيت الرسالة",
@ -669,7 +536,13 @@
"placeholder_reply_encrypted": "أرسل جواباً مشفراً …",
"placeholder_reply": "أرسل جواباً …",
"placeholder_encrypted": "أرسل رسالة مشفرة …",
"placeholder": "أرسل رسالة …"
"placeholder": "أرسل رسالة …",
"room_upgraded_link": "تستمر المحادثة هنا.",
"room_upgraded_notice": "تم استبدال هذه الغرفة ولم تعد نشطة.",
"no_perms_notice": "ليس لديك إذن للنشر في هذه الغرفة",
"poll_button_no_perms_title": "التصريح مطلوب",
"format_italics": "مائل",
"replying_title": "الرد"
},
"power_level": {
"default": "المبدئي",
@ -755,7 +628,14 @@
"inline_url_previews_room_account": "تمكين معاينة الروابط لهذه الغرفة (يؤثر عليك فقط)",
"inline_url_previews_room": "تمكين معاينة الروابط أصلاً لأي مشارك في هذه الغرفة",
"voip": {
"mirror_local_feed": "محاكاة تغذية الفيديو المحلية"
"mirror_local_feed": "محاكاة تغذية الفيديو المحلية",
"missing_permissions_prompt": "إذن الوسائط مفقود ، انقر الزر أدناه لطلب الإذن.",
"request_permissions": "اطلب الإذن للوسائط",
"audio_output": "مخرج الصوت",
"audio_output_empty": "لم يتم الكشف عن مخرجات الصوت",
"audio_input_empty": "لم يتم الكشف عن أجهزة ميكروفون",
"video_input_empty": "لم يتم الكشف عن كاميرات الويب",
"title": "الصوت والفيديو"
},
"security": {
"send_analytics": "إرسال بيانات التحليلات",
@ -808,7 +688,10 @@
"key_backup_connect": "اربط هذا الاتصال باحتياطي مفتاح",
"key_backup_complete": "جميع المفاتيح منسوخة في الاحتياطي",
"key_backup_algorithm": "الخوارزمية:",
"key_backup_inactive_warning": "مفاتيحك <b>لا احتياطيَّ لها من هذا الاتصال</b>."
"key_backup_inactive_warning": "مفاتيحك <b>لا احتياطيَّ لها من هذا الاتصال</b>.",
"key_backup_active_version_none": "لا شيء",
"ignore_users_section": "المستخدمون المتجاهَلون",
"e2ee_default_disabled_warning": "قام مسؤول الخادم بتعطيل التشفير من طرف إلى طرف أصلاً في الغرف الخاصة والرسائل الخاصّة."
},
"preferences": {
"room_list_heading": "قائمة الغرفة",
@ -831,7 +714,39 @@
"add_msisdn_dialog_title": "أضِف رقم الهاتف",
"name_placeholder": "لا اسم ظاهر",
"error_saving_profile_title": "تعذر حفظ ملفك الشخصي",
"error_saving_profile": "تعذر إتمام العملية"
"error_saving_profile": "تعذر إتمام العملية",
"error_password_change_403": "فشلت عملية تعديل الكلمة السرية. هل كلمتك السرية صحيحة ؟",
"emails_heading": "عنوان البريد الإلكتروني",
"msisdns_heading": "أرقام الهواتف",
"discovery_needs_terms": "وافق على شروط خدمة خادم الهوية %(serverName)s لتكون قابلاً للاكتشاف عن طريق عنوان البريد الإلكتروني أو رقم الهاتف.",
"deactivate_section": "تعطيل الحساب",
"account_management_section": "إدارة الحساب",
"discovery_section": "الاكتشاف",
"error_revoke_email_discovery": "تعذر إبطال مشاركة عنوان البريد الإلكتروني",
"error_share_email_discovery": "تعذرت مشاركة البريد الإلتكروني",
"email_not_verified": "لم يتم التحقق من عنوان بريدك الإلكتروني حتى الآن",
"email_verification_instructions": "انقر الرابط الواصل لبريدك الإلكتروني للتحقق ثم انقر \"متابعة\" مرة أخرى.",
"error_email_verification": "تعذر التحقق من عنوان البريد الإلكتروني.",
"discovery_email_verification_instructions": "تحقق من الرابط في بريدك الوارد",
"discovery_email_empty": "ستظهر خيارات الاكتشاف بمجرد إضافة بريد إلكتروني أعلاه.",
"error_revoke_msisdn_discovery": "تعذر إبطال مشاركة رقم الهاتف",
"error_share_msisdn_discovery": "تعذرت مشاركة رقم الهاتف",
"error_msisdn_verification": "تعذر التحقق من رقم الهاتف.",
"incorrect_msisdn_verification": "رمز التحقق غير صحيح",
"msisdn_verification_instructions": "الرجاء إدخال رمز التحقق المرسل عبر النص.",
"msisdn_verification_field_label": "رمز التحقق",
"discovery_msisdn_empty": "ستظهر خيارات الاكتشاف بمجرد إضافة رقم هاتف أعلاه.",
"error_set_name": "تعذر تعيين الاسم الظاهر",
"error_remove_3pid": "غير قادر على إزالة معلومات التواصل",
"remove_email_prompt": "حذف %(email)s؟",
"error_invalid_email": "عنوان البريد الإلكتروني غير صالح",
"error_invalid_email_detail": "لا يبدو عنوان البريد الإلكتروني هذا صالحاً",
"error_add_email": "تعذرت إضافة عنوان البريد الإلكتروني",
"add_email_instructions": "لقد أرسلنا إليك بريدًا إلكترونيًا للتحقق من عنوانك. يرجى اتباع التعليمات الموجودة هناك ثم نقر الزر أدناه.",
"email_address_label": "عنوان بريد الكتروني",
"remove_msisdn_prompt": "حذف %(phone)s؟",
"add_msisdn_instructions": "تم إرسال رسالة نصية إلى +%(msisdn)s. الرجاء إدخال رمز التحقق الذي فيها.",
"msisdn_label": "رقم الهاتف"
}
},
"devtools": {
@ -969,6 +884,9 @@
"lightbox_title": "%(senderDisplayName)s غير صورة الغرفة %(roomName)s",
"removed": "%(senderDisplayName)s حذف صورة الغرفة.",
"changed_img": "%(senderDisplayName)s غير صورة الغرفة إلى <img/>"
},
"url_preview": {
"close": "إغلاق المعاينة"
}
},
"slash_command": {
@ -1132,7 +1050,14 @@
"send_event_type": "إرسال أحداث من نوع %(eventType)s",
"title": "الأدوار والصلاحيات",
"permissions_section": "الصلاحيات",
"permissions_section_description_room": "حدد الأدوار المطلوبة لتغيير أجزاء مختلفة من الغرفة"
"permissions_section_description_room": "حدد الأدوار المطلوبة لتغيير أجزاء مختلفة من الغرفة",
"error_unbanning": "تعذر فك الحظر",
"banned_by": "حظره %(displayName)s",
"ban_reason": "السبب",
"error_changing_pl_reqs_title": "تعذر تغيير متطلبات مستوى القوة",
"error_changing_pl_reqs_description": "تعذر تغيير مستوى قوة الغرفة. تأكد من أن لديك صلاحيات كافية وحاول مرة أخرى.",
"error_changing_pl_title": "تعذر تغيير مستوى القوة",
"error_changing_pl_description": "تعذر تغيير مستوى قوة المستخدم. تأكد من أن لديك صلاحيات كافية وحاول مرة أخرى."
},
"security": {
"strict_encryption": "لا ترسل أبدًا رسائل مشفرة إلى اتصالات التي لم يتم التحقق منها في هذه الغرفة من هذا الاتصال",
@ -1157,14 +1082,28 @@
"default_url_previews_off": "معاينات URL معطلة بشكل أصلي للمشاركين في هذه الغرفة.",
"url_preview_encryption_warning": "في الغرف المشفرة ، مثل هذه الغرفة ، يتم تعطيل معاينات URL أصلاً للتأكد من أن خادمك الوسيط (حيث يتم إنشاء المعاينات) لا يمكنه جمع معلومات حول الروابط التي تراها في هذه الغرفة.",
"url_preview_explainer": "عندما يضع شخص ما عنوان URL في رسالته ، يمكن عرض معاينة عنوان URL لإعطاء مزيد من المعلومات حول هذا الرابط مثل العنوان والوصف وصورة من موقع الويب.",
"url_previews_section": "معاينة الروابط"
"url_previews_section": "معاينة الروابط",
"aliases_section": "عناوين الغرف",
"other_section": "أخرى"
},
"advanced": {
"unfederated": "لا يمكن الوصول إلى هذه الغرفة بواسطة خوادم Matrix البعيدة",
"room_upgrade_button": "قم بترقية هذه الغرفة إلى إصدار الغرفة الموصى به",
"room_predecessor": "عرض رسائل أقدم في %(roomName)s.",
"room_version_section": "إصدار الغرفة",
"room_version": "إصدار الغرفة:"
"room_version": "إصدار الغرفة:",
"information_section_room": "معلومات الغرفة"
},
"bridges": {
"description": "تعمل هذه الغرفة على توصيل الرسائل بالمنصات التالية. <a> اعرف المزيد. </a>",
"title": "الجسور"
},
"notifications": {
"uploaded_sound": "صوت تمام الرفع",
"sounds_section": "الأصوات",
"notification_sound": "صوت الإشعار",
"custom_sound_prompt": "تعيين صوت مخصص جديد",
"browse_button": "تصفح"
}
},
"encryption": {
@ -1258,7 +1197,9 @@
"other": "أظهر %(count)s زيادة"
},
"show_less": "أظهر أقل",
"notification_options": "خيارات الإشعارات"
"notification_options": "خيارات الإشعارات",
"breadcrumbs_empty": "لا توجد غرف تمت زيارتها مؤخرًا",
"add_room_label": "أضف غرفة"
},
"a11y": {
"n_unread_messages_mentions": {
@ -1270,7 +1211,8 @@
"other": "%(count)s من الرسائل غير مقروءة."
},
"unread_messages": "رسائل غير المقروءة.",
"jump_first_invite": "الانتقال لأول دعوة."
"jump_first_invite": "الانتقال لأول دعوة.",
"room_name": "الغرفة %(name)s"
},
"setting": {
"help_about": {
@ -1421,7 +1363,8 @@
"lists_heading": "قوائم متشرك بها",
"lists_description_1": "سيؤدي الاشتراك في قائمة الحظر إلى انضمامك إليها!",
"lists_description_2": "إذا لم يكن هذا ما تريده ، فيرجى استخدام أداة مختلفة لتجاهل المستخدمين.",
"lists_new_label": "معرف الغرفة أو عنوان قائمة الحظر"
"lists_new_label": "معرف الغرفة أو عنوان قائمة الحظر",
"rules_empty": "لا شيء"
},
"room": {
"drop_file_prompt": "قم بإسقاط الملف هنا ليُرفَع",
@ -1443,8 +1386,40 @@
"unfavourite": "فُضلت",
"favourite": "تفضيل",
"low_priority": "أولوية منخفضة",
"forget": "انسَ الغرفة"
}
"forget": "انسَ الغرفة",
"title": "خيارات الغرفة"
},
"invite_this_room": "ادع لهذه الغرفة",
"header": {
"forget_room_button": "انسَ الغرفة",
"hide_widgets_button": "إخفاء عناصر الواجهة",
"show_widgets_button": "إظهار عناصر الواجهة"
},
"join_title_account": "انضم للمحادثة بحساب",
"join_button_account": "سجل",
"kick_reason": "السبب: %(reason)s",
"forget_room": "انسَ هذه الغرفة",
"rejoin_button": "أعِد الانضمام",
"banned_from_room_by": "لقد حُظِرت من غرفة %(roomName)s من قِبَل %(memberName)s",
"3pid_invite_error_title_room": "حدث خطأ في دعوتك إلى %(roomName)s",
"3pid_invite_error_invite_subtitle": "لا يمكن الإنضمام إليها إلا بدعوة صالحة.",
"3pid_invite_error_invite_action": "حاول الانضمام على أي حال",
"join_the_discussion": "انضم للنقاش",
"3pid_invite_email_not_found_account_room": "تم إرسال الدعوة إلى %(roomName)s إلى %(email)s الذي لا يرتبط بحسابك",
"link_email_to_receive_3pid_invite": "اربط هذا البريد الإلكتروني بحسابك في الإعدادات لتلقي الدعوات مباشرةً في%(brand)s.",
"invite_sent_to_email_room": "الدعوة إلى %(roomName)s أرسلت إلى %(email)s",
"3pid_invite_no_is_subtitle": "استخدم خادم هوية في الإعدادات لتلقي الدعوات مباشرة في %(brand)s.",
"invite_email_mismatch_suggestion": "شارك هذا البريد الإلكتروني في الإعدادات لتلقي الدعوات مباشرةً في %(brand)s.",
"dm_invite_title": "هل تريد محادثة %(user)s؟",
"dm_invite_subtitle": "<userName/> يريد محادثتك",
"dm_invite_action": "ابدأ المحادثة",
"invite_title": "هل تريد أن تنضم إلى %(roomName)s؟",
"invite_subtitle": "<userName/> دعاك",
"invite_reject_ignore": "رفض الدعوة وتجاهل الداعي",
"peek_join_prompt": "أنت تعاين %(roomName)s. تريد الانضمام إليها؟",
"no_peek_join_prompt": "لا يمكن معاينة %(roomName)s. هل تريد الانضمام إليها؟",
"not_found_title_name": "الغرفة %(roomName)s ليست موجودة.",
"inaccessible_name": "لا يمكن الوصول إلى %(roomName)s في الوقت الحالي."
},
"space": {
"context_menu": {
@ -1502,7 +1477,9 @@
"colour_bold": "ثخين",
"error_change_title": "تغيير إعدادات الإشعار",
"mark_all_read": "أشر عليها بأنها قرأت",
"class_other": "أخرى"
"class_other": "أخرى",
"default": "المبدئي",
"all_messages": "كل الرسائل"
},
"error": {
"admin_contact_short": "تواصل مع <a>مدير الخادم</a> الخاص بك.",
@ -1522,6 +1499,43 @@
"a11y_jump_first_unread_room": "الانتقال لأول غرفة لم تقرأ.",
"integration_manager": {
"error_connecting_heading": "لا يمكن الاتصال بمدير التكامل",
"error_connecting": "مدري التكامل غير متصل بالإنرتنت أو لا يمكنه الوصول إلى خادمك الوسيط."
"error_connecting": "مدري التكامل غير متصل بالإنرتنت أو لا يمكنه الوصول إلى خادمك الوسيط.",
"use_im_default": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"use_im": "استخدم مدير التكامل لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
"manage_title": "إدارة التكاملات",
"explainer": "يتلقى مديرو التكامل بيانات الضبط، ويمكنهم تعديل عناصر واجهة المستخدم، وإرسال دعوات الغرف، وتعيين مستويات القوة نيابة عنك."
},
"identity_server": {
"url_not_https": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS",
"error_invalid": "ليس خادوم هوية صالح (رمز الحالة %(code)s)",
"error_connection": "تعذر الاتصال بخادوم الهوية",
"checking": "فحص خادم",
"change": "تغيير خادم الهوية",
"change_prompt": "انفصل عن خادم الهوية <current /> واتصل بآخر <new /> بدلاً منه؟",
"error_invalid_or_terms": "شروط الخدمة لم تُقبل أو أن خادم الهوية مردود.",
"no_terms": "خادم الهوية الذي اخترت ليس له شروط خدمة.",
"disconnect": "افصل خادم الهوية",
"disconnect_server": "انفصل عن خادم الهوية <idserver />؟",
"disconnect_offline_warning": "لابد من محو <b>بيانات الشخصية</b> من خادم الهوية <idserver /> قبل الانفصال. لسوء الحظ ، خادم الهوية <idserver /> حاليًّا خارج الشبكة أو لا يمكن الوصول إليه.",
"suggestions": "يجب عليك:",
"suggestions_1": "تحقق من المكونات الإضافية للمتصفح الخاص بك بحثًا عن أي شيء قد يحظر خادم الهوية (مثل Privacy Badger)",
"suggestions_2": "اتصل بمديري خادم الهوية <idserver />",
"suggestions_3": "انتظر وعاوِد لاحقًا",
"disconnect_anyway": "افصل على أي حال",
"disconnect_personal_data_warning_1": "لا زالت <b>بياناتك الشخصية مشاعة</b> على خادم الهوية <idserver />.",
"disconnect_personal_data_warning_2": "نوصي بإزالة عناوين البريد الإلكتروني وأرقام الهواتف من خادم الهوية قبل قطع الاتصال.",
"url": "خادوم الهوية (%(server)s)",
"description_connected": "أنت تستخدم حاليًا <server> </server> لاكتشاف جهات الاتصال الحالية التي تعرفها وتجعل نفسك قابلاً للاكتشاف. يمكنك تغيير خادم الهوية الخاص بك أدناه.",
"change_server_prompt": "إذا كنت لا تريد استخدام <server /> لاكتشاف جهات الاتصال الموجودة التي تعرفها وتكون قابلاً للاكتشاف ، فأدخل خادم هوية آخر أدناه.",
"description_disconnected": "أنت لا تستخدم حاليًا خادم هوية. لاكتشاف جهات الاتصال الحالية التي تعرفها وتكون قابلاً للاكتشاف ، أضف واحداً أدناه.",
"disconnect_warning": "قطع الاتصال بخادم الهوية الخاص بك يعني أنك لن تكون قابلاً للاكتشاف من قبل المستخدمين الآخرين ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"description_optional": "استخدام خادم الهوية اختياري. إذا اخترت عدم استخدام خادم هوية ، فلن يتمكن المستخدمون الآخرون من اكتشافك ولن تتمكن من دعوة الآخرين عبر البريد الإلكتروني أو الهاتف.",
"do_not_use": "لا تستخدم خادم هوية",
"url_field_label": "أدخل خادم هوية جديدًا"
},
"member_list": {
"invited_list_heading": "مدعو",
"filter_placeholder": "تصفية أعضاء الغرفة",
"power_label": "%(userName)s (قوة %(powerLevelNumber)s)"
}
}

View file

@ -22,26 +22,12 @@
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
"Default": "Varsayılan olaraq",
"Moderator": "Moderator",
"Reason": "Səbəb",
"Incorrect verification code": "Təsdiq etmənin səhv kodu",
"Authentication": "Müəyyənləşdirilmə",
"Failed to set display name": "Görünüş adını təyin etmək bacarmadı",
"not specified": "qeyd edilmədi",
"Failed to ban user": "İstifadəçini bloklamağı bacarmadı",
"Failed to mute user": "İstifadəçini kəsməyi bacarmadı",
"Are you sure?": "Siz əminsiniz?",
"Unignore": "Blokdan çıxarmaq",
"Invited": "Dəvət edilmişdir",
"Filter room members": "İştirakçılara görə axtarış",
"You do not have permission to post to this room": "Siz bu otağa yaza bilmirsiniz",
"Join Room": "Otağa girmək",
"Forget room": "Otağı unutmaq",
"Low priority": "Əhəmiyyətsizlər",
"Historical": "Arxiv",
"Failed to unban": "Blokdan çıxarmağı bacarmadı",
"Banned by %(displayName)s": "%(displayName)s bloklanıb",
"unknown error code": "naməlum səhv kodu",
"Failed to forget room %(errCode)s": "Otağı unutmağı bacarmadı: %(errCode)s",
"Sunday": "Bazar",
@ -52,14 +38,9 @@
"Create new room": "Otağı yaratmaq",
"Home": "Başlanğıc",
"%(items)s and %(lastItem)s": "%(items)s və %(lastItem)s",
"Deactivate Account": "Hesabı bağlamaq",
"An error has occurred.": "Səhv oldu.",
"Invalid Email Address": "Yanlış email",
"Verification Pending": "Gözləmə təsdiq etmələr",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Öz elektron poçtunu yoxlayın və olan istinadı basın. Bundan sonra düyməni Davam etməyə basın.",
"Unable to add email address": "Email-i əlavə etməyə müvəffəq olmur",
"Unable to verify email address.": "Email-i yoxlamağı bacarmadı.",
"Failed to change password. Is your password correct?": "Şifrəni əvəz etməyi bacarmadı. Siz cari şifrə düzgün daxil etdiniz?",
"Reject invitation": "Dəvəti rədd etmək",
"Are you sure you want to reject the invitation?": "Siz əminsiniz ki, siz dəvəti rədd etmək istəyirsiniz?",
"Failed to reject invitation": "Dəvəti rədd etməyi bacarmadı",
@ -68,18 +49,14 @@
"No more results": "Daha çox nəticə yoxdur",
"Failed to reject invite": "Dəvəti rədd etməyi bacarmadı",
"Failed to load timeline position": "Xronologiyadan nişanı yükləməyi bacarmadı",
"Unable to remove contact information": "Əlaqə məlumatlarının silməyi bacarmadı",
"A new password must be entered.": "Yeni parolu daxil edin.",
"New passwords must match each other.": "Yeni şifrələr uyğun olmalıdır.",
"Return to login screen": "Girişin ekranına qayıtmaq",
"Confirm passphrase": "Şifrəni təsdiqləyin",
"Permission Required": "İzn tələb olunur",
"Send": "Göndər",
"PM": "24:00",
"AM": "12:00",
"Restricted": "Məhduddur",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"Explore rooms": "Otaqları kəşf edin",
"common": {
"analytics": "Analitik",
"error": "Səhv",
@ -96,7 +73,10 @@
"identity_server": "Eyniləşdirmənin serveri",
"on": "Qoşmaq",
"advanced": "Təfərrüatlar",
"profile": "Profil"
"profile": "Profil",
"authentication": "Müəyyənləşdirilmə",
"low_priority": "Əhəmiyyətsizlər",
"historical": "Arxiv"
},
"action": {
"continue": "Davam etmək",
@ -114,7 +94,9 @@
"close": "Bağlamaq",
"accept": "Qəbul etmək",
"register": "Qeydiyyatdan keçmək",
"unban": "Blokdan çıxarmaq"
"unban": "Blokdan çıxarmaq",
"unignore": "Blokdan çıxarmaq",
"explore_rooms": "Otaqları kəşf edin"
},
"keyboard": {
"home": "Başlanğıc"
@ -155,7 +137,18 @@
"msisdn_in_use": "Bu telefon nömrəsi artıq istifadə olunur",
"add_email_dialog_title": "Emal ünvan əlavə etmək",
"add_email_failed_verification": "Email-i yoxlamağı bacarmadı: əmin olun ki, siz məktubda istinaddakı ünvana keçdiniz",
"add_msisdn_dialog_title": "Telefon nömrəsi əlavə etmək"
"add_msisdn_dialog_title": "Telefon nömrəsi əlavə etmək",
"error_password_change_403": "Şifrəni əvəz etməyi bacarmadı. Siz cari şifrə düzgün daxil etdiniz?",
"deactivate_section": "Hesabı bağlamaq",
"error_email_verification": "Email-i yoxlamağı bacarmadı.",
"incorrect_msisdn_verification": "Təsdiq etmənin səhv kodu",
"error_set_name": "Görünüş adını təyin etmək bacarmadı",
"error_remove_3pid": "Əlaqə məlumatlarının silməyi bacarmadı",
"error_invalid_email": "Yanlış email",
"error_add_email": "Email-i əlavə etməyə müvəffəq olmur"
},
"key_export_import": {
"confirm_passphrase": "Şifrəni təsdiqləyin"
}
},
"timeline": {
@ -280,7 +273,9 @@
"autocomplete": {
"command_description": "Komandalar",
"user_description": "İstifadəçilər"
}
},
"no_perms_notice": "Siz bu otağa yaza bilmirsiniz",
"poll_button_no_perms_title": "İzn tələb olunur"
},
"space": {
"context_menu": {
@ -291,12 +286,18 @@
"permissions": {
"no_privileged_users": "Heç bir istifadəçi bu otaqda xüsusi hüquqlara malik deyil",
"banned_users_section": "Bloklanmış istifadəçilər",
"permissions_section": "Girişin hüquqları"
"permissions_section": "Girişin hüquqları",
"error_unbanning": "Blokdan çıxarmağı bacarmadı",
"banned_by": "%(displayName)s bloklanıb",
"ban_reason": "Səbəb"
},
"security": {
"history_visibility_legend": "Kim tarixi oxuya bilər?"
},
"upload_avatar_label": "Avatar-ı yükləmək"
"upload_avatar_label": "Avatar-ı yükləmək",
"general": {
"other_section": "Digər"
}
},
"failed_load_async_component": "Yükləmək olmur! Şəbəkə bağlantınızı yoxlayın və yenidən cəhd edin.",
"upload_failed_generic": "'%(fileName)s' faylı yüklənə bilmədi.",
@ -337,16 +338,24 @@
"upgrade_error_description": "Serverinizin seçilmiş otaq versiyasını dəstəklədiyini bir daha yoxlayın və yenidən cəhd edin.",
"context_menu": {
"favourite": "Seçilmiş"
},
"header": {
"forget_room_button": "Otağı unutmaq"
}
},
"notifications": {
"enable_prompt_toast_title": "Xəbərdarlıqlar",
"class_other": "Digər"
"class_other": "Digər",
"default": "Varsayılan olaraq"
},
"encryption": {
"not_supported": "<dəstəklənmir>"
},
"error": {
"update_power_level": "Hüquqların səviyyəsini dəyişdirməyi bacarmadı"
},
"member_list": {
"invited_list_heading": "Dəvət edilmişdir",
"filter_placeholder": "İştirakçılara görə axtarış"
}
}

View file

@ -1,7 +1,5 @@
{
"Failed to forget room %(errCode)s": "Не атрымалася забыць пакой %(errCode)s",
"All messages": "Усе паведамленні",
"Invite to this room": "Запрасіць у гэты пакой",
"common": {
"error": "Памылка",
"mute": "Без гуку",
@ -34,7 +32,8 @@
"failed_generic": "Не атрымалася выканаць аперацыю"
},
"notifications": {
"enable_prompt_toast_title": "Апавяшчэнні"
"enable_prompt_toast_title": "Апавяшчэнні",
"all_messages": "Усе паведамленні"
},
"timeline": {
"context_menu": {
@ -45,6 +44,7 @@
"context_menu": {
"favourite": "Улюбёнае",
"low_priority": "Нізкі прыярытэт"
}
},
"invite_this_room": "Запрасіць у гэты пакой"
}
}

View file

@ -1,6 +1,5 @@
{
"Send": "Изпрати",
"Failed to change password. Is your password correct?": "Неуспешна промяна. Правилно ли сте въвели Вашата парола?",
"Sun": "нд.",
"Mon": "пн.",
"Tue": "вт.",
@ -26,50 +25,31 @@
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s",
"unknown error code": "неизвестен код за грешка",
"Failed to forget room %(errCode)s": "Неуспешно забравяне на стаята %(errCode)s",
"Rooms": "Стаи",
"Unnamed room": "Стая без име",
"Warning!": "Внимание!",
"PM": "PM",
"AM": "AM",
"Default": "По подразбиране",
"Restricted": "Ограничен",
"Moderator": "Модератор",
"Reason": "Причина",
"Incorrect verification code": "Неправилен код за потвърждение",
"Authentication": "Автентикация",
"Failed to set display name": "Неуспешно задаване на име",
"Failed to ban user": "Неуспешно блокиране на потребителя",
"Failed to mute user": "Неуспешно заглушаване на потребителя",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "След като си намалите нивото на достъп, няма да можете да възвърнете тази промяна. Ако сте последния потребител с привилегии в тази стая, ще бъде невъзможно да възвърнете привилегии си.",
"Are you sure?": "Сигурни ли сте?",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Няма да можете да възвърнете тази промяна, тъй като повишавате този потребител до същото ниво на достъп като Вашето.",
"Unignore": "Премахни игнорирането",
"Admin Tools": "Инструменти на администратора",
"and %(count)s others...": {
"other": "и %(count)s други...",
"one": "и още един..."
},
"Invited": "Поканен",
"Filter room members": "Филтриране на членовете",
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (ниво на достъп %(powerLevelNumber)s)",
"You do not have permission to post to this room": "Нямате разрешение да публикувате в тази стая",
"%(duration)ss": "%(duration)sсек",
"%(duration)sm": "%(duration)sмин",
"%(duration)sh": "%(duration)sч",
"%(duration)sd": "%(duration)sд",
"Replying": "Отговаря",
"(~%(count)s results)": {
"other": "(~%(count)s резултати)",
"one": "(~%(count)s резултат)"
},
"Join Room": "Присъединяване към стаята",
"Forget room": "Забрави стаята",
"Low priority": "Нисък приоритет",
"Historical": "Архив",
"%(roomName)s does not exist.": "%(roomName)s не съществува.",
"%(roomName)s is not accessible at this time.": "%(roomName)s не е достъпна към този момент.",
"Failed to unban": "Неуспешно отблокиране",
"Banned by %(displayName)s": "Блокиран от %(displayName)s",
"Jump to first unread message.": "Отиди до първото непрочетено съобщение.",
"not specified": "неопределен",
"This room has no local addresses": "Тази стая няма локални адреси",
@ -94,17 +74,11 @@
"other": "И %(count)s други..."
},
"Confirm Removal": "Потвърдете премахването",
"Deactivate Account": "Затвори акаунта",
"Verification Pending": "Очакване на потвърждение",
"An error has occurred.": "Възникна грешка.",
"Unable to restore session": "Неуспешно възстановяване на сесията",
"Invalid Email Address": "Невалиден имейл адрес",
"This doesn't appear to be a valid email address": "Това не изглежда да е валиден имейл адрес",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Моля, проверете своя имейл адрес и натиснете връзката, която той съдържа. След като направите това, натиснете продължи.",
"Unable to add email address": "Неуспешно добавяне на имейл адрес",
"Unable to verify email address.": "Неуспешно потвърждение на имейл адрес.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Беше направен опит да се зареди конкретна точка в хронологията на тази стая, но не я намери.",
"Unable to remove contact information": "Неуспешно премахване на информацията за контакти",
"This will allow you to reset your password and receive notifications.": "Това ще Ви позволи да възстановите Вашата парола и да получавате известия.",
"Reject invitation": "Отхвърли поканата",
"Are you sure you want to reject the invitation?": "Сигурни ли сте, че искате да отхвърлите поканата?",
@ -126,24 +100,12 @@
"one": "Качване на %(filename)s и %(count)s друг"
},
"Uploading %(filename)s": "Качване на %(filename)s",
"No Microphones detected": "Няма открити микрофони",
"No Webcams detected": "Няма открити уеб камери",
"A new password must be entered.": "Трябва да бъде въведена нова парола.",
"New passwords must match each other.": "Новите пароли трябва да съвпадат една с друга.",
"Return to login screen": "Връщане към страницата за влизане в профила",
"Session ID": "Идентификатор на сесията",
"Passphrases must match": "Паролите трябва да съвпадат",
"Passphrase must not be empty": "Паролата не трябва да е празна",
"Export room keys": "Експортиране на ключове за стаята",
"Enter passphrase": "Въведи парола",
"Confirm passphrase": "Потвърди парола",
"Import room keys": "Импортиране на ключове за стая",
"File to import": "Файл за импортиране",
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "На път сте да бъдете отведени до друг сайт, където можете да удостоверите профила си за използване с %(integrationsUrl)s. Искате ли да продължите?",
"If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Ако преди сте използвали по-нова версия на %(brand)s, Вашата сесия може да не бъде съвместима с текущата версия. Затворете този прозорец и се върнете в по-новата версия.",
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Този процес Ви позволява да експортирате във файл ключовете за съобщения в шифровани стаи. Така ще можете да импортирате файла в друг Matrix клиент, така че той също да може да разшифрова такива съобщения.",
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Този процес позволява да импортирате ключове за шифроване, които преди сте експортирали от друг Matrix клиент. Тогава ще можете да разшифровате всяко съобщение, което другият клиент може да разшифрова.",
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Експортираният файл може да бъде предпазен с парола. Трябва да въведете парола тук, за да разшифровате файла.",
"You don't currently have any stickerpacks enabled": "В момента нямате включени пакети със стикери",
"Sunday": "Неделя",
"Today": "Днес",
@ -160,8 +122,6 @@
"Monday": "Понеделник",
"All Rooms": "Във всички стаи",
"Wednesday": "Сряда",
"All messages": "Всички съобщения",
"Invite to this room": "Покани в тази стая",
"You cannot delete this message. (%(code)s)": "Това съобщение не може да бъде изтрито. (%(code)s)",
"Thursday": "Четвъртък",
"Logs sent": "Логовете са изпратени",
@ -179,13 +139,10 @@
"Share User": "Споделяне на потребител",
"Share Room Message": "Споделяне на съобщение от стая",
"Link to selected message": "Създай връзка към избраното съобщение",
"No Audio Outputs detected": "Не са открити аудио изходи",
"Audio Output": "Аудио изходи",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Не можете да изпращате съобщения докато не прегледате и се съгласите с <consentLink>нашите правила и условия</consentLink>.",
"Demote yourself?": "Понижете себе си?",
"Demote": "Понижение",
"This event could not be displayed": "Това събитие не може да бъде показано",
"Permission Required": "Необходимо е разрешение",
"Only room administrators will see this warning": "Само администратори на стаята виждат това предупреждение",
"Upgrade Room Version": "Обнови версията на стаята",
"Create a new room with the same name, description and avatar": "Създадем нова стая със същото име, описание и снимка",
@ -194,8 +151,6 @@
"Put a link back to the old room at the start of the new room so people can see old messages": "Поставим връзка в новата стая, водещо обратно към старата, за да може хората да виждат старите съобщения",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Съобщението Ви не бе изпратено, защото този сървър е достигнал лимита си за потребители на месец. Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Съобщението Ви не бе изпратено, защото този сървър е някой от лимитите си. Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
"This room has been replaced and is no longer active.": "Тази стая е била заменена и вече не е активна.",
"The conversation continues here.": "Разговора продължава тук.",
"Failed to upgrade room": "Неуспешно обновяване на стаята",
"The room upgrade could not be completed": "Обновяването на тази стая не можа да бъде завършено",
"Upgrade this room to version %(version)s": "Обновете тази стая до версия %(version)s",
@ -218,33 +173,11 @@
"Invalid homeserver discovery response": "Невалиден отговор по време на откриването на конфигурацията за сървъра",
"Invalid identity server discovery response": "Невалиден отговор по време на откриването на конфигурацията за сървъра за самоличност",
"General failure": "Обща грешка",
"That matches!": "Това съвпада!",
"That doesn't match.": "Това не съвпада.",
"Go back to set it again.": "Върнете се за да настройте нова.",
"Unable to create key backup": "Неуспешно създаване на резервно копие на ключа",
"Unable to load commit detail: %(msg)s": "Неуспешно зареждане на информация за commit: %(msg)s",
"New Recovery Method": "Нов метод за възстановяване",
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ако не сте настройвали новия метод за възстановяване, вероятно някой се опитва да проникне в акаунта Ви. Веднага променете паролата на акаунта си и настройте нов метод за възстановяване от Настройки.",
"Set up Secure Messages": "Настрой Защитени Съобщения",
"Go to Settings": "Отиди в Настройки",
"The following users may not exist": "Следните потребители може да не съществуват",
"Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Не бяга открити профили за изброените по-долу Matrix идентификатори. Желаете ли да ги поканите въпреки това?",
"Invite anyway and never warn me again": "Покани въпреки това и не питай отново",
"Invite anyway": "Покани въпреки това",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Изпратихме Ви имейл за да потвърдим адреса Ви. Последвайте инструкциите в имейла и след това кликнете на бутона по-долу.",
"Email Address": "Имейл адрес",
"Unable to verify phone number.": "Неуспешно потвърждение на телефонния номер.",
"Verification code": "Код за потвърждение",
"Phone Number": "Телефонен номер",
"Room information": "Информация за стаята",
"Room Addresses": "Адреси на стаята",
"Email addresses": "Имейл адреси",
"Phone numbers": "Телефонни номера",
"Account management": "Управление на акаунта",
"Ignored users": "Игнорирани потребители",
"Missing media permissions, click the button below to request.": "Липсва достъп до медийните устройства. Кликнете бутона по-долу за да поискате такъв.",
"Request media permissions": "Поискай достъп до медийните устройства",
"Voice & Video": "Глас и видео",
"Main address": "Основен адрес",
"Room avatar": "Снимка на стаята",
"Room Name": "Име на стаята",
@ -253,8 +186,6 @@
"Incoming Verification Request": "Входяща заявка за потвърждение",
"Email (optional)": "Имейл (незадължително)",
"Join millions for free on the largest public server": "Присъединете се безплатно към милиони други на най-големия публичен сървър",
"Recovery Method Removed": "Методът за възстановяване беше премахнат",
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ако не сте премахнали метода за възстановяване, е възможно нападател да се опитва да получи достъп до акаунта Ви. Променете паролата на акаунта и настройте нов метод за възстановяване веднага от Настройки.",
"Dog": "Куче",
"Cat": "Котка",
"Lion": "Лъв",
@ -327,8 +258,6 @@
"You'll lose access to your encrypted messages": "Ще загубите достъп до шифрованите си съобщения",
"Are you sure you want to sign out?": "Сигурни ли сте, че искате да излезете от профила?",
"<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Внимание</b>: настройването на резервно копие на ключовете трябва да се прави само от доверен компютър.",
"Your keys are being backed up (the first backup could take a few minutes).": "Прави се резервно копие на ключовете Ви (първото копие може да отнеме няколко минути).",
"Success!": "Успешно!",
"Scissors": "Ножици",
"Error updating main address": "Грешка при обновяване на основния адрес",
"There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Случи се грешка при обновяването на основния адрес за стаята. Може да не е позволено от сървъра, или да се е случила друга временна грешка.",
@ -359,31 +288,10 @@
"Cancel All": "Откажи всички",
"Upload Error": "Грешка при качване",
"Remember my selection for this widget": "Запомни избора ми за това приспособление",
"Join the conversation with an account": "Присъедини се към разговор с акаунт",
"Sign Up": "Регистриране",
"Reason: %(reason)s": "Причина: %(reason)s",
"Forget this room": "Пропусни тази стая",
"Re-join": "Връщане",
"You were banned from %(roomName)s by %(memberName)s": "Получихте забрана за %(roomName)s от %(memberName)s",
"Something went wrong with your invite to %(roomName)s": "Нещо нежелано се случи с вашата покана към %(roomName)s",
"You can only join it with a working invite.": "Да се присъедините можете само с активна покана.",
"Join the discussion": "Присъединете се към разговора",
"Try to join anyway": "Опитай да се присъединиш все пак",
"Do you want to chat with %(user)s?": "Желаете ли да си поговорите с %(user)s?",
"Do you want to join %(roomName)s?": "Желаете ли да се присъедините към %(roomName)s?",
"<userName/> invited you": "<userName/> ви покани",
"You're previewing %(roomName)s. Want to join it?": "Предварителен преглед на %(roomName)s. Желаете ли да се влезете?",
"%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s не може да бъде прегледана предварително. Желаете ли да се влезете?",
"Uploaded sound": "Качен звук",
"Sounds": "Звуци",
"Notification sound": "Звук за уведомление",
"Set a new custom sound": "Настрой нов собствен звук",
"Browse": "Избор",
"This room has already been upgraded.": "Тази стая вече е била обновена.",
"edited": "редактирано",
"Edit message": "Редактирай съобщението",
"Some characters not allowed": "Някои символи не са позволени",
"Add room": "Добави стая",
"Failed to get autodiscovery configuration from server": "Неуспешно автоматично откриване на конфигурацията за сървъра",
"Invalid base_url for m.homeserver": "Невалиден base_url в m.homeserver",
"Homeserver URL does not appear to be a valid Matrix homeserver": "Homeserver адресът не изглежда да е валиден Matrix сървър",
@ -398,55 +306,15 @@
"Clear all data": "Изчисти всички данни",
"Your homeserver doesn't seem to support this feature.": "Не изглежда сървърът ви да поддържа тази функция.",
"Resend %(unsentCount)s reaction(s)": "Изпрати наново %(unsentCount)s реакция(и)",
"Failed to re-authenticate due to a homeserver problem": "Неуспешна повторна автентикация поради проблем със сървъра",
"Clear personal data": "Изчисти личните данни",
"Find others by phone or email": "Открийте други по телефон или имейл",
"Be found by phone or email": "Бъдете открит по телефон или имейл",
"Checking server": "Проверка на сървъра",
"The identity server you have chosen does not have any terms of service.": "Избраният от вас сървър за самоличност няма условия за ползване на услугата.",
"Terms of service not accepted or the identity server is invalid.": "Условията за ползване не бяха приети или сървъра за самоличност е невалиден.",
"Disconnect from the identity server <idserver />?": "Прекъсване на връзката със сървър за самоличност <idserver />?",
"You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "В момента използвате <server></server> за да откривате и да бъдете открити от познати ваши контакти. Може да промените сървъра за самоличност по-долу.",
"You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "В момента не използвате сървър за самоличност. За да откривате и да бъдете открити от познати ваши контакти, добавете такъв по-долу.",
"Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Прекъсването на връзката със сървъра ви за самоличност означава че няма да можете да бъдете открити от други потребители или да каните хора по имейл или телефонен номер.",
"Enter a new identity server": "Въведете нов сървър за самоличност",
"Discovery": "Откриване",
"Deactivate account": "Деактивиране на акаунт",
"Unable to revoke sharing for email address": "Неуспешно оттегляне на споделянето на имейл адреса",
"Unable to share email address": "Неуспешно споделяне на имейл адрес",
"Discovery options will appear once you have added an email above.": "Опциите за откриване ще се покажат след като добавите имейл адрес по-горе.",
"Unable to revoke sharing for phone number": "Неуспешно оттегляне на споделянето на телефонен номер",
"Unable to share phone number": "Неуспешно споделяне на телефонен номер",
"Please enter verification code sent via text.": "Въведете кода за потвърждение получен в SMS.",
"Discovery options will appear once you have added a phone number above.": "Опциите за откриване ще се покажат след като добавите телефонен номер по-горе.",
"Remove %(email)s?": "Премахни %(email)s?",
"Remove %(phone)s?": "Премахни %(phone)s?",
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Беше изпратено SMS съобщение към +%(msisdn)s. Въведете съдържащият се код за потвърждение.",
"Command Help": "Помощ за команди",
"If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Ако не искате да използвате <server /> за да откривате и да бъдете откриваеми от познати ваши контакти, въведете друг сървър за самоличност по-долу.",
"Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Използването на сървър за самоличност не е задължително. Ако не използвате такъв, няма да бъдете откриваеми от други потребители и няма да можете да ги каните по имейл или телефон.",
"Do not use an identity server": "Не ползвай сървър за самоличност",
"Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Приемете условията за ползване на сървъра за самоличност (%(serverName)s) за да бъдете откриваеми по имейл адрес или телефонен номер.",
"Error changing power level": "Грешка при промяната на нивото на достъп",
"An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "Възникна грешка при промяната на нивото на достъп на потребителя. Уверете се, че имате необходимите привилегии и опитайте пак.",
"Deactivate user?": "Деактивиране на потребителя?",
"Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Деактивирането на потребителя ще ги изхвърли от профила и няма да им позволи да влязат пак. Също така, ще напуснат всички стаи, в които са. Действието е необратимо. Сигурните ли сте, че искате да деактивирате този потребител?",
"Deactivate user": "Деактивирай потребителя",
"This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Тази покана за %(roomName)s е била изпратена към адрес %(email)s, който не е асоцииран с профила ви",
"Link this email with your account in Settings to receive invites directly in %(brand)s.": "Свържете този имейл адрес с профила си от Настройки за да получавате покани директно в %(brand)s.",
"This invite to %(roomName)s was sent to %(email)s": "Тази покана за %(roomName)s беше изпратена към адрес %(email)s",
"Use an identity server in Settings to receive invites directly in %(brand)s.": "Използвайте сървър за самоличност от Настройки за да получавате покани директно в %(brand)s.",
"Share this email in Settings to receive invites directly in %(brand)s.": "Споделете този имейл в Настройки за да получавате покани директно в %(brand)s.",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Използвайте сървър за самоличност за да каните по имейл. <default>Използвайте сървъра за самоличност по подразбиране (%(defaultIdentityServerName)s)</default> или настройте друг в <settings>Настройки</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Използвайте сървър за самоличност за да каните по имейл. Управлявайте в <settings>Настройки</settings>.",
"Change identity server": "Промени сървъра за самоличност",
"Disconnect from the identity server <current /> and connect to <new /> instead?": "Прекъсване на връзката със сървър за самоличност <current /> и свързване с <new />?",
"Disconnect identity server": "Прекъсни връзката със сървъра за самоличност",
"You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Все още <b>споделяте лични данни</b> със сървър за самоличност <idserver />.",
"We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Препоръчваме да премахнете имейл адреса и телефонния си номер от сървъра за самоличност преди прекъсване на връзката.",
"Disconnect anyway": "Прекъсни въпреки всичко",
"Error changing power level requirement": "Грешка при промяна на изискванията за ниво на достъп",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Възникна грешка при промяна на изискванията за нива на достъп до стаята. Уверете се, че имате необходимите права и опитайте пак.",
"No recent messages by %(user)s found": "Не са намерени скорошни съобщения от %(user)s",
"Try scrolling up in the timeline to see if there are any earlier ones.": "Опитайте се да проверите по-нагоре в историята за по-ранни.",
"Remove recent messages by %(user)s": "Премахване на скорошни съобщения от %(user)s",
@ -456,19 +324,8 @@
"one": "Премахни 1 съобщение"
},
"Remove recent messages": "Премахни скорошни съобщения",
"Italics": "Наклонено",
"Explore rooms": "Открий стаи",
"Verify the link in your inbox": "Потвърдете линка във вашата пощенска кутия",
"e.g. my-room": "например my-room",
"Close dialog": "Затвори прозореца",
"You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Би било добре да <b>премахнете личните си данни</b> от сървъра за самоличност <idserver /> преди прекъсване на връзката. За съжаление, сървърът за самоличност <idserver /> в момента не е достъпен.",
"You should:": "Ще е добре да:",
"check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "проверите браузър добавките за всичко, което може да блокира връзката със сървъра за самоличност (например Privacy Badger)",
"contact the administrators of identity server <idserver />": "се свържете с администратора на сървъра за самоличност <idserver />",
"wait and try again later": "изчакате и опитате пак",
"Your email address hasn't been verified yet": "Имейл адресът ви все още не е потвърден",
"Click the link in the email you received to verify and then click continue again.": "Кликнете на връзката получена по имейл за да потвърдите, а след това натиснете продължи отново.",
"Room %(name)s": "Стая %(name)s",
"Failed to deactivate user": "Неуспешно деактивиране на потребител",
"This client does not support end-to-end encryption.": "Този клиент не поддържа шифроване от край до край.",
"Messages in this room are not end-to-end encrypted.": "Съобщенията в тази стая не са шифровани от край до край.",
@ -484,12 +341,7 @@
"%(name)s cancelled": "%(name)s отказа",
"%(name)s wants to verify": "%(name)s иска да извърши потвърждение",
"You sent a verification request": "Изпратихте заявка за потвърждение",
"Manage integrations": "Управление на интеграциите",
"None": "Няма нищо",
"Unencrypted": "Нешифровано",
"Close preview": "Затвори прегледа",
"<userName/> wants to chat": "<userName/> иска да чати",
"Start chatting": "Започни чат",
"Failed to connect to integration manager": "Неуспешна връзка с мениджъра на интеграции",
"Hide verified sessions": "Скрий потвърдените сесии",
"%(count)s verified sessions": {
@ -508,7 +360,6 @@
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "Ще обновите стаята от <oldVersion /> до <newVersion />.",
"Country Dropdown": "Падащо меню за избор на държава",
"Verification Request": "Заявка за потвърждение",
"Unable to set up secret storage": "Неуспешна настройка на секретно складиране",
"Recent Conversations": "Скорошни разговори",
"Show more": "Покажи повече",
"Direct Messages": "Директни съобщения",
@ -520,8 +371,6 @@
"Lock": "Заключи",
"This backup is trusted because it has been restored on this session": "Това резервно копие е доверено, защото е било възстановено в текущата сесия",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "За да съобщените за проблем със сигурността свързан с Matrix, прочетете <a>Политиката за споделяне на проблеми със сигурността</a> на Matrix.org.",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "Тази стая препредава съобщения със следните платформи. <a>Научи повече.</a>",
"Bridges": "Мостове",
"This user has not verified all of their sessions.": "Този потребител не е верифицирал всичките си сесии.",
"You have not verified this user.": "Не сте верифицирали този потребител.",
"You have verified this user. This user has verified all of their sessions.": "Верифицирали сте този потребител. Този потребител е верифицирал всичките си сесии.",
@ -531,7 +380,6 @@
"Encrypted by an unverified session": "Шифровано от неверифицирана сесия",
"Encrypted by a deleted session": "Шифровано от изтрита сесия",
"Scroll to most recent messages": "Отиди до най-скорошните съобщения",
"Reject & Ignore user": "Откажи и игнорирай потребителя",
"There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Възникна грешка при обновяване на алтернативните адреси на стаята. Или не е позволено от сървъра или се е случила временна грешка.",
"Local address": "Локален адрес",
"Published Addresses": "Публикувани адреси",
@ -639,24 +487,11 @@
"Keys restored": "Ключовете бяха възстановени",
"Successfully restored %(sessionCount)s keys": "Успешно бяха възстановени %(sessionCount)s ключа",
"Sign in with SSO": "Влезте със SSO",
"Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Администраторът на сървъра е изключил шифроване от край-до-край по подразбиране за лични стаи и за директни съобщения.",
"Switch theme": "Смени темата",
"Confirm encryption setup": "Потвърждение на настройки за шифроване",
"Click the button below to confirm setting up encryption.": "Кликнете бутона по-долу за да потвърдите настройването на шифроване.",
"Enter your account password to confirm the upgrade:": "Въведете паролата за профила си за да потвърдите обновлението:",
"Restore your key backup to upgrade your encryption": "Възстановете резервното копие на ключа за да обновите шифроването",
"You'll need to authenticate with the server to confirm the upgrade.": "Ще трябва да се автентикирате пред сървъра за да потвърдите обновяването.",
"Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Обновете тази сесия, за да може да потвърждава други сесии, давайки им достъп до шифрованите съобщения и маркирайки ги като доверени за другите потребители.",
"Use a different passphrase?": "Използвай друга парола?",
"Unable to query secret storage status": "Неуспешно допитване за състоянието на секретното складиране",
"Upgrade your encryption": "Обновете шифроването",
"Create key backup": "Създай резервно копие на ключовете",
"This session is encrypting history using the new recovery method.": "Тази сесия шифрова историята използвайки новия метод за възстановяване.",
"If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Ако сте направили това без да искате, може да настройте защитени съобщения за тази сесия, което ще зашифрова наново историята на съобщенията използвайки новия метод за възстановяване.",
"No recently visited rooms": "Няма наскоро-посетени стаи",
"The authenticity of this encrypted message can't be guaranteed on this device.": "Автентичността на това шифровано съобщение не може да бъде гарантирана на това устройство.",
"Message preview": "Преглед на съобщението",
"Room options": "Настройки на стаята",
"This room is public": "Тази стая е публична",
"Unable to set up keys": "Неуспешна настройка на ключовете",
"Use your Security Key to continue.": "Използвайте ключа си за сигурност за да продължите.",
@ -705,23 +540,7 @@
"You can only pin up to %(count)s widgets": {
"other": "Може да закачите максимум %(count)s приспособления"
},
"Explore public rooms": "Прегледай публични стаи",
"Show Widgets": "Покажи приспособленията",
"Hide Widgets": "Скрий приспособленията",
"Backup version:": "Версия на резервното копие:",
"Save your Security Key": "Запази ключа за сигурност",
"Confirm Security Phrase": "Потвърди фразата за сигурност",
"Set a Security Phrase": "Настрой фраза за сигурност",
"You can also set up Secure Backup & manage your keys in Settings.": "Също така, може да конфигурирате защитено резервно копиране и да управлявате ключовете си от Настройки.",
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Ако се откажете сега, може да загубите достъп до шифрованите съобщения и данни, в случай че загубите достъп до тази сесия.",
"Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Използвайте секретна фраза, която знаете само вие. При необходимост запазете и ключа за сигурност за резервното копие.",
"Enter a Security Phrase": "Въведете фраза за сигурност",
"Generate a Security Key": "Генерирай ключ за сигурност",
"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Предпазете се от загуба на достъп до шифрованите съобщения и данни като направите резервно копие на ключовете за шифроване върху сървъра.",
"This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Тази сесия откри, че вашата фраза за сигурност и ключ за защитени съобщения бяха премахнати.",
"A new Security Phrase and key for Secure Messages have been detected.": "Новa фраза за сигурност и ключ за защитени съобщения бяха открити.",
"Great! This Security Phrase looks strong enough.": "Чудесно! Тази фраза за сигурност изглежда достатъчно силна.",
"Confirm your Security Phrase": "Потвърдете вашата фраза за сигурност",
"Anguilla": "Ангила",
"British Indian Ocean Territory": "Британска територия в Индийския океан",
"Pitcairn Islands": "острови Питкерн",
@ -971,18 +790,10 @@
"Afghanistan": "Афганистан",
"United States": "Съединените щати",
"United Kingdom": "Обединеното кралство",
"Add existing room": "Добави съществуваща стая",
"Leave space": "Напусни пространство",
"Create a space": "Създаване на пространство",
"unknown person": "",
"Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Вашият %(brand)s не позволява да използвате мениджъра на интеграции за да направите това. Свържете се с администратор.",
"Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Мениджърът на интеграции получава конфигурационни данни, може да модифицира приспособления, да изпраща покани за стаи и да настройва нива на достъп от ваше име.",
"Use an integration manager to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции за управление на ботове, приспособления и стикери.",
"Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции <b>%(serverName)s</b> за управление на ботове, приспособления и стикери.",
"Identity server (%(server)s)": "Сървър за самоличност (%(server)s)",
"Could not connect to identity server": "Неуспешна връзка със сървъра за самоличност",
"Not a valid identity server (status code %(code)s)": "Невалиден сървър за самоличност (статус код %(code)s)",
"Identity server URL must be HTTPS": "Адресът на сървъра за самоличност трябва да бъде HTTPS",
"common": {
"about": "Относно",
"analytics": "Статистика",
@ -1054,7 +865,13 @@
"general": "Общи",
"profile": "Профил",
"display_name": "Име",
"user_avatar": "Профилна снимка"
"user_avatar": "Профилна снимка",
"authentication": "Автентикация",
"rooms": "Стаи",
"low_priority": "Нисък приоритет",
"historical": "Архив",
"go_to_settings": "Отиди в Настройки",
"setup_secure_messages": "Настрой Защитени Съобщения"
},
"action": {
"continue": "Продължи",
@ -1136,7 +953,11 @@
"unban": "Отблокирай",
"click_to_copy": "Натиснете за копиране",
"hide_advanced": "Скрий разширени настройки",
"show_advanced": "Покажи разширени настройки"
"show_advanced": "Покажи разширени настройки",
"unignore": "Премахни игнорирането",
"explore_rooms": "Открий стаи",
"add_existing_room": "Добави съществуваща стая",
"explore_public_rooms": "Прегледай публични стаи"
},
"a11y": {
"user_menu": "Потребителско меню",
@ -1149,7 +970,8 @@
"one": "1 непрочетено съобщение."
},
"unread_messages": "Непрочетени съобщения.",
"jump_first_invite": "Отиди до първата покана."
"jump_first_invite": "Отиди до първата покана.",
"room_name": "Стая %(name)s"
},
"labs": {
"pinning": "Функция за закачане на съобщения",
@ -1223,7 +1045,13 @@
"room_a11y": "Подсказка за стаи",
"user_description": "Потребители",
"user_a11y": "Подсказка за потребители"
}
},
"room_upgraded_link": "Разговора продължава тук.",
"room_upgraded_notice": "Тази стая е била заменена и вече не е активна.",
"no_perms_notice": "Нямате разрешение да публикувате в тази стая",
"poll_button_no_perms_title": "Необходимо е разрешение",
"format_italics": "Наклонено",
"replying_title": "Отговаря"
},
"Code": "Код",
"power_level": {
@ -1335,7 +1163,14 @@
"inline_url_previews_room_account": "Включване на URL прегледи за тази стая (засяга само Вас)",
"inline_url_previews_room": "Включване по подразбиране на URL прегледи за участници в тази стая",
"voip": {
"mirror_local_feed": "Показвай ми огледално моя видео образ"
"mirror_local_feed": "Показвай ми огледално моя видео образ",
"missing_permissions_prompt": "Липсва достъп до медийните устройства. Кликнете бутона по-долу за да поискате такъв.",
"request_permissions": "Поискай достъп до медийните устройства",
"audio_output": "Аудио изходи",
"audio_output_empty": "Не са открити аудио изходи",
"audio_input_empty": "Няма открити микрофони",
"video_input_empty": "Няма открити уеб камери",
"title": "Глас и видео"
},
"security": {
"message_search_disable_warning": "Ако е изключено, съобщения от шифровани стаи няма да се показват в резултатите от търсения.",
@ -1397,7 +1232,10 @@
"key_backup_connect": "Свържи тази сесия с резервно копие на ключовете",
"key_backup_complete": "Всички ключове са в резервното копие",
"key_backup_algorithm": "Алгоритъм:",
"key_backup_inactive_warning": "На ключовете ви <b>не се прави резервно копие от тази сесия</b>."
"key_backup_inactive_warning": "На ключовете ви <b>не се прави резервно копие от тази сесия</b>.",
"key_backup_active_version_none": "Няма нищо",
"ignore_users_section": "Игнорирани потребители",
"e2ee_default_disabled_warning": "Администраторът на сървъра е изключил шифроване от край-до-край по подразбиране за лични стаи и за директни съобщения."
},
"preferences": {
"room_list_heading": "Списък със стаи",
@ -1426,7 +1264,81 @@
"add_msisdn_dialog_title": "Добави телефонен номер",
"name_placeholder": "Няма име",
"error_saving_profile_title": "Неуспешно запазване на профила ви",
"error_saving_profile": "Операцията не можа да бъде завършена"
"error_saving_profile": "Операцията не можа да бъде завършена",
"error_password_change_403": "Неуспешна промяна. Правилно ли сте въвели Вашата парола?",
"emails_heading": "Имейл адреси",
"msisdns_heading": "Телефонни номера",
"discovery_needs_terms": "Приемете условията за ползване на сървъра за самоличност (%(serverName)s) за да бъдете откриваеми по имейл адрес или телефонен номер.",
"deactivate_section": "Затвори акаунта",
"account_management_section": "Управление на акаунта",
"discovery_section": "Откриване",
"error_revoke_email_discovery": "Неуспешно оттегляне на споделянето на имейл адреса",
"error_share_email_discovery": "Неуспешно споделяне на имейл адрес",
"email_not_verified": "Имейл адресът ви все още не е потвърден",
"email_verification_instructions": "Кликнете на връзката получена по имейл за да потвърдите, а след това натиснете продължи отново.",
"error_email_verification": "Неуспешно потвърждение на имейл адрес.",
"discovery_email_verification_instructions": "Потвърдете линка във вашата пощенска кутия",
"discovery_email_empty": "Опциите за откриване ще се покажат след като добавите имейл адрес по-горе.",
"error_revoke_msisdn_discovery": "Неуспешно оттегляне на споделянето на телефонен номер",
"error_share_msisdn_discovery": "Неуспешно споделяне на телефонен номер",
"error_msisdn_verification": "Неуспешно потвърждение на телефонния номер.",
"incorrect_msisdn_verification": "Неправилен код за потвърждение",
"msisdn_verification_instructions": "Въведете кода за потвърждение получен в SMS.",
"msisdn_verification_field_label": "Код за потвърждение",
"discovery_msisdn_empty": "Опциите за откриване ще се покажат след като добавите телефонен номер по-горе.",
"error_set_name": "Неуспешно задаване на име",
"error_remove_3pid": "Неуспешно премахване на информацията за контакти",
"remove_email_prompt": "Премахни %(email)s?",
"error_invalid_email": "Невалиден имейл адрес",
"error_invalid_email_detail": "Това не изглежда да е валиден имейл адрес",
"error_add_email": "Неуспешно добавяне на имейл адрес",
"add_email_instructions": "Изпратихме Ви имейл за да потвърдим адреса Ви. Последвайте инструкциите в имейла и след това кликнете на бутона по-долу.",
"email_address_label": "Имейл адрес",
"remove_msisdn_prompt": "Премахни %(phone)s?",
"add_msisdn_instructions": "Беше изпратено SMS съобщение към +%(msisdn)s. Въведете съдържащият се код за потвърждение.",
"msisdn_label": "Телефонен номер"
},
"key_backup": {
"backup_in_progress": "Прави се резервно копие на ключовете Ви (първото копие може да отнеме няколко минути).",
"backup_success": "Успешно!",
"create_title": "Създай резервно копие на ключовете",
"cannot_create_backup": "Неуспешно създаване на резервно копие на ключа",
"setup_secure_backup": {
"generate_security_key_title": "Генерирай ключ за сигурност",
"enter_phrase_title": "Въведете фраза за сигурност",
"description": "Предпазете се от загуба на достъп до шифрованите съобщения и данни като направите резервно копие на ключовете за шифроване върху сървъра.",
"requires_password_confirmation": "Въведете паролата за профила си за да потвърдите обновлението:",
"requires_key_restore": "Възстановете резервното копие на ключа за да обновите шифроването",
"requires_server_authentication": "Ще трябва да се автентикирате пред сървъра за да потвърдите обновяването.",
"session_upgrade_description": "Обновете тази сесия, за да може да потвърждава други сесии, давайки им достъп до шифрованите съобщения и маркирайки ги като доверени за другите потребители.",
"phrase_strong_enough": "Чудесно! Тази фраза за сигурност изглежда достатъчно силна.",
"pass_phrase_match_success": "Това съвпада!",
"use_different_passphrase": "Използвай друга парола?",
"pass_phrase_match_failed": "Това не съвпада.",
"set_phrase_again": "Върнете се за да настройте нова.",
"confirm_security_phrase": "Потвърдете вашата фраза за сигурност",
"secret_storage_query_failure": "Неуспешно допитване за състоянието на секретното складиране",
"cancel_warning": "Ако се откажете сега, може да загубите достъп до шифрованите съобщения и данни, в случай че загубите достъп до тази сесия.",
"settings_reminder": "Също така, може да конфигурирате защитено резервно копиране и да управлявате ключовете си от Настройки.",
"title_upgrade_encryption": "Обновете шифроването",
"title_set_phrase": "Настрой фраза за сигурност",
"title_confirm_phrase": "Потвърди фразата за сигурност",
"title_save_key": "Запази ключа за сигурност",
"unable_to_setup": "Неуспешна настройка на секретно складиране",
"use_phrase_only_you_know": "Използвайте секретна фраза, която знаете само вие. При необходимост запазете и ключа за сигурност за резервното копие."
}
},
"key_export_import": {
"export_title": "Експортиране на ключове за стаята",
"export_description_1": "Този процес Ви позволява да експортирате във файл ключовете за съобщения в шифровани стаи. Така ще можете да импортирате файла в друг Matrix клиент, така че той също да може да разшифрова такива съобщения.",
"enter_passphrase": "Въведи парола",
"confirm_passphrase": "Потвърди парола",
"phrase_cannot_be_empty": "Паролата не трябва да е празна",
"phrase_must_match": "Паролите трябва да съвпадат",
"import_title": "Импортиране на ключове за стая",
"import_description_1": "Този процес позволява да импортирате ключове за шифроване, които преди сте експортирали от друг Matrix клиент. Тогава ще можете да разшифровате всяко съобщение, което другият клиент може да разшифрова.",
"import_description_2": "Експортираният файл може да бъде предпазен с парола. Трябва да въведете парола тук, за да разшифровате файла.",
"file_to_import": "Файл за импортиране"
}
},
"devtools": {
@ -1665,6 +1577,9 @@
"creation_summary_room": "%(creator)s създаде и настрой стаята.",
"context_menu": {
"external_url": "URL на източника"
},
"url_preview": {
"close": "Затвори прегледа"
}
},
"slash_command": {
@ -1836,7 +1751,14 @@
"send_event_type": "Изпрати %(eventType)s събития",
"title": "Роли и привилегии",
"permissions_section": "Разрешения",
"permissions_section_description_room": "Изберете ролите необходими за промяна на различни части от стаята"
"permissions_section_description_room": "Изберете ролите необходими за промяна на различни части от стаята",
"error_unbanning": "Неуспешно отблокиране",
"banned_by": "Блокиран от %(displayName)s",
"ban_reason": "Причина",
"error_changing_pl_reqs_title": "Грешка при промяна на изискванията за ниво на достъп",
"error_changing_pl_reqs_description": "Възникна грешка при промяна на изискванията за нива на достъп до стаята. Уверете се, че имате необходимите права и опитайте пак.",
"error_changing_pl_title": "Грешка при промяната на нивото на достъп",
"error_changing_pl_description": "Възникна грешка при промяната на нивото на достъп на потребителя. Уверете се, че имате необходимите привилегии и опитайте пак."
},
"security": {
"strict_encryption": "Никога не изпращай шифровани съобщения към непотвърдени сесии в тази стая от тази сесия",
@ -1861,16 +1783,30 @@
"default_url_previews_off": "URL прегледи са изключени по подразбиране за участниците в тази стая.",
"url_preview_encryption_warning": "В шифровани стаи като тази, по подразбиране URL прегледите са изключени, за да се подсигури че сървърът (където става генерирането на прегледите) не може да събира информация за връзките споделени в стаята.",
"url_preview_explainer": "Когато се сподели URL връзка в съобщение, може да бъде показан URL преглед даващ повече информация за връзката (заглавие, описание и картинка от уебсайта).",
"url_previews_section": "URL прегледи"
"url_previews_section": "URL прегледи",
"aliases_section": "Адреси на стаята",
"other_section": "Други"
},
"advanced": {
"unfederated": "Тази стая не е достъпна за отдалечени Matrix сървъри",
"room_upgrade_button": "Обнови тази стая до препоръчаната версия на стаята",
"room_predecessor": "Виж по-стари съобщения в %(roomName)s.",
"room_version_section": "Версия на стаята",
"room_version": "Версия на стаята:"
"room_version": "Версия на стаята:",
"information_section_room": "Информация за стаята"
},
"upload_avatar_label": "Качи профилна снимка"
"upload_avatar_label": "Качи профилна снимка",
"bridges": {
"description": "Тази стая препредава съобщения със следните платформи. <a>Научи повече.</a>",
"title": "Мостове"
},
"notifications": {
"uploaded_sound": "Качен звук",
"sounds_section": "Звуци",
"notification_sound": "Звук за уведомление",
"custom_sound_prompt": "Настрой нов собствен звук",
"browse_button": "Избор"
}
},
"encryption": {
"verification": {
@ -1912,7 +1848,19 @@
"cross_signing_ready": "Кръстосаното-подписване е готово за използване.",
"cross_signing_untrusted": "Профилът ви има самоличност за кръстосано подписване в секретно складиране, но все още не е доверено от тази сесия.",
"cross_signing_not_ready": "Кръстосаното-подписване не е настроено.",
"not_supported": "<не се поддържа>"
"not_supported": "<не се поддържа>",
"new_recovery_method_detected": {
"title": "Нов метод за възстановяване",
"description_1": "Новa фраза за сигурност и ключ за защитени съобщения бяха открити.",
"description_2": "Тази сесия шифрова историята използвайки новия метод за възстановяване.",
"warning": "Ако не сте настройвали новия метод за възстановяване, вероятно някой се опитва да проникне в акаунта Ви. Веднага променете паролата на акаунта си и настройте нов метод за възстановяване от Настройки."
},
"recovery_method_removed": {
"title": "Методът за възстановяване беше премахнат",
"description_1": "Тази сесия откри, че вашата фраза за сигурност и ключ за защитени съобщения бяха премахнати.",
"description_2": "Ако сте направили това без да искате, може да настройте защитени съобщения за тази сесия, което ще зашифрова наново историята на съобщенията използвайки новия метод за възстановяване.",
"warning": "Ако не сте премахнали метода за възстановяване, е възможно нападател да се опитва да получи достъп до акаунта Ви. Променете паролата на акаунта и настройте нов метод за възстановяване веднага от Настройки."
}
},
"emoji": {
"category_frequently_used": "Често използвани",
@ -2019,7 +1967,9 @@
"autodiscovery_unexpected_error_hs": "Неочаквана грешка в намирането на сървърната конфигурация",
"autodiscovery_unexpected_error_is": "Неочаквана грешка при откриване на конфигурацията на сървъра за самоличност",
"incorrect_credentials_detail": "Моля, обърнете внимание, че влизате в %(hs)s сървър, а не в matrix.org.",
"create_account_title": "Създай акаунт"
"create_account_title": "Създай акаунт",
"failed_soft_logout_homeserver": "Неуспешна повторна автентикация поради проблем със сървъра",
"soft_logout_subheading": "Изчисти личните данни"
},
"export_chat": {
"messages": "Съобщения"
@ -2038,7 +1988,9 @@
"show_less": "Покажи по-малко",
"notification_options": "Настройки за уведомление",
"failed_remove_tag": "Неуспешно премахване на %(tagName)s етикет от стаята",
"failed_add_tag": "Неуспешно добавяне на %(tagName)s етикет в стаята"
"failed_add_tag": "Неуспешно добавяне на %(tagName)s етикет в стаята",
"breadcrumbs_empty": "Няма наскоро-посетени стаи",
"add_room_label": "Добави стая"
},
"report_content": {
"missing_reason": "Въведете защо докладвате.",
@ -2146,7 +2098,8 @@
"lists_heading": "Абонирани списъци",
"lists_description_1": "Абонирането към списък ще направи така, че да се присъедините към него!",
"lists_description_2": "Ако това не е каквото искате, използвайте друг инструмент за игнориране на потребители.",
"lists_new_label": "Идентификатор или адрес на стая списък за блокиране"
"lists_new_label": "Идентификатор или адрес на стая списък за блокиране",
"rules_empty": "Няма нищо"
},
"create_space": {
"name_required": "Моля, въведете име на пространството",
@ -2183,8 +2136,40 @@
"unfavourite": "В любими",
"favourite": "Любим",
"low_priority": "Нисък приоритет",
"forget": "Забрави стаята"
}
"forget": "Забрави стаята",
"title": "Настройки на стаята"
},
"invite_this_room": "Покани в тази стая",
"header": {
"forget_room_button": "Забрави стаята",
"hide_widgets_button": "Скрий приспособленията",
"show_widgets_button": "Покажи приспособленията"
},
"join_title_account": "Присъедини се към разговор с акаунт",
"join_button_account": "Регистриране",
"kick_reason": "Причина: %(reason)s",
"forget_room": "Пропусни тази стая",
"rejoin_button": "Връщане",
"banned_from_room_by": "Получихте забрана за %(roomName)s от %(memberName)s",
"3pid_invite_error_title_room": "Нещо нежелано се случи с вашата покана към %(roomName)s",
"3pid_invite_error_invite_subtitle": "Да се присъедините можете само с активна покана.",
"3pid_invite_error_invite_action": "Опитай да се присъединиш все пак",
"join_the_discussion": "Присъединете се към разговора",
"3pid_invite_email_not_found_account_room": "Тази покана за %(roomName)s е била изпратена към адрес %(email)s, който не е асоцииран с профила ви",
"link_email_to_receive_3pid_invite": "Свържете този имейл адрес с профила си от Настройки за да получавате покани директно в %(brand)s.",
"invite_sent_to_email_room": "Тази покана за %(roomName)s беше изпратена към адрес %(email)s",
"3pid_invite_no_is_subtitle": "Използвайте сървър за самоличност от Настройки за да получавате покани директно в %(brand)s.",
"invite_email_mismatch_suggestion": "Споделете този имейл в Настройки за да получавате покани директно в %(brand)s.",
"dm_invite_title": "Желаете ли да си поговорите с %(user)s?",
"dm_invite_subtitle": "<userName/> иска да чати",
"dm_invite_action": "Започни чат",
"invite_title": "Желаете ли да се присъедините към %(roomName)s?",
"invite_subtitle": "<userName/> ви покани",
"invite_reject_ignore": "Откажи и игнорирай потребителя",
"peek_join_prompt": "Предварителен преглед на %(roomName)s. Желаете ли да се влезете?",
"no_peek_join_prompt": "%(roomName)s не може да бъде прегледана предварително. Желаете ли да се влезете?",
"not_found_title_name": "%(roomName)s не съществува.",
"inaccessible_name": "%(roomName)s не е достъпна към този момент."
},
"file_panel": {
"guest_note": "Трябва да се <a>регистрирате</a>, за да използвате тази функционалност",
@ -2317,7 +2302,9 @@
"colour_bold": "Удебелено",
"error_change_title": "Промяна на настройките за уведомление",
"mark_all_read": "Маркирай всичко като прочетено",
"class_other": "Други"
"class_other": "Други",
"default": "По подразбиране",
"all_messages": "Всички съобщения"
},
"mobile_guide": {
"toast_title": "Използвайте приложението за по-добра работа",
@ -2338,6 +2325,43 @@
"a11y_jump_first_unread_room": "Отиди до първата непрочетена стая.",
"integration_manager": {
"error_connecting_heading": "Неуспешна връзка с мениджъра на интеграции",
"error_connecting": "Мениджъра на интеграции е офлайн или не може да се свърже със сървъра ви."
"error_connecting": "Мениджъра на интеграции е офлайн или не може да се свърже със сървъра ви.",
"use_im_default": "Използвай мениджър на интеграции <b>%(serverName)s</b> за управление на ботове, приспособления и стикери.",
"use_im": "Използвай мениджър на интеграции за управление на ботове, приспособления и стикери.",
"manage_title": "Управление на интеграциите",
"explainer": "Мениджърът на интеграции получава конфигурационни данни, може да модифицира приспособления, да изпраща покани за стаи и да настройва нива на достъп от ваше име."
},
"identity_server": {
"url_not_https": "Адресът на сървъра за самоличност трябва да бъде HTTPS",
"error_invalid": "Невалиден сървър за самоличност (статус код %(code)s)",
"error_connection": "Неуспешна връзка със сървъра за самоличност",
"checking": "Проверка на сървъра",
"change": "Промени сървъра за самоличност",
"change_prompt": "Прекъсване на връзката със сървър за самоличност <current /> и свързване с <new />?",
"error_invalid_or_terms": "Условията за ползване не бяха приети или сървъра за самоличност е невалиден.",
"no_terms": "Избраният от вас сървър за самоличност няма условия за ползване на услугата.",
"disconnect": "Прекъсни връзката със сървъра за самоличност",
"disconnect_server": "Прекъсване на връзката със сървър за самоличност <idserver />?",
"disconnect_offline_warning": "Би било добре да <b>премахнете личните си данни</b> от сървъра за самоличност <idserver /> преди прекъсване на връзката. За съжаление, сървърът за самоличност <idserver /> в момента не е достъпен.",
"suggestions": "Ще е добре да:",
"suggestions_1": "проверите браузър добавките за всичко, което може да блокира връзката със сървъра за самоличност (например Privacy Badger)",
"suggestions_2": "се свържете с администратора на сървъра за самоличност <idserver />",
"suggestions_3": "изчакате и опитате пак",
"disconnect_anyway": "Прекъсни въпреки всичко",
"disconnect_personal_data_warning_1": "Все още <b>споделяте лични данни</b> със сървър за самоличност <idserver />.",
"disconnect_personal_data_warning_2": "Препоръчваме да премахнете имейл адреса и телефонния си номер от сървъра за самоличност преди прекъсване на връзката.",
"url": "Сървър за самоличност (%(server)s)",
"description_connected": "В момента използвате <server></server> за да откривате и да бъдете открити от познати ваши контакти. Може да промените сървъра за самоличност по-долу.",
"change_server_prompt": "Ако не искате да използвате <server /> за да откривате и да бъдете откриваеми от познати ваши контакти, въведете друг сървър за самоличност по-долу.",
"description_disconnected": "В момента не използвате сървър за самоличност. За да откривате и да бъдете открити от познати ваши контакти, добавете такъв по-долу.",
"disconnect_warning": "Прекъсването на връзката със сървъра ви за самоличност означава че няма да можете да бъдете открити от други потребители или да каните хора по имейл или телефонен номер.",
"description_optional": "Използването на сървър за самоличност не е задължително. Ако не използвате такъв, няма да бъдете откриваеми от други потребители и няма да можете да ги каните по имейл или телефон.",
"do_not_use": "Не ползвай сървър за самоличност",
"url_field_label": "Въведете нов сървър за самоличност"
},
"member_list": {
"invited_list_heading": "Поканен",
"filter_placeholder": "Филтриране на членовете",
"power_label": "%(userName)s (ниво на достъп %(powerLevelNumber)s)"
}
}

Some files were not shown because too many files have changed in this diff Show more