fix(i18n): review translations and fix i18n library misuse (#2750)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 22:58:09 +02:00
1 parent 78f783f0c4
commit 8cb097f954
575 files changed
+192671 -160540

No files matched your search

+7 -1
View File
@@ -42,6 +42,8 @@ let trayActionBridgeWebContentsId: number | null = null;
let trayForcedExitTimer: NodeJS.Timeout | null = null;
let pendingTrayActions: Array<TrayActionPayload> = [];
const BIDI_FIRST_STRONG_ISOLATE = '\u2068';
const BIDI_POP_DIRECTIONAL_ISOLATE = '\u2069';
const MAX_PENDING_TRAY_ACTIONS = 16;
const LINUX_TRAY_MENU_ACTION_DELAY_MS = 25;
const TRAY_FORCED_EXIT_DELAY_MS = 5000;
@@ -296,6 +298,10 @@ function runTrayMenuAction(action: () => void): void {
timeout.unref?.();
}
function isolateUserText(value: string): string {
return `${BIDI_FIRST_STRONG_ISOLATE}${value}${BIDI_POP_DIRECTIONAL_ISOLATE}`;
}
function buildTrayMenu(): Menu {
const mainWindow = controller?.getMainWindow() ?? null;
const visible = Boolean(
@@ -368,7 +374,7 @@ function buildTrayMenu(): Menu {
});
menuTemplate.push({
label: trayState.voiceChannelLabel
? t('desktop.tray.disconnectFrom', {channel: trayState.voiceChannelLabel})
? t('desktop.tray.disconnectFrom', {channel: isolateUserText(trayState.voiceChannelLabel)})
: t('desktop.tray.disconnectVoice'),
click: () => runTrayMenuAction(() => dispatchTrayAction({action: 'disconnect-voice'})),
});
+14 -3
View File
@@ -10,7 +10,7 @@ import {createChildLogger} from '@electron/common/Logger';
import {TASK_ARG_PREFIX} from '@electron/main/JumpList';
import {getStableLinuxLaunchPath} from '@electron/main/LinuxLaunchPath';
import {isFlatpakRuntime} from '@electron/main/LinuxSandbox';
import {t} from '@electron/main/MainI18n';
import {getNativeLocale, t} from '@electron/main/MainI18n';
import {app} from 'electron';
const logger = createChildLogger('LinuxDesktopEntry');
@@ -18,6 +18,8 @@ const APP_NAME = DESKTOP_APP_NAME;
const APP_ID = LINUX_DESKTOP_ENTRY_ID;
export const WM_CLASS = APP_ID;
const DESKTOP_FILE_BASENAME = `${APP_ID}.desktop`;
const GENERIC_NAME_DEFAULT = 'Instant Messenger';
const COMMENT_DEFAULT = 'Instant messaging and VoIP';
const GENERATED_MARKER = '# X-Generated-By=fluxer-desktop';
const DESKTOP_ACTIONS = [
{
@@ -123,6 +125,15 @@ function buildDesktopActionEntries(execPath: string): Array<string> {
return entries;
}
function buildLocalizedEntryLines(key: string, defaultValue: string, localizedValue: string): Array<string> {
const lines = [`${key}=${escapeDesktopValue(defaultValue)}`];
if (localizedValue !== defaultValue) {
const entryLocale = getNativeLocale().replace(/-/g, '_');
lines.push(`${key}[${entryLocale}]=${escapeDesktopValue(localizedValue)}`);
}
return lines;
}
function buildDesktopFileContents(execPath: string, hidden: boolean): string {
const execLine = `${quoteExecArg(execPath)} %U`;
return [
@@ -131,8 +142,8 @@ function buildDesktopFileContents(execPath: string, hidden: boolean): string {
'Type=Application',
'Version=1.5',
`Name=${escapeDesktopValue(APP_NAME)}`,
'GenericName=Instant Messenger',
'Comment=Instant messaging and VoIP',
...buildLocalizedEntryLines('GenericName', GENERIC_NAME_DEFAULT, t('desktop.linuxEntry.genericName')),
...buildLocalizedEntryLines('Comment', COMMENT_DEFAULT, t('desktop.linuxEntry.comment')),
`Exec=${escapeDesktopValue(execLine)}`,
`TryExec=${escapeDesktopValue(execPath)}`,
`Icon=${escapeDesktopValue(resolveIconHint())}`,
+45 -21
View File
@@ -13,56 +13,80 @@ const FALLBACK_STRINGS: Readonly<Record<string, string>> = Object.freeze({
'desktop.appMenu.preferencesPlain': 'Preferences',
'desktop.appMenu.hide': 'Hide {appName}',
'desktop.appMenu.quit': 'Quit {appName}',
'desktop.appMenu.exit': 'Exit',
'desktop.appMenu.file': 'File',
'desktop.appMenu.edit': 'Edit',
'desktop.appMenu.undo': 'Undo',
'desktop.appMenu.redo': 'Redo',
'desktop.appMenu.cut': 'Cut',
'desktop.appMenu.copy': 'Copy',
'desktop.appMenu.paste': 'Paste',
'desktop.appMenu.pasteAndMatchStyle': 'Paste and match style',
'desktop.appMenu.delete': 'Delete',
'desktop.appMenu.selectAll': 'Select all',
'desktop.appMenu.speech': 'Speech',
'desktop.appMenu.startSpeaking': 'Start speaking',
'desktop.appMenu.stopSpeaking': 'Stop speaking',
'desktop.appMenu.view': 'View',
'desktop.appMenu.toggleDeveloperTools': 'Toggle Developer Tools',
'desktop.appMenu.actualSize': 'Actual Size',
'desktop.appMenu.zoomIn': 'Zoom In',
'desktop.appMenu.zoomOut': 'Zoom Out',
'desktop.appMenu.reload': 'Reload',
'desktop.appMenu.forceReload': 'Force reload',
'desktop.appMenu.toggleDeveloperTools': 'Toggle developer tools',
'desktop.appMenu.actualSize': 'Actual size',
'desktop.appMenu.zoomIn': 'Zoom in',
'desktop.appMenu.zoomOut': 'Zoom out',
'desktop.appMenu.toggleFullScreen': 'Toggle full screen',
'desktop.appMenu.window': 'Window',
'desktop.appMenu.minimize': 'Minimize',
'desktop.appMenu.zoomWindow': 'Zoom',
'desktop.appMenu.close': 'Close',
'desktop.appMenu.help': 'Help',
'desktop.appMenu.website': 'Website',
'desktop.appMenu.github': 'GitHub',
'desktop.appMenu.reportIssue': 'Report Issue',
'desktop.appMenu.reportIssue': 'Report issue',
'desktop.appMenu.troubleshooting': 'Troubleshooting',
'desktop.troubleshooting.disableHardwareAccelerationAndRestart': 'Disable Hardware Acceleration and Restart',
'desktop.troubleshooting.enableHardwareAccelerationAndRestart': 'Enable Hardware Acceleration and Restart',
'desktop.troubleshooting.disableHardwareAccelerationAndRestart': 'Disable hardware acceleration and restart',
'desktop.troubleshooting.enableHardwareAccelerationAndRestart': 'Enable hardware acceleration and restart',
'desktop.troubleshooting.reload': 'Reload',
'desktop.troubleshooting.resetAppDataAndRestart': 'Reset App Data and Restart',
'desktop.troubleshooting.resetTitle': 'Reset App Data',
'desktop.troubleshooting.resetMessage': 'Are you sure you want to reset Fluxer\u2019s app data?',
'desktop.troubleshooting.resetAppDataAndRestart': 'Reset app data and restart',
'desktop.troubleshooting.resetTitle': 'Reset app data',
'desktop.troubleshooting.resetMessage': "Are you sure you want to reset {appName}'s app data?",
'desktop.troubleshooting.resetDetail':
'This will sign you out, clear cached files and stored sessions, and restart the desktop app. Your account, communities, and messages on the server are not affected.',
'desktop.troubleshooting.resetConfirm': 'Reset and Restart',
'This will sign you out, clear cached files and stored sessions, and restart the desktop app. Your account, communities, and messages in {appName} are not affected.',
'desktop.troubleshooting.resetConfirm': 'Reset and restart',
'desktop.troubleshooting.resetCancel': 'Cancel',
'desktop.tray.show': 'Show {appName}',
'desktop.tray.hide': 'Hide {appName}',
'desktop.tray.openSettings': 'Open Settings',
'desktop.tray.openSettings': 'Open settings',
'desktop.tray.status': 'Status',
'desktop.tray.statusOnline': 'Online',
'desktop.tray.statusIdle': 'Idle',
'desktop.tray.statusDnd': 'Do Not Disturb',
'desktop.tray.statusDnd': 'Do not disturb',
'desktop.tray.statusInvisible': 'Invisible',
'desktop.tray.muteMic': 'Mute Microphone',
'desktop.tray.unmuteMic': 'Unmute Microphone',
'desktop.tray.muteMic': 'Mute microphone',
'desktop.tray.unmuteMic': 'Unmute microphone',
'desktop.tray.deafen': 'Deafen',
'desktop.tray.undeafen': 'Undeafen',
'desktop.tray.disconnectFrom': 'Disconnect from {channel}',
'desktop.tray.disconnectVoice': 'Disconnect from Voice',
'desktop.tray.checkForUpdates': 'Check for Updates',
'desktop.tray.copyBuildInfo': 'Copy Build Info',
'desktop.tray.disconnectVoice': 'Disconnect from voice',
'desktop.tray.checkForUpdates': 'Check for updates',
'desktop.tray.copyBuildInfo': 'Copy build info',
'desktop.tray.restart': 'Restart {appName}',
'desktop.tray.quit': 'Quit {appName}',
'desktop.autostart.portalReason': 'Start {appName} automatically when you sign in.',
'desktop.jumpList.tasks': 'Tasks',
'desktop.jumpList.openSettings': 'Open Settings',
'desktop.jumpList.openSettings': 'Open settings',
'desktop.jumpList.openSettingsDescription': 'Open {appName} settings',
'desktop.jumpList.newDirectMessage': 'New Direct Message',
'desktop.jumpList.newDirectMessage': 'New direct message',
'desktop.jumpList.newDirectMessageDescription': 'Compose a new direct message',
'desktop.jumpList.recent': 'Recent',
'desktop.notifications.open': 'Open',
'desktop.themes.addLocalFiles': 'Add local files',
'desktop.themes.importFolder': 'Import folder',
'desktop.badge.unreadMessages': 'Unread messages',
'desktop.badge.unreadMessagesCount': 'Unread messages: {count}',
'desktop.startup.failedTitle': '{appName} failed to start',
'desktop.linuxEntry.genericName': 'Instant Messenger',
'desktop.linuxEntry.comment': 'Instant messaging and VoIP',
});
const STORAGE_FILE_NAME = 'native-strings.json';
+27 -16
View File
@@ -66,30 +66,40 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
},
},
{type: 'separator'},
{role: 'quit'},
{
role: 'quit',
label: t('desktop.appMenu.exit'),
},
],
});
template.push({
label: t('desktop.appMenu.edit'),
submenu: [
{role: 'undo'},
{role: 'redo'},
{role: 'undo', label: t('desktop.appMenu.undo')},
{role: 'redo', label: t('desktop.appMenu.redo')},
{type: 'separator'},
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{role: 'cut', label: t('desktop.appMenu.cut')},
{role: 'copy', label: t('desktop.appMenu.copy')},
{role: 'paste', label: t('desktop.appMenu.paste')},
...(isMac
? [
{role: 'pasteAndMatchStyle' as const},
{role: 'delete' as const},
{role: 'selectAll' as const},
{role: 'pasteAndMatchStyle' as const, label: t('desktop.appMenu.pasteAndMatchStyle')},
{role: 'delete' as const, label: t('desktop.appMenu.delete')},
{role: 'selectAll' as const, label: t('desktop.appMenu.selectAll')},
{type: 'separator' as const},
{
label: t('desktop.appMenu.speech'),
submenu: [{role: 'startSpeaking' as const}, {role: 'stopSpeaking' as const}],
submenu: [
{role: 'startSpeaking' as const, label: t('desktop.appMenu.startSpeaking')},
{role: 'stopSpeaking' as const, label: t('desktop.appMenu.stopSpeaking')},
],
},
]
: [{role: 'delete' as const}, {type: 'separator' as const}, {role: 'selectAll' as const}]),
: [
{role: 'delete' as const, label: t('desktop.appMenu.delete')},
{type: 'separator' as const},
{role: 'selectAll' as const, label: t('desktop.appMenu.selectAll')},
]),
],
});
const zoomInHandler = () => {
@@ -101,8 +111,8 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
template.push({
label: t('desktop.appMenu.view'),
submenu: [
{role: 'reload'},
{role: 'forceReload'},
{role: 'reload', label: t('desktop.appMenu.reload')},
{role: 'forceReload', label: t('desktop.appMenu.forceReload')},
{
label: t('desktop.appMenu.toggleDeveloperTools'),
accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I',
@@ -146,12 +156,13 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
},
},
{type: 'separator'},
{role: 'togglefullscreen'},
{role: 'togglefullscreen', label: t('desktop.appMenu.toggleFullScreen')},
...(isMac
? []
: [
{
role: 'togglefullscreen' as const,
label: t('desktop.appMenu.toggleFullScreen'),
accelerator: 'Alt+Enter',
visible: false,
},
@@ -161,8 +172,8 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
template.push({
label: t('desktop.appMenu.window'),
submenu: [
{role: 'minimize'},
{role: 'zoom'},
{role: 'minimize', label: t('desktop.appMenu.minimize')},
{role: 'zoom', label: t('desktop.appMenu.zoomWindow')},
...(isMac
? [
{type: 'separator' as const},
+2 -1
View File
@@ -5,6 +5,7 @@ import {DESKTOP_APP_NAME, LINUX_DESKTOP_ENTRY_ID} from '@electron/common/Desktop
import {createChildLogger} from '@electron/common/Logger';
import type {NotificationOptions} from '@electron/common/Types';
import {getNativeNotificationsMode} from '@electron/main/LaunchOptions';
import {t} from '@electron/main/MainI18n';
import {resolveNotificationIcon} from '@electron/main/NotificationIcon';
import {shouldPlayNotificationSound} from '@electron/main/NotificationState';
import {requirePrivilegedRendererDocumentSender} from '@electron/main/PrivilegedRendererDocuments';
@@ -291,7 +292,7 @@ async function showLinuxNativeNotification(
actionIcons: false,
...(imageData ? {imageData} : {}),
},
actions: [{key: 'default', label: 'Open'}],
actions: [{key: 'default', label: t('desktop.notifications.open')}],
};
try {
const nativeId = await client.notify(payload);
+3 -2
View File
@@ -9,6 +9,7 @@ import {
} from '@electron/common/DesktopConfig';
import {createChildLogger} from '@electron/common/Logger';
import type {ThemeDirectoryCssFile, ThemeLocalFileReadResult, ThemeLocalFileReference} from '@electron/common/Types';
import {t} from '@electron/main/MainI18n';
import {BrowserWindow, dialog, ipcMain} from 'electron';
const logger = createChildLogger('ThemeLocalFiles');
@@ -97,7 +98,7 @@ export function registerThemeLocalFileHandlers(getMainWindow: () => BrowserWindo
ipcMain.handle('theme-local-files-pick', async (event): Promise<Array<ThemeLocalFileReference>> => {
const parent = BrowserWindow.fromWebContents(event.sender) ?? getMainWindow();
const result = await showThemeOpenDialog(parent, {
title: 'Add theme files',
title: t('desktop.themes.addLocalFiles'),
properties: ['openFile', 'multiSelections'],
});
if (result.canceled) {
@@ -159,7 +160,7 @@ export function registerThemeLocalFileHandlers(getMainWindow: () => BrowserWindo
ipcMain.handle('theme-directory-import', async (event): Promise<Array<ThemeDirectoryCssFile>> => {
const parent = BrowserWindow.fromWebContents(event.sender) ?? getMainWindow();
const result = await showThemeOpenDialog(parent, {
title: 'Import theme directory',
title: t('desktop.themes.importFolder'),
properties: ['openDirectory'],
});
if (result.canceled || result.filePaths.length === 0) {
+14 -3
View File
@@ -2,6 +2,7 @@
import fs from 'node:fs';
import path from 'node:path';
import {getNativeLocale, t} from '@electron/main/MainI18n';
import type {BrowserWindow, NativeImage} from 'electron';
import {nativeImage} from 'electron';
@@ -9,6 +10,7 @@ const badgeIcons: Array<NativeImage | null> = [];
let hasInit = false;
let lastIndex: number | null = null;
let lastDescription: string | null = null;
let lastCount: number | null = null;
function isSupported(): boolean {
@@ -32,6 +34,14 @@ function ensureInitialized(): void {
}
}
function formatBadgeCount(count: number): string {
try {
return count.toLocaleString(getNativeLocale());
} catch {
return String(count);
}
}
function getOverlayIconData(count: number): {
index: number | null;
description: string;
@@ -39,7 +49,7 @@ function getOverlayIconData(count: number): {
if (count === -1) {
return {
index: 10,
description: 'Unread messages',
description: t('desktop.badge.unreadMessages'),
};
}
if (count === 0) {
@@ -51,7 +61,7 @@ function getOverlayIconData(count: number): {
const index = Math.max(1, Math.min(count, 10)) - 1;
return {
index,
description: `${index} notifications`,
description: t('desktop.badge.unreadMessagesCount', {count: formatBadgeCount(count)}),
};
}
@@ -63,7 +73,7 @@ function applyOverlay(win: BrowserWindow | null, count: number, force: boolean):
return;
}
const {index, description} = getOverlayIconData(count);
if (force || lastIndex !== index) {
if (force || lastIndex !== index || lastDescription !== description) {
if (index == null) {
win.setOverlayIcon(null, description);
} else {
@@ -71,6 +81,7 @@ function applyOverlay(win: BrowserWindow | null, count: number, force: boolean):
win.setOverlayIcon(icon ?? null, description);
}
lastIndex = index;
lastDescription = description;
}
lastCount = count;
}
+7 -7
View File
@@ -57,7 +57,7 @@ import {cleanupIpcHandlers, registerIpcHandlers} from '@electron/main/IpcHandler
import {initializeJumpList} from '@electron/main/JumpList';
import {describeLaunchDiagnosticOptions, shouldStartHiddenAtLogin} from '@electron/main/LaunchOptions';
import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudioCapture';
import {initializeMainI18n} from '@electron/main/MainI18n';
import {initializeMainI18n, t} from '@electron/main/MainI18n';
import {createApplicationMenu} from '@electron/main/Menu';
import {cleanupNativeAudio, registerNativeAudioHandlers} from '@electron/main/NativeAudio';
import {
@@ -237,6 +237,11 @@ if (launchConfigurationError) {
if (hasLegacySquirrelArg) {
app.exit(0);
}
try {
runStartupPhase('main-i18n', initializeMainI18n);
} catch (error) {
log.error('[Init] Failed to initialize native i18n:', error);
}
try {
runStartupPhase('native-module-preflight', runNativeModulePreflight);
} catch (error) {
@@ -244,7 +249,7 @@ if (launchConfigurationError) {
log.error('[NativeModulePreflight] Fatal native module preflight failure:', error);
console.error(message);
try {
dialog.showErrorBox('Fluxer failed to start', message);
dialog.showErrorBox(t('desktop.startup.failedTitle'), message);
} catch {}
app.exit(1);
process.exit(1);
@@ -317,11 +322,6 @@ if (launchConfigurationError) {
} catch (error) {
log.error('[DebugInfo] Failed to collect desktop debug info:', error);
}
try {
runStartupPhase('main-i18n', initializeMainI18n);
} catch (error) {
log.error('[Init] Failed to initialize native i18n:', error);
}
try {
runStartupPhase('deep-links', initializeDeepLinks);
} catch (error) {