Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ pub mod smart_playlists;
pub mod spotify;
pub mod stats;
pub mod track;
pub mod tray;
pub mod wrapped;
54 changes: 54 additions & 0 deletions src-tauri/src/commands/tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Tray menu localisation bridge.
//!
//! The system tray menu (Play/Pause, Previous, Next, Open WaveFlow, Quit)
//! is created in Rust at startup before the frontend has loaded i18next,
//! so the labels are seeded in English and the frontend pushes a
//! localised set once `i18nReady` resolves — and again on every
//! `languageChanged`. The `MenuItem` handles are stashed in
//! [`TrayMenuItems`] so this command can call `set_text` without
//! rebuilding the menu.

use tauri::{menu::MenuItem, AppHandle, Manager, Runtime, State};

/// Holds the five user-facing tray `MenuItem`s so their labels can be
/// retitled at runtime when the UI language changes.
pub struct TrayMenuItems<R: Runtime> {
pub play_pause: MenuItem<R>,
pub previous: MenuItem<R>,
pub next: MenuItem<R>,
pub show: MenuItem<R>,
pub quit: MenuItem<R>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrayLabels {
pub play_pause: String,
pub previous: String,
pub next: String,
pub show: String,
pub quit: String,
}

#[tauri::command]
pub fn set_tray_labels<R: Runtime>(
app: AppHandle<R>,
labels: TrayLabels,
) -> Result<(), String> {
let Some(items) = app.try_state::<TrayMenuItems<R>>() else {
return Ok(());
};
apply(&items, &labels).map_err(|e| e.to_string())
}

fn apply<R: Runtime>(
items: &State<'_, TrayMenuItems<R>>,
labels: &TrayLabels,
) -> tauri::Result<()> {
items.play_pause.set_text(&labels.play_pause)?;
items.previous.set_text(&labels.previous)?;
items.next.set_text(&labels.next)?;
items.show.set_text(&labels.show)?;
items.quit.set_text(&labels.quit)?;
Ok(())
}
41 changes: 32 additions & 9 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use queue::Direction;
use state::AppState;
use watcher::WatcherManager;

/// Set to `true` by the tray "Quitter" menu before calling `app.exit()`.
/// Set to `true` by the tray "Quit" menu before calling `app.exit()`.
/// `WindowEvent::CloseRequested` checks the flag: if armed, the close
/// proceeds to actual shutdown; otherwise the close is intercepted and
/// the window is hidden instead (close-to-tray default).
Expand Down Expand Up @@ -295,25 +295,47 @@ pub fn run() {

// System tray (status icon).
//
// Menu: Lecture/Pause, Précédent, Suivant, Ouvrir WaveFlow,
// Quitter. Left-click on the icon mirrors "Ouvrir WaveFlow"
// for the common case where the window was hidden via the
// Labels are seeded in English because Rust runs before the
// frontend has had a chance to load i18next; the React layer
// pushes a localised set via `set_tray_labels` once
// `i18nReady` resolves, and again on every `languageChanged`
// event. The `MenuItem` handles are stashed in
// `TrayMenuItems` so retitling doesn't rebuild the menu.
// Left-click on the icon mirrors "Open WaveFlow" for the
// common case where the window was hidden via the
// close-to-tray path. Tooltip is updated to "Title — Artist"
// by `commands::player::emit_track_changed` whenever a new
// track starts.
let play_pause_item =
MenuItem::with_id(app, "play_pause", "Play / Pause", true, None::<&str>)?;
let previous_item =
MenuItem::with_id(app, "previous", "Previous", true, None::<&str>)?;
let next_item = MenuItem::with_id(app, "next", "Next", true, None::<&str>)?;
let show_item =
MenuItem::with_id(app, "show", "Open WaveFlow", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;

let menu = Menu::with_items(
app,
&[
&MenuItem::with_id(app, "play_pause", "Lecture / Pause", true, None::<&str>)?,
&MenuItem::with_id(app, "previous", "Précédent", true, None::<&str>)?,
&MenuItem::with_id(app, "next", "Suivant", true, None::<&str>)?,
&play_pause_item,
&previous_item,
&next_item,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "show", "Ouvrir WaveFlow", true, None::<&str>)?,
&show_item,
&PredefinedMenuItem::separator(app)?,
&MenuItem::with_id(app, "quit", "Quitter", true, None::<&str>)?,
&quit_item,
],
)?;

app.manage(commands::tray::TrayMenuItems {
play_pause: play_pause_item,
previous: previous_item,
next: next_item,
show: show_item,
quit: quit_item,
});

let icon = app
.default_window_icon()
.cloned()
Expand Down Expand Up @@ -464,6 +486,7 @@ pub fn run() {
commands::preferences::set_minimize_to_tray,
commands::preferences::get_auto_start,
commands::preferences::set_auto_start,
commands::tray::set_tray_labels,
commands::lyrics::get_lyrics,
commands::lyrics::fetch_lyrics,
commands::lyrics::import_lrc_file,
Expand Down
24 changes: 23 additions & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import i18n from "i18next";
import { setTrayLabels } from "../lib/tauri/tray";
import type {
BackendModule,
InitOptions,
Expand Down Expand Up @@ -139,6 +140,23 @@ function applyDocumentLanguage(code: string | undefined) {
document.documentElement.dir = i18n.dir(normalizedCode);
}

// Push the localised tray menu labels to the Rust backend. The tray
// is built at startup with English seed strings (frontend hasn't had
// time to load i18next yet); this re-titles each item once the user
// language is known and on every subsequent `languageChanged`.
function pushTrayLabels() {
setTrayLabels({
playPause: i18n.t("system.tray.playPause"),
previous: i18n.t("system.tray.previous"),
next: i18n.t("system.tray.next"),
show: i18n.t("system.tray.show"),
quit: i18n.t("system.tray.quit"),
}).catch(() => {
// Tauri command unavailable (e.g. running outside the desktop
// shell during a Vite-only dev session) — drop silently.
});
}

const dynamicLocaleBackend: BackendModule = {
type: "backend",
init(
Expand Down Expand Up @@ -189,8 +207,12 @@ export const i18nReady = i18n
})
.then(() => {
applyDocumentLanguage(i18n.resolvedLanguage ?? i18n.language);
pushTrayLabels();
});

i18n.on("languageChanged", applyDocumentLanguage);
i18n.on("languageChanged", (code) => {
applyDocumentLanguage(code);
pushTrayLabels();
});

export default i18n;
54 changes: 50 additions & 4 deletions src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
},
"sidebar": {
"nav": {
"home": "الصفحة الرئيسية"
"home": "الصفحة الرئيسية",
"spotify": "Spotify"
},
"sections": {
"open": "فتح",
Expand Down Expand Up @@ -213,6 +214,16 @@
"recentlyAdded": {
"title": "أُضيف حديثاً"
},
"dailyMix": {
"title": "من أجلك",
"regenerate": "إعادة التوليد",
"regenerating": "جارٍ التوليد…",
"emptyTitle": "لا يوجد Daily Mix بعد",
"emptyDescription": "استمع إلى بعض المقطوعات ثم اضغط على إعادة التوليد لإنشاء مزيجك الخاص.",
"label": "Daily Mix",
"trackCount_one": "{{count}} مقطوعة",
"trackCount_other": "{{count}} مقطوعة"
},
"wrapped": {
"eyebrow": "WaveFlow Wrapped",
"title": "ملخّصك لعام {{year}}",
Expand Down Expand Up @@ -590,7 +601,8 @@
"slider": "شريط تمرير السرعة",
"custom": "سرعة مخصصة",
"pitchHint": "تتبع طبقة الصوت السرعة (بدون تمديد زمني)."
}
},
"seek": "الموضع"
},
"queue": {
"title": "قائمة انتظار التشغيل",
Expand Down Expand Up @@ -664,7 +676,13 @@
"iconLabel": "أيقونة",
"iconAria": "أيقونة {{icon}}",
"submit": "إنشاء",
"editSubmit": "حفظ"
"editSubmit": "حفظ",
"coverChoose": "اختر صورة",
"coverMenu": "خيارات الغلاف",
"coverChange": "تغيير الصورة",
"coverRemove": "إزالة الصورة",
"coverAutoHint": "غلاف تلقائي — يُحدَّث مع المحتوى",
"coverManualHint": "صورة مخصّصة"
},
"playlistView": {
"badge": "قائمة التشغيل",
Expand All @@ -687,7 +705,8 @@
"noneTitle": "لم يتم تحديد أي قائمة تشغيل",
"noneDescription": "اختر قائمة تشغيل من الشريط الجانبي لعرضها هنا.",
"notFoundTitle": "لا يمكن العثور على قائمة التشغيل",
"notFoundDescription": "هذه القائمة الموسيقية لم تعد موجودة في الملف الشخصي النشط."
"notFoundDescription": "هذه القائمة الموسيقية لم تعد موجودة في الملف الشخصي النشط.",
"smartLabel": "Daily Mix"
},
"trackActions": {
"addToPlaylist": "إضافة إلى قائمة التشغيل",
Expand Down Expand Up @@ -1107,6 +1126,16 @@
"statusRunning": "يعمل",
"statusStopped": "متوقف",
"copyUrl": "نسخ الرابط"
},
"spotify": {
"title": "Spotify",
"subtitle": "اربط Spotify Premium باستخدام Client ID الخاص بك من Spotify Developer.",
"clientIdPlaceholder": "Spotify Client ID",
"redirectHint": "أضف Redirect URI هذا في Spotify Developer Dashboard: http://127.0.0.1:49387/spotify/callback",
"connectedAs": "متصل باسم",
"disconnect": "قطع الاتصال",
"connecting": "جارٍ الاتصال…",
"connect": "الاتصال بـ Spotify"
}
},
"crossfade": {
Expand Down Expand Up @@ -1277,5 +1306,22 @@
"runningSubtitle": "{{current}} / {{total}} ملف",
"doneTitle": "اكتمل التحليل",
"doneSubtitle": "تمت إضافة {{added}} · تحديث {{updated}} · تخطّي {{skipped}}"
},
"system": {
"tray": {
"playPause": "تشغيل / إيقاف مؤقت",
"previous": "السابق",
"next": "التالي",
"show": "فتح WaveFlow",
"quit": "خروج"
}
},
"spotify": {
"deviceReady": "جهاز Spotify الخاص بـ WaveFlow جاهز",
"devicePending": "جارٍ تحضير تشغيل Spotify…",
"searchPlaceholder": "البحث في Spotify",
"tracks": "المقطوعات",
"playlists": "قوائم التشغيل",
"emptyPlaylist": "لا توجد مقطوعات متاحة في قائمة التشغيل هذه (قد لا يعرض Spotify الملفات المحلية أو قوائم التشغيل التي لا تملكها)."
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
54 changes: 50 additions & 4 deletions src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
},
"sidebar": {
"nav": {
"home": "Startseite"
"home": "Startseite",
"spotify": "Spotify"
},
"sections": {
"open": "Öffnen",
Expand Down Expand Up @@ -213,6 +214,16 @@
"recentlyAdded": {
"title": "Kürzlich hinzugefügt"
},
"dailyMix": {
"title": "Für dich",
"regenerate": "Neu generieren",
"regenerating": "Wird generiert…",
"emptyTitle": "Noch kein Daily Mix",
"emptyDescription": "Höre ein paar Titel und klicke dann auf Neu generieren, um deine persönlichen Mixe zu erstellen.",
"label": "Daily Mix",
"trackCount_one": "{{count}} Titel",
"trackCount_other": "{{count}} Titel"
},
"wrapped": {
"eyebrow": "WaveFlow Wrapped",
"title": "Dein Rückblick {{year}}",
Expand Down Expand Up @@ -590,7 +601,8 @@
"slider": "Geschwindigkeitsregler",
"custom": "Benutzerdefinierte Geschwindigkeit",
"pitchHint": "Tonhöhe folgt der Geschwindigkeit (kein Time-Stretching)."
}
},
"seek": "Position"
},
"queue": {
"title": "Warteschlange abspielen",
Expand Down Expand Up @@ -664,7 +676,13 @@
"iconLabel": "Symbol",
"iconAria": "{{icon}}-Symbol",
"submit": "Erstellen",
"editSubmit": "Speichern"
"editSubmit": "Speichern",
"coverChoose": "Foto wählen",
"coverMenu": "Cover-Optionen",
"coverChange": "Foto ändern",
"coverRemove": "Foto entfernen",
"coverAutoHint": "Automatisches Cover — passt sich dem Inhalt an",
"coverManualHint": "Eigenes Bild"
},
"playlistView": {
"badge": "Playlist",
Expand All @@ -687,7 +705,8 @@
"noneTitle": "Es wurde keine Wiedergabeliste ausgewählt",
"noneDescription": "Wähle eine Wiedergabeliste in der Seitenleiste aus, um sie hier anzuzeigen.",
"notFoundTitle": "Playlist nicht gefunden",
"notFoundDescription": "Diese Wiedergabeliste ist im aktiven Profil nicht mehr vorhanden."
"notFoundDescription": "Diese Wiedergabeliste ist im aktiven Profil nicht mehr vorhanden.",
"smartLabel": "Daily Mix"
},
"trackActions": {
"addToPlaylist": "Zur Wiedergabeliste hinzufügen",
Expand Down Expand Up @@ -1107,6 +1126,16 @@
"statusRunning": "Aktiv",
"statusStopped": "Gestoppt",
"copyUrl": "URL kopieren"
},
"spotify": {
"title": "Spotify",
"subtitle": "Verbinde Spotify Premium mit deiner eigenen Spotify Developer Client-ID.",
"clientIdPlaceholder": "Spotify Client ID",
"redirectHint": "Füge diese Redirect URI im Spotify Developer Dashboard hinzu: http://127.0.0.1:49387/spotify/callback",
"connectedAs": "Verbunden als",
"disconnect": "Trennen",
"connecting": "Verbindung wird hergestellt…",
"connect": "Spotify verbinden"
}
},
"crossfade": {
Expand Down Expand Up @@ -1277,5 +1306,22 @@
"runningSubtitle": "{{current}} / {{total}} Dateien",
"doneTitle": "Analyse abgeschlossen",
"doneSubtitle": "{{added}} hinzugefügt · {{updated}} aktualisiert · {{skipped}} übersprungen"
},
"system": {
"tray": {
"playPause": "Wiedergabe / Pause",
"previous": "Zurück",
"next": "Weiter",
"show": "WaveFlow öffnen",
"quit": "Beenden"
}
},
"spotify": {
"deviceReady": "WaveFlow Spotify-Gerät bereit",
"devicePending": "Spotify-Wiedergabe wird vorbereitet…",
"searchPlaceholder": "Spotify durchsuchen",
"tracks": "Titel",
"playlists": "Playlists",
"emptyPlaylist": "Keine abspielbaren Titel in dieser Playlist (Spotify blendet lokale Dateien oder Playlists, die dir nicht gehören, möglicherweise aus)."
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading
Loading