Custom error handler and some misc fixes

This commit is contained in:
AAGaming
2024-05-25 19:14:54 -04:00
parent 96cc72f2ca
commit a84a13c76d
28 changed files with 1140 additions and 1126 deletions
@@ -0,0 +1,201 @@
import { sleep } from '@decky/ui';
import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
import { uninstallPlugin } from '../plugin';
import { doRestart, doShutdown } from '../updater';
interface ReactErrorInfo {
error: Error;
info: ErrorInfo;
}
interface DeckyErrorBoundaryProps {
error: ReactErrorInfo;
errorKey: string;
reset: () => void;
}
declare global {
interface Window {
SystemNetworkStore?: any;
}
}
const pluginErrorRegex = /\(http:\/\/localhost:1337\/plugins\/(.*)\//;
const pluginSourceMapErrorRegex = /\(decky:\/\/decky\/plugin\/(.*)\//;
const legacyPluginErrorRegex = /\(decky:\/\/decky\/legacy_plugin\/(.*)\/index.js/;
function getLikelyErrorSource(error: ReactErrorInfo): [source: string, wasPlugin: boolean] {
const pluginMatch = error.error.stack?.match(pluginErrorRegex);
if (pluginMatch) {
return [decodeURIComponent(pluginMatch[1]), true];
}
const pluginMatchViaMap = error.error.stack?.match(pluginSourceMapErrorRegex);
if (pluginMatchViaMap) {
return [decodeURIComponent(pluginMatchViaMap[1]), true];
}
const legacyPluginMatch = error.error.stack?.match(legacyPluginErrorRegex);
if (legacyPluginMatch) {
return [decodeURIComponent(legacyPluginMatch[1]), true];
}
if (error.error.stack?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false];
}
return ['Steam', false];
}
export const startSSH = DeckyBackend.callable('utilities/start_ssh');
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
function ipToString(ip: number) {
return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
}
// Intentionally not localized since we can't really trust React here
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, reset }) => {
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error);
return (
<div
style={{
overflow: 'scroll',
marginLeft: '15px',
color: 'white',
fontSize: '16px',
userSelect: 'auto',
backgroundColor: 'black',
marginTop: '48px', // Incase this is a page
}}
>
<h1
style={{
fontSize: '20px',
display: 'inline-block',
marginTop: '15px',
userSelect: 'auto',
}}
>
An error occured rendering this content.
</h1>
<p>This error likely occured in {getLikelyErrorSource(error)}.</p>
{actionLog?.length > 0 && (
<pre>
<code>
Running actions...
{actionLog}
</code>
</pre>
)}
{actionsEnabled && (
<>
<h3>Actions: </h3>
<p>Use the touch screen.</p>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
Retry
</button>
<button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}>
Restart Steam
</button>
</div>
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Reloading UI...');
}}
>
Restart Decky
</button>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine('Stopping Decky...');
doShutdown();
await sleep(5000);
addLogLine('Restarting Steam...');
SteamClient.User.StartRestart();
}}
>
Disable Decky until next boot
</button>
</div>
{debugAllowed && (
<div style={{ display: 'block', marginBottom: '5px' }}>
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setDebugAllowed(false);
addLogLine('Enabling CEF debugger forwarding...');
await starrCEFForwarding();
addLogLine('Enabling SSH...');
await startSSH();
addLogLine('Ready for debugging!');
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
addLogLine(`CEF Debugger: http://${ip}:8081`);
addLogLine(`SSH: deck@${ip}`);
}
}}
>
Allow remote debugging and SSH until next boot
</button>
</div>
)}
{wasCausedByPlugin && (
<div style={{ display: 'block', marginBottom: '5px' }}>
{'\n'}
<button
style={{ marginRight: '5px', padding: '5px' }}
onClick={async () => {
setActionsEnabled(false);
addLogLine(`Uninstalling ${errorSource}...`);
await uninstallPlugin(errorSource);
await DeckyPluginLoader.frozenPluginsService.invalidate();
await DeckyPluginLoader.hiddenPluginsService.invalidate();
await sleep(1000);
addLogLine('Restarting Decky...');
doRestart();
await sleep(2000);
addLogLine('Restarting Steam...');
await sleep(500);
SteamClient.User.StartRestart();
}}
>
Uninstall {errorSource} and restart Decky
</button>
</div>
)}
</>
)}
<pre
style={{
marginTop: '15px',
opacity: 0.7,
userSelect: 'auto',
}}
>
<code>
{error.error.stack}
{'\n\n'}
Component Stack:
{error.info.componentStack}
</code>
</pre>
</div>
);
};
export default DeckyErrorBoundary;
@@ -1,4 +1,4 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
interface Props {
deckyGlobalComponentsState: DeckyGlobalComponentsState;
children: ReactNode;
}
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
+2 -1
View File
@@ -1,4 +1,4 @@
import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';
export interface RouterEntry {
@@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
interface Props {
deckyRouterState: DeckyRouterState;
children: ReactNode;
}
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
+2 -1
View File
@@ -1,4 +1,4 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
@@ -134,6 +134,7 @@ export const useDeckyState = () => useContext(DeckyStateContext);
interface Props {
deckyState: DeckyState;
children?: ReactNode;
}
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
+3 -2
View File
@@ -1,4 +1,5 @@
import { ToastData, joinClassNames } from '@decky/ui';
import type { ToastData } from '@decky/api';
import { joinClassNames } from '@decky/ui';
import { FC, useEffect, useState } from 'react';
import { ReactElement } from 'react-markdown/lib/react-markdown';
@@ -28,7 +29,7 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
}
useEffect(() => {
// not actually node but TS is shit
let interval: NodeJS.Timer | null;
let interval: NodeJS.Timeout | number | null;
if (renderedToast) {
interval = setTimeout(
() => {
@@ -1,4 +1,4 @@
import { ToastData } from '@decky/ui';
import type { ToastData } from '@decky/api';
import { FC, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyToasterState {
+2 -1
View File
@@ -1,4 +1,5 @@
import { ToastData, findModule, joinClassNames } from '@decky/ui';
import type { ToastData } from '@decky/api';
import { findModule, joinClassNames } from '@decky/ui';
import { FunctionComponent } from 'react';
interface ToastProps {
@@ -59,7 +59,7 @@ const DropdownMultiselect: FC<{
const [itemsSelected, setItemsSelected] = useState<any>(selected);
const { t } = useTranslation();
const handleItemSelect = useCallback((checked, value) => {
const handleItemSelect = useCallback((checked: boolean, value: any) => {
setItemsSelected((x: any) =>
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
);
@@ -60,10 +60,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
strTitle={
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="title"
i18n_args={{ artifact: artifact }}
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="title"
i18nArgs={{ artifact: artifact }}
installType={installType}
/>
</div>
}
@@ -71,17 +71,17 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
loading ? (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_processing"
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_processing"
installType={installType}
/>
</div>
) : (
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="button_idle"
install_type={installType}
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="button_idle"
installType={installType}
/>
</div>
)
@@ -89,13 +89,13 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
>
<div>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
trans_text="desc"
i18n_args={{
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
transText="desc"
i18nArgs={{
artifact: artifact,
version: version,
}}
install_type={installType}
installType={installType}
/>
</div>
{loading && (
+3 -3
View File
@@ -40,11 +40,11 @@ export async function setShowValveInternal(show: boolean) {
export async function setShouldConnectToReactDevTools(enable: boolean) {
DeckyPluginLoader.toaster.toast({
title: enable ? (
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'enabling'} />
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'enabling'} />
) : (
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'disabling'} />
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'disabling'} />
),
body: <TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'5secreload'} />,
body: <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'5secreload'} />,
icon: <FaReact />,
});
await sleep(5000);
+69
View File
@@ -0,0 +1,69 @@
import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
import Logger from './logger';
declare global {
interface Window {
__ERRORBOUNDARY_HOOK_INSTANCE: any;
}
}
class ErrorBoundaryHook extends Logger {
private errorBoundaryPatch?: Patch;
constructor() {
super('ErrorBoundaryHook');
this.log('Initialized');
window.__ERRORBOUNDARY_HOOK_INSTANCE?.deinit?.();
window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
}
init() {
// valve writes only the sanest of code
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/;
const initErrorReportingStore = findModuleExport(
(e) => typeof e == 'function' && e?.toString && exp.test(e.toString()),
);
if (!initErrorReportingStore) {
this.error('could not find initErrorReportingStore! error boundary hook disabled!');
return;
}
// will replace the existing one for us seemingly? doesnt matter anyway lol
const errorReportingStore = initErrorReportingStore();
// NUH UH.
Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
get: () => false,
});
errorReportingStore.m_bEnabled = false;
// @ts-ignore
// window.errorStore = errorReportingStore;
const ValveErrorBoundary = findModuleExport(
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
);
if (!ValveErrorBoundary) {
this.error('could not find ValveErrorBoundary');
return;
}
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
if (this.state.error) {
return (
<DeckyErrorBoundary error={this.state.error} errorKey={this.state.errorKey} reset={() => this.Reset()} />
);
}
return callOriginal;
});
}
deinit() {
this.errorBoundaryPatch?.unpatch();
}
}
export default ErrorBoundaryHook;
+26 -16
View File
@@ -21,6 +21,7 @@ import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense';
import ErrorBoundaryHook from './errorboundary-hook';
import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
@@ -61,6 +62,7 @@ const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: stri
class PluginLoader extends Logger {
private plugins: Plugin[] = [];
private errorBoundaryHook: ErrorBoundaryHook = new ErrorBoundaryHook();
private tabsHook: TabsHook = new TabsHook();
private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster();
@@ -79,6 +81,8 @@ class PluginLoader extends Logger {
constructor() {
super(PluginLoader.name);
this.errorBoundaryHook.init();
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
@@ -185,12 +189,12 @@ class PluginLoader extends Logger {
this.deckyState.setHasLoaderUpdate(true);
if (this.notificationService.shouldNotify('deckyUpdates')) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
body: (
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="decky_update_available"
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
transClass={TranslationClass.PLUGIN_LOADER}
transText="decky_update_available"
i18nArgs={{ tag_name: versionInfo?.remote?.tag_name }}
/>
),
onClick: () => Router.Navigate('/decky/settings'),
@@ -213,12 +217,12 @@ class PluginLoader extends Logger {
const updates = await this.checkPluginUpdates();
if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
this.toaster.toast({
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
body: (
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="plugin_update"
i18n_args={{ count: updates.size }}
transClass={TranslationClass.PLUGIN_LOADER}
transText="plugin_update"
i18nArgs={{ count: updates.size }}
/>
),
onClick: () => Router.Navigate('/decky/settings/plugins'),
@@ -294,6 +298,10 @@ class PluginLoader extends Logger {
this.routerHook.removeRoute('/decky/settings');
deinitSteamFixes();
deinitFilepickerPatches();
this.routerHook.deinit();
this.tabsHook.deinit();
this.toaster.deinit();
this.errorBoundaryHook.deinit();
}
public unloadPlugin(name: string) {
@@ -365,7 +373,9 @@ class PluginLoader extends Logger {
},
});
if (res.ok) {
let plugin_export: (serverAPI: any) => Plugin = await eval(await res.text());
let plugin_export: (serverAPI: any) => Plugin = await eval(
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
);
let plugin = plugin_export(this.createLegacyPluginAPI(name));
this.plugins.push({
...plugin,
@@ -384,7 +394,7 @@ class PluginLoader extends Logger {
<PanelSection>
<PanelSectionRow>
<div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
<TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="error" />
</div>
</PanelSectionRow>
<PanelSectionRow>
@@ -395,9 +405,9 @@ class PluginLoader extends Logger {
<PanelSectionRow>
<div className={quickAccessMenuClasses.Text}>
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="plugin_error_uninstall"
i18n_args={{ name: name }}
transClass={TranslationClass.PLUGIN_LOADER}
transText="plugin_error_uninstall"
i18nArgs={{ name: name }}
/>
</div>
</PanelSectionRow>
@@ -412,9 +422,9 @@ class PluginLoader extends Logger {
this.toaster.toast({
title: (
<TranslationHelper
trans_class={TranslationClass.PLUGIN_LOADER}
trans_text="plugin_load_error.toast"
i18n_args={{ name: name }}
transClass={TranslationClass.PLUGIN_LOADER}
transText="plugin_load_error.toast"
i18nArgs={{ name: name }}
/>
),
body: '' + e,
+2 -2
View File
@@ -1,5 +1,5 @@
// import reloadFix from './reload';
// import restartFix from './restart';
import restartFix from './restart';
let fixes: Function[] = [];
export function deinitSteamFixes() {
@@ -8,5 +8,5 @@ export function deinitSteamFixes() {
export async function initSteamFixes() {
// fixes.push(await reloadFix());
// fixes.push(await restartFix());
fixes.push(await restartFix());
}
-21
View File
@@ -1,21 +0,0 @@
import { getFocusNavController, sleep } from '@decky/ui';
import Logger from '../logger';
const logger = new Logger('ReloadSteamFix');
declare global {
var GamepadNavTree: any;
}
export default async function reloadFix() {
// Hack to unbreak the ui when reloading it
await sleep(4000);
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
logger.log('Applied UI reload fix.');
}
// This steamfix does not need to deinit.
return () => {};
}
+4 -4
View File
@@ -1,7 +1,7 @@
import type { ToastData } from '@decky/api';
import {
Export,
Patch,
ToastData,
afterPatch,
findClass,
findInReactTree,
@@ -124,12 +124,12 @@ class Toaster extends Logger {
this.node.alternate.type = this.node.type;
}
};
const oRender = this.rNode.stateNode.__proto__.render;
let int: NodeJS.Timer | undefined;
const oRender = Object.getPrototypeOf(this.rNode.stateNode).render;
let int: NodeJS.Timeout | undefined;
this.rNode.stateNode.render = (...args: any[]) => {
const ret = oRender.call(this.rNode.stateNode, ...args);
if (ret && !this?.node?.return?.return) {
clearInterval(int);
int && clearInterval(int);
int = setInterval(() => {
const n = findToasterRoot(tree, 0);
if (n?.return) {
+1 -4
View File
@@ -25,9 +25,6 @@ export interface VerInfo {
export const doUpdate = DeckyBackend.callable('updater/do_update');
export const doRestart = DeckyBackend.callable('updater/do_restart');
export const doShutdown = DeckyBackend.callable('updater/do_shutdown');
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
DeckyBackend.addEventListener('updater/finish_download', async () => {
await doRestart();
});
+22 -27
View File
@@ -11,47 +11,42 @@ export enum TranslationClass {
}
interface TranslationHelperProps {
trans_class: TranslationClass;
trans_text: string;
i18n_args?: {};
install_type?: number;
transClass: TranslationClass;
transText: string;
i18nArgs?: {};
installType?: number;
}
const logger = new Logger('TranslationHelper');
const TranslationHelper: FC<TranslationHelperProps> = ({
trans_class,
trans_text,
i18n_args = null,
install_type = 0,
}) => {
const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null, installType = 0 }) => {
return (
<Translation>
{(t, {}) => {
switch (trans_class) {
switch (transClass) {
case TranslationClass.PLUGIN_LOADER:
return i18n_args
? t(TranslationClass.PLUGIN_LOADER + '.' + trans_text, i18n_args)
: t(TranslationClass.PLUGIN_LOADER + '.' + trans_text);
return i18nArgs
? t(TranslationClass.PLUGIN_LOADER + '.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_LOADER + '.' + transText);
case TranslationClass.PLUGIN_INSTALL_MODAL:
switch (install_type) {
switch (installType) {
case InstallType.INSTALL:
return i18n_args
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text, i18n_args)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text);
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText);
case InstallType.REINSTALL:
return i18n_args
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text, i18n_args)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text);
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText);
case InstallType.UPDATE:
return i18n_args
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text, i18n_args)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text);
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText);
}
case TranslationClass.DEVELOPER:
return i18n_args
? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args)
: t(TranslationClass.DEVELOPER + '.' + trans_text);
return i18nArgs
? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs)
: t(TranslationClass.DEVELOPER + '.' + transText);
default:
logger.error('We should never fall in the default case!');
return '';