Skip to content

Commit b15cfdf

Browse files
committed
Consume server-resolved translations for server-provided strings
The frontend kept its own copy of translatable strings for objects the server provides (config entries, media item names, genres, recommendations, browse folders) and resolved them locally. The server now resolves these for the connection locale and sends them already localized, so consume those values directly and stop resolving the keys client-side. - Render server-resolved label/description/option title/category_label/name instead of looking them up via vue-i18n. - Send translations/set_locale on connect and on UI language change, then refresh state so server-provided strings re-resolve (also covers the Ingress path where the auth command is skipped). - Pre-translate the frontend-only settings entries locally; those stay frontend-owned. - Remove the now-dead server-object keys from the locale files. - Update getGenreDisplayName tests for the server-resolved behaviour.
1 parent 6a11103 commit b15cfdf

41 files changed

Lines changed: 91 additions & 7875 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/App.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,19 @@ onMounted(async () => {
402402
}
403403
},
404404
);
405+
406+
// Push UI locale changes to the server so server-provided strings (config labels, media/folder
407+
// names, provider descriptions) re-localize. Initial/reconnect locale is sent from the api on
408+
// ServerInfo; this catches later changes (language preference applied, manual switch).
409+
watch(
410+
() => i18n.global.locale.value,
411+
async (locale) => {
412+
await api.setLocale(locale as string);
413+
if (api.state.value === ConnectionState.AUTHENTICATED) {
414+
await api.fetchState();
415+
}
416+
},
417+
);
405418
window
406419
.matchMedia("(prefers-color-scheme: dark)")
407420
.addEventListener("change", setTheme);

src/components/HomeWidgetRows.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,8 @@ watch(
325325
},
326326
);
327327
328-
const folderTitle = (folder: RecommendationFolder) =>
329-
folder.translation_key
330-
? $t(`recommendations.${folder.translation_key}`, folder.name)
331-
: folder.name;
328+
// recommendation folder names are resolved server-side for the connection locale
329+
const folderTitle = (folder: RecommendationFolder) => folder.name;
332330
333331
const folderProvider = (folder: RecommendationFolder) => folder.provider || "";
334332

src/components/ItemsListing.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,10 +1758,9 @@ const getSortName = function (
17581758
preferSortName = false,
17591759
) {
17601760
if (!item) return "";
1761-
if ("translation_key" in item && item.translation_key && item.name)
1762-
return t(item.translation_key, [item.name]);
1763-
if ("translation_key" in item && item.translation_key)
1764-
return t(item.translation_key);
1761+
// names (incl. translated folder/media names) are resolved server-side; sort by item.name,
1762+
// falling back to sort_name when explicitly preferred for regular items.
1763+
if ("translation_key" in item && item.translation_key) return item.name;
17651764
if (preferSortName && "sort_name" in item && item.sort_name)
17661765
return item.sort_name;
17671766
return item.name;

src/helpers/utils.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,24 +158,13 @@ const genreKeyFromName = function (name: string): string {
158158

159159
export const getGenreDisplayName = function (
160160
name: string,
161-
translationKey: string | undefined,
162-
t: (key: string) => string,
163-
te: (key: string) => boolean,
161+
// translationKey/t/te are no longer used: genre names are resolved server-side for the
162+
// connection locale, so the `name` passed in is already the localized display value. The
163+
// parameters are kept so the (many) call sites don't all need to change.
164+
_translationKey?: string | undefined,
165+
_t?: (key: string) => string,
166+
_te?: (key: string) => boolean,
164167
): string {
165-
// First try the translation key as-is (in case backend sends full key like 'genre_names.afrobeats')
166-
if (translationKey && te(translationKey)) return t(translationKey);
167-
168-
// Then try with genre_names prefix (in case backend sends just the key name like 'afrobeats')
169-
if (translationKey) {
170-
const keyWithPrefix = `genre_names.${translationKey}`;
171-
if (te(keyWithPrefix)) return t(keyWithPrefix);
172-
}
173-
174-
// Fallback: generate key from name
175-
const key = `genre_names.${genreKeyFromName(name)}`;
176-
if (te(key)) return t(key);
177-
178-
// No translation found - apply sentence case for user-created/promoted genres
179168
return name;
180169
};
181170

src/plugins/api/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { store } from "../store";
22
/* eslint-disable no-constant-condition */
33
import { computed, reactive, ref } from "vue";
44
import { toast } from "vue-sonner";
5-
import { $t } from "../i18n";
5+
import { $t, i18n } from "../i18n";
66
import type { ITransport } from "../remote/transport";
77
import { WebSocketTransport } from "../remote/websocket-transport";
88
import { getDeviceName } from "./helpers";
@@ -2388,13 +2388,32 @@ export class MusicAssistantApi {
23882388
this.serverInfo.value = msg;
23892389
// ServerInfo means transport is connected and server is ready, but not yet authenticated
23902390
this.state.value = ConnectionState.CONNECTED;
2391+
// declare our UI locale so the server localizes server-provided strings (config labels,
2392+
// media/folder names, provider descriptions). Handled server-side before the auth gate,
2393+
// so it also works on the Ingress path where the frontend skips the auth command.
2394+
void this.setLocale(i18n.global.locale.value);
23912395
this.signalEvent({
23922396
event: EventType.CONNECTED,
23932397
object_id: "",
23942398
data: msg,
23952399
});
23962400
}
23972401

2402+
/**
2403+
* Declare the connection's UI locale to the server (translations/set_locale).
2404+
*
2405+
* The server resolves server-provided strings for this locale at serialization. Tolerant of
2406+
* older servers that do not implement the command.
2407+
*/
2408+
public async setLocale(locale: string): Promise<void> {
2409+
if (!locale || locale === "auto") return;
2410+
try {
2411+
await this.sendCommand("translations/set_locale", { locale });
2412+
} catch {
2413+
// server may predate the set_locale command — ignore
2414+
}
2415+
}
2416+
23982417
/**
23992418
* Signal an event to all registered listeners.
24002419
*/

src/plugins/api/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,8 @@ export interface ConfigEntry {
561561
category_translation_key?: string;
562562
// category_translation_params: optional parameters for the category translation key
563563
category_translation_params?: string[];
564+
// category_label: localized category display name, resolved server-side
565+
category_label?: string | null;
564566
// advanced: indicates this is an advanced setting (hidden by default)
565567
advanced?: boolean;
566568

src/translations/bg_BG.json

Lines changed: 0 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -126,73 +126,6 @@
126126
"radio_in_library": "Радиа в библиотеката",
127127
"download_log": "Изтегляне на журнал",
128128
"add_new_provider_button": "Добавяне на {0} доставчик",
129-
"log_level": {
130-
"label": "Ниво на журнала",
131-
"description": "Задайте детайлността на журнала за този доставчик.",
132-
"options": {
133-
"global": "Глобално",
134-
"info": "Информация",
135-
"warning": "Предупреждение",
136-
"error": "Грешка",
137-
"debug": "Отстраняване на грешки"
138-
}
139-
},
140-
"auto_play": {
141-
"label": "Автоматично възпроизвеждане (възобновяване при включване на захранването)",
142-
"description": "Когато този плейър е включен, автоматично започва да възпроизвежда (ако има елементи в опашката)."
143-
},
144-
"output_channels": {
145-
"description": "Можете да конфигурирате този плейър да възпроизвежда само левия или десния канал, например за да създадете стерео двойка с 2 плейъра.",
146-
"options": {
147-
"stereo": "Стерео (и двата канала)",
148-
"left": "Само ляв канал",
149-
"right": "Само десен канал",
150-
"mono": "Моно (и двата канала)"
151-
}
152-
},
153-
"volume_normalization": {
154-
"label": "Активиране на нормализирането на силата на звука (базирано на EBU-R128)"
155-
},
156-
"eq_bass": {
157-
"label": "Еквалайзер: бас",
158-
"description": "Използвайте вградения основен еквалайзер, за да регулирате нивото на баса (ниските тонове) на звука."
159-
},
160-
"eq_mid": {
161-
"label": "Еквалайзер: mid",
162-
"description": "Използвайте вградения основен еквалайзер, за да регулирате средното ниво на звука."
163-
},
164-
"eq_treble": {
165-
"label": "Еквалайзер: високи честоти",
166-
"description": "Използвайте вградения основен еквалайзер, за да регулирате нивото на treble (високите тонове) на звука."
167-
},
168-
"crossfade": {
169-
"label": "Активиране на плавно преливане",
170-
"description": "Активиране на преход с плавно преливане между (опашка) песни."
171-
},
172-
"crossfade_duration": {
173-
"label": "Продължителност на плавното преливане",
174-
"description": "Продължителност в секунди на плавното преливане между песните (ако е активирано)"
175-
},
176-
"hide_player": {
177-
"label": "Скриване на този плейър в потребителския интерфейс",
178-
"description": "Скриване на този плейър в потребителския интерфейс.\n\nИмайте предвид, че той все още може да се контролира и ще се показва във всички синхронизирани групи, към които принадлежи."
179-
},
180-
"default_enqueue_select_artist": {
181-
"description": "Ако искате се възпроизведе изпълнител, кои елементи трябва да бъдат поставени в опашката?",
182-
"options": {
183-
"library_album_tracks": "Всички песни от всички албуми в библиотеката"
184-
}
185-
},
186-
"default_enqueue_select_album": {
187-
"description": "Ако искате да се възпроизведе албум, кои елементи трябва да бъдат поставени в опашката?"
188-
},
189-
"default_enqueue_action_radio": {
190-
"options": {
191-
"play": "Пускане на радиостанцията сега и запазване на останалите елементи от опашката.",
192-
"replace": "Изчистване на опашката и пускане на радиостанцията",
193-
"add": "Добавете радиостанцията(ите) в края на опашката."
194-
}
195-
},
196129
"default": "по подразбиране",
197130
"provider_depends_on_confirm": "Преди да можете да настроите {0}, трябва да конфигурирате доставчика {1}. Искате ли да го направите сега?",
198131
"category": {
@@ -203,26 +136,6 @@
203136
"web_player": "Уеб плейър",
204137
"options": "Опции на плейъра"
205138
},
206-
"announce_volume_strategy": {
207-
"options": {
208-
"none": "Да не се регулира силата на звука"
209-
}
210-
},
211-
"announce_volume": {
212-
"label": "Сила на звука за обявления"
213-
},
214-
"announce_volume_min": {
215-
"label": "Минимално ниво на звука за обявления",
216-
"description": "Силата на звука (регулирането) на обявленията не трябва да пада под това ниво."
217-
},
218-
"announce_volume_max": {
219-
"label": "Максимално ниво на силата на звука за обявления",
220-
"description": "Силата на звука (регулирането) на обявленията не трябва да надвишава това ниво."
221-
},
222-
"icon": {
223-
"label": "Икона",
224-
"description": "Material design икона за този плейър.\n\nВижте https:\/\/pictogrammers.com\/library\/mdi\/."
225-
},
226139
"theme": {
227140
"label": "Тема",
228141
"options": {
@@ -270,20 +183,6 @@
270183
"no_music_providers": "Няма конфигурирани доставчици на музика",
271184
"no_music_providers_detail": "Започнете вашето изживяване с Music Assistant, като добавите вашите музикални доставчици.",
272185
"no_player_providers": "Няма конфигурирани доставчици на плейъри",
273-
"volume_normalization_radio": {
274-
"label": "Метод за нормализиране на силата на звука, който да се използва за радио потоци",
275-
"options": {
276-
"disabled": "Деактивирано",
277-
"fixed_gain": "Фиксирана корекция на усилването"
278-
}
279-
},
280-
"volume_normalization_tracks": {
281-
"label": "Метод за нормализиране на силата на звука, който да се използва за песни",
282-
"options": {
283-
"disabled": "Деактивирано",
284-
"fixed_gain": "Фиксирана корекция на усилването"
285-
}
286-
},
287186
"dsp": {
288187
"disabled_message": "В момента DSP е изключен. Включете го с помощта на превключвателя по-горе",
289188
"input": "Вход",
@@ -318,9 +217,6 @@
318217
"FR": "Д"
319218
}
320219
},
321-
"volume_control": {
322-
"label": "Контрол на силата на звука"
323-
},
324220
"dsp_enabled": "DSP е активиран",
325221
"dsp_disabled": "DSP е дезактивиран",
326222
"dsp_note_multi_device_group": {
@@ -661,32 +557,6 @@
661557
"music_assistant_library": "Библиотека на Music Assistant",
662558
"select_all_confirmation": "Избиране на всички елементи? Зареждането на всички данни може да отнеме известно време.",
663559
"this_device": "Това устройство",
664-
"recommendations": {
665-
"in_progress_items": "Продължете да слушате...",
666-
"recommended_new_tracks": "Препоръчани нови песни",
667-
"recently_played": "Наскоро възпроизвеждани",
668-
"suggested_new_albums_for_you": "Предложени нови албуми за вас",
669-
"random_artists": "Случайни изпълнители",
670-
"random_albums": "Случайни албуми",
671-
"custom_mixes": "Персонализирани миксове",
672-
"your_listening_history": "Вашата история на слушане",
673-
"popular_albums": "Популярни албуми",
674-
"radio_stations_for_you": "Радиостанции за вас",
675-
"producers__songwriters": "Продуценти и автори на песни",
676-
"new_releases_for_you": "Нови версии за вас",
677-
"recent_favorite_tracks": "Наскоро добавени към любими песни",
678-
"favorite_playlists": "Любими плейлисти",
679-
"favorite_radio_stations": "Любими радиостанции",
680-
"listen_again": "Слушайте отново",
681-
"recommended": "Препоръчано",
682-
"recently_added": "Наскоро добавени",
683-
"episodes_recently_added": "Наскоро добавени епизоди",
684-
"newest_authors": "Най-новите автори",
685-
"newest_episodes": "Най-новите епизоди",
686-
"discover": "Откриване",
687-
"libraries": "Библиотеки",
688-
"library": "Библиотека"
689-
},
690560
"homescreen_edit_enable": "Редактиране на началния екран",
691561
"homescreen_edit_disable": "Изход от режима за редактиране на началния екран",
692562
"lyrics_will_appear_soon": "Текстът ще се появи, когато песента започне...",
@@ -912,44 +782,6 @@
912782
"description": "Описание",
913783
"genre": "Жанр",
914784
"genres": "Жанрове",
915-
"genre_names": {
916-
"asian_music": "Азиатска музика",
917-
"bluegrass": "Блуграс",
918-
"blues": "Блус",
919-
"brazilian_music": "Бразилска музика",
920-
"chanson": "Шансон",
921-
"childrens_music": "Детска музика",
922-
"christmas_music": "Коледна музика",
923-
"church_music": "Църковна музика",
924-
"classical": "Класическа",
925-
"comedy": "Комедия",
926-
"country": "Кънтри",
927-
"dance": "Денс",
928-
"disco": "Диско",
929-
"electronic": "Електронна",
930-
"experimental": "Експериментална",
931-
"folk": "Фолк",
932-
"funk": "Фънк",
933-
"gangsta_rap": "Гангстерски рап",
934-
"hip_hop": "Хип хоп",
935-
"industrial": "Индустриална",
936-
"jazz": "Джаз",
937-
"latin": "Латино",
938-
"metal": "Метал",
939-
"middle_eastern_music": "Близкоизточна музика",
940-
"musical": "Мюзикъл",
941-
"pop": "Поп",
942-
"punk": "Пънк",
943-
"r_b": "R&b",
944-
"reggae": "Реге",
945-
"rock": "Рок",
946-
"salsa": "Салса",
947-
"sound_effects": "Звукови ефекти",
948-
"soundtrack": "Саундтрак",
949-
"tango": "Танго",
950-
"waltz": "Валс",
951-
"wellness": "Уелнес"
952-
},
953785
"link": "Връзка",
954786
"link_alias_failed": "Свързването на псевдонима не е успешно",
955787
"alias_linked_successfully": "Псевдонимът е свързан успешно",

0 commit comments

Comments
 (0)