From 7401cf69ad007970237fd9f37cd6ad7cdf2da955 Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Fri, 1 Dec 2023 01:30:46 +0900 Subject: [PATCH] feat: add support i18n (#1468) --- package.json | 1 + pnpm-lock.yaml | 9 + src/config/defaults.ts | 1 + src/i18n/index.ts | 18 + src/i18n/resources/en.json | 572 ++++++++++++++++++ src/i18n/resources/index.ts | 11 + src/i18n/resources/ko.json | 7 + src/index.ts | 92 ++- src/loader/main.ts | 29 +- src/loader/menu.ts | 12 +- src/loader/preload.ts | 13 +- src/loader/renderer.ts | 9 +- src/menu.ts | 115 ++-- src/plugins/adblocker/index.ts | 8 +- src/plugins/album-color-theme/index.ts | 6 +- src/plugins/ambient-mode/index.ts | 34 +- src/plugins/audio-compressor.ts | 6 +- src/plugins/blur-nav-bar/index.ts | 5 +- src/plugins/bypass-age-restrictions/index.ts | 5 +- src/plugins/captions-selector/back.ts | 7 +- src/plugins/captions-selector/index.ts | 9 +- src/plugins/compact-sidebar/index.ts | 5 +- src/plugins/crossfade/index.ts | 22 +- src/plugins/disable-autoplay/index.ts | 8 +- src/plugins/discord/index.ts | 5 +- src/plugins/discord/main.ts | 19 +- src/plugins/discord/menu.ts | 20 +- src/plugins/downloader/index.ts | 5 +- src/plugins/downloader/main/index.ts | 129 +++- src/plugins/downloader/menu.ts | 7 +- src/plugins/downloader/renderer.ts | 8 +- .../downloader/templates/download.html | 2 +- src/plugins/exponential-volume/index.ts | 6 +- src/plugins/in-app-menu/index.ts | 5 +- src/plugins/in-app-menu/menu.ts | 4 +- src/plugins/in-app-menu/menu/panel.ts | 2 +- src/plugins/last-fm/index.ts | 5 +- src/plugins/lumiastream/index.ts | 5 +- src/plugins/lyrics-genius/index.ts | 7 +- src/plugins/lyrics-genius/renderer.ts | 8 +- src/plugins/navigation/index.ts | 7 +- src/plugins/no-google-login/index.ts | 5 +- src/plugins/notifications/index.ts | 6 +- src/plugins/notifications/menu.ts | 18 +- src/plugins/picture-in-picture/index.ts | 5 +- src/plugins/picture-in-picture/menu.ts | 18 +- .../templates/picture-in-picture.html | 2 +- src/plugins/playback-speed/index.ts | 6 +- .../playback-speed/templates/slider.html | 2 +- src/plugins/precise-volume/index.ts | 39 +- src/plugins/quality-changer/index.ts | 14 +- src/plugins/shortcuts/index.ts | 6 +- src/plugins/shortcuts/menu.ts | 16 +- src/plugins/skip-silences/index.ts | 5 +- src/plugins/sponsorblock/index.ts | 7 +- src/plugins/taskbar-mediacontrol/index.ts | 5 +- src/plugins/touchbar/index.ts | 5 +- src/plugins/tuna-obs/index.ts | 5 +- src/plugins/video-toggle/index.ts | 27 +- .../templates/button_template.html | 6 +- src/plugins/visualizer/index.ts | 7 +- src/preload.ts | 8 +- src/renderer.ts | 29 + src/reset.d.ts | 4 + src/utils/index.ts | 36 +- 65 files changed, 1226 insertions(+), 303 deletions(-) create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/resources/en.json create mode 100644 src/i18n/resources/index.ts create mode 100644 src/i18n/resources/ko.json diff --git a/package.json b/package.json index c7fdb48f..171caa47 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "filenamify": "6.0.0", "howler": "2.2.4", "html-to-text": "9.0.5", + "i18next": "23.7.7", "keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevents-areequal": "0.2.2", "node-html-parser": "6.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cadf292..1a6f78fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ dependencies: html-to-text: specifier: 9.0.5 version: 9.0.5 + i18next: + specifier: 23.7.7 + version: 23.7.7 keyboardevent-from-electron-accelerator: specifier: 2.0.0 version: 2.0.0 @@ -3856,6 +3859,12 @@ packages: engines: {node: '>=14.18.0'} dev: true + /i18next@23.7.7: + resolution: {integrity: sha512-peTvdT+Lma+o0LfLFD7IC2M37N9DJ04dH0IJYOyOHRhDfLo6nK36v7LkrQH35C2l8NHiiXZqGirhKESlEb/5PA==} + dependencies: + '@babel/runtime': 7.23.2 + dev: false + /iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} diff --git a/src/config/defaults.ts b/src/config/defaults.ts index c0cc051e..a09f3194 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -14,6 +14,7 @@ export interface DefaultConfig { 'window-position': WindowPositionConfig; url: string; options: { + language?: string; tray: boolean; appVisible: boolean; autoUpdates: boolean; diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 00000000..c3d3c2dc --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,18 @@ +import i18next, { init, t as i18t, changeLanguage } from 'i18next'; + +import { languageResources } from '@/i18n/resources'; + +export const loadI18n = async () => + await init({ + resources: languageResources, + lng: 'en', + fallbackLng: 'en', + interpolation: { + escapeValue: false + } + }); + +export const setLanguage = async (language: string) => await changeLanguage(language); + +export const t = i18t.bind(i18next); + diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json new file mode 100644 index 00000000..3d2a98cb --- /dev/null +++ b/src/i18n/resources/en.json @@ -0,0 +1,572 @@ +{ + "language": { + "name": "English", + "local-name": "English", + "code": "en" + }, + + "plugins": { + "adblocker": { + "name": "Adblocker", + "description": "Block all ads and tracking out of the box", + "menu": { + "blocker": "Blocker" + } + }, + "album-color-theme": { + "name": "Album Color Theme", + "description": "Applies a dynamic theme and visual effects based on the album color palette" + }, + "ambient-mode": { + "name": "Ambient Mode", + "description": "Applies a lighting effect by casting gentle colors from the video, into your screen’s background.", + "menu": { + "smoothness-transition": { + "label": "Smoothness transition", + "submenu": { + "during": "During {{interpolationTime}}s" + } + }, + "quality": { + "label": "Quality", + "submenu": { + "pixels": "{{quality}} pixels" + } + }, + "size": { + "label": "Size", + "submenu": { + "percent": "{{size}}%" + } + }, + "buffer": { + "label": "Buffer", + "submenu": { + "buffer": "{{buffer}}" + } + }, + "opacity": { + "label": "Opacity", + "submenu": { + "percent": "{{opacity}}%" + } + }, + "blur-amount": { + "label": "Blur amount", + "submenu": { + "pixels": "{{blurAmount}} pixels" + } + }, + "use-fullscreen": { + "label": "Using fullscreen" + } + } + }, + "audio-compressor": { + "name": "Audio Compressor", + "description": "Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)" + }, + "blur-nav-bar": { + "name": "Blur Navigation Bar", + "description": "Makes navigation bar transparent and blurry" + }, + "bypass-age-restrictions": { + "name": "Bypass Age Restrictions", + "description": "Bypass YouTube's age verification" + }, + "captions-selector": { + "name": "Captions Selector", + "description": "Caption selector for YouTube Music audio tracks", + "menu": { + "autoload": "Automatically select last used caption", + "disable-captions": "No captions by default" + }, + "templates": { + "title": "Open captions selector" + }, + "prompt": { + "selector": { + "title": "Select caption language", + "label": "Current caption language: {{language}}", + "none": "None" + } + } + }, + "compact-sidebar": { + "name": "Compact Sidebar", + "description": "Always set the sidebar in compact mode" + }, + "crossfade": { + "name": "Crossfade [beta]", + "description": "Crossfade between songs", + "menu": { + "advanced": "Advanced" + }, + "prompt": { + "options": { + "title": "Crossfade options", + "multi-input": { + "fade-in-duration": "Fade in duration (milliseconds)", + "fade-out-duration": "Fade out duration (milliseconds)", + "seconds-before-end": "Crossfade N seconds before end", + "fade-scaling": { + "label": "Fade scaling", + "linear": "Linear", + "logarithmic": "Logarithmic" + } + } + } + } + }, + "disable-autoplay": { + "name": "Disable Autoplay", + "description": "Makes song start in \"paused\" mode", + "menu": { + "apply-once": "Applies only on startup" + } + }, + "discord": { + "name": "Discord Rich Presence", + "description": "Show your friends what you listen to with Rich Presence", + "menu": { + "auto-reconnect": "Auto reconnect", + "clear-activity": "Clear activity", + "clear-activity-after-timeout": "Clear activity after timeout", + "play-on-youtube-music": "Play on YouTube Music", + "hide-github-button": "Hide GitHub link Button", + "hide-duration-left": "Hide duration left", + "set-inactivity-timeout": "Set inactivity timeout" + }, + "prompt": { + "set-inactivity-timeout": { + "title": "Set inactivity timeout", + "label": "Enter inactivity timeout in seconds:" + } + }, + "backend": { + "connected": "Connected to Discord", + "disconnected": "Disconnected from Discord", + "already-connected": "Attempted to connect with active connection" + } + }, + "downloader": { + "name": "Downloader", + "description": "Downloads MP3 / source audio directly from the interface", + "menu": { + "choose-download-folder": "Choose download folder", + "presets": "Presets", + "skip-existing": "Skip existing files" + }, + "templates": { + "button": "Download" + }, + "backend": { + "dialog": { + "error": { + "title": "Error in download!", + "message": "Argh! Apologies, download failed…", + "buttons": { + "ok": "OK" + } + }, + "start-download-playlist": { + "title": "Download started", + "message": "Downloading Playlist {{playlistTitle}}", + "detail": "({{playlistSize}} songs)", + "buttons": { + "ok": "OK" + } + } + }, + "feedback": { + "downloading": "Downloading…", + "download-progress": "Download: {{percent}}%", + "loading": "Loading…", + "preparing-file": "Preparing file…", + "converting": "Converting…", + "conversion-progress": "Conversion: {{percent}}%", + "saving": "Saving…", + "writing-id3": "Writing ID3 tags…", + "playlist-id-not-found": "No playlist ID found", + "video-id-not-found": "Video not found", + "download-info": "Downloading {{artist}} - {{title}} [{{videoId}}", + "done": "Done: {{filePath}}", + "trying-to-get-playlist-id": "Trying to get playlist ID: {{playlistId}}", + "getting-playlist-info": "Getting playlist info…", + "playlist-is-mix-or-private": "Error getting playlist info: make sure it isn't a private or \"Mixed for you\" playlist\n\n{{error}}", + "playlist-is-empty": "Playlist is empty", + "playlist-has-only-one-song": "Playlist has only one item, downloading it directly", + "folder-already-exists": "The folder {{playlistFolder}} already exists", + "downloading-playlist": "Downloading playlist \"{{playlistTitle}}\" - {{playlistSize}} songs ({{playlistId}})", + "downloading-counter": "Downloading {{current}}/{{total}}…", + "error-while-downloading": "Error downloading \"{{author}} - {{title}}\": {{error}}" + } + }, + "renderer": { + "can-not-update-progress": "Cannot update progress" + } + }, + "exponential-volume": { + "name": "Exponential Volume", + "description": "Makes the volume slider exponential so it's easier to select lower volumes." + }, + "in-app-menu": { + "name": "In-App Menu", + "description": "Gives menu-bars a fancy, dark or album-color look", + "menu": { + "hide-dom-window-controls": "Hide DOM window controls" + } + }, + "last-fm": { + "name": "Last.fm", + "description": "Add scrobbling support for Last.fm" + }, + "lumiastream": { + "name": "Lumia Stream [beta]", + "description": "Adds Lumia Stream support" + }, + "lyrics-genius": { + "name": "Lyrics Genius", + "description": "Adds lyrics support for most songs", + "menu": { + "romanized-lyrics": "Romanized Lyrics" + }, + "renderer": { + "fetched-lyrics": "Fetched lyrics for Genius" + } + }, + "navigation": { + "name": "Navigation", + "description": "Next/Back navigation arrows directly integrated in the interface, like in your favorite browser" + }, + "no-google-login": { + "name": "No Google Login", + "description": "Remove Google login buttons and links from the interface" + }, + "notifications": { + "name": "Notifications", + "description": "Display a notification when a song starts playing (interactive notifications are available on Windows)", + "menu": { + "priority": "Notification Priority", + "interactive": "Interactive Notifications", + "interactive-settings": { + "label": "Interactive Settings", + "submenu": { + "tray-controls": "Open/Close on tray click", + "hide-button-text": "Hide button text", + "refresh-on-play-pause": "Refresh on Play/Pause" + } + }, + "toast-style": "Toast style", + "unpause-notification": "Show notification on unpause" + } + }, + "picture-in-picture": { + "name": "Picture in Picture", + "description": "Allows to switch the app to picture-in-picture mode", + "menu": { + "always-on-top": "Always on top", + "save-window-position": "Save window position", + "save-window-size": "Save window size", + "hotkey": { + "label": "Hotkey", + "prompt": { + "title": "Picture in Picture Hotkey", + "label": "Choose a hotkey for toggle Picture in Picture", + "keybind-options": { + "hotkey": "Hotkey" + } + } + }, + "use-native-pip": "Use browser native PiP" + }, + "templates": { + "button": "Picture in Picture" + } + }, + "playback-speed": { + "name": "Playback Speed", + "description": "Listen fast, listen slow! Adds a slider that controls song speed", + "templates": { + "button": "Speed" + } + }, + "precise-volume": { + "name": "Precise Volume", + "description": "Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps", + "menu": { + "arrows-shortcuts": "Local Arrow-keys Controls", + "global-shortcuts": "Global Hotkeys", + "custom-volume-steps": "Set Custom Volume Steps" + }, + "prompt": { + "volume-steps": { + "title": "Volume Steps", + "label": "Choose Volume Increase/Decrease Steps" + }, + "global-shortcuts": { + "title": "Global Volume Keybinds", + "label": "Choose Global Volume Keybinds:", + "keybind-options": { + "increase": "Increase Volume", + "decrease": "Decrease Volume" + } + } + } + }, + "quality-changer": { + "name": "Video Quality Changer", + "description": "Allows changing the video quality with a button on the video overlay", + "backend": { + "dialog": { + "quality-changer": { + "title": "Choose Video Quality", + "message": "Choose Video Quality:", + "detail": "Current Quality: {{quality}}" + } + } + } + }, + "shortcuts": { + "name": "Shortcuts (& MPRIS)", + "description": "Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users", + "prompt": { + "keybind": { + "title": "Global Keybinds", + "label": "Choose Global Keybinds for Songs Control:", + "keybind-options": { + "play-pause": "Play / Pause", + "next": "Next", + "previous": "Previous" + } + } + }, + "menu": { + "set-keybinds": "Set Global Song Controls", + "override-media-keys": "Override Media Keys" + } + }, + "skip-silences": { + "name": "Skip Silences", + "description": "Automatically skip silences sections in songs" + }, + "sponsorblock": { + "name": "SponsorBlock", + "description": "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing" + }, + "taskbar-mediacontrol": { + "name": "Taskbar Media Control", + "description": "Control playback from your Windows taskbar" + }, + "touchbar": { + "name": "TouchBar", + "description": "Adds a TouchBar widget for macOS users" + }, + "tuna-obs": { + "name": "Tuna OBS", + "description": "Integration with OBS's plugin Tuna" + }, + "video-toggle": { + "name": "Video Toggle", + "description": "Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab", + "menu": { + "mode": { + "label": "Mode", + "submenu": { + "custom": "Custom toggle", + "native": "Native toggle", + "disabled": "Disabled" + } + }, + "align": { + "label": "Alignment", + "submenu": { + "left": "Left", + "middle": "Middle", + "right": "Right" + } + }, + "force-hide": "Force remove video tab" + }, + "templates": { + "button": "Song" + } + }, + "visualizer": { + "name": "Visualizer", + "description": "Adds a visualizer to the player", + "menu": { + "visualizer-type": "Visualizer Type" + } + } + }, + "main": { + "dialog": { + "need-to-restart": { + "title": "Restart Required", + "message": "\"{{pluginName}}\" needs to restart", + "detail": "\"{{pluginName}}\" plugin requires a restart to take effect", + "buttons": { + "restart-now": "Restart Now", + "later": "Later" + } + }, + "update-available": { + "title": "Update Available", + "message": "A new version is available", + "detail": "A new version is available and can be downloaded at {{downloadLink}}", + "buttons": { + "ok": "OK", + "download": "Download", + "disable": "Disable Updates" + } + }, + "hide-menu-enabled": { + "title": "Hide Menu Enabled", + "message": "Hide Menu is enabled", + "detail": "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)" + }, + "unresponsive": { + "title": "Window Unresponsive", + "message": "The Application is Unresponsive", + "detail": "We are sorry for the inconvenience! please choose what to do:", + "buttons": { + "wait": "Wait", + "relaunch": "Relaunch", + "quit": "Quit" + } + } + }, + "console": { + "i18n": { + "loaded": "i18n loaded" + }, + "theme": { + "css-file-not-found": "CSS file \"{{cssFile}}\" does not exist, ignoring" + }, + "window": { + "tried-to-render-offscreen": "Window tried to render offscreen, windowSize={{windowSize}}, displaySize={{displaySize}}, position={{position}}" + }, + "when-ready": { + "clearing-cache-after-20s": "Clearing app cache" + }, + "second-instance": { + "receive-command": "Received command over protocol: \"{{command}}\"" + }, + "unresponsive": { + "details": "Unresponsive Error!\n{{error}}" + }, + "did-finish-load": { + "dev-tools": "did finish load. dev tools opened" + } + }, + "menu": { + "plugins": { + "label": "Plugins", + "enabled": "Enabled" + }, + "options": { + "label": "Options", + "submenu": { + "auto-update": "Auto Update", + "resume-on-start": "Resume last song when app starts", + "starting-page": { + "label": "Starting page", + "unset": "Unset" + }, + "visual-tweaks": { + "label": "Visual Tweaks", + "submenu": { + "remove-upgrade-button": "Remove upgrade button", + "like-buttons": { + "label": "Like buttons", + "default": "Default", + "force-show": "Force show", + "hide": "Hide" + }, + "theme": { + "label": "Theme", + "submenu": { + "no-theme": "No theme", + "import-css-file": "Import custom CSS file" + } + } + } + }, + "single-instance-lock": "Single Instance Lock", + "always-on-top": "Always on top", + "hide-menu": { + "label": "Hide Menu", + "dialog": { + "title": "Hide Menu Enabled", + "message": "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)" + } + }, + "start-at-login": "Start at login", + "tray": { + "label": "Tray", + "submenu": { + "disabled": "Disabled", + "enabled-and-show-app": "Enabled and show app", + "enabled-and-hide-app": "Enabled and hide app", + "play-pause-on-click": "Play/Pause on click" + } + }, + "language": { + "label": "Language", + "dialog": { + "title": "Language Changed", + "message": "Language will be changed after restart" + } + }, + "advanced-options": { + "label": "Advanced options", + "submenu": { + "set-proxy": { + "label": "Set proxy", + "prompt": { + "title": "Set proxy", + "label": "Enter Proxy Address: (leave empty to disable)", + "placeholder": "Example: socks5://127.0.0.1:9999" + } + }, + "override-user-agent": "Override User-Agent", + "disable-hardware-acceleration": "Disable hardware acceleration", + "restart-on-config-changes": "Restart on config changes", + "auto-reset-app-cache": "Reset App cache when app starts", + "toggle-dev-tools": "Toggle DevTools", + "edit-config-json": "Edit config.json" + } + } + } + }, + "view": { + "label": "View" + }, + "navigation": { + "label": "Navigation", + "submenu": { + "go-back": "Go back", + "go-forward": "Go forward", + "copy-current-url": "Copy current URL", + "restart": "Restart App" + } + }, + "about": "About" + } + }, + "common": { + "console": { + "plugins": { + "load-all": "Loading all plugins", + "unloaded": "Plugin \"{{pluginName}}\" unloaded", + "unload-failed": "Failed to unload plugin \"{{pluginName}}\"", + "load-failed": "Failed to load plugin \"{{pluginName}}\"", + "initialize-failed": "Failed to initialize plugin \"{{pluginName}}\"", + "loaded": "Plugin \"{{pluginName}}\" loaded", + "executed-at-ms": "Plugin {{pluginName}}::{{contextName}} executed at {{ms}}ms", + "execute-failed": "Failed to execute plugin {{pluginName}}::{{contextName}}" + } + } + } +} diff --git a/src/i18n/resources/index.ts b/src/i18n/resources/index.ts new file mode 100644 index 00000000..b624df7a --- /dev/null +++ b/src/i18n/resources/index.ts @@ -0,0 +1,11 @@ +import enJson from './en.json'; +import koJson from './ko.json'; + +export const languageResources = { + en: { + translation: enJson + }, + ko: { + translation: koJson + } +}; diff --git a/src/i18n/resources/ko.json b/src/i18n/resources/ko.json new file mode 100644 index 00000000..b66ad0f5 --- /dev/null +++ b/src/i18n/resources/ko.json @@ -0,0 +1,7 @@ +{ + "language": { + "name": "Korean", + "local-name": "한국어", + "code": "ko" + } +} diff --git a/src/index.ts b/src/index.ts index 18de66f6..879b3ef9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import url from 'node:url'; import fs from 'node:fs'; +import process from 'node:process'; import { BrowserWindow, @@ -49,6 +50,9 @@ import { } from '@/loader/main'; import { LoggerPrefix } from '@/utils'; +import { loadI18n, setLanguage, t } from '@/i18n'; + +import { languageResources } from '@/i18n/resources'; import type { PluginConfig } from '@/types/plugins'; @@ -183,10 +187,17 @@ const showNeedToRestartDialog = (id: string) => { const dialogOptions: Electron.MessageBoxOptions = { type: 'info', - buttons: ['Restart Now', 'Later'], - title: 'Restart Required', - message: `"${plugin?.name ?? id}" needs to restart`, - detail: `"${plugin?.name ?? id}" plugin requires a restart to take effect`, + buttons: [ + t('main.dialog.need-to-restart.buttons.restart-now'), + t('main.dialog.need-to-restart.buttons.later'), + ], + title: t('main.dialog.need-to-restart.title'), + message: t('main.dialog.need-to-restart.message', { + pluginName: plugin?.name ?? id, + }), + detail: t('main.dialog.need-to-restart.detail', { + pluginName: plugin?.name ?? id, + }), defaultId: 0, cancelId: 1, }; @@ -227,7 +238,7 @@ function initTheme(win: BrowserWindow) { () => { console.warn( LoggerPrefix, - `CSS file "${cssFile}" does not exist, ignoring`, + t('main.console.theme.css-file-not-found', { cssFile }), ); }, ); @@ -236,7 +247,7 @@ function initTheme(win: BrowserWindow) { win.webContents.once('did-finish-load', () => { if (is.dev()) { - console.log(LoggerPrefix, 'did finish load'); + console.debug(LoggerPrefix, t('main.console.did-finish-load.dev-tools')); win.webContents.openDevTools(); } }); @@ -305,13 +316,13 @@ async function createMainWindow() { ) { // Window is offscreen if (is.dev()) { - console.log( - `Window tried to render offscreen, windowSize=${String( - winSize, - )}, displaySize=${String(display.bounds)}, position=${String( - windowPosition, - )}`, - ); + console.warn( + LoggerPrefix, + t('main.console.window.tried-to-render-offscreen', { + winSize: String(winSize), + displaySize: String(display.bounds), + windowPosition: String(windowPosition), + })); } } else { win.setSize(scaledWidth, scaledHeight); @@ -543,12 +554,27 @@ app.on('activate', async () => { } }); +const getDefaultLocale = (locale: string) => + Object.keys(languageResources).includes(locale) ? locale : 'en'; + app.whenReady().then(async () => { + if (!config.get('options.language')) { + config.set('options.language', getDefaultLocale(app.getLocale())); + } + + await loadI18n().then(async () => { + await setLanguage(config.get('options.language') ?? 'en'); + console.log(LoggerPrefix, t('main.console.i18n.loaded')); + }); + if (config.get('options.autoResetAppCache')) { // Clear cache after 20s const clearCacheTimeout = setTimeout(() => { if (is.dev()) { - console.log('Clearing app cache.'); + console.log( + LoggerPrefix, + t('main.console.when-ready.clearing-cache-after-20s'), + ); } session.defaultSession.clearCache(); @@ -614,7 +640,7 @@ app.whenReady().then(async () => { const lastIndex = protocolArgv.endsWith('/') ? -1 : undefined; const command = protocolArgv.slice(uri.length, lastIndex); if (is.dev()) { - console.debug(`Received command over protocol: "${command}"`); + console.debug(LoggerPrefix, t('main.console.second-instance.receive-command', { command })); } handleProtocol(command); @@ -651,10 +677,14 @@ app.whenReady().then(async () => { 'https://github.com/th-ch/youtube-music/releases/latest'; const dialogOptions: Electron.MessageBoxOptions = { type: 'info', - buttons: ['OK', 'Download', 'Disable updates'], - title: 'Application Update', - message: 'A new version is available', - detail: `A new version is available and can be downloaded at ${downloadLink}`, + buttons: [ + t('main.dialog.update-available.buttons.download'), + t('main.dialog.update-available.buttons.later'), + t('main.dialog.update-available.buttons.disable'), + ], + title: t('main.dialog.update-available.title'), + message: t('main.dialog.update-available.message'), + detail: t('main.dialog.update-available.detail', { downloadLink }), }; let dialogPromise: Promise; @@ -689,9 +719,8 @@ app.whenReady().then(async () => { if (config.get('options.hideMenu') && !config.get('options.hideMenuWarned')) { dialog.showMessageBox(mainWindow, { type: 'info', - title: 'Hide Menu Enabled', - message: - "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)", + title: t('main.dialog.hide-menu-enabled.title'), + message: t('main.dialog.hide-menu-enabled.message'), }); config.set('options.hideMenuWarned', true); } @@ -722,16 +751,25 @@ function showUnresponsiveDialog( details: Electron.RenderProcessGoneDetails, ) { if (details) { - console.log('Unresponsive Error!\n' + JSON.stringify(details, null, '\t')); + console.error( + LoggerPrefix, + t('main.console.unresponsive.details', { + error: JSON.stringify(details, null, '\t'), + }), + ); } dialog .showMessageBox(win, { type: 'error', - title: 'Window Unresponsive', - message: 'The Application is Unresponsive', - detail: 'We are sorry for the inconvenience! please choose what to do:', - buttons: ['Wait', 'Relaunch', 'Quit'], + title: t('main.dialog.unresponsive.title'), + message: t('main.dialog.unresponsive.message'), + detail: t('main.dialog.unresponsive.detail'), + buttons: [ + t('main.dialog.unresponsive.buttons.wait'), + t('main.dialog.unresponsive.buttons.relaunch'), + t('main.dialog.unresponsive.buttons.quit'), + ], cancelId: 0, }) .then((result) => { diff --git a/src/loader/main.ts b/src/loader/main.ts index ed78300b..6b503532 100644 --- a/src/loader/main.ts +++ b/src/loader/main.ts @@ -6,6 +6,8 @@ import { allPlugins, mainPlugins } from 'virtual:plugins'; import config from '@/config'; import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils'; +import { t } from '@/i18n'; + import type { PluginConfig, PluginDef } from '@/types/plugins'; import type { BackendContext } from '@/types/contexts'; @@ -67,14 +69,23 @@ export const forceUnloadMainPlugin = async ( plugin.backend) ) { delete loadedPluginMap[id]; - console.log(LoggerPrefix, `"${id}" plugin is unloaded`); + console.log(LoggerPrefix, t( + 'common.console.plugins.unloaded', + { pluginName: id }, + )); return; } else { - console.log(LoggerPrefix, `Cannot unload "${id}" plugin`); + console.log( + LoggerPrefix, + t('common.console.plugins.unload-failed', { pluginName: id }), + ); return Promise.reject(); } } catch (err) { - console.error(LoggerPrefix, `Cannot unload "${id}" plugin`); + console.error( + LoggerPrefix, + t('common.console.plugins.unload-failed', { pluginName: id }), + ); console.trace(err); return Promise.reject(err); } @@ -100,18 +111,24 @@ export const forceLoadMainPlugin = async ( ) { loadedPluginMap[id] = plugin; } else { - console.log(LoggerPrefix, `Cannot load "${id}" plugin`); + console.log( + LoggerPrefix, + t('common.console.plugins.load-failed', { pluginName: id }), + ); return Promise.reject(); } } catch (err) { - console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `); + console.error( + LoggerPrefix, + t('common.console.plugins.initialize-failed', { pluginName: id }), + ); console.trace(err); return Promise.reject(err); } }; export const loadAllMainPlugins = async (win: BrowserWindow) => { - console.log(LoggerPrefix, 'Loading all plugins'); + console.log(LoggerPrefix, t('common.console.plugins.load-all')); const pluginConfigs = config.plugins.getPlugins(); const queue: Promise[] = []; diff --git a/src/loader/menu.ts b/src/loader/menu.ts index cb01cecb..252a6de2 100644 --- a/src/loader/menu.ts +++ b/src/loader/menu.ts @@ -6,6 +6,8 @@ import { setApplicationMenu } from '@/menu'; import { LoggerPrefix } from '@/utils'; +import { t } from '@/i18n'; + import type { MenuContext } from '@/types/contexts'; import type { BrowserWindow, MenuItemConstructorOptions } from 'electron'; import type { PluginConfig } from '@/types/plugins'; @@ -48,9 +50,15 @@ export const forceLoadMenuPlugin = async (id: string, win: BrowserWindow) => { } } else return; - console.log(LoggerPrefix, `Successfully loaded '${id}::menu'`); + console.log( + LoggerPrefix, + t('common.console.plugins.loaded', { pluginName: `${id}::menu` }) + ); } catch (err) { - console.error(LoggerPrefix, `Cannot initialize '${id}::menu': `); + console.error( + LoggerPrefix, + t('common.console.plugins.initialize-failed', { pluginName: `${id}::menu` }), + ); console.trace(err); } }; diff --git a/src/loader/preload.ts b/src/loader/preload.ts index 2c08fe43..7de032f1 100644 --- a/src/loader/preload.ts +++ b/src/loader/preload.ts @@ -5,6 +5,8 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils'; import config from '@/config'; +import { t } from '@/i18n'; + import type { PreloadContext } from '@/types/contexts'; import type { PluginConfig, PluginDef } from '@/types/plugins'; @@ -31,10 +33,13 @@ export const forceUnloadPreloadPlugin = async (id: string) => { context: createContext(id), }); if (hasStopped || (hasStopped === null && loadedPluginMap[id].preload)) { - console.log(LoggerPrefix, `"${id}" plugin is unloaded`); + console.log( + LoggerPrefix, + t('common.console.plugins.unloaded', { pluginName: id }), + ); delete loadedPluginMap[id]; } else { - console.error(LoggerPrefix, `Cannot stop "${id}" plugin`); + console.error(LoggerPrefix, t('common.console.plugins.unload-failed', { pluginName: id })); } }; @@ -57,9 +62,9 @@ export const forceLoadPreloadPlugin = async (id: string) => { loadedPluginMap[id] = plugin; } - console.log(LoggerPrefix, `"${id}" plugin is loaded`); + console.log(LoggerPrefix, t('common.console.plugins.loaded', { pluginName: id })); } catch (err) { - console.error(LoggerPrefix, `Cannot initialize "${id}" plugin: `); + console.error(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id })); console.trace(err); } }; diff --git a/src/loader/renderer.ts b/src/loader/renderer.ts index cf8391f9..370d22bf 100644 --- a/src/loader/renderer.ts +++ b/src/loader/renderer.ts @@ -6,6 +6,7 @@ import { LoggerPrefix, startPlugin, stopPlugin } from '@/utils'; import type { RendererContext } from '@/types/contexts'; import type { PluginConfig, PluginDef } from '@/types/plugins'; +import { t } from '@/i18n'; const unregisterStyleMap: Record void)[]> = {}; const loadedPluginMap: Record< @@ -54,9 +55,9 @@ export const forceUnloadRendererPlugin = async (id: string) => { document.querySelector(`style#plugin-${id}`)?.remove(); } if (hasStopped || (hasStopped === null && plugin?.renderer)) { - console.log(LoggerPrefix, `"${id}" plugin is unloaded`); + console.log(LoggerPrefix, t('common.console.plugins.unloaded', { pluginName: id })); } else { - console.error(LoggerPrefix, `Cannot stop "${id}" plugin`); + console.error(LoggerPrefix, t('common.console.plugins.unload-failed', { pluginName: id })); } }; @@ -92,9 +93,9 @@ export const forceLoadRendererPlugin = async (id: string) => { ]; } - console.log(LoggerPrefix, `"${id}" plugin is loaded`); + console.log(LoggerPrefix, t('common.console.plugins.loaded', { pluginName: id })); } else { - console.log(LoggerPrefix, `Cannot initialize "${id}" plugin`); + console.log(LoggerPrefix, t('common.console.plugins.initialize-failed', { pluginName: id })); } }; diff --git a/src/menu.ts b/src/menu.ts index e3295b70..455cfc7d 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -18,6 +18,8 @@ import { startingPages } from './providers/extracted-data'; import promptOptions from './providers/prompt-options'; import { getAllMenuTemplate, loadAllMenuPlugins } from './loader/menu'; +import { setLanguage, t } from '@/i18n'; +import { languageResources } from '@/i18n/resources'; export type MenuTemplate = Electron.MenuItemConstructorOptions[]; @@ -76,7 +78,7 @@ export const mainMenuTemplate = async ( { label: pluginLabel, submenu: [ - pluginEnabledMenu(id, 'Enabled', true, innerRefreshMenu), + pluginEnabledMenu(id, t('main.menu.plugins.enabled'), true, innerRefreshMenu), { type: 'separator' }, ...template, ], @@ -102,16 +104,18 @@ export const mainMenuTemplate = async ( return pluginEnabledMenu(id, pluginLabel, true, innerRefreshMenu); }); + const availableLanguages = Object.keys(languageResources) as unknown as (keyof typeof languageResources)[]; + return [ { - label: 'Plugins', + label: t('main.menu.plugins.label'), submenu: pluginMenus, }, { - label: 'Options', + label: t('main.menu.options.label'), submenu: [ { - label: 'Auto-update', + label: t('main.menu.options.submenu.auto-update'), type: 'checkbox', checked: config.get('options.autoUpdates'), click(item: MenuItem) { @@ -119,7 +123,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Resume last song when app starts', + label: t('main.menu.options.submenu.resume-on-start'), type: 'checkbox', checked: config.get('options.resumeOnStart'), click(item: MenuItem) { @@ -127,7 +131,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Starting page', + label: t('main.menu.options.submenu.starting-page.label'), submenu: (() => { const subMenuArray: Electron.MenuItemConstructorOptions[] = Object.keys(startingPages).map((name) => ({ @@ -139,7 +143,7 @@ export const mainMenuTemplate = async ( }, })); subMenuArray.unshift({ - label: 'Unset', + label: t('main.menu.options.submenu.starting-page.unset'), type: 'radio', checked: config.get('options.startingPage') === '', click() { @@ -150,10 +154,10 @@ export const mainMenuTemplate = async ( })(), }, { - label: 'Visual Tweaks', + label: t('main.menu.options.submenu.visual-tweaks.label'), submenu: [ { - label: 'Remove upgrade button', + label: t('main.menu.options.submenu.visual-tweaks.submenu.remove-upgrade-button'), type: 'checkbox', checked: config.get('options.removeUpgradeButton'), click(item: MenuItem) { @@ -164,10 +168,10 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Like buttons', + label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.label'), submenu: [ { - label: 'Default', + label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.default'), type: 'radio', checked: !config.get('options.likeButtons'), click() { @@ -175,7 +179,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Force show', + label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.force-show'), type: 'radio', checked: config.get('options.likeButtons') === 'force', click() { @@ -183,7 +187,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Hide', + label: t('main.menu.options.submenu.visual-tweaks.submenu.like-buttons.hide'), type: 'radio', checked: config.get('options.likeButtons') === 'hide', click() { @@ -193,10 +197,10 @@ export const mainMenuTemplate = async ( ], }, { - label: 'Theme', + label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.label'), submenu: [ { - label: 'No theme', + label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.no-theme'), type: 'radio', checked: config.get('options.themes')?.length === 0, // Todo rename "themes" click() { @@ -205,7 +209,7 @@ export const mainMenuTemplate = async ( }, { type: 'separator' }, { - label: 'Import custom CSS file', + label: t('main.menu.options.submenu.visual-tweaks.submenu.theme.submenu.import-css-file'), type: 'normal', async click() { const { filePaths } = await dialog.showOpenDialog({ @@ -222,7 +226,7 @@ export const mainMenuTemplate = async ( ], }, { - label: 'Single instance lock', + label: t('main.menu.options.submenu.single-instance-lock'), type: 'checkbox', checked: true, click(item: MenuItem) { @@ -234,7 +238,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Always on top', + label: t('main.menu.options.submenu.always-on-top'), type: 'checkbox', checked: config.get('options.alwaysOnTop'), click(item: MenuItem) { @@ -245,7 +249,7 @@ export const mainMenuTemplate = async ( ...((is.windows() || is.linux() ? [ { - label: 'Hide menu', + label: t('main.menu.options.submenu.hide-menu.label'), type: 'checkbox', checked: config.get('options.hideMenu'), click(item) { @@ -253,9 +257,8 @@ export const mainMenuTemplate = async ( if (item.checked && !config.get('options.hideMenuWarned')) { dialog.showMessageBox(win, { type: 'info', - title: 'Hide Menu Enabled', - message: - 'Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)', + title: t('main.menu.options.submenu.hide-menu.dialog.title'), + message: t('main.menu.options.submenu.hide-menu.dialog.message'), }); } }, @@ -267,7 +270,7 @@ export const mainMenuTemplate = async ( // https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows [ { - label: 'Start at login', + label: t('main.menu.options.submenu.start-at-login'), type: 'checkbox', checked: config.get('options.startAtLogin'), click(item) { @@ -277,10 +280,10 @@ export const mainMenuTemplate = async ( ] : []) satisfies Electron.MenuItemConstructorOptions[]), { - label: 'Tray', + label: t('main.menu.options.submenu.tray.label'), submenu: [ { - label: 'Disabled', + label: t('main.menu.options.submenu.tray.submenu.disabled'), type: 'radio', checked: !config.get('options.tray'), click() { @@ -289,7 +292,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Enabled + app visible', + label: t('main.menu.options.submenu.tray.submenu.enabled-and-show-app'), type: 'radio', checked: config.get('options.tray') && config.get('options.appVisible'), @@ -299,7 +302,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Enabled + app hidden', + label: t('main.menu.options.submenu.tray.submenu.enabled-and-hide-app'), type: 'radio', checked: config.get('options.tray') && !config.get('options.appVisible'), @@ -310,7 +313,7 @@ export const mainMenuTemplate = async ( }, { type: 'separator' }, { - label: 'Play/Pause on click', + label: t('main.menu.options.submenu.tray.submenu.play-pause-on-click'), type: 'checkbox', checked: config.get('options.trayClickPlayPause'), click(item: MenuItem) { @@ -322,19 +325,39 @@ export const mainMenuTemplate = async ( }, ], }, + { + label: t('main.menu.options.submenu.language.label'), + submenu: availableLanguages.map((lang): Electron.MenuItemConstructorOptions => ({ + label: `${languageResources[lang].translation.language.name} (${languageResources[lang].translation.language['local-name']})`, + type: 'checkbox', + checked: config.get('options.language') === lang, + click() { + config.setMenuOption('options.language', lang); + refreshMenu(win); + setLanguage(lang); + dialog.showMessageBox( + win, + { + title: t('main.menu.options.submenu.language.dialog.title'), + message: t('main.menu.options.submenu.language.dialog.message'), + } + ); + }, + })), + }, { type: 'separator' }, { - label: 'Advanced options', + label: t('main.menu.options.submenu.advanced-options.label'), submenu: [ { - label: 'Set Proxy', + label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.label'), type: 'normal', async click(item: MenuItem) { await setProxy(item, win); }, }, { - label: 'Override useragent', + label: t('main.menu.options.submenu.advanced-options.submenu.override-user-agent'), type: 'checkbox', checked: config.get('options.overrideUserAgent'), click(item: MenuItem) { @@ -342,7 +365,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Disable hardware acceleration', + label: t('main.menu.options.submenu.advanced-options.submenu.disable-hardware-acceleration'), type: 'checkbox', checked: config.get('options.disableHardwareAcceleration'), click(item: MenuItem) { @@ -353,7 +376,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Restart on config changes', + label: t('main.menu.options.submenu.advanced-options.submenu.restart-on-config-changes'), type: 'checkbox', checked: config.get('options.restartOnConfigChanges'), click(item: MenuItem) { @@ -364,7 +387,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Reset App cache when app starts', + label: t('main.menu.options.submenu.advanced-options.submenu.auto-reset-app-cache'), type: 'checkbox', checked: config.get('options.autoResetAppCache'), click(item: MenuItem) { @@ -374,7 +397,7 @@ export const mainMenuTemplate = async ( { type: 'separator' }, is.macOS() ? { - label: 'Toggle DevTools', + label: t('main.menu.options.submenu.advanced-options.submenu.toggle-dev-tools'), // Cannot use "toggleDevTools" role in macOS click() { const { webContents } = win; @@ -387,7 +410,7 @@ export const mainMenuTemplate = async ( } : { role: 'toggleDevTools' }, { - label: 'Edit config.json', + label: t('main.menu.options.submenu.advanced-options.submenu.edit-config-json'), click() { config.edit(); }, @@ -397,7 +420,7 @@ export const mainMenuTemplate = async ( ], }, { - label: 'View', + label: t('main.menu.view.label'), submenu: [ { role: 'reload' }, { role: 'forceReload' }, @@ -416,10 +439,10 @@ export const mainMenuTemplate = async ( ], }, { - label: 'Navigation', + label: t('main.menu.navigation.label'), submenu: [ { - label: 'Go back', + label: t('main.menu.navigation.submenu.go-back'), click() { if (win.webContents.canGoBack()) { win.webContents.goBack(); @@ -427,7 +450,7 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Go forward', + label: t('main.menu.navigation.submenu.go-forward'), click() { if (win.webContents.canGoForward()) { win.webContents.goForward(); @@ -435,21 +458,21 @@ export const mainMenuTemplate = async ( }, }, { - label: 'Copy current URL', + label: t('main.menu.navigation.submenu.copy-current-url'), click() { const currentURL = win.webContents.getURL(); clipboard.writeText(currentURL); }, }, { - label: 'Restart App', + label: t('main.menu.navigation.submenu.restart'), click: restart, }, { role: 'quit' }, ], }, { - label: 'About', + label: t('main.menu.about'), submenu: [{ role: 'about' }], }, ]; @@ -486,13 +509,13 @@ export const setApplicationMenu = async (win: Electron.BrowserWindow) => { async function setProxy(item: Electron.MenuItem, win: BrowserWindow) { const output = await prompt( { - title: 'Set Proxy', - label: 'Enter Proxy Address: (leave empty to disable)', + title: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.title'), + label: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.label'), value: config.get('options.proxy'), type: 'input', inputAttrs: { type: 'url', - placeholder: "Example: 'socks5://127.0.0.1:9999", + placeholder: t('main.menu.options.submenu.advanced-options.submenu.set-proxy.prompt.placeholder'), }, width: 450, ...promptOptions(), diff --git a/src/plugins/adblocker/index.ts b/src/plugins/adblocker/index.ts index 755405a5..19d15437 100644 --- a/src/plugins/adblocker/index.ts +++ b/src/plugins/adblocker/index.ts @@ -9,6 +9,8 @@ import { import injectCliqzPreload from './injectors/inject-cliqz-preload'; import { inject, isInjected } from './injectors/inject'; +import { t } from '@/i18n'; + import type { BrowserWindow } from 'electron'; interface AdblockerConfig { @@ -41,8 +43,8 @@ interface AdblockerConfig { } export default createPlugin({ - name: 'Adblocker', - description: 'Block all ads and tracking out of the box', + name: t('plugins.adblocker.name'), + description: t('plugins.adblocker.description'), restartNeeded: false, config: { enabled: true, @@ -56,7 +58,7 @@ export default createPlugin({ return [ { - label: 'Blocker', + label: t('plugins.adblocker.menu.blocker'), submenu: Object.values(blockers).map((blocker) => ({ label: blocker, type: 'radio', diff --git a/src/plugins/album-color-theme/index.ts b/src/plugins/album-color-theme/index.ts index c46d858a..7d7648ec 100644 --- a/src/plugins/album-color-theme/index.ts +++ b/src/plugins/album-color-theme/index.ts @@ -3,13 +3,13 @@ import { FastAverageColor } from 'fast-average-color'; import style from './style.css?inline'; import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; import type { VideoDataChanged } from '@/types/video-data-changed'; export default createPlugin({ - name: 'Album Color Theme', - description: - 'Applies a dynamic theme and visual effects based on the album color palette', + name: t('plugins.album-color-theme.name'), + description: t('plugins.album-color-theme.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/ambient-mode/index.ts b/src/plugins/ambient-mode/index.ts index 53eeb46f..06c8c813 100644 --- a/src/plugins/ambient-mode/index.ts +++ b/src/plugins/ambient-mode/index.ts @@ -1,6 +1,7 @@ import style from './style.css?inline'; import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export type AmbientModePluginConfig = { enabled: boolean; @@ -24,9 +25,8 @@ const defaultConfig: AmbientModePluginConfig = { }; export default createPlugin({ - name: 'Ambient Mode', - description: - 'Applies a lighting effect by casting gentle colors from the video, into your screen’s background.', + name: t('plugins.ambient-mode.name'), + description: t('plugins.ambient-mode.description'), restartNeeded: false, config: defaultConfig, stylesheets: [style], @@ -42,9 +42,11 @@ export default createPlugin({ return [ { - label: 'Smoothness transition', + label: t('plugins.ambient-mode.menu.smoothness-transition.label'), submenu: interpolationTimeList.map((interpolationTime) => ({ - label: `During ${interpolationTime / 1000}s`, + label: t('plugins.ambient-mode.menu.smoothness-transition.submenu.during', { + interpolationTime: interpolationTime / 1000, + }), type: 'radio', checked: config.interpolationTime === interpolationTime, click() { @@ -53,9 +55,9 @@ export default createPlugin({ })), }, { - label: 'Quality', + label: t('plugins.ambient-mode.menu.quality.label'), submenu: qualityList.map((quality) => ({ - label: `${quality} pixels`, + label: t('plugins.ambient-mode.menu.quality.submenu.pixels', { quality }), type: 'radio', checked: config.quality === quality, click() { @@ -64,9 +66,9 @@ export default createPlugin({ })), }, { - label: 'Size', + label: t('plugins.ambient-mode.menu.size.label'), submenu: sizeList.map((size) => ({ - label: `${size}%`, + label: t('plugins.ambient-mode.menu.size.submenu.percent', { size }), type: 'radio', checked: config.size === size, click() { @@ -75,9 +77,9 @@ export default createPlugin({ })), }, { - label: 'Buffer', + label: t('plugins.ambient-mode.menu.buffer.label'), submenu: bufferList.map((buffer) => ({ - label: `${buffer}`, + label: t('plugins.ambient-mode.menu.buffer.submenu.buffer', { buffer }), type: 'radio', checked: config.buffer === buffer, click() { @@ -86,9 +88,9 @@ export default createPlugin({ })), }, { - label: 'Opacity', + label: t('plugins.ambient-mode.menu.opacity.label'), submenu: opacityList.map((opacity) => ({ - label: `${opacity * 100}%`, + label: t('plugins.ambient-mode.menu.opacity.submenu.percent', { opacity: opacity * 100 }), type: 'radio', checked: config.opacity === opacity, click() { @@ -97,9 +99,9 @@ export default createPlugin({ })), }, { - label: 'Blur amount', + label: t('plugins.ambient-mode.menu.blur-amount.label'), submenu: blurAmountList.map((blur) => ({ - label: `${blur} pixels`, + label: t('plugins.ambient-mode.menu.blur-amount.submenu.pixels', { blurAmount: blur }), type: 'radio', checked: config.blur === blur, click() { @@ -108,7 +110,7 @@ export default createPlugin({ })), }, { - label: 'Using fullscreen', + label: t('plugins.ambient-mode.menu.use-fullscreen.label'), type: 'checkbox', checked: config.fullscreen, click(item) { diff --git a/src/plugins/audio-compressor.ts b/src/plugins/audio-compressor.ts index e66d6a33..4b7f4cbd 100644 --- a/src/plugins/audio-compressor.ts +++ b/src/plugins/audio-compressor.ts @@ -1,9 +1,9 @@ import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Audio Compressor', - description: - 'Apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)', + name: t('plugins.audio-compressor.name'), + description: t('plugins.audio-compressor.description'), renderer() { document.addEventListener( diff --git a/src/plugins/blur-nav-bar/index.ts b/src/plugins/blur-nav-bar/index.ts index e8ffc8a2..aee4f456 100644 --- a/src/plugins/blur-nav-bar/index.ts +++ b/src/plugins/blur-nav-bar/index.ts @@ -1,9 +1,10 @@ import { createPlugin } from '@/utils'; import style from './style.css?inline'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Blur Navigation Bar', - description: 'makes navigation bar transparent and blurry', + name: t('plugins.blur-nav-bar.name'), + description: t('plugins.blur-nav-bar.description'), restartNeeded: true, stylesheets: [style], renderer() {}, diff --git a/src/plugins/bypass-age-restrictions/index.ts b/src/plugins/bypass-age-restrictions/index.ts index 7fa7f270..6ef90d21 100644 --- a/src/plugins/bypass-age-restrictions/index.ts +++ b/src/plugins/bypass-age-restrictions/index.ts @@ -1,8 +1,9 @@ import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Bypass Age Restrictions', - description: "bypass YouTube's age verification", + name: t('plugins.bypass-age-restrictions.name'), + description: t('plugins.bypass-age-restrictions.description'), restartNeeded: true, // See https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass#userscript diff --git a/src/plugins/captions-selector/back.ts b/src/plugins/captions-selector/back.ts index 573e77c2..ebddf083 100644 --- a/src/plugins/captions-selector/back.ts +++ b/src/plugins/captions-selector/back.ts @@ -2,6 +2,7 @@ import prompt from 'custom-electron-prompt'; import promptOptions from '@/providers/prompt-options'; import { createBackend } from '@/utils'; +import { t } from '@/i18n'; export default createBackend({ start({ ipc: { handle }, window }) { @@ -10,8 +11,10 @@ export default createBackend({ async (captionLabels: Record, currentIndex: string) => await prompt( { - title: 'Choose Caption', - label: `Current Caption: ${captionLabels[currentIndex] || 'None'}`, + title: t('plugins.captions-selector.prompt.selector.title'), + label: t('plugins.captions-selector.prompt.selector.label', { + language: captionLabels[currentIndex] || t('plugins.captions-selector.prompt.selector.none'), + }), type: 'select', value: currentIndex, selectOptions: captionLabels, diff --git a/src/plugins/captions-selector/index.ts b/src/plugins/captions-selector/index.ts index 71f12353..3055f140 100644 --- a/src/plugins/captions-selector/index.ts +++ b/src/plugins/captions-selector/index.ts @@ -3,6 +3,7 @@ import { YoutubePlayer } from '@/types/youtube-player'; import backend from './back'; import renderer, { CaptionsSelectorConfig, LanguageOptions } from './renderer'; +import { t } from '@/i18n'; export default createPlugin< unknown, @@ -18,8 +19,8 @@ export default createPlugin< }, CaptionsSelectorConfig >({ - name: 'Captions Selector', - description: 'Caption selector for YouTube Music audio tracks', + name: t('plugins.captions-selector.name'), + description: t('plugins.captions-selector.description'), config: { enabled: false, disableCaptions: false, @@ -31,7 +32,7 @@ export default createPlugin< const config = await getConfig(); return [ { - label: 'Automatically select last used caption', + label: t('plugins.captions-selector.menu.autoload'), type: 'checkbox', checked: config.autoload as boolean, click(item) { @@ -39,7 +40,7 @@ export default createPlugin< }, }, { - label: 'No captions by default', + label: t('plugins.captions-selector.menu.disable-captions'), type: 'checkbox', checked: config.disableCaptions as boolean, click(item) { diff --git a/src/plugins/compact-sidebar/index.ts b/src/plugins/compact-sidebar/index.ts index 9a4c5f97..db96c7bb 100644 --- a/src/plugins/compact-sidebar/index.ts +++ b/src/plugins/compact-sidebar/index.ts @@ -1,4 +1,5 @@ import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export default createPlugin< unknown, @@ -8,8 +9,8 @@ export default createPlugin< isCompactSidebarDisabled: () => boolean; } >({ - name: 'Compact Sidebar', - description: 'Always set the sidebar in compact mode', + name: t('plugins.compact-sidebar.name'), + description: t('plugins.compact-sidebar.description'), restartNeeded: false, config: { enabled: false, diff --git a/src/plugins/crossfade/index.ts b/src/plugins/crossfade/index.ts index 7f6b09d5..25cb4cc4 100644 --- a/src/plugins/crossfade/index.ts +++ b/src/plugins/crossfade/index.ts @@ -11,6 +11,7 @@ import { createPlugin } from '@/utils'; import { VolumeFader } from './fader'; import type { RendererContext } from '@/types/contexts'; +import { t } from '@/i18n'; export type CrossfadePluginConfig = { enabled: boolean; @@ -29,8 +30,8 @@ export default createPlugin< }, CrossfadePluginConfig >({ - name: 'Crossfade [beta]', - description: 'Crossfade between songs', + name: t('plugins.crossfade.name'), + description: t('plugins.crossfade.description'), restartNeeded: true, config: { enabled: false, @@ -67,11 +68,11 @@ export default createPlugin< ): Promise | undefined> => { const res = await prompt( { - title: 'Crossfade Options', + title: t('plugins.crossfade.prompt.options'), type: 'multiInput', multiInputOptions: [ { - label: 'Fade in duration (ms)', + label: t('plugins.crossfade.prompt.options.multi-input.fade-in-duration'), value: options.fadeInDuration, inputAttrs: { type: 'number', @@ -81,7 +82,7 @@ export default createPlugin< }, }, { - label: 'Fade out duration (ms)', + label: t('plugins.crossfade.prompt.options.multi-input.fade-out-duration'), value: options.fadeOutDuration, inputAttrs: { type: 'number', @@ -91,7 +92,7 @@ export default createPlugin< }, }, { - label: 'Crossfade x seconds before end', + label: t('plugins.crossfade.prompt.options.multi-input.seconds-before-end'), value: options.secondsBeforeEnd, inputAttrs: { type: 'number', @@ -100,8 +101,11 @@ export default createPlugin< }, }, { - label: 'Fade scaling', - selectOptions: { linear: 'Linear', logarithmic: 'Logarithmic' }, + label: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.label'), + selectOptions: { + linear: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.linear'), + logarithmic: t('plugins.crossfade.prompt.options.multi-input.fade-scaling.logarithmic'), + }, value: options.fadeScaling, }, ], @@ -135,7 +139,7 @@ export default createPlugin< return [ { - label: 'Advanced', + label: t('plugins.crossfade.menu.advanced'), async click() { const newOptions = await promptCrossfadeValues( window, diff --git a/src/plugins/disable-autoplay/index.ts b/src/plugins/disable-autoplay/index.ts index 84d1ce52..6a6b5aae 100644 --- a/src/plugins/disable-autoplay/index.ts +++ b/src/plugins/disable-autoplay/index.ts @@ -1,5 +1,7 @@ import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; + import type { VideoDataChanged } from '@/types/video-data-changed'; import type { YoutubePlayer } from '@/types/youtube-player'; @@ -19,8 +21,8 @@ export default createPlugin< }, DisableAutoPlayPluginConfig >({ - name: 'Disable Autoplay', - description: 'Makes every song start in "paused" mode', + name: t('plugins.disable-autoplay.name'), + description: t('plugins.disable-autoplay.description'), restartNeeded: false, config: { enabled: false, @@ -31,7 +33,7 @@ export default createPlugin< return [ { - label: 'Applies only on startup', + label: t('plugins.disable-autoplay.menu.apply-once'), type: 'checkbox', checked: config.applyOnce, async click() { diff --git a/src/plugins/discord/index.ts b/src/plugins/discord/index.ts index ce86e0e3..7b066170 100644 --- a/src/plugins/discord/index.ts +++ b/src/plugins/discord/index.ts @@ -1,6 +1,7 @@ import { createPlugin } from '@/utils'; import { backend } from './main'; import { onMenu } from './menu'; +import { t } from '@/i18n'; export type DiscordPluginConfig = { enabled: boolean; @@ -35,8 +36,8 @@ export type DiscordPluginConfig = { }; export default createPlugin({ - name: 'Discord Rich Presence', - description: 'Show your friends what you listen to with Rich Presence', + name: t('plugins.discord.name'), + description: t('plugins.discord.description'), restartNeeded: false, config: { enabled: false, diff --git a/src/plugins/discord/main.ts b/src/plugins/discord/main.ts index 925dc7cb..5ec35741 100644 --- a/src/plugins/discord/main.ts +++ b/src/plugins/discord/main.ts @@ -6,7 +6,9 @@ import { SetActivity } from '@xhayper/discord-rpc/dist/structures/ClientUser'; import registerCallback, { type SongInfo } from '@/providers/song-info'; -import { createBackend } from '@/utils'; +import { createBackend, LoggerPrefix } from '@/utils'; + +import { t } from '@/i18n'; import type { DiscordPluginConfig } from './index'; @@ -38,7 +40,10 @@ const resetInfo = () => { info.ready = false; clearTimeout(clearActivity); if (dev()) { - console.log('discord disconnected'); + console.log( + LoggerPrefix, + t('plugins.discord.backend.disconnected') + ); } for (const cb of refreshCallbacks) { @@ -68,7 +73,10 @@ let window: Electron.BrowserWindow; export const connect = (showError = false) => { if (info.rpc.isConnected) { if (dev()) { - console.log('Attempted to connect with active connection'); + console.log( + LoggerPrefix, + t('plugins.discord.backend.already-connected') + ); } return; @@ -206,7 +214,10 @@ export const backend = createBackend< info.rpc.on('connected', () => { if (dev()) { - console.log('discord connected'); + console.log( + LoggerPrefix, + t('plugins.discord.backend.connected') + ); } for (const cb of refreshCallbacks) { diff --git a/src/plugins/discord/menu.ts b/src/plugins/discord/menu.ts index 99ad270e..eb7ae584 100644 --- a/src/plugins/discord/menu.ts +++ b/src/plugins/discord/menu.ts @@ -6,6 +6,8 @@ import { singleton } from '@/providers/decorators'; import promptOptions from '@/providers/prompt-options'; import { setMenuOptions } from '@/config/plugins'; +import { t } from '@/i18n'; + import type { MenuContext } from '@/types/contexts'; import type { DiscordPluginConfig } from './index'; @@ -31,7 +33,7 @@ export const onMenu = async ({ click: () => connect(), }, { - label: 'Auto reconnect', + label: t('plugins.discord.menu.auto-reconnect'), type: 'checkbox', checked: config.autoReconnect, click(item: Electron.MenuItem) { @@ -41,11 +43,11 @@ export const onMenu = async ({ }, }, { - label: 'Clear activity', + label: t('plugins.discord.menu.clear-activity'), click: clear, }, { - label: 'Clear activity after timeout', + label: t('plugins.discord.menu.clear-activity-after-timeout'), type: 'checkbox', checked: config.activityTimeoutEnabled, click(item: Electron.MenuItem) { @@ -55,7 +57,7 @@ export const onMenu = async ({ }, }, { - label: 'Play on YouTube Music', + label: t('plugins.discord.menu.play-on-youtube-music'), type: 'checkbox', checked: config.playOnYouTubeMusic, click(item: Electron.MenuItem) { @@ -65,7 +67,7 @@ export const onMenu = async ({ }, }, { - label: 'Hide GitHub link Button', + label: t('plugins.discord.menu.hide-github-button'), type: 'checkbox', checked: config.hideGitHubButton, click(item: Electron.MenuItem) { @@ -75,7 +77,7 @@ export const onMenu = async ({ }, }, { - label: 'Hide duration left', + label: t('plugins.discord.menu.hide-duration-left'), type: 'checkbox', checked: config.hideDurationLeft, click(item: Electron.MenuItem) { @@ -85,7 +87,7 @@ export const onMenu = async ({ }, }, { - label: 'Set inactivity timeout', + label: t('plugins.discord.menu.set-inactivity-timeout'), click: () => setInactivityTimeout(window, config), }, ]; @@ -97,8 +99,8 @@ async function setInactivityTimeout( ) { const output = await prompt( { - title: 'Set Inactivity Timeout', - label: 'Enter inactivity timeout in seconds:', + title: t('plugins.discord.prompt.set-inactivity-timeout.title'), + label: t('plugins.discord.prompt.set-inactivity-timeout.label'), value: String(Math.round((options.activityTimeoutTime ?? 0) / 1e3)), type: 'counter', counterOptions: { minimum: 0, multiFire: true }, diff --git a/src/plugins/downloader/index.ts b/src/plugins/downloader/index.ts index 2982f1c6..50c0d37c 100644 --- a/src/plugins/downloader/index.ts +++ b/src/plugins/downloader/index.ts @@ -5,6 +5,7 @@ import style from './style.css?inline'; import { createPlugin } from '@/utils'; import { onConfigChange, onMainLoad } from './main'; import { onPlayerApiReady, onRendererLoad } from './renderer'; +import { t } from '@/i18n'; export type DownloaderPluginConfig = { enabled: boolean; @@ -25,8 +26,8 @@ export const defaultConfig: DownloaderPluginConfig = { }; export default createPlugin({ - name: 'Downloader', - description: 'Downloads MP3 / source audio directly from the interface', + name: t('plugins.downloader.name'), + description: t('plugins.downloader.description'), restartNeeded: true, config: defaultConfig, stylesheets: [style], diff --git a/src/plugins/downloader/main/index.ts b/src/plugins/downloader/main/index.ts index 9845e690..b2dbdc15 100644 --- a/src/plugins/downloader/main/index.ts +++ b/src/plugins/downloader/main/index.ts @@ -34,6 +34,8 @@ import { cleanupName, getImage, SongInfo } from '@/providers/song-info'; import { getNetFetchAsFetch } from '@/plugins/utils/main'; import { cache } from '@/providers/decorators'; +import { t } from '@/i18n'; + import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types'; import type { DownloaderPluginConfig } from '../index'; @@ -74,9 +76,9 @@ const sendError = (error: Error, source?: string) => { console.trace(error); dialog.showMessageBox(win, { type: 'info', - buttons: ['OK'], - title: 'Error in download!', - message: 'Argh! Apologies, download failed…', + buttons: [t('plugins.downloader.backend.dialog.error.buttons.ok')], + title: t('plugins.downloader.backend.dialog.error.title'), + message: t('plugins.downloader.backend.dialog.error.message'), detail: message, }); }; @@ -179,20 +181,27 @@ async function downloadSongUnsafe( } }; - sendFeedback('Downloading...', 2); + sendFeedback( + t('plugins.downloader.backend.feedback.downloading'), + 2, + ); let id: string | null; if (isId) { id = idOrUrl; } else { id = getVideoId(idOrUrl); - if (typeof id !== 'string') throw new Error('Video not found'); + if (typeof id !== 'string') throw new Error( + t('plugins.downloader.backend.feedback.video-id-not-found'), + ); } let info: TrackInfo | VideoInfo = await yt.music.getInfo(id); if (!info) { - throw new Error('Video not found'); + throw new Error( + t('plugins.downloader.backend.feedback.video-id-not-found'), + ); } const metadata = getMetadata(info); @@ -277,7 +286,11 @@ async function downloadSongUnsafe( const stream = await info.download(downloadOptions); console.info( - `Downloading ${metadata.artist} - ${metadata.title} [${metadata.videoId}]`, + t('plugins.downloader.backend.feedback.download-info', { + artist: metadata.artist, + title: metadata.title, + videoId: metadata.videoId, + }), ); const iterableStream = Utils.streamToIterable(stream); @@ -312,7 +325,9 @@ async function downloadSongUnsafe( } sendFeedback(null, -1); - console.info(`Done: "${filePath}"`); + console.info(t('plugins.downloader.backend.feedback.done', { + filePath, + })); } async function iterableStreamToTargetFile( @@ -331,13 +346,21 @@ async function iterableStreamToTargetFile( chunks.push(chunk); const ratio = downloaded / contentLength; const progress = Math.floor(ratio * 100); - sendFeedback(`Download: ${progress}%`, ratio); + sendFeedback( + t('plugins.downloader.backend.feedback.downloading-progress', { + percent: progress, + }), + ratio, + ); // 15% for download, 85% for conversion // This is a very rough estimate, trying to make the progress bar look nice increasePlaylistProgress(ratio * 0.15); } - sendFeedback('Loading…', 2); // Indefinite progress bar after download + sendFeedback( + t('plugins.downloader.backend.feedback.loading'), + 2, + ); // Indefinite progress bar after download const buffer = Buffer.concat(chunks); const safeVideoName = randomBytes(32).toString('hex'); @@ -348,13 +371,18 @@ async function iterableStreamToTargetFile( await ffmpeg.load(); } - sendFeedback('Preparing file…'); + sendFeedback(t('plugins.downloader.backend.feedback.preparing-file')); ffmpeg.FS('writeFile', safeVideoName, buffer); - sendFeedback('Converting…'); + sendFeedback(t('plugins.downloader.backend.feedback.converting')); ffmpeg.setProgress(({ ratio }) => { - sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio); + sendFeedback( + t('plugins.downloader.backend.feedback.conversion-progress', { + percent: Math.floor(ratio * 100), + }), + ratio, + ); increasePlaylistProgress(0.15 + (ratio * 0.85)); }); @@ -371,7 +399,9 @@ async function iterableStreamToTargetFile( ffmpeg.FS('unlink', safeVideoName); } - sendFeedback('Saving…'); + sendFeedback( + t('plugins.downloader.backend.feedback.saving'), + ); try { return ffmpeg.FS('readFile', safeVideoNameWithExtension); @@ -397,7 +427,9 @@ async function writeID3( sendFeedback: (str: string, value?: number) => void, ) { try { - sendFeedback('Writing ID3 tags...'); + sendFeedback( + t('plugins.downloader.backend.feedback.writing-id3'), + ); const tags: NodeID3.Tags = {}; // Create the metadata tags @@ -452,14 +484,22 @@ export async function downloadPlaylist(givenUrl?: string | URL) { getPlaylistID(givenUrl) || getPlaylistID(new URL(playingUrl)); if (!playlistId) { - sendError(new Error('No playlist ID found')); + sendError(new Error( + t('plugins.downloader.backend.feedback.playlist-id-not-found'), + )); return; } const sendFeedback = (message?: unknown) => sendFeedback_(win, message); - console.log(`trying to get playlist ID: '${playlistId}'`); - sendFeedback('Getting playlist info…'); + console.log( + t('plugins.downloader.backend.feedback.trying-to-get-playlist-id', { + playlistId, + }), + ); + sendFeedback( + t('plugins.downloader.backend.feedback.getting-playlist-info'), + ); let playlist: Playlist; const items: YTNodes.MusicResponsiveListItem[] = []; try { @@ -470,16 +510,18 @@ export async function downloadPlaylist(givenUrl?: string | URL) { } catch (error: unknown) { sendError( Error( - `Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${String( - error, - )}`, + t('plugins.downloader.backend.feedback.playlist-is-mix-or-private', { + error: String(error), + }), ), ); return; } if (!playlist || !playlist.items || playlist.items.length === 0) { - sendError(new Error('Playlist is empty')); + sendError(new Error( + t('plugins.downloader.backend.feedback.playlist-is-empty'), + )); } const normalPlaylistTitle = playlist.header?.title?.text; @@ -500,7 +542,9 @@ export async function downloadPlaylist(givenUrl?: string | URL) { } if (items.length === 1) { - sendFeedback('Playlist has only one item, downloading it directly'); + sendFeedback( + t('plugins.downloader.backend.feedback.playlist-has-only-one-song'), + ); await downloadSongFromId(items.at(0)!.id!); return; } @@ -514,7 +558,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) { const playlistFolder = join(folder, safePlaylistTitle); if (existsSync(playlistFolder)) { if (!config.skipExisting) { - sendError(new Error(`The folder ${playlistFolder} already exists`)); + sendError(new Error( + t('plugins.downloader.backend.feedback.folder-already-exists', { + playlistFolder, + }) + )); return; } } else { @@ -523,15 +571,23 @@ export async function downloadPlaylist(givenUrl?: string | URL) { dialog.showMessageBox(win, { type: 'info', - buttons: ['OK'], - title: 'Started Download', - message: `Downloading Playlist "${playlistTitle}"`, - detail: `(${items.length} songs)`, + buttons: [t('plugins.downloader.backend.dialog.start-download-playlist.buttons.ok')], + title: t('plugins.downloader.backend.dialog.start-download-playlist.title'), + message: t('plugins.downloader.backend.dialog.start-download-playlist.message', { + playlistTitle, + }), + detail: t('plugins.downloader.backend.dialog.start-download-playlist.detail', { + playlistSize: items.length, + }), }); if (is.dev()) { console.log( - `Downloading playlist "${playlistTitle}" - ${items.length} songs (${playlistId})`, + t('plugins.downloader.backend.feedback.downloading-playlist', { + playlistTitle, + playlistSize: items.length, + playlistId, + }), ); } @@ -551,7 +607,12 @@ export async function downloadPlaylist(givenUrl?: string | URL) { try { for (const song of items) { - sendFeedback(`Downloading ${counter}/${items.length}...`); + sendFeedback( + t('plugins.downloader.backend.feedback.downloading-counter', { + current: counter, + total: items.length, + }) + ); const trackId = isAlbum ? counter : undefined; await downloadSongFromId( song.id!, @@ -561,9 +622,11 @@ export async function downloadPlaylist(givenUrl?: string | URL) { ).catch((error) => sendError( new Error( - `Error downloading "${ - song.author!.name - } - ${song.title!}":\n ${error}`, + t('plugins.downloader.backend.feedback.error-while-downloading', { + author: song.author!.name, + title: song.title!, + error: String(error), + }), ), ), ); diff --git a/src/plugins/downloader/menu.ts b/src/plugins/downloader/menu.ts index f0322491..2c80423b 100644 --- a/src/plugins/downloader/menu.ts +++ b/src/plugins/downloader/menu.ts @@ -8,6 +8,7 @@ import type { MenuContext } from '@/types/contexts'; import type { MenuTemplate } from '@/menu'; import type { DownloaderPluginConfig } from './index'; +import { t } from '@/i18n'; export const onMenu = async ({ getConfig, @@ -21,7 +22,7 @@ export const onMenu = async ({ click: () => downloadPlaylist(), }, { - label: 'Choose download folder', + label: t('plugins.downloader.menu.choose-download-folder'), click() { const result = dialog.showOpenDialogSync({ properties: ['openDirectory', 'createDirectory'], @@ -33,7 +34,7 @@ export const onMenu = async ({ }, }, { - label: 'Presets', + label: t('plugins.downloader.menu.presets'), submenu: Object.keys(DefaultPresetList).map((preset) => ({ label: preset, type: 'radio', @@ -44,7 +45,7 @@ export const onMenu = async ({ })), }, { - label: 'Skip existing files', + label: t('plugins.downloader.menu.skip-existing'), type: 'checkbox', checked: config.skipExisting, click(item) { diff --git a/src/plugins/downloader/renderer.ts b/src/plugins/downloader/renderer.ts index b04f843d..23a09d27 100644 --- a/src/plugins/downloader/renderer.ts +++ b/src/plugins/downloader/renderer.ts @@ -4,11 +4,14 @@ import defaultConfig from '@/config/defaults'; import { getSongMenu } from '@/providers/dom-elements'; import { getSongInfo } from '@/providers/song-info-front'; +import { LoggerPrefix } from '@/utils'; + import { ElementFromHtml } from '../utils/renderer'; import type { RendererContext } from '@/types/contexts'; import type { DownloaderPluginConfig } from './index'; +import { t } from '@/i18n'; let menu: Element | null = null; let progress: Element | null = null; @@ -75,7 +78,10 @@ export const onRendererLoad = ({ if (progress) { progress.innerHTML = feedback || 'Download'; } else { - console.warn('Cannot update progress'); + console.warn( + LoggerPrefix, + t('plugins.downloader.renderer.can-not-update-progress'), + ); } }); }; diff --git a/src/plugins/downloader/templates/download.html b/src/plugins/downloader/templates/download.html index 8c50dcbb..57a8830d 100644 --- a/src/plugins/downloader/templates/download.html +++ b/src/plugins/downloader/templates/download.html @@ -39,7 +39,7 @@ class="text style-scope ytmusic-menu-navigation-item-renderer" id="ytmcustom-download" > - Download + diff --git a/src/plugins/exponential-volume/index.ts b/src/plugins/exponential-volume/index.ts index 6f54abca..ec0cf85d 100644 --- a/src/plugins/exponential-volume/index.ts +++ b/src/plugins/exponential-volume/index.ts @@ -1,9 +1,9 @@ import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Exponential Volume', - description: - "Makes the volume slider exponential so it's easier to select lower volumes.", + name: t('plugins.exponential-volume.name'), + description: t('plugins.exponential-volume.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/in-app-menu/index.ts b/src/plugins/in-app-menu/index.ts index 974cf26d..47a16494 100644 --- a/src/plugins/in-app-menu/index.ts +++ b/src/plugins/in-app-menu/index.ts @@ -3,14 +3,15 @@ import { createPlugin } from '@/utils'; import { onMainLoad } from './main'; import { onMenu } from './menu'; import { onPlayerApiReady, onRendererLoad } from './renderer'; +import { t } from '@/i18n'; export interface InAppMenuConfig { enabled: boolean; hideDOMWindowControls: boolean; } export default createPlugin({ - name: 'In-App Menu', - description: 'gives menu-bars a fancy, dark or album-color look', + name: t('plugins.in-app-menu.name'), + description: t('plugins.in-app-menu.description'), restartNeeded: true, config: { enabled: diff --git a/src/plugins/in-app-menu/menu.ts b/src/plugins/in-app-menu/menu.ts index 4c875679..0935cecb 100644 --- a/src/plugins/in-app-menu/menu.ts +++ b/src/plugins/in-app-menu/menu.ts @@ -1,5 +1,7 @@ import is from 'electron-is'; +import { t } from '@/i18n'; + import type { InAppMenuConfig } from './index'; import type { MenuContext } from '@/types/contexts'; import type { MenuTemplate } from '@/menu'; @@ -13,7 +15,7 @@ export const onMenu = async ({ if (is.linux()) { return [ { - label: 'Hide DOM Window Controls', + label: t('plugins.in-app-menu.hide-dom-window-controls'), type: 'checkbox', checked: config.hideDOMWindowControls, click(item) { diff --git a/src/plugins/in-app-menu/menu/panel.ts b/src/plugins/in-app-menu/menu/panel.ts index 0407e22f..bca25b7d 100644 --- a/src/plugins/in-app-menu/menu/panel.ts +++ b/src/plugins/in-app-menu/menu/panel.ts @@ -99,7 +99,7 @@ export const createPanel = ( children.push(...children); } - panel.appendChild(menu); + return panel.appendChild(menu); }); /* methods */ diff --git a/src/plugins/last-fm/index.ts b/src/plugins/last-fm/index.ts index 885db09c..4072b955 100644 --- a/src/plugins/last-fm/index.ts +++ b/src/plugins/last-fm/index.ts @@ -1,6 +1,7 @@ import { createPlugin } from '@/utils'; import registerCallback from '@/providers/song-info'; import { addScrobble, getAndSetSessionKey, setNowPlaying } from './main'; +import { t } from '@/i18n'; export interface LastFmPluginConfig { enabled: boolean; @@ -33,8 +34,8 @@ export interface LastFmPluginConfig { } export default createPlugin({ - name: 'Last.fm', - description: 'Add scrobbling support for Last.fm', + name: t('plugins.last-fm.name'), + description: t('plugins.last-fm.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/lumiastream/index.ts b/src/plugins/lumiastream/index.ts index 9e1bf68d..12af93ce 100644 --- a/src/plugins/lumiastream/index.ts +++ b/src/plugins/lumiastream/index.ts @@ -2,6 +2,7 @@ import { net } from 'electron'; import { createPlugin } from '@/utils'; import registerCallback from '@/providers/song-info'; +import { t } from '@/i18n'; type LumiaData = { origin: string; @@ -23,8 +24,8 @@ type LumiaData = { }; export default createPlugin({ - name: 'Lumia Stream [beta]', - description: 'Adds Lumia Stream support', + name: t('plugins.lumiastream.name'), + description: t('plugins.lumiastream.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/lyrics-genius/index.ts b/src/plugins/lyrics-genius/index.ts index fecbdc5d..ebf16e14 100644 --- a/src/plugins/lyrics-genius/index.ts +++ b/src/plugins/lyrics-genius/index.ts @@ -2,6 +2,7 @@ import style from './style.css?inline'; import { createPlugin } from '@/utils'; import { onConfigChange, onMainLoad } from './main'; import { onRendererLoad } from './renderer'; +import { t } from '@/i18n'; export type LyricsGeniusPluginConfig = { enabled: boolean; @@ -9,8 +10,8 @@ export type LyricsGeniusPluginConfig = { }; export default createPlugin({ - name: 'Lyrics Genius', - description: 'Adds lyrics support for most songs', + name: t('plugins.lyrics-genius.name'), + description: t('plugins.lyrics-genius.description'), restartNeeded: true, config: { enabled: false, @@ -22,7 +23,7 @@ export default createPlugin({ return [ { - label: 'Romanized Lyrics', + label: t('plugins.lyrics-genius.menu.romanized-lyrics'), type: 'checkbox', checked: config.romanizedLyrics, click(item) { diff --git a/src/plugins/lyrics-genius/renderer.ts b/src/plugins/lyrics-genius/renderer.ts index e46ab2aa..223d0357 100644 --- a/src/plugins/lyrics-genius/renderer.ts +++ b/src/plugins/lyrics-genius/renderer.ts @@ -1,6 +1,9 @@ +import { LoggerPrefix } from '@/utils'; + import type { SongInfo } from '@/providers/song-info'; import type { RendererContext } from '@/types/contexts'; import type { LyricsGeniusPluginConfig } from '@/plugins/lyrics-genius/index'; +import { t } from '@/i18n'; export const onRendererLoad = ({ ipc: { invoke, on }, @@ -55,7 +58,10 @@ export const onRendererLoad = ({ } if (window.electronIs.dev()) { - console.log('Fetched lyrics from Genius'); + console.log( + LoggerPrefix, + t('plugins.lyric-genius.renderer.fetched-lyrics'), + ); } const tryToInjectLyric = (callback?: () => void) => { diff --git a/src/plugins/navigation/index.ts b/src/plugins/navigation/index.ts index a0e8aef2..de4227cb 100644 --- a/src/plugins/navigation/index.ts +++ b/src/plugins/navigation/index.ts @@ -2,13 +2,14 @@ import style from './style.css?inline'; import { createPlugin } from '@/utils'; import { ElementFromHtml } from '@/plugins/utils/renderer'; +import { t } from '@/i18n'; + import forwardHTML from './templates/forward.html?raw'; import backHTML from './templates/back.html?raw'; export default createPlugin({ - name: 'Navigation', - description: - 'Next/Back navigation arrows directly integrated in the interface, like in your favorite browser', + name: t('plugins.navigation.name'), + description: t('plugins.navigation.description'), restartNeeded: true, config: { enabled: true, diff --git a/src/plugins/no-google-login/index.ts b/src/plugins/no-google-login/index.ts index 43f1ecbd..520388d4 100644 --- a/src/plugins/no-google-login/index.ts +++ b/src/plugins/no-google-login/index.ts @@ -1,9 +1,10 @@ import style from './style.css?inline'; import { createPlugin } from '@/utils'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Remove Google Login', - description: 'Remove Google login buttons and links from the interface', + name: t('plugins.no-google-login.name'), + description: t('plugins.no-google-login.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/notifications/index.ts b/src/plugins/notifications/index.ts index f3080435..58519ef3 100644 --- a/src/plugins/notifications/index.ts +++ b/src/plugins/notifications/index.ts @@ -2,6 +2,7 @@ import { createPlugin } from '@/utils'; import { onConfigChange, onMainLoad } from './main'; import { onMenu } from './menu'; +import { t } from '@/i18n'; export interface NotificationsPluginConfig { enabled: boolean; @@ -35,9 +36,8 @@ export const defaultConfig: NotificationsPluginConfig = { }; export default createPlugin({ - name: 'Notifications', - description: - 'Display a notification when a song starts playing (interactive notifications are available on windows)', + name: t('plugins.notifications.name'), + description: t('plugins.notifications.description'), restartNeeded: true, config: defaultConfig, menu: onMenu, diff --git a/src/plugins/notifications/menu.ts b/src/plugins/notifications/menu.ts index ee1492d1..c9e382a0 100644 --- a/src/plugins/notifications/menu.ts +++ b/src/plugins/notifications/menu.ts @@ -3,6 +3,8 @@ import { MenuItem } from 'electron'; import { snakeToCamel, ToastStyles, urgencyLevels } from './utils'; +import { t } from '@/i18n'; + import type { NotificationsPluginConfig } from './index'; import type { MenuTemplate } from '@/menu'; @@ -34,7 +36,7 @@ export const onMenu = async ({ if (is.linux()) { return [ { - label: 'Notification Priority', + label: t('plugins.notifications.menu.priority'), submenu: urgencyLevels.map((level) => ({ label: level.name, type: 'radio', @@ -46,7 +48,7 @@ export const onMenu = async ({ } else if (is.windows()) { return [ { - label: 'Interactive Notifications', + label: t('plugins.notifications.menu.interactive'), type: 'checkbox', checked: config.interactive, // Doesn't update until restart @@ -54,24 +56,24 @@ export const onMenu = async ({ }, { // Submenu with settings for interactive notifications (name shouldn't be too long) - label: 'Interactive Settings', + label: t('plugins.notifications.menu.interactive-settings.label'), submenu: [ { - label: 'Open/Close on tray click', + label: t('plugins.notifications.menu.interactive-settings.submenu.tray-controls'), type: 'checkbox', checked: config.trayControls, click: (item: MenuItem) => setConfig({ trayControls: item.checked }), }, { - label: 'Hide Button Text', + label: t('plugins.notifications.menu.interactive-settings.submenu.hide-button-text'), type: 'checkbox', checked: config.hideButtonText, click: (item: MenuItem) => setConfig({ hideButtonText: item.checked }), }, { - label: 'Refresh on Play/Pause', + label: t('plugins.notifications.menu.interactive-settings.submenu.refresh-on-play-pause'), type: 'checkbox', checked: config.refreshOnPlayPause, click: (item: MenuItem) => @@ -80,7 +82,7 @@ export const onMenu = async ({ ], }, { - label: 'Style', + label: t('plugins.notifications.menu.toast-style'), submenu: getToastStyleMenuItems(config), }, ]; @@ -92,7 +94,7 @@ export const onMenu = async ({ return [ ...getMenu(), { - label: 'Show notification on unpause', + label: t('plugins.notifications.menu.unpause-notification'), type: 'checkbox', checked: config.unpauseNotification, click: (item) => setConfig({ unpauseNotification: item.checked }), diff --git a/src/plugins/picture-in-picture/index.ts b/src/plugins/picture-in-picture/index.ts index 0955a430..08a7a112 100644 --- a/src/plugins/picture-in-picture/index.ts +++ b/src/plugins/picture-in-picture/index.ts @@ -4,6 +4,7 @@ import { createPlugin } from '@/utils'; import { onConfigChange, onMainLoad } from './main'; import { onMenu } from './menu'; import { onPlayerApiReady, onRendererLoad } from './renderer'; +import { t } from '@/i18n'; export type PictureInPicturePluginConfig = { enabled: boolean; @@ -18,8 +19,8 @@ export type PictureInPicturePluginConfig = { }; export default createPlugin({ - name: 'Picture In Picture', - description: 'Allows to switch the app to picture-in-picture mode', + name: t('plugins.picture-in-picture.name'), + description: t('plugins.picture-in-picture.description'), restartNeeded: true, config: { 'enabled': false, diff --git a/src/plugins/picture-in-picture/menu.ts b/src/plugins/picture-in-picture/menu.ts index 1f74dd73..3efd1146 100644 --- a/src/plugins/picture-in-picture/menu.ts +++ b/src/plugins/picture-in-picture/menu.ts @@ -2,6 +2,8 @@ import prompt from 'custom-electron-prompt'; import promptOptions from '@/providers/prompt-options'; +import { t } from '@/i18n'; + import type { PictureInPicturePluginConfig } from './index'; import type { MenuContext } from '@/types/contexts'; @@ -16,7 +18,7 @@ export const onMenu = async ({ return [ { - label: 'Always on top', + label: t('plugins.picture-in-picture.menu.always-on-top'), type: 'checkbox', checked: config.alwaysOnTop, click(item) { @@ -25,7 +27,7 @@ export const onMenu = async ({ }, }, { - label: 'Save window position', + label: t('plugins.picture-in-picture.menu.save-window-position'), type: 'checkbox', checked: config.savePosition, click(item) { @@ -33,7 +35,7 @@ export const onMenu = async ({ }, }, { - label: 'Save window size', + label: t('plugins.picture-in-picture.menu.save-window-size'), type: 'checkbox', checked: config.saveSize, click(item) { @@ -41,19 +43,19 @@ export const onMenu = async ({ }, }, { - label: 'Hotkey', + label: t('plugins.picture-in-picture.menu.hotkey.label'), type: 'checkbox', checked: !!config.hotkey, async click(item) { const output = await prompt( { - title: 'Picture in Picture Hotkey', - label: 'Choose a hotkey for toggling Picture in Picture', + title: t('plugins.picture-in-picture.menu.prompt.title'), + label: t('plugins.picture-in-picture.menu.prompt.label'), type: 'keybind', keybindOptions: [ { value: 'hotkey', - label: 'Hotkey', + label: t('plugins.picture-in-picture.menu.prompt.keybind-options.hotkey'), default: config.hotkey, }, ], @@ -74,7 +76,7 @@ export const onMenu = async ({ }, }, { - label: 'Use native PiP', + label: t('plugins.picture-in-picture.menu.use-native-pip'), type: 'checkbox', checked: config.useNativePiP, click(item) { diff --git a/src/plugins/picture-in-picture/templates/picture-in-picture.html b/src/plugins/picture-in-picture/templates/picture-in-picture.html index 0d5033b8..d560ed97 100644 --- a/src/plugins/picture-in-picture/templates/picture-in-picture.html +++ b/src/plugins/picture-in-picture/templates/picture-in-picture.html @@ -44,7 +44,7 @@ class="text style-scope ytmusic-menu-navigation-item-renderer" id="ytmcustom-pip" > - Picture in picture + diff --git a/src/plugins/playback-speed/index.ts b/src/plugins/playback-speed/index.ts index 76314f6e..b3c26f06 100644 --- a/src/plugins/playback-speed/index.ts +++ b/src/plugins/playback-speed/index.ts @@ -1,10 +1,10 @@ import { createPlugin } from '@/utils'; import { onPlayerApiReady, onUnload } from './renderer'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Playback Speed', - description: - 'Listen fast, listen slow! Adds a slider that controls song speed', + name: t('plugins.playback-speed.name'), + description: t('plugins.playback-speed.description'), restartNeeded: false, config: { enabled: false, diff --git a/src/plugins/playback-speed/templates/slider.html b/src/plugins/playback-speed/templates/slider.html index 0952d914..fb74405e 100644 --- a/src/plugins/playback-speed/templates/slider.html +++ b/src/plugins/playback-speed/templates/slider.html @@ -83,7 +83,7 @@ class="text style-scope ytmusic-menu-navigation-item-renderer" id="ytmcustom-playback-speed" > - Speed (1) + (1) diff --git a/src/plugins/precise-volume/index.ts b/src/plugins/precise-volume/index.ts index d58f0ed4..edf5fa71 100644 --- a/src/plugins/precise-volume/index.ts +++ b/src/plugins/precise-volume/index.ts @@ -7,32 +7,41 @@ import { createPlugin } from '@/utils'; import promptOptions from '@/providers/prompt-options'; import { overrideListener } from './override'; import { onConfigChange, onPlayerApiReady } from './renderer'; +import { t } from '@/i18n'; export type PreciseVolumePluginConfig = { enabled: boolean; + /** + * Percentage of volume to change + */ steps: number; + /** + * Enable ArrowUp + ArrowDown local shortcuts + */ arrowsShortcut: boolean; globalShortcuts: { volumeUp: string; volumeDown: string; }; + /** + * Plugin save volume between session here + */ savedVolume: number | undefined; }; export default createPlugin({ - name: 'Precise Volume', - description: - 'Control the volume precisely using mousewheel/hotkeys, with a custom HUD and customizable volume steps', + name: t('plugins.precise-volume.name'), + description: t('plugins.precise-volume.description'), restartNeeded: true, config: { enabled: false, - steps: 1, // Percentage of volume to change - arrowsShortcut: true, // Enable ArrowUp + ArrowDown local shortcuts + steps: 1, + arrowsShortcut: true, globalShortcuts: { volumeUp: '', volumeDown: '', }, - savedVolume: undefined, // Plugin save volume between session here + savedVolume: undefined, } as PreciseVolumePluginConfig, stylesheets: [hudStyle], menu: async ({ setConfig, getConfig, window }) => { @@ -66,8 +75,8 @@ export default createPlugin({ async function promptVolumeSteps(options: PreciseVolumePluginConfig) { const output = await prompt( { - title: 'Volume Steps', - label: 'Choose Volume Increase/Decrease Steps', + title: t('plugins.precise-volume.prompt.volume-steps.title'), + label: t('plugins.precise-volume.prompt.volume-steps.label'), value: options.steps || 1, type: 'counter', counterOptions: { minimum: 0, maximum: 100, multiFire: true }, @@ -89,17 +98,17 @@ export default createPlugin({ ) { const output = await prompt( { - title: 'Global Volume Keybinds', - label: 'Choose Global Volume Keybinds:', + title: t('plugins.precise-volume.prompt.global-shortcuts.title'), + label: t('plugins.precise-volume.prompt.global-shortcuts.label'), type: 'keybind', keybindOptions: [ kb( - 'Increase Volume', + t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.increase'), 'volumeUp', options.globalShortcuts?.volumeUp, ), kb( - 'Decrease Volume', + t('plugins.precise-volume.prompt.global-shortcuts.keybind-options.decrease'), 'volumeDown', options.globalShortcuts?.volumeDown, ), @@ -132,7 +141,7 @@ export default createPlugin({ return [ { - label: 'Local Arrowkeys Controls', + label: t('plugins.precise-volume.menu.arrows-shortcuts'), type: 'checkbox', checked: Boolean(config.arrowsShortcut), click(item) { @@ -140,7 +149,7 @@ export default createPlugin({ }, }, { - label: 'Global Hotkeys', + label: t('plugins.precise-volume.menu.global-shortcuts'), type: 'checkbox', checked: Boolean( config.globalShortcuts?.volumeUp ?? @@ -149,7 +158,7 @@ export default createPlugin({ click: (item) => promptGlobalShortcuts(config, item), }, { - label: 'Set Custom Volume Steps', + label: t('plugins.precise-volume.menu.custom-volume-steps'), click: () => promptVolumeSteps(config), }, ]; diff --git a/src/plugins/quality-changer/index.ts b/src/plugins/quality-changer/index.ts index 337e3f05..12e19cde 100644 --- a/src/plugins/quality-changer/index.ts +++ b/src/plugins/quality-changer/index.ts @@ -4,13 +4,13 @@ import QualitySettingsTemplate from './templates/qualitySettingsTemplate.html?ra import { createPlugin } from '@/utils'; import { ElementFromHtml } from '@/plugins/utils/renderer'; +import { t } from '@/i18n'; import type { YoutubePlayer } from '@/types/youtube-player'; export default createPlugin({ - name: 'Video Quality Changer', - description: - 'Allows changing the video quality with a button on the video overlay', + name: t('plugins.quality-changer.name'), + description: t('plugins.quality-changer.description'), restartNeeded: false, config: { enabled: false, @@ -24,9 +24,11 @@ export default createPlugin({ type: 'question', buttons: qualityLabels, defaultId: currentIndex, - title: 'Choose Video Quality', - message: 'Choose Video Quality:', - detail: `Current Quality: ${qualityLabels[currentIndex]}`, + title: t('plugins.quality-changer.backend.dialog.title'), + message: t('plugins.quality-changer.backend.dialog.message'), + detail: t('plugins.quality-changer.backend.dialog.detail', { + quality: qualityLabels[currentIndex], + }), cancelId: -1, }), ); diff --git a/src/plugins/shortcuts/index.ts b/src/plugins/shortcuts/index.ts index bc897981..ae9d837c 100644 --- a/src/plugins/shortcuts/index.ts +++ b/src/plugins/shortcuts/index.ts @@ -1,6 +1,7 @@ import { createPlugin } from '@/utils'; import { onMainLoad } from './main'; import { onMenu } from './menu'; +import { t } from '@/i18n'; export type ShortcutMappingType = { previous: string; @@ -15,9 +16,8 @@ export type ShortcutsPluginConfig = { }; export default createPlugin({ - name: 'Shortcuts (& MPRIS)', - description: - 'Allows setting global hotkeys for playback (play/pause/next/previous) + disable media osd by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + custom hotkeys for advanced users', + name: t('plugins.shortcuts.name'), + description: t('plugins.shortcuts.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/shortcuts/menu.ts b/src/plugins/shortcuts/menu.ts index 28f2d46e..03043b05 100644 --- a/src/plugins/shortcuts/menu.ts +++ b/src/plugins/shortcuts/menu.ts @@ -2,6 +2,8 @@ import prompt, { KeybindOptions } from 'custom-electron-prompt'; import promptOptions from '@/providers/prompt-options'; +import { t } from '@/i18n'; + import type { ShortcutsPluginConfig } from './index'; import type { BrowserWindow } from 'electron'; import type { MenuContext } from '@/types/contexts'; @@ -29,14 +31,14 @@ export const onMenu = async ({ ) { const output = await prompt( { - title: 'Global Keybinds', - label: 'Choose Global Keybinds for Songs Control:', + title: t('plugins.shortcuts.prompt.keybind.title'), + label: t('plugins.shortcuts.prompt.keybind.label'), type: 'keybind', keybindOptions: [ // If default=undefined then no default is used - kb('Previous', 'previous', config.global?.previous), - kb('Play / Pause', 'playPause', config.global?.playPause), - kb('Next', 'next', config.global?.next), + kb(t('plugins.shortcuts.prompt.keybind.keybind-options.previous'), 'previous', config.global?.previous), + kb(t('plugins.shortcuts.prompt.keybind.keybind-options.play-pause'), 'playPause', config.global?.playPause), + kb(t('plugins.shortcuts.prompt.keybind.keybind-options.next'), 'next', config.global?.next), ], height: 270, ...promptOptions(), @@ -59,11 +61,11 @@ export const onMenu = async ({ return [ { - label: 'Set Global Song Controls', + label: t('plugins.shortcuts.menu.set-keybinds'), click: () => promptKeybind(config, window), }, { - label: 'Override MediaKeys', + label: t('plugins.shortcuts.menu.override-media-keys'), type: 'checkbox', checked: config.overrideMediaKeys, click: (item) => setConfig({ overrideMediaKeys: item.checked }), diff --git a/src/plugins/skip-silences/index.ts b/src/plugins/skip-silences/index.ts index 9d8dbcbd..df2d0e73 100644 --- a/src/plugins/skip-silences/index.ts +++ b/src/plugins/skip-silences/index.ts @@ -1,5 +1,6 @@ import { createPlugin } from '@/utils'; import { onRendererLoad, onRendererUnload } from './renderer'; +import { t } from '@/i18n'; export type SkipSilencesPluginConfig = { enabled: boolean; @@ -7,8 +8,8 @@ export type SkipSilencesPluginConfig = { }; export default createPlugin({ - name: 'Skip Silences', - description: 'Automatically skip silenced sections', + name: t('plugins.skip-silences.name'), + description: t('plugins.skip-silences.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/sponsorblock/index.ts b/src/plugins/sponsorblock/index.ts index a5486c40..50574613 100644 --- a/src/plugins/sponsorblock/index.ts +++ b/src/plugins/sponsorblock/index.ts @@ -4,6 +4,8 @@ import { createPlugin } from '@/utils'; import { sortSegments } from './segments'; +import { t } from '@/i18n'; + import type { GetPlayerResponse } from '@/types/get-player-response'; import type { Segment, SkipSegment } from './types'; @@ -23,9 +25,8 @@ export type SponsorBlockPluginConfig = { let currentSegments: Segment[] = []; export default createPlugin({ - name: 'SponsorBlock', - description: - "Automatically Skips non-music parts like intro/outro or parts of music videos where the song isn't playing", + name: t('plugins.sponsorblock.name'), + description: t('plugins.sponsorblock.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/taskbar-mediacontrol/index.ts b/src/plugins/taskbar-mediacontrol/index.ts index c4fad613..2d39b90e 100644 --- a/src/plugins/taskbar-mediacontrol/index.ts +++ b/src/plugins/taskbar-mediacontrol/index.ts @@ -9,10 +9,11 @@ import { createPlugin } from '@/utils'; import getSongControls from '@/providers/song-controls'; import registerCallback, { type SongInfo } from '@/providers/song-info'; import { mediaIcons } from '@/types/media-icons'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'Taskbar Media Control', - description: 'Control playback from your Windows taskbar', + name: t('plugins.taskbar-mediacontrol.name'), + description: t('plugins.taskbar-mediacontrol.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/touchbar/index.ts b/src/plugins/touchbar/index.ts index 49d09cea..435bf6ab 100644 --- a/src/plugins/touchbar/index.ts +++ b/src/plugins/touchbar/index.ts @@ -3,10 +3,11 @@ import { type NativeImage, TouchBar } from 'electron'; import { createPlugin } from '@/utils'; import getSongControls from '@/providers/song-controls'; import registerCallback from '@/providers/song-info'; +import { t } from '@/i18n'; export default createPlugin({ - name: 'TouchBar', - description: 'Custom TouchBar layout for macOS', + name: t('plugins.touchbar.name'), + description: t('plugins.touchbar.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/tuna-obs/index.ts b/src/plugins/tuna-obs/index.ts index da79acdb..16aabc8a 100644 --- a/src/plugins/tuna-obs/index.ts +++ b/src/plugins/tuna-obs/index.ts @@ -4,6 +4,7 @@ import is from 'electron-is'; import { createPlugin } from '@/utils'; import registerCallback from '@/providers/song-info'; +import { t } from '@/i18n'; interface Data { album: string | null | undefined; @@ -18,8 +19,8 @@ interface Data { } export default createPlugin({ - name: 'Tuna OBS', - description: "Integration with OBS's plugin Tuna", + name: t('plugins.tuna-obs.name'), + description: t('plugins.tuna-obs.description'), restartNeeded: true, config: { enabled: false, diff --git a/src/plugins/video-toggle/index.ts b/src/plugins/video-toggle/index.ts index ec625def..bd7779af 100644 --- a/src/plugins/video-toggle/index.ts +++ b/src/plugins/video-toggle/index.ts @@ -6,6 +6,8 @@ import { createPlugin } from '@/utils'; import { moveVolumeHud as preciseVolumeMoveVolumeHud } from '@/plugins/precise-volume/renderer'; import { ElementFromHtml } from '@/plugins/utils/renderer'; import { ThumbnailElement } from '@/types/get-player-response'; +import { t } from '@/i18n'; +import { MenuTemplate } from '@/menu'; export type VideoTogglePluginConfig = { enabled: boolean; @@ -16,9 +18,8 @@ export type VideoTogglePluginConfig = { }; export default createPlugin({ - name: 'Video Toggle', - description: - 'Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab', + name: t('plugins.video-toggle.name'), + description: t('plugins.video-toggle.description'), restartNeeded: true, config: { enabled: false, @@ -28,15 +29,15 @@ export default createPlugin({ align: 'left', } as VideoTogglePluginConfig, stylesheets: [buttonSwitcherStyle, forceHideStyle], - menu: async ({ getConfig, setConfig }) => { + menu: async ({ getConfig, setConfig }): Promise => { const config = await getConfig(); return [ { - label: 'Mode', + label: t('plugins.video-toggle.menu.mode.label'), submenu: [ { - label: 'Custom toggle', + label: t('plugins.video-toggle.menu.mode.submenu.custom'), type: 'radio', checked: config.mode === 'custom', click() { @@ -44,7 +45,7 @@ export default createPlugin({ }, }, { - label: 'Native toggle', + label: t('plugins.video-toggle.menu.mode.submenu.native'), type: 'radio', checked: config.mode === 'native', click() { @@ -52,7 +53,7 @@ export default createPlugin({ }, }, { - label: 'Disabled', + label: t('plugins.video-toggle.menu.mode.submenu.disabled'), type: 'radio', checked: config.mode === 'disabled', click() { @@ -62,10 +63,10 @@ export default createPlugin({ ], }, { - label: 'Alignment', + label: t('plugins.video-toggle.menu.align.label'), submenu: [ { - label: 'Left', + label: t('plugins.video-toggle.menu.align.submenu.left'), type: 'radio', checked: config.align === 'left', click() { @@ -73,7 +74,7 @@ export default createPlugin({ }, }, { - label: 'Middle', + label: t('plugins.video-toggle.menu.align.submenu.middle'), type: 'radio', checked: config.align === 'middle', click() { @@ -81,7 +82,7 @@ export default createPlugin({ }, }, { - label: 'Right', + label: t('plugins.video-toggle.menu.align.submenu.right'), type: 'radio', checked: config.align === 'right', click() { @@ -91,7 +92,7 @@ export default createPlugin({ ], }, { - label: 'Force Remove Video Tab', + label: t('plugins.video-toggle.menu.force-hide'), type: 'checkbox', checked: config.forceHide, click(item) { diff --git a/src/plugins/video-toggle/templates/button_template.html b/src/plugins/video-toggle/templates/button_template.html index 76d81de3..8350b650 100644 --- a/src/plugins/video-toggle/templates/button_template.html +++ b/src/plugins/video-toggle/templates/button_template.html @@ -1,4 +1,8 @@
- +
diff --git a/src/plugins/visualizer/index.ts b/src/plugins/visualizer/index.ts index 1edecedd..2d1eab7f 100644 --- a/src/plugins/visualizer/index.ts +++ b/src/plugins/visualizer/index.ts @@ -6,6 +6,7 @@ import { VudioVisualizer as vudio, WaveVisualizer as wave, } from './visualizers'; +import { t } from '@/i18n'; type WaveColor = { gradient: string[]; @@ -57,8 +58,8 @@ export type VisualizerPluginConfig = { }; export default createPlugin({ - name: 'Visualizer', - description: 'Adds a visualizer to the player', + name: t('plugins.visualizer.name'), + description: t('plugins.visualizer.description'), restartNeeded: true, config: { enabled: false, @@ -133,7 +134,7 @@ export default createPlugin({ return [ { - label: 'Type', + label: t('plugins.visualizer.menu.visualizer-type'), submenu: visualizerTypes.map((visualizerType) => ({ label: visualizerType, type: 'radio', diff --git a/src/preload.ts b/src/preload.ts index b5cf7618..574c8a71 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,3 +1,5 @@ +import process from 'node:process'; + import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import is from 'electron-is'; @@ -8,8 +10,12 @@ import { forceUnloadPreloadPlugin, loadAllPreloadPlugins, } from './loader/preload'; +import { loadI18n, setLanguage } from '@/i18n'; -loadAllPreloadPlugins(); +loadI18n().then(async () => { + await setLanguage(config.get('options.language') ?? 'en'); + loadAllPreloadPlugins(); +}); ipcRenderer.on('plugin:unload', async (_, id: string) => { await forceUnloadPreloadPlugin(id); diff --git a/src/renderer.ts b/src/renderer.ts index be081875..c728c6b7 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,3 +1,5 @@ +import i18next from 'i18next'; + import { startingPages } from './providers/extracted-data'; import setupSongInfo from './providers/song-info-front'; import { @@ -9,6 +11,8 @@ import { loadAllRendererPlugins, } from './loader/renderer'; +import { loadI18n, setLanguage, t as i18t } from '@/i18n'; + import type { PluginConfig } from '@/types/plugins'; import type { YoutubePlayer } from '@/types/youtube-player'; @@ -151,7 +155,32 @@ async function onApiLoaded() { } } +/** + * YouTube Music still using ES5, so we need to define custom elements using ES5 style + */ +const defineYTMDTransElements = () => { + const YTMDTrans = function() {}; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + YTMDTrans.prototype = Object.create(HTMLElement.prototype); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + YTMDTrans.prototype.connectedCallback = function() { + const that = (this as HTMLElement); + const key = that.getAttribute('key'); + if (key) { + that.innerHTML = i18t(key); + } + }; + customElements.define('ytmd-trans', YTMDTrans as unknown as CustomElementConstructor); +}; + (async () => { + await loadI18n(); + await setLanguage(window.mainConfig.get('options.language') ?? 'en'); + window.i18n = { + t: i18t.bind(i18next), + }; + defineYTMDTransElements(); + await loadAllRendererPlugins(); isPluginLoaded = true; diff --git a/src/reset.d.ts b/src/reset.d.ts index 7c75d820..94956f6e 100644 --- a/src/reset.d.ts +++ b/src/reset.d.ts @@ -5,6 +5,7 @@ import type is from 'electron-is'; import type config from './config'; import type { VideoDataChanged } from '@/types/video-data-changed'; +import type { t } from '@/i18n'; declare global { interface Compressor { @@ -30,6 +31,9 @@ declare global { download: () => void; togglePictureInPicture: () => void; reload: () => void; + i18n: { + t: typeof t, + } } } diff --git a/src/utils/index.ts b/src/utils/index.ts index 1db92323..21dcc916 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n'; + import type { BackendContext, PreloadContext, @@ -106,12 +108,25 @@ export const startPlugin = async ( console.log( LoggerPrefix, - `Executed ${id}::${options.ctx} in ${performance.now() - start} ms`, + t('common.console.plugins.executed-at-ms', { + pluginName: id, + contextName: options.ctx, + ms: performance.now() - start, + }), ); return lifecycle ? true : null; } catch (err) { - console.error(LoggerPrefix, `Failed to start ${id}::${options.ctx}`); + console.error( + LoggerPrefix, + t( + 'common.console.plugins.execute-failed', + { + pluginName: id, + contextName: options.ctx, + }, + ), + ); console.trace(err); return false; } @@ -140,12 +155,25 @@ export const stopPlugin = async ( console.log( LoggerPrefix, - `Executed ${id}::${options.ctx} in ${performance.now() - start} ms`, + t('common.console.plugins.executed-at-ms', { + pluginName: id, + contextName: options.ctx, + ms: performance.now() - start, + }), ); return true; } catch (err) { - console.error(LoggerPrefix, `Failed to execute ${id}::${options.ctx}`); + console.error( + LoggerPrefix, + t( + 'common.console.plugins.execute-failed', + { + pluginName: id, + contextName: options.ctx, + }, + ), + ); console.trace(err); return false; }