Replace `newTranslatableError` with `UserFriendlyError` (#10440
* Introduce UserFriendlyError * Replace newTranslatableError with UserFriendlyError * Remove ITranslatableError * Fix up some strict lints * Document when we/why we can remove * Update matrix-web-i18n Includes changes to find `new UserFriendlyError`, see https://github.com/matrix-org/matrix-web-i18n/pull/6 * Include room ID in error * Translate fallback error * Translate better * Update i18n strings * Better re-use * Minor comment fixespull/28788/head^2
parent
567248d5c5
commit
ff1468b6d3
|
@ -28,7 +28,7 @@
|
|||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"matrix_i18n_extra_translation_funcs": [
|
||||
"newTranslatableError"
|
||||
"UserFriendlyError"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
|
@ -203,7 +203,7 @@
|
|||
"jest-mock": "^29.2.2",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"matrix-web-i18n": "^1.3.0",
|
||||
"matrix-web-i18n": "^1.4.0",
|
||||
"mocha-junit-reporter": "^2.2.0",
|
||||
"node-fetch": "2",
|
||||
"postcss-scss": "^4.0.4",
|
||||
|
|
|
@ -187,6 +187,11 @@ declare global {
|
|||
}
|
||||
|
||||
interface Error {
|
||||
// Standard
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
||||
cause?: unknown;
|
||||
|
||||
// Non-standard
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
|
||||
fileName?: string;
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber
|
||||
|
@ -195,6 +200,22 @@ declare global {
|
|||
columnNumber?: number;
|
||||
}
|
||||
|
||||
// We can remove these pieces if we ever update to `target: "es2022"` in our
|
||||
// TypeScript config which supports the new `cause` property, see
|
||||
// https://github.com/vector-im/element-web/issues/24913
|
||||
interface ErrorOptions {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
||||
cause?: unknown;
|
||||
}
|
||||
|
||||
interface ErrorConstructor {
|
||||
new (message?: string, options?: ErrorOptions): Error;
|
||||
(message?: string, options?: ErrorOptions): Error;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var Error: ErrorConstructor;
|
||||
|
||||
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
|
||||
interface AudioWorkletProcessor {
|
||||
readonly port: MessagePort;
|
||||
|
|
|
@ -30,7 +30,7 @@ import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/
|
|||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import { _t, _td, ITranslatableError, newTranslatableError } from "./languageHandler";
|
||||
import { _t, _td, UserFriendlyError } from "./languageHandler";
|
||||
import Modal from "./Modal";
|
||||
import MultiInviter from "./utils/MultiInviter";
|
||||
import { Linkify, topicToHtml } from "./HtmlUtils";
|
||||
|
@ -110,7 +110,7 @@ export const CommandCategories = {
|
|||
other: _td("Other"),
|
||||
};
|
||||
|
||||
export type RunResult = XOR<{ error: Error | ITranslatableError }, { promise: Promise<IContent | undefined> }>;
|
||||
export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>;
|
||||
|
||||
type RunFn = (this: Command, roomId: string, args?: string) => RunResult;
|
||||
|
||||
|
@ -163,14 +163,15 @@ export class Command {
|
|||
public run(roomId: string, threadId: string | null, args?: string): RunResult {
|
||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||
if (!this.runFn) {
|
||||
return reject(newTranslatableError("Command error: Unable to handle slash command."));
|
||||
return reject(new UserFriendlyError("Command error: Unable to handle slash command."));
|
||||
}
|
||||
|
||||
const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room;
|
||||
if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) {
|
||||
return reject(
|
||||
newTranslatableError("Command error: Unable to find rendering type (%(renderingType)s)", {
|
||||
new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", {
|
||||
renderingType,
|
||||
cause: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -310,7 +311,7 @@ export const Commands = [
|
|||
const room = cli.getRoom(roomId);
|
||||
if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) {
|
||||
return reject(
|
||||
newTranslatableError("You do not have the required permissions to use this command."),
|
||||
new UserFriendlyError("You do not have the required permissions to use this command."),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -345,10 +346,10 @@ export const Commands = [
|
|||
(async (): Promise<void> => {
|
||||
const unixTimestamp = Date.parse(args);
|
||||
if (!unixTimestamp) {
|
||||
throw newTranslatableError(
|
||||
throw new UserFriendlyError(
|
||||
"We were unable to understand the given date (%(inputDate)s). " +
|
||||
"Try using the format YYYY-MM-DD.",
|
||||
{ inputDate: args },
|
||||
{ inputDate: args, cause: undefined },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -496,7 +497,10 @@ export const Commands = [
|
|||
const room = cli.getRoom(roomId);
|
||||
if (!room) {
|
||||
return reject(
|
||||
newTranslatableError("Failed to get room topic: Unable to find room (%(roomId)s", { roomId }),
|
||||
new UserFriendlyError("Failed to get room topic: Unable to find room (%(roomId)s", {
|
||||
roomId,
|
||||
cause: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -576,13 +580,13 @@ export const Commands = [
|
|||
setToDefaultIdentityServer();
|
||||
return;
|
||||
}
|
||||
throw newTranslatableError(
|
||||
throw new UserFriendlyError(
|
||||
"Use an identity server to invite by email. Manage in Settings.",
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return reject(
|
||||
newTranslatableError("Use an identity server to invite by email. Manage in Settings."),
|
||||
new UserFriendlyError("Use an identity server to invite by email. Manage in Settings."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -594,7 +598,15 @@ export const Commands = [
|
|||
})
|
||||
.then(() => {
|
||||
if (inviter.getCompletionState(address) !== "invited") {
|
||||
throw new Error(inviter.getErrorText(address));
|
||||
const errorStringFromInviterUtility = inviter.getErrorText(address);
|
||||
if (errorStringFromInviterUtility) {
|
||||
throw new Error(errorStringFromInviterUtility);
|
||||
} else {
|
||||
throw new UserFriendlyError(
|
||||
"User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility",
|
||||
{ user: address, roomId, cause: undefined },
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -743,7 +755,12 @@ export const Commands = [
|
|||
return room.getCanonicalAlias() === roomAlias || room.getAltAliases().includes(roomAlias);
|
||||
})?.roomId;
|
||||
if (!targetRoomId) {
|
||||
return reject(newTranslatableError("Unrecognised room address: %(roomAlias)s", { roomAlias }));
|
||||
return reject(
|
||||
new UserFriendlyError("Unrecognised room address: %(roomAlias)s", {
|
||||
roomAlias,
|
||||
cause: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -898,7 +915,10 @@ export const Commands = [
|
|||
const room = cli.getRoom(roomId);
|
||||
if (!room) {
|
||||
return reject(
|
||||
newTranslatableError("Command failed: Unable to find room (%(roomId)s", { roomId }),
|
||||
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
|
||||
roomId,
|
||||
cause: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const member = room.getMember(userId);
|
||||
|
@ -906,7 +926,7 @@ export const Commands = [
|
|||
!member?.membership ||
|
||||
getEffectiveMembership(member.membership) === EffectiveMembership.Leave
|
||||
) {
|
||||
return reject(newTranslatableError("Could not find user in room"));
|
||||
return reject(new UserFriendlyError("Could not find user in room"));
|
||||
}
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
||||
|
@ -940,13 +960,16 @@ export const Commands = [
|
|||
const room = cli.getRoom(roomId);
|
||||
if (!room) {
|
||||
return reject(
|
||||
newTranslatableError("Command failed: Unable to find room (%(roomId)s", { roomId }),
|
||||
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
|
||||
roomId,
|
||||
cause: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent?.getContent().users[args]) {
|
||||
return reject(newTranslatableError("Could not find user in room"));
|
||||
return reject(new UserFriendlyError("Could not find user in room"));
|
||||
}
|
||||
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
||||
}
|
||||
|
@ -975,7 +998,7 @@ export const Commands = [
|
|||
!isCurrentLocalRoom(),
|
||||
runFn: function (roomId, widgetUrl) {
|
||||
if (!widgetUrl) {
|
||||
return reject(newTranslatableError("Please supply a widget URL or embed code"));
|
||||
return reject(new UserFriendlyError("Please supply a widget URL or embed code"));
|
||||
}
|
||||
|
||||
// Try and parse out a widget URL from iframes
|
||||
|
@ -988,14 +1011,14 @@ export const Commands = [
|
|||
if (iframe.tagName.toLowerCase() === "iframe" && iframe.attrs) {
|
||||
const srcAttr = iframe.attrs.find((a) => a.name === "src");
|
||||
logger.log("Pulling URL out of iframe (embed code)");
|
||||
if (!srcAttr) return reject(newTranslatableError("iframe has no src attribute"));
|
||||
if (!srcAttr) return reject(new UserFriendlyError("iframe has no src attribute"));
|
||||
widgetUrl = srcAttr.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!widgetUrl.startsWith("https://") && !widgetUrl.startsWith("http://")) {
|
||||
return reject(newTranslatableError("Please supply a https:// or http:// widget URL"));
|
||||
return reject(new UserFriendlyError("Please supply a https:// or http:// widget URL"));
|
||||
}
|
||||
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
|
@ -1017,7 +1040,7 @@ export const Commands = [
|
|||
|
||||
return success(WidgetUtils.setRoomWidget(roomId, widgetId, type, widgetUrl, name, data));
|
||||
} else {
|
||||
return reject(newTranslatableError("You cannot modify widgets in this room."));
|
||||
return reject(new UserFriendlyError("You cannot modify widgets in this room."));
|
||||
}
|
||||
},
|
||||
category: CommandCategories.admin,
|
||||
|
@ -1041,18 +1064,22 @@ export const Commands = [
|
|||
(async (): Promise<void> => {
|
||||
const device = cli.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
throw newTranslatableError("Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", {
|
||||
userId,
|
||||
deviceId,
|
||||
});
|
||||
throw new UserFriendlyError(
|
||||
"Unknown (user, session) pair: (%(userId)s, %(deviceId)s)",
|
||||
{
|
||||
userId,
|
||||
deviceId,
|
||||
cause: undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
|
||||
|
||||
if (deviceTrust.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw newTranslatableError("Session already verified!");
|
||||
throw new UserFriendlyError("Session already verified!");
|
||||
} else {
|
||||
throw newTranslatableError(
|
||||
throw new UserFriendlyError(
|
||||
"WARNING: session already verified, but keys do NOT MATCH!",
|
||||
);
|
||||
}
|
||||
|
@ -1060,7 +1087,7 @@ export const Commands = [
|
|||
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw newTranslatableError(
|
||||
throw new UserFriendlyError(
|
||||
"WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session" +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
|
@ -1069,6 +1096,7 @@ export const Commands = [
|
|||
userId,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
cause: undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1217,7 +1245,7 @@ export const Commands = [
|
|||
return success(
|
||||
(async (): Promise<void> => {
|
||||
const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId);
|
||||
if (!room) throw newTranslatableError("No virtual room for this room");
|
||||
if (!room) throw new UserFriendlyError("No virtual room for this room");
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
|
@ -1245,7 +1273,7 @@ export const Commands = [
|
|||
if (isPhoneNumber) {
|
||||
const results = await LegacyCallHandler.instance.pstnLookup(userId);
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
throw newTranslatableError("Unable to find Matrix ID for phone number");
|
||||
throw new UserFriendlyError("Unable to find Matrix ID for phone number");
|
||||
}
|
||||
userId = results[0].userid;
|
||||
}
|
||||
|
@ -1308,7 +1336,7 @@ export const Commands = [
|
|||
runFn: function (roomId, args) {
|
||||
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
return reject(newTranslatableError("No active call in this room"));
|
||||
return reject(new UserFriendlyError("No active call in this room"));
|
||||
}
|
||||
call.setRemoteOnHold(true);
|
||||
return success();
|
||||
|
@ -1323,7 +1351,7 @@ export const Commands = [
|
|||
runFn: function (roomId, args) {
|
||||
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
|
||||
if (!call) {
|
||||
return reject(newTranslatableError("No active call in this room"));
|
||||
return reject(new UserFriendlyError("No active call in this room"));
|
||||
}
|
||||
call.setRemoteOnHold(false);
|
||||
return success();
|
||||
|
@ -1337,7 +1365,7 @@ export const Commands = [
|
|||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function (roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return reject(newTranslatableError("Could not find room"));
|
||||
if (!room) return reject(new UserFriendlyError("Could not find room"));
|
||||
return success(guessAndSetDMRoom(room, true));
|
||||
},
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
|
@ -1349,7 +1377,7 @@ export const Commands = [
|
|||
isEnabled: () => !isCurrentLocalRoom(),
|
||||
runFn: function (roomId, args) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) return reject(newTranslatableError("Could not find room"));
|
||||
if (!room) return reject(new UserFriendlyError("Could not find room"));
|
||||
return success(guessAndSetDMRoom(room, false));
|
||||
},
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
|
|
|
@ -34,7 +34,7 @@ import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
|||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
@ -448,7 +448,15 @@ export const UserOptionsSection: React.FC<{
|
|||
const inviter = new MultiInviter(roomId || "");
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||
throw new Error(inviter.getErrorText(member.userId) ?? undefined);
|
||||
const errorStringFromInviterUtility = inviter.getErrorText(member.userId);
|
||||
if (errorStringFromInviterUtility) {
|
||||
throw new Error(errorStringFromInviterUtility);
|
||||
} else {
|
||||
throw new UserFriendlyError(
|
||||
`User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility`,
|
||||
{ user: member.userId, roomId, cause: undefined },
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import { IContent } from "matrix-js-sdk/src/models/event";
|
|||
import EditorModel from "./model";
|
||||
import { Type } from "./parts";
|
||||
import { Command, CommandCategories, getCommand } from "../SlashCommands";
|
||||
import { ITranslatableError, _t, _td } from "../languageHandler";
|
||||
import { UserFriendlyError, _t, _td } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../components/views/dialogs/QuestionDialog";
|
||||
|
@ -65,7 +65,7 @@ export async function runSlashCommand(
|
|||
): Promise<[content: IContent | null, success: boolean]> {
|
||||
const result = cmd.run(roomId, threadId, args);
|
||||
let messageContent: IContent | null = null;
|
||||
let error = result.error;
|
||||
let error: any = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
|
||||
|
@ -86,9 +86,8 @@ export async function runSlashCommand(
|
|||
let errText;
|
||||
if (typeof error === "string") {
|
||||
errText = error;
|
||||
} else if ((error as ITranslatableError).translatedMessage) {
|
||||
// Check for translatable errors (newTranslatableError)
|
||||
errText = (error as ITranslatableError).translatedMessage;
|
||||
} else if (error instanceof UserFriendlyError) {
|
||||
errText = error.translatedMessage;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
|
|
|
@ -435,6 +435,7 @@
|
|||
"Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.",
|
||||
"Continue": "Continue",
|
||||
"Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.",
|
||||
"User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility": "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility",
|
||||
"Joins room with given address": "Joins room with given address",
|
||||
"Leave room": "Leave room",
|
||||
"Unrecognised room address: %(roomAlias)s": "Unrecognised room address: %(roomAlias)s",
|
||||
|
|
|
@ -46,21 +46,49 @@ counterpart.setSeparator("|");
|
|||
const FALLBACK_LOCALE = "en";
|
||||
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
||||
|
||||
export interface ITranslatableError extends Error {
|
||||
translatedMessage: string;
|
||||
interface ErrorOptions {
|
||||
// Because we're mixing the subsitution variables and `cause` into the same object
|
||||
// below, we want them to always explicitly say whether there is an underlying error
|
||||
// or not to avoid typos of "cause" slipping through unnoticed.
|
||||
cause: unknown | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create an error which has an English message
|
||||
* with a translatedMessage property for use by the consumer.
|
||||
* @param {string} message Message to translate.
|
||||
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
|
||||
* @returns {Error} The constructed error.
|
||||
* Used to rethrow an error with a user-friendly translatable message while maintaining
|
||||
* access to that original underlying error. Downstream consumers can display the
|
||||
* `translatedMessage` property in the UI and inspect the underlying error with the
|
||||
* `cause` property.
|
||||
*
|
||||
* The error message will display as English in the console and logs so Element
|
||||
* developers can easily understand the error and find the source in the code. It also
|
||||
* helps tools like Sentry deduplicate the error, or just generally searching in
|
||||
* rageshakes to find all instances regardless of the users locale.
|
||||
*
|
||||
* @param message - The untranslated error message text, e.g "Something went wrong with %(foo)s".
|
||||
* @param substitutionVariablesAndCause - Variable substitutions for the translation and
|
||||
* original cause of the error. If there is no cause, just pass `undefined`, e.g { foo:
|
||||
* 'bar', cause: err || undefined }
|
||||
*/
|
||||
export function newTranslatableError(message: string, variables?: IVariables): ITranslatableError {
|
||||
const error = new Error(message) as ITranslatableError;
|
||||
error.translatedMessage = _t(message, variables);
|
||||
return error;
|
||||
export class UserFriendlyError extends Error {
|
||||
public readonly translatedMessage: string;
|
||||
|
||||
public constructor(message: string, substitutionVariablesAndCause?: IVariables & ErrorOptions) {
|
||||
const errorOptions = {
|
||||
cause: substitutionVariablesAndCause?.cause,
|
||||
};
|
||||
// Prevent "Could not find /%\(cause\)s/g in x" logs to the console by removing
|
||||
// it from the list
|
||||
const substitutionVariables = { ...substitutionVariablesAndCause };
|
||||
delete substitutionVariables["cause"];
|
||||
|
||||
// Create the error with the English version of the message that we want to show
|
||||
// up in the logs
|
||||
const englishTranslatedMessage = _t(message, { ...substitutionVariables, locale: "en" });
|
||||
super(englishTranslatedMessage, errorOptions);
|
||||
|
||||
// Also provide a translated version of the error in the users locale to display
|
||||
this.translatedMessage = _t(message, substitutionVariables);
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserLanguage(): string {
|
||||
|
@ -373,12 +401,18 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
|
|||
}
|
||||
}
|
||||
if (!matchFoundSomewhere) {
|
||||
// The current regexp did not match anything in the input
|
||||
// Missing matches is entirely possible because you might choose to show some variables only in the case
|
||||
// of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it.
|
||||
// However, not showing count is so common that it's not worth logging. And other commonly unused variables
|
||||
// here, if there are any.
|
||||
if (regexpString !== "%\\(count\\)s") {
|
||||
if (
|
||||
// The current regexp did not match anything in the input. Missing
|
||||
// matches is entirely possible because you might choose to show some
|
||||
// variables only in the case of e.g. plurals. It's still a bit
|
||||
// suspicious, and could be due to an error, so log it. However, not
|
||||
// showing count is so common that it's not worth logging. And other
|
||||
// commonly unused variables here, if there are any.
|
||||
regexpString !== "%\\(count\\)s" &&
|
||||
// Ignore the `locale` option which can be used to override the locale
|
||||
// in counterpart
|
||||
regexpString !== "%\\(locale\\)s"
|
||||
) {
|
||||
logger.log(`Could not find ${regexp} in ${text}`);
|
||||
}
|
||||
}
|
||||
|
@ -652,7 +686,11 @@ function doRegisterTranslations(customTranslations: ICustomTranslations): void {
|
|||
* This function should be called *after* registering other translations data to
|
||||
* ensure it overrides strings properly.
|
||||
*/
|
||||
export async function registerCustomTranslations(): Promise<void> {
|
||||
export async function registerCustomTranslations({
|
||||
testOnlyIgnoreCustomTranslationsCache = false,
|
||||
}: {
|
||||
testOnlyIgnoreCustomTranslationsCache?: boolean;
|
||||
} = {}): Promise<void> {
|
||||
const moduleTranslations = ModuleRunner.instance.allTranslations;
|
||||
doRegisterTranslations(moduleTranslations);
|
||||
|
||||
|
@ -661,7 +699,7 @@ export async function registerCustomTranslations(): Promise<void> {
|
|||
|
||||
try {
|
||||
let json: Optional<ICustomTranslations>;
|
||||
if (Date.now() >= cachedCustomTranslationsExpire) {
|
||||
if (testOnlyIgnoreCustomTranslationsCache || Date.now() >= cachedCustomTranslationsExpire) {
|
||||
json = CustomTranslationOptions.lookupFn
|
||||
? CustomTranslationOptions.lookupFn(lookupUrl)
|
||||
: ((await (await fetch(lookupUrl)).json()) as ICustomTranslations);
|
||||
|
|
|
@ -19,7 +19,7 @@ import { AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/autodiscovery";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { IClientWellKnown } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t, _td, newTranslatableError } from "../languageHandler";
|
||||
import { _t, UserFriendlyError } from "../languageHandler";
|
||||
import { makeType } from "./TypeUtils";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import { ValidatedServerConfig } from "./ValidatedServerConfig";
|
||||
|
@ -147,7 +147,7 @@ export default class AutoDiscoveryUtils {
|
|||
syntaxOnly = false,
|
||||
): Promise<ValidatedServerConfig> {
|
||||
if (!homeserverUrl) {
|
||||
throw newTranslatableError(_td("No homeserver URL provided"));
|
||||
throw new UserFriendlyError("No homeserver URL provided");
|
||||
}
|
||||
|
||||
const wellknownConfig: IClientWellKnown = {
|
||||
|
@ -199,7 +199,7 @@ export default class AutoDiscoveryUtils {
|
|||
// This shouldn't happen without major misconfiguration, so we'll log a bit of information
|
||||
// in the log so we can find this bit of codee but otherwise tell teh user "it broke".
|
||||
logger.error("Ended up in a state of not knowing which homeserver to connect to.");
|
||||
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
|
||||
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
|
||||
}
|
||||
|
||||
const hsResult = discoveryResult["m.homeserver"];
|
||||
|
@ -221,9 +221,9 @@ export default class AutoDiscoveryUtils {
|
|||
logger.error("Error determining preferred identity server URL:", isResult);
|
||||
if (isResult.state === AutoDiscovery.FAIL_ERROR) {
|
||||
if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error as string) !== -1) {
|
||||
throw newTranslatableError(isResult.error as string);
|
||||
throw new UserFriendlyError(String(isResult.error));
|
||||
}
|
||||
throw newTranslatableError(_td("Unexpected error resolving identity server configuration"));
|
||||
throw new UserFriendlyError("Unexpected error resolving identity server configuration");
|
||||
} // else the error is not related to syntax - continue anyways.
|
||||
|
||||
// rewrite homeserver error since we don't care about problems
|
||||
|
@ -237,9 +237,9 @@ export default class AutoDiscoveryUtils {
|
|||
logger.error("Error processing homeserver config:", hsResult);
|
||||
if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(hsResult.error)) {
|
||||
if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error as string) !== -1) {
|
||||
throw newTranslatableError(hsResult.error as string);
|
||||
throw new UserFriendlyError(String(hsResult.error));
|
||||
}
|
||||
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
|
||||
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
|
||||
} // else the error is not related to syntax - continue anyways.
|
||||
}
|
||||
|
||||
|
@ -252,7 +252,7 @@ export default class AutoDiscoveryUtils {
|
|||
// It should have been set by now, so check it
|
||||
if (!preferredHomeserverName) {
|
||||
logger.error("Failed to parse homeserver name from homeserver URL");
|
||||
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
|
||||
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
|
||||
}
|
||||
|
||||
return makeType(ValidatedServerConfig, {
|
||||
|
|
|
@ -21,8 +21,25 @@ import {
|
|||
ICustomTranslations,
|
||||
registerCustomTranslations,
|
||||
setLanguage,
|
||||
UserFriendlyError,
|
||||
} from "../src/languageHandler";
|
||||
|
||||
async function setupTranslationOverridesForTests(overrides: ICustomTranslations) {
|
||||
const lookupUrl = "/translations.json";
|
||||
const fn = (url: string): ICustomTranslations => {
|
||||
expect(url).toEqual(lookupUrl);
|
||||
return overrides;
|
||||
};
|
||||
|
||||
SdkConfig.add({
|
||||
custom_translations_url: lookupUrl,
|
||||
});
|
||||
CustomTranslationOptions.lookupFn = fn;
|
||||
await registerCustomTranslations({
|
||||
testOnlyIgnoreCustomTranslationsCache: true,
|
||||
});
|
||||
}
|
||||
|
||||
describe("languageHandler", () => {
|
||||
afterEach(() => {
|
||||
SdkConfig.unset();
|
||||
|
@ -33,38 +50,72 @@ describe("languageHandler", () => {
|
|||
const str = "This is a test string that does not exist in the app.";
|
||||
const enOverride = "This is the English version of a custom string.";
|
||||
const deOverride = "This is the German version of a custom string.";
|
||||
const overrides: ICustomTranslations = {
|
||||
|
||||
// First test that overrides aren't being used
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(str);
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(str);
|
||||
|
||||
await setupTranslationOverridesForTests({
|
||||
[str]: {
|
||||
en: enOverride,
|
||||
de: deOverride,
|
||||
},
|
||||
};
|
||||
|
||||
const lookupUrl = "/translations.json";
|
||||
const fn = (url: string): ICustomTranslations => {
|
||||
expect(url).toEqual(lookupUrl);
|
||||
return overrides;
|
||||
};
|
||||
|
||||
// First test that overrides aren't being used
|
||||
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(str);
|
||||
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(str);
|
||||
});
|
||||
|
||||
// Now test that they *are* being used
|
||||
SdkConfig.add({
|
||||
custom_translations_url: lookupUrl,
|
||||
});
|
||||
CustomTranslationOptions.lookupFn = fn;
|
||||
await registerCustomTranslations();
|
||||
|
||||
await setLanguage("en");
|
||||
expect(_t(str)).toEqual(enOverride);
|
||||
|
||||
await setLanguage("de");
|
||||
expect(_t(str)).toEqual(deOverride);
|
||||
});
|
||||
|
||||
describe("UserFriendlyError", () => {
|
||||
const testErrorMessage = "This email address is already in use (%(email)s)";
|
||||
beforeEach(async () => {
|
||||
// Setup some strings with variable substituations that we can use in the tests.
|
||||
const deOverride = "Diese E-Mail-Adresse wird bereits verwendet (%(email)s)";
|
||||
await setupTranslationOverridesForTests({
|
||||
[testErrorMessage]: {
|
||||
en: testErrorMessage,
|
||||
de: deOverride,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("includes English message and localized translated message", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: undefined,
|
||||
});
|
||||
|
||||
// Ensure message is in English so it's readable in the logs
|
||||
expect(friendlyError.message).toStrictEqual("This email address is already in use (test@example.com)");
|
||||
// Ensure the translated message is localized appropriately
|
||||
expect(friendlyError.translatedMessage).toStrictEqual(
|
||||
"Diese E-Mail-Adresse wird bereits verwendet (test@example.com)",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes underlying cause error", async () => {
|
||||
await setLanguage("de");
|
||||
|
||||
const underlyingError = new Error("Fake underlying error");
|
||||
const friendlyError = new UserFriendlyError(testErrorMessage, {
|
||||
email: "test@example.com",
|
||||
cause: underlyingError,
|
||||
});
|
||||
|
||||
expect(friendlyError.cause).toStrictEqual(underlyingError);
|
||||
});
|
||||
|
||||
it("ok to omit the substitution variables and cause object, there just won't be any cause", async () => {
|
||||
const friendlyError = new UserFriendlyError("foo error");
|
||||
expect(friendlyError.cause).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
56
yarn.lock
56
yarn.lock
|
@ -110,7 +110,7 @@
|
|||
dependencies:
|
||||
eslint-rule-composer "^0.3.0"
|
||||
|
||||
"@babel/generator@^7.21.0", "@babel/generator@^7.21.1":
|
||||
"@babel/generator@^7.21.0":
|
||||
version "7.21.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.1.tgz#951cc626057bc0af2c35cd23e9c64d384dea83dd"
|
||||
integrity sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==
|
||||
|
@ -120,6 +120,16 @@
|
|||
"@jridgewell/trace-mapping" "^0.3.17"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/generator@^7.21.1", "@babel/generator@^7.21.3":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.3.tgz#232359d0874b392df04045d72ce2fd9bb5045fce"
|
||||
integrity sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.21.3"
|
||||
"@jridgewell/gen-mapping" "^0.3.2"
|
||||
"@jridgewell/trace-mapping" "^0.3.17"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/generator@^7.7.2":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95"
|
||||
|
@ -405,11 +415,16 @@
|
|||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.21.2":
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.21.0":
|
||||
version "7.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.2.tgz#dacafadfc6d7654c3051a66d6fe55b6cb2f2a0b3"
|
||||
integrity sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==
|
||||
|
||||
"@babel/parser@^7.18.5", "@babel/parser@^7.20.7", "@babel/parser@^7.21.2", "@babel/parser@^7.21.3":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3"
|
||||
integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
|
||||
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
|
||||
|
@ -1163,7 +1178,7 @@
|
|||
"@babel/parser" "^7.18.10"
|
||||
"@babel/types" "^7.18.10"
|
||||
|
||||
"@babel/traverse@^7.12.12", "@babel/traverse@^7.18.5", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.7.2":
|
||||
"@babel/traverse@^7.12.12", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.7.2":
|
||||
version "7.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.2.tgz#ac7e1f27658750892e815e60ae90f382a46d8e75"
|
||||
integrity sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==
|
||||
|
@ -1179,6 +1194,22 @@
|
|||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.18.5":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.3.tgz#4747c5e7903d224be71f90788b06798331896f67"
|
||||
integrity sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.18.6"
|
||||
"@babel/generator" "^7.21.3"
|
||||
"@babel/helper-environment-visitor" "^7.18.9"
|
||||
"@babel/helper-function-name" "^7.21.0"
|
||||
"@babel/helper-hoist-variables" "^7.18.6"
|
||||
"@babel/helper-split-export-declaration" "^7.18.6"
|
||||
"@babel/parser" "^7.21.3"
|
||||
"@babel/types" "^7.21.3"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84"
|
||||
|
@ -1197,7 +1228,16 @@
|
|||
"@babel/helper-validator-identifier" "^7.19.1"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.18.6", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2":
|
||||
"@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.3":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.3.tgz#4865a5357ce40f64e3400b0f3b737dc6d4f64d05"
|
||||
integrity sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.19.4"
|
||||
"@babel/helper-validator-identifier" "^7.19.1"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.20.0", "@babel/types@^7.20.2":
|
||||
version "7.21.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.2.tgz#92246f6e00f91755893c2876ad653db70c8310d1"
|
||||
integrity sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==
|
||||
|
@ -6528,10 +6568,10 @@ matrix-mock-request@^2.5.0:
|
|||
dependencies:
|
||||
expect "^28.1.0"
|
||||
|
||||
matrix-web-i18n@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-1.3.0.tgz#d85052635215173541f56ea1af0cbefd6e09ecb3"
|
||||
integrity sha512-4QumouFjd4//piyRCtkfr24kjMPHkzNQNz09B1oEX4W3d4gdd5F+lwErqcQrys7Yl09U0S0iKCD8xPBRV178qg==
|
||||
matrix-web-i18n@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-web-i18n/-/matrix-web-i18n-1.4.0.tgz#f383a3ebc29d3fd6eb137d38cc4c3198771cc073"
|
||||
integrity sha512-+NP2h4zdft+2H/6oFQ0i2PBm00Ei6HpUHke8rklgpe/yCABBG5Q7gIQdZoxazi0DXWWtcvvIfgamPZmkg6oRwA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.18.5"
|
||||
"@babel/traverse" "^7.18.5"
|
||||
|
|
Loading…
Reference in New Issue