Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit ff1468b

Browse files
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 matrix-org/matrix-web-i18n#6 * Include room ID in error * Translate fallback error * Translate better * Update i18n strings * Better re-use * Minor comment fixes
1 parent 567248d commit ff1468b

File tree

10 files changed

+282
-96
lines changed

10 files changed

+282
-96
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"matrix_lib_main": "./lib/index.ts",
2929
"matrix_lib_typings": "./lib/index.d.ts",
3030
"matrix_i18n_extra_translation_funcs": [
31-
"newTranslatableError"
31+
"UserFriendlyError"
3232
],
3333
"scripts": {
3434
"prepublishOnly": "yarn build",
@@ -203,7 +203,7 @@
203203
"jest-mock": "^29.2.2",
204204
"jest-raw-loader": "^1.0.1",
205205
"matrix-mock-request": "^2.5.0",
206-
"matrix-web-i18n": "^1.3.0",
206+
"matrix-web-i18n": "^1.4.0",
207207
"mocha-junit-reporter": "^2.2.0",
208208
"node-fetch": "2",
209209
"postcss-scss": "^4.0.4",

src/@types/global.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ declare global {
187187
}
188188

189189
interface Error {
190+
// Standard
191+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
192+
cause?: unknown;
193+
194+
// Non-standard
190195
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
191196
fileName?: string;
192197
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber
@@ -195,6 +200,22 @@ declare global {
195200
columnNumber?: number;
196201
}
197202

203+
// We can remove these pieces if we ever update to `target: "es2022"` in our
204+
// TypeScript config which supports the new `cause` property, see
205+
// https://github.com/vector-im/element-web/issues/24913
206+
interface ErrorOptions {
207+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
208+
cause?: unknown;
209+
}
210+
211+
interface ErrorConstructor {
212+
new (message?: string, options?: ErrorOptions): Error;
213+
(message?: string, options?: ErrorOptions): Error;
214+
}
215+
216+
// eslint-disable-next-line no-var
217+
var Error: ErrorConstructor;
218+
198219
// https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278
199220
interface AudioWorkletProcessor {
200221
readonly port: MessagePort;

src/SlashCommands.tsx

Lines changed: 61 additions & 33 deletions
Large diffs are not rendered by default.

src/components/views/right_panel/UserInfo.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
3434

3535
import dis from "../../../dispatcher/dispatcher";
3636
import Modal from "../../../Modal";
37-
import { _t } from "../../../languageHandler";
37+
import { _t, UserFriendlyError } from "../../../languageHandler";
3838
import DMRoomMap from "../../../utils/DMRoomMap";
3939
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
4040
import SdkConfig from "../../../SdkConfig";
@@ -448,7 +448,15 @@ export const UserOptionsSection: React.FC<{
448448
const inviter = new MultiInviter(roomId || "");
449449
await inviter.invite([member.userId]).then(() => {
450450
if (inviter.getCompletionState(member.userId) !== "invited") {
451-
throw new Error(inviter.getErrorText(member.userId) ?? undefined);
451+
const errorStringFromInviterUtility = inviter.getErrorText(member.userId);
452+
if (errorStringFromInviterUtility) {
453+
throw new Error(errorStringFromInviterUtility);
454+
} else {
455+
throw new UserFriendlyError(
456+
`User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility`,
457+
{ user: member.userId, roomId, cause: undefined },
458+
);
459+
}
452460
}
453461
});
454462
} catch (err) {

src/editor/commands.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { IContent } from "matrix-js-sdk/src/models/event";
2121
import EditorModel from "./model";
2222
import { Type } from "./parts";
2323
import { Command, CommandCategories, getCommand } from "../SlashCommands";
24-
import { ITranslatableError, _t, _td } from "../languageHandler";
24+
import { UserFriendlyError, _t, _td } from "../languageHandler";
2525
import Modal from "../Modal";
2626
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
2727
import QuestionDialog from "../components/views/dialogs/QuestionDialog";
@@ -65,7 +65,7 @@ export async function runSlashCommand(
6565
): Promise<[content: IContent | null, success: boolean]> {
6666
const result = cmd.run(roomId, threadId, args);
6767
let messageContent: IContent | null = null;
68-
let error = result.error;
68+
let error: any = result.error;
6969
if (result.promise) {
7070
try {
7171
if (cmd.category === CommandCategories.messages || cmd.category === CommandCategories.effects) {
@@ -86,9 +86,8 @@ export async function runSlashCommand(
8686
let errText;
8787
if (typeof error === "string") {
8888
errText = error;
89-
} else if ((error as ITranslatableError).translatedMessage) {
90-
// Check for translatable errors (newTranslatableError)
91-
errText = (error as ITranslatableError).translatedMessage;
89+
} else if (error instanceof UserFriendlyError) {
90+
errText = error.translatedMessage;
9291
} else if (error.message) {
9392
errText = error.message;
9493
} else {

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@
435435
"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.",
436436
"Continue": "Continue",
437437
"Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.",
438+
"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",
438439
"Joins room with given address": "Joins room with given address",
439440
"Leave room": "Leave room",
440441
"Unrecognised room address: %(roomAlias)s": "Unrecognised room address: %(roomAlias)s",

src/languageHandler.tsx

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,49 @@ counterpart.setSeparator("|");
4646
const FALLBACK_LOCALE = "en";
4747
counterpart.setFallbackLocale(FALLBACK_LOCALE);
4848

49-
export interface ITranslatableError extends Error {
50-
translatedMessage: string;
49+
interface ErrorOptions {
50+
// Because we're mixing the subsitution variables and `cause` into the same object
51+
// below, we want them to always explicitly say whether there is an underlying error
52+
// or not to avoid typos of "cause" slipping through unnoticed.
53+
cause: unknown | undefined;
5154
}
5255

5356
/**
54-
* Helper function to create an error which has an English message
55-
* with a translatedMessage property for use by the consumer.
56-
* @param {string} message Message to translate.
57-
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
58-
* @returns {Error} The constructed error.
57+
* Used to rethrow an error with a user-friendly translatable message while maintaining
58+
* access to that original underlying error. Downstream consumers can display the
59+
* `translatedMessage` property in the UI and inspect the underlying error with the
60+
* `cause` property.
61+
*
62+
* The error message will display as English in the console and logs so Element
63+
* developers can easily understand the error and find the source in the code. It also
64+
* helps tools like Sentry deduplicate the error, or just generally searching in
65+
* rageshakes to find all instances regardless of the users locale.
66+
*
67+
* @param message - The untranslated error message text, e.g "Something went wrong with %(foo)s".
68+
* @param substitutionVariablesAndCause - Variable substitutions for the translation and
69+
* original cause of the error. If there is no cause, just pass `undefined`, e.g { foo:
70+
* 'bar', cause: err || undefined }
5971
*/
60-
export function newTranslatableError(message: string, variables?: IVariables): ITranslatableError {
61-
const error = new Error(message) as ITranslatableError;
62-
error.translatedMessage = _t(message, variables);
63-
return error;
72+
export class UserFriendlyError extends Error {
73+
public readonly translatedMessage: string;
74+
75+
public constructor(message: string, substitutionVariablesAndCause?: IVariables & ErrorOptions) {
76+
const errorOptions = {
77+
cause: substitutionVariablesAndCause?.cause,
78+
};
79+
// Prevent "Could not find /%\(cause\)s/g in x" logs to the console by removing
80+
// it from the list
81+
const substitutionVariables = { ...substitutionVariablesAndCause };
82+
delete substitutionVariables["cause"];
83+
84+
// Create the error with the English version of the message that we want to show
85+
// up in the logs
86+
const englishTranslatedMessage = _t(message, { ...substitutionVariables, locale: "en" });
87+
super(englishTranslatedMessage, errorOptions);
88+
89+
// Also provide a translated version of the error in the users locale to display
90+
this.translatedMessage = _t(message, substitutionVariables);
91+
}
6492
}
6593

6694
export function getUserLanguage(): string {
@@ -373,12 +401,18 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
373401
}
374402
}
375403
if (!matchFoundSomewhere) {
376-
// The current regexp did not match anything in the input
377-
// Missing matches is entirely possible because you might choose to show some variables only in the case
378-
// of e.g. plurals. It's still a bit suspicious, and could be due to an error, so log it.
379-
// However, not showing count is so common that it's not worth logging. And other commonly unused variables
380-
// here, if there are any.
381-
if (regexpString !== "%\\(count\\)s") {
404+
if (
405+
// The current regexp did not match anything in the input. Missing
406+
// matches is entirely possible because you might choose to show some
407+
// variables only in the case of e.g. plurals. It's still a bit
408+
// suspicious, and could be due to an error, so log it. However, not
409+
// showing count is so common that it's not worth logging. And other
410+
// commonly unused variables here, if there are any.
411+
regexpString !== "%\\(count\\)s" &&
412+
// Ignore the `locale` option which can be used to override the locale
413+
// in counterpart
414+
regexpString !== "%\\(locale\\)s"
415+
) {
382416
logger.log(`Could not find ${regexp} in ${text}`);
383417
}
384418
}
@@ -652,7 +686,11 @@ function doRegisterTranslations(customTranslations: ICustomTranslations): void {
652686
* This function should be called *after* registering other translations data to
653687
* ensure it overrides strings properly.
654688
*/
655-
export async function registerCustomTranslations(): Promise<void> {
689+
export async function registerCustomTranslations({
690+
testOnlyIgnoreCustomTranslationsCache = false,
691+
}: {
692+
testOnlyIgnoreCustomTranslationsCache?: boolean;
693+
} = {}): Promise<void> {
656694
const moduleTranslations = ModuleRunner.instance.allTranslations;
657695
doRegisterTranslations(moduleTranslations);
658696

@@ -661,7 +699,7 @@ export async function registerCustomTranslations(): Promise<void> {
661699

662700
try {
663701
let json: Optional<ICustomTranslations>;
664-
if (Date.now() >= cachedCustomTranslationsExpire) {
702+
if (testOnlyIgnoreCustomTranslationsCache || Date.now() >= cachedCustomTranslationsExpire) {
665703
json = CustomTranslationOptions.lookupFn
666704
? CustomTranslationOptions.lookupFn(lookupUrl)
667705
: ((await (await fetch(lookupUrl)).json()) as ICustomTranslations);

src/utils/AutoDiscoveryUtils.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/autodiscovery";
1919
import { logger } from "matrix-js-sdk/src/logger";
2020
import { IClientWellKnown } from "matrix-js-sdk/src/matrix";
2121

22-
import { _t, _td, newTranslatableError } from "../languageHandler";
22+
import { _t, UserFriendlyError } from "../languageHandler";
2323
import { makeType } from "./TypeUtils";
2424
import SdkConfig from "../SdkConfig";
2525
import { ValidatedServerConfig } from "./ValidatedServerConfig";
@@ -147,7 +147,7 @@ export default class AutoDiscoveryUtils {
147147
syntaxOnly = false,
148148
): Promise<ValidatedServerConfig> {
149149
if (!homeserverUrl) {
150-
throw newTranslatableError(_td("No homeserver URL provided"));
150+
throw new UserFriendlyError("No homeserver URL provided");
151151
}
152152

153153
const wellknownConfig: IClientWellKnown = {
@@ -199,7 +199,7 @@ export default class AutoDiscoveryUtils {
199199
// This shouldn't happen without major misconfiguration, so we'll log a bit of information
200200
// in the log so we can find this bit of codee but otherwise tell teh user "it broke".
201201
logger.error("Ended up in a state of not knowing which homeserver to connect to.");
202-
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
202+
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
203203
}
204204

205205
const hsResult = discoveryResult["m.homeserver"];
@@ -221,9 +221,9 @@ export default class AutoDiscoveryUtils {
221221
logger.error("Error determining preferred identity server URL:", isResult);
222222
if (isResult.state === AutoDiscovery.FAIL_ERROR) {
223223
if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error as string) !== -1) {
224-
throw newTranslatableError(isResult.error as string);
224+
throw new UserFriendlyError(String(isResult.error));
225225
}
226-
throw newTranslatableError(_td("Unexpected error resolving identity server configuration"));
226+
throw new UserFriendlyError("Unexpected error resolving identity server configuration");
227227
} // else the error is not related to syntax - continue anyways.
228228

229229
// rewrite homeserver error since we don't care about problems
@@ -237,9 +237,9 @@ export default class AutoDiscoveryUtils {
237237
logger.error("Error processing homeserver config:", hsResult);
238238
if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(hsResult.error)) {
239239
if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error as string) !== -1) {
240-
throw newTranslatableError(hsResult.error as string);
240+
throw new UserFriendlyError(String(hsResult.error));
241241
}
242-
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
242+
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
243243
} // else the error is not related to syntax - continue anyways.
244244
}
245245

@@ -252,7 +252,7 @@ export default class AutoDiscoveryUtils {
252252
// It should have been set by now, so check it
253253
if (!preferredHomeserverName) {
254254
logger.error("Failed to parse homeserver name from homeserver URL");
255-
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
255+
throw new UserFriendlyError("Unexpected error resolving homeserver configuration");
256256
}
257257

258258
return makeType(ValidatedServerConfig, {

0 commit comments

Comments
 (0)