diff --git a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.android.kt b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.android.kt new file mode 100644 index 00000000..0562a6fb --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.android.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.i18n + +import android.content.res.Configuration +import android.content.res.Resources +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidedValue +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import java.util.Locale + +@Composable +internal actual fun provideAppLocale(languageTag: String?): ProvidedValue<*> { + // System resources are never touched by the override below, so they always report the + // device language (also after the user changes it while the app is running). + val locale = languageTag?.let(Locale::forLanguageTag) + ?: Resources.getSystem().configuration.locales[0] + // Compose resources read Locale.getDefault(); also used by non-UI code (notifications). + Locale.setDefault(locale) + + val configuration = Configuration(LocalConfiguration.current).apply { setLocale(locale) } + val resources = LocalContext.current.resources + @Suppress("DEPRECATION") + resources.updateConfiguration(configuration, resources.displayMetrics) + return LocalConfiguration provides configuration +} diff --git a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/MediaBrowseHelper.kt b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/MediaBrowseHelper.kt index 9a0a19d8..060d6d50 100644 --- a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/MediaBrowseHelper.kt +++ b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/MediaBrowseHelper.kt @@ -41,6 +41,17 @@ import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository import dev.krtirtho.spotube.modules.search.SearchRepository import dev.krtirtho.spotube.modules.settings.SettingsRepository import kotlinx.coroutines.flow.first +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.library_tab_albums +import spotube.composeapp.generated.resources.library_tab_artists +import spotube.composeapp.generated.resources.library_tab_playlists +import spotube.composeapp.generated.resources.library_toolbar_compact_title +import spotube.composeapp.generated.resources.media_browser_browse +import spotube.composeapp.generated.resources.media_browser_featured +import spotube.composeapp.generated.resources.media_browser_now_playing +import spotube.composeapp.generated.resources.media_browser_saved_tracks +import spotube.composeapp.generated.resources.media_browser_top_tracks class MediaBrowseHelper( private val pluginManager: PluginManager, @@ -221,13 +232,13 @@ class MediaBrowseHelper( } } - private fun buildRootChildren(): List { + private suspend fun buildRootChildren(): List { return listOf( MediaItem.Builder() .setMediaId(MEDIA_ID_BROWSE) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Browse") + .setTitle(getString(Res.string.media_browser_browse)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -237,7 +248,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_LIBRARY) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Library") + .setTitle(getString(Res.string.library_toolbar_compact_title)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -247,7 +258,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_QUEUE) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Now Playing") + .setTitle(getString(Res.string.media_browser_now_playing)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -264,7 +275,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_BROWSE_FEATURED) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Featured") + .setTitle(getString(Res.string.media_browser_featured)) .setIsBrowsable(true) .setIsPlayable(false) .setExtras(gridExtras) @@ -353,7 +364,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_SAVED_PLAYLISTS) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Playlists") + .setTitle(getString(Res.string.library_tab_playlists)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -363,7 +374,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_SAVED_ALBUMS) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Albums") + .setTitle(getString(Res.string.library_tab_albums)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -373,7 +384,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_SAVED_ARTISTS) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Artists") + .setTitle(getString(Res.string.library_tab_artists)) .setIsBrowsable(true) .setIsPlayable(false) .build() @@ -383,7 +394,7 @@ class MediaBrowseHelper( .setMediaId(MEDIA_ID_SAVED_TRACKS) .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Saved Tracks") + .setTitle(getString(Res.string.media_browser_saved_tracks)) .setIsPlayable(false) .setIsBrowsable(true) .build() @@ -457,7 +468,7 @@ class MediaBrowseHelper( .setMediaId("$MEDIA_ID_ARTIST_TRACKS:$artistId") .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Top Tracks") + .setTitle(getString(Res.string.media_browser_top_tracks)) .setIsPlayable(false) .setIsBrowsable(true) .build() @@ -467,7 +478,7 @@ class MediaBrowseHelper( .setMediaId("$MEDIA_ID_ARTIST_ALBUMS:$artistId") .setMediaMetadata( MediaMetadata.Builder() - .setTitle("Albums") + .setTitle(getString(Res.string.library_tab_albums)) .setIsBrowsable(true) .setIsPlayable(false) .build() diff --git a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt index 3c49b84c..c4177b0e 100644 --- a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt +++ b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt @@ -49,9 +49,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.notification_playback_channel_description +import spotube.composeapp.generated.resources.notification_playback_channel_name +import spotube.composeapp.generated.resources.notification_playback_service_text @OptIn(UnstableApi::class) class PlaybackService : MediaLibraryService(), KoinComponent { @@ -80,7 +86,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent { val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Spotube") - .setContentText("Playback service") + .setContentText(runBlocking { getString(Res.string.notification_playback_service_text) }) .setSmallIcon(android.R.drawable.ic_media_play) .setContentIntent(sessionActivity) .setOngoing(true) @@ -112,10 +118,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, - "Playback", + runBlocking { getString(Res.string.notification_playback_channel_name) }, NotificationManager.IMPORTANCE_LOW ).apply { - description = "Media playback controls" + description = runBlocking { + getString(Res.string.notification_playback_channel_description) + } setShowBadge(false) } val manager = getSystemService(NotificationManager::class.java) diff --git a/composeApp/src/commonMain/composeResources/values-ar/strings.xml b/composeApp/src/commonMain/composeResources/values-ar/strings.xml new file mode 100644 index 00000000..674b83a9 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-ar/strings.xml @@ -0,0 +1,962 @@ + + + + قائمة الانتظار + مسح قائمة الانتظار + تصفية قائمة الانتظار... + لا توجد عناصر في قائمة الانتظار + + ابحث عن أغانٍ وفنانين وألبومات... + مسح السجل بالكامل + بحث... + + + مكتبتك + المكتبة + قوائم التشغيل + الألبومات + الفنانون + المقاطع المحلية + التنزيلات + ابحث في قوائم التشغيل المحفوظة… + ابحث في الألبومات المحفوظة… + ابحث في الفنانين المحفوظين… + ابحث في المجلدات أو المقاطع… + تصفية المكتبة + إغلاق التصفية + إنشاء قائمة تشغيل + تحديث المقاطع المحلية + التبديل إلى عرض الشبكة + التبديل إلى عرض القائمة + لا توجد قوائم تشغيل محفوظة + لا توجد قوائم تشغيل تطابق “%1$s” + لا توجد ألبومات محفوظة + لا توجد ألبومات تطابق “%1$s” + لا يوجد فنانون محفوظون + لا يوجد فنانون يطابقون “%1$s” + + + المجلدات + مجلد + %1$s • %2$s + + %1$d مقطع + مقطع واحد + %1$d مقطعان + %1$d مقاطع + %1$d مقطعًا + %1$d مقطع + + + %1$d مجلد + مجلد واحد + %1$d مجلدان + %1$d مجلدات + %1$d مجلدًا + %1$d مجلد + + العودة إلى المجلدات + تشغيل المجلد + يلزم الوصول إلى الصوت + اسمح بالوصول إلى الصوت لتصفح الموسيقى على جهازك. + منح الإذن + لم يتم العثور على مجلدات محلية + لا توجد مقاطع في هذا المجلد + لا توجد مجلدات أو مقاطع مطابقة + تعذّر تحديث الوسائط المحلية + + + %1$d نشط • %2$d مكتمل + مسح المكتملة + لا توجد تنزيلات بعد + جارٍ التنزيل %1$d% + في قائمة الانتظار + مكتمل + ملغى + فشل + فشل: %1$s + إلغاء التنزيل + إعادة محاولة التنزيل + إزالة التنزيل + + + فنان + محظور + المقاطع التي أعجبتك + أغانيك المحفوظة + فنان غير معروف + عنوان المقطع + اسم الفنان + + محدد + + الإعدادات + + الإضافات + اللغة والمنطقة + المظهر + التشغيل + التنزيلات + التخزين المؤقت + سطح المكتب + التحديثات + الدعم + حول + + إغلاق + حفظ + إلغاء + + اللغة + لغة واجهة التطبيق. الحالية: %1$s + افتراضي النظام + البلد + البلد المستخدم للمحتوى والتوفر. الحالي: %1$s + %1$s (%2$s) + + السمة + اختر كيف يتوافق Spotube مع مظهر نظامك. الحالية: %1$s + فاتح + داكن + النظام + + السطح + مادة خلفية التطبيق. الحالية: %1$s + مائي + جليدي + كروم + أكريليك + زجاج + + لون التمييز + لوحة الألوان الحالية: %1$s + لون التمييز + اختر لوحة ألوان تمييز واعرض معاينتها في السمتين الفاتحة والداكنة. + فاتح + داكن + معاينة + زر + أخضر غوبلن + بنفسجي كهربائي + سماوي محيطي + برتقالي الغروب + حديقة الورد + أزرق منتصف الليل + أردوازي معدني + + الشكل والمظهر + مادة السطح ولون التمييز. الحالي: %1$s · %2$s + الشكل والمظهر + اختر مادة السطح، ثم لون تمييز يتناسب معها. كل خيار هو تركيبة منتقاة بعناية. + السطح + لون التمييز + ثانوي + + لوحة الألوان + لوحة ألوان تمييز Millennium. الحالية: %1$s + لوحة الألوان + اختر لوحة ألوان تمييز Millennium واعرض معاينتها في الوضعين الفاتح والداكن. + لوحة الألوان + لوحة ألوان Millennium ولون التمييز. الحالية: %1$s + أزرق + برتقالي + ليموني + + تخصيص مجلد التنزيل + المجلد الحالي: %1$s + المجلد الحالي: مجلد الموسيقى الافتراضي + اتركه فارغًا لاستخدام مجلد الموسيقى الافتراضي. + C:\Music\Spotube + مجلدات الوسائط المحلية + مجلدات مخصصة للفحص على سطح المكتب. المضافة: %1$d + لم تتم إضافة أي مجلد مخصص بعد + مجلدات مخصصة لمكتبة موسيقاك المحلية. + إدارة + إضافة مجلد + إزالة المجلد + لم تتم إضافة مجلدات مخصصة. + إزالة %1$s من قائمة الفحص + يتم تضمين مجلد التنزيل ومجلد ذاكرة التخزين المؤقت للموسيقى تلقائيًا. + يتم تضمين مجلد التنزيل تلقائيًا. + يتم تضمين مجلد ذاكرة التخزين المؤقت للموسيقى تلقائيًا. + جارٍ الحفظ… + تعذّر حفظ المجلدات. لا تزال تغييراتك موجودة؛ حاول مرة أخرى. + + %1$d مجلد + مجلد واحد + %1$d مجلدان + %1$d مجلدات + %1$d مجلدًا + %1$d مجلد + + صيغة تنزيل الموسيقى + الترميز والحاوية المفضلان للتنزيلات. الحالي: %1$s + جودة تنزيل الموسيقى + + صيغة بث الموسيقى + الترميز والحاوية المفضلان لبث الموسيقى. الحالي: %1$s + جودة بث الموسيقى + الحالي: %1$s + تفعيل التخزين المؤقت للموسيقى + خزّن الصوت الذي يتم بثه محليًا لتسريع إعادة التشغيل. + مجلد ذاكرة التخزين المؤقت + الحالي: %1$s + الحالي: ذاكرة التخزين المؤقت الافتراضية للتطبيق + الحد الأقصى لحجم ذاكرة التخزين المؤقت + بلا حد + الحد الحالي: %1$d MB + حدد الحجم الأقصى لذاكرة التخزين المؤقت بوحدة MB. أدخل 0 أو اتركه فارغًا لعدم وضع حد. + 500 + يجب أن يكون الحجم 0 أو رقمًا موجبًا. + تفعيل التشغيل اللانهائي + واصل التشغيل بمقاطع مقترحة عند انتهاء قائمة الانتظار. + تفعيل Connect + إتاحة عناصر التحكم في التشغيل عن بُعد عبر ميزة Spotube Connect. + منفذ خادم وكيل التشغيل + السماح بالتحكم عن بُعد + اسمح للأجهزة الأخرى على الشبكة نفسها بالتحكم في التشغيل عند اتصالها. + اسم الجهاز للتحكم عن بُعد + الاسم الذي تراه الأجهزة الأخرى عند اكتشاف هذا الجهاز. الحالي: %1$s + اختر اسمًا مألوفًا يظهر للأجهزة الأخرى على شبكتك عند اكتشافها لنسخة Spotube هذه. + مثال: Spotube غرفة المعيشة + اسم مضيف الجهاز + المنفذ الذي يستخدمه خادم وكيل التشغيل. الحالي: %1$d + اختر منفذًا بين 1 و65535. + 14769 + أدخل عددًا صحيحًا صالحًا. + يجب أن يكون المنفذ بين 1 و65535. + + قائمة الحظر + إدارة المقاطع والفنانين المحظورين + + التصغير إلى شريط النظام + إبقاء Spotube قيد التشغيل في شريط النظام عند تصغير النافذة. + حالة Discord التفاعلية + اعرض حالة التشغيل الحالية في Discord على سطح المكتب. + + التحقق من التحديثات تلقائيًا + البحث عن إصدارات Spotube الجديدة في الخلفية. + + إدارة الإضافات + أدِر إضافاتك + إضافة %1$s الافتراضية + البيانات الوصفية + الصوت + كلمات الأغاني + تسجيل الاستماع + لم يتم تحديد إضافة + تغيير + تحديد + محددة + مسح التحديد + لا تتوفر إضافات + إضافة %1$s + مسح + + ضبط + مدير الإضافات + يُرجى إدخال URL + يجب أن يبدأ URL بـ http:// أو https:// + تعذّر تنزيل الإضافة + لم يتم تثبيت أي إضافات بعد + الصق رابط تنزيل أو اختر ملفًا أعلاه. + + %1$d إضافة مثبتة + إضافة واحدة مثبتة + %1$d إضافتان مثبتتان + %1$d إضافات مثبتة + %1$d إضافة مثبتة + %1$d إضافة مثبتة + + تثبيت إضافة + https://example.com/plugin.smplug + تنزيل + التثبيت من ملف (.smplug) + نشطة + v%1$s + مدمجة + إزالة الإضافة + تسجيل الدخول + تسجيل الخروج + التنزيل من URL + التثبيت من ملف + تثبيت + + • %1$s + الأذونات المطلوبة + لا تتطلب هذه الإضافة أي أذونات خاصة. + المثبتة مقابل المقدَّمة + المثبتة + المقدَّمة + API %1$s → %2$s + التخزين الدائم + قراءة البيانات وكتابتها على القرص + طلبات الشبكة + إرسال البيانات واستقبالها عبر الإنترنت + WebView + عرض محتوى الويب داخل التطبيق + الجام الجماعي + وسيط MQTT + مضيف الوسيط + %1$s:%2$d + منفذ الوسيط + استخدام TLS + اسم المستخدم (اختياري) + كلمة المرور (اختياري) + بادئة معرّف العميل + تُستخدم لتعريف نسخة Spotube هذه + تشفير الاتصال بالوسيط + تحقق من إعدادات الوسيط الحالية قبل بدء غرفة. + اختبار الاتصال + جارٍ الاختبار… + تم الاتصال خلال %1$d ms + فشل: %1$s + وسيط مؤقت — اضبط وسيطك الخاص للاستضافة الذاتية + الاتصال + بيانات الاعتماد + الهوية + التشخيص + + + الجام الجماعي + إنشاء + انضمام + أنشئ غرفة، ثم شارك رمزها مع أصدقائك. + إنشاء غرفة + الانضمام إلى غرفة + جارٍ الاتصال… + رمز الغرفة + ABC123 + الوسيط: %1$s + اضبط وسيطًا في الإعدادات لاستضافة غرفة أو الانضمام إليها. + إعدادات الوسيط + متصل + جارٍ إعادة الاتصال… + نسخ الرمز + تم النسخ! + مشاركة + المشاركون (%1$d) + المضيف + مستمع + في انتظار المستمعين… + إزالة + حظر + إدارة + مغادرة الغرفة + تعذّر الاتصال + تجاهل + تعذّر إنشاء جلسة الجام + تعذّر الانضمام إلى جلسة الجام + أدخل رمز غرفة مكوّنًا من ستة أحرف وأرقام. + انضم إلى جام Spotube الخاص بي باستخدام الرمز: %1$s + + الانتقال إلى الألبوم + مؤقت النوم + تفاصيل المقطع + تشغيل + إيقاف مؤقت + لا شيء قيد التشغيل + إضافة إلى قائمة الانتظار + + تجاهل + تحديث + يتوفر Spotube %1$s + اطّلع على الجديد في هذا الإصدار في صفحة تنزيلات Spotube. + يتوفر %1$s %2$s + المثبت حاليًا: %1$s + اطّلع على الجديد في هذا التحديث في صفحة الإضافة. + + تخطٍّ + التالي + رجوع + البدء + Spotube + موسيقى بلا قيود. + مشغّل مفتوح المصدر لعالم من الموسيقى.\nلك أن تستكشفه. ولك أن تجعله على طريقتك. + ساعد في استمرار هذا المشروع. + يبني Spotube أناسٌ حقيقيون، لا شركة اشتراكات. إن أصبح جزءًا من حياتك اليومية، فإن القليل من الدعم يساعدنا على منحه الرعاية التي يستحقها.\n\nشكرًا لإفساحك المجال للبرمجيات المستقلة. + ابدأ بإضافة كتالوج للعثور على الأغاني وقوائم التشغيل. اختر إضافة صوت لتشغيلها. كلمات الأغاني وسجل الاستماع اختياريان. اكتشف إضافة أدناه، وراجع أذوناتها، وثبّتها، ثم حدّدها هنا. + اكتشاف الإضافات + مصادرك + إعادة المحاولة + المزيد من الإضافات + لم يتم العثور على إضافات جديدة. يمكنك أيضًا تثبيت إضافة من رابط أو من ملف .smplug أعلاه. + اكتشاف الإضافات غير متاح حاليًا. حاول مرة أخرى، أو ثبّت من رابط أو ملف. + مرحبًا + الدعم + الإضافات + الدليل + + كيفية استخدام Spotube + أربع خطوات سريعة وتبدأ الاستماع. + تسجيل الدخول + اربط حسابك على Spotify، أو اختر إضافة الكتالوج الخاصة بك. + ابحث وشغّل + اعثر على أي أغنية أو ألبوم أو فنان، ثم اضغط تشغيل. + استمع بلا إنترنت + نزّل المقاطع لمواصلة التشغيل دون اتصال. + تابع كلمات الأغاني + افتح كلمات الأغاني المتزامنة من المشغّل أثناء تشغيل المقطع. + + + موسيقاك. مصادرك. قواعدك. + مشغّل موسيقى مجاني ومفتوح المصدر، بناه مجتمع يحب الموسيقى ببساطة. + مفتوح المصدر + بلا إعلانات + بلا قياس عن بُعد + مدعوم بالإضافات + هيا بنا + + + الداعمون يبقوننا مستمرين + صُنع بشغف. ويستمر بفضلك. + Spotube مجاني 100%، وحر، ومفتوح المصدر — بلا إعلانات، وبلا اشتراكات، وبلا تتبع. كل ميزة موجودة لأن أشخاصًا مثلك اختاروا المساعدة في استمرار تدفق الشيفرة. + Patreon + راعٍ شهري + ادعم التطوير والبنية التحتية بشكل مستمر + Buy Me a Coffee + قهوة لمرة واحدة + موّل جلسة برمجة ليلية بإكرامية سريعة + Open Collective + صندوق المجتمع + دعم مالي شفاف يديره المجتمع + نشكرك من أعماق قلوبنا لإيمانك بالموسيقى المفتوحة. + الدعم اختياري تمامًا — يبقى Spotube مجانيًا في كل الأحوال. + ادعم + فتح %1$s + + + ادعم Spotube + ادعم Spotube + Spotube مجاني وحر ومفتوح المصدر — بلا إعلانات، وبلا اشتراكات، وبلا تتبع. إن كان يُسعدك، ففكّر في دعم المشروع. + يمكنك إيقاف هذه التذكيرات من الإعدادات ← الدعم. + ربما لاحقًا + تذكيرات الدعم + عرض تذكير لطيف لدعم المشروع كل 7 أيام. + + + بنية معيارية + اختر مصدر موسيقاك. + يفصل Spotube بين اكتشاف الكتالوج وبث الصوت وكلمات الأغاني المتزامنة. اضبط محركاتك أدناه أو بدّلها في أي وقت. + محرك البيانات الوصفية + يشغّل اكتشاف الموسيقى والبحث وقوائم المقاطع وقوائم التشغيل. + محرك الصوت + يجلب تدفقات الصوت التي يتم تشغيلها ويفك ترميزها. + محرك كلمات الأغاني + يعرض كلمات كاريوكي متزامنة زمنيًا في الوقت الفعلي. + محرك تسجيل الاستماع + يسجّل سجل الاستماع في Last.fm أو ListenBrainz. + نشط + مدمج + جاهز + اختياري + اختر محركًا + المحركات المثبتة + تعيين كافتراضي + NewPipe YouTube وLRCLib Lyrics مدمجان وجاهزان للبث فورًا. + أدِر إضافات المجتمع أو أضف إضافات جديدة في أي وقت من الإعدادات ← الإضافات. + + + بنية عصرية بروح كلاسيكية + صوت Hi-Fi نقي، بلا وسطاء + إليك كيف ينسّق Spotube التشغيل اللامركزي مباشرةً على جهازك. + اتصال مباشر من نظير إلى نظير • 256 kbps Opus + الساعة الزرقاء + Spotube · عرض توضيحي للمشغّل + قليل من الموسيقى للحظات العابرة. + 1. اكتشاف لامركزي + تذهب الاستعلامات مباشرةً إلى إضافة الكتالوج التي اخترتها. لا ينشئ Spotube أبدًا ملفًا تعريفيًا لذوقك. + 2. تحديد البث المباشر + يُبث الصوت مباشرةً من خوادم المحتوى إلى جهازك. بلا قفزات عبر وكلاء ولا جمع للبيانات عن بُعد. + 3. معالجة صوتية وتشغيل محليان + يتم فك الترميز والتخزين المؤقت محليًا مع عناصر تحكم في المعادل وخيار التنزيل للاستماع دون اتصال. + 4. كلمات متزامنة وجام + تتبع كلمات الكاريوكي توقيت التشغيل، مع تحكم عن بُعد عبر LAN والجام الجماعي. + لا خوادم وسيطة. يتم التشغيل على جهازك مباشرةً — الخصوصية في صميم التصميم. + + + اختر جهازًا + %1$s: «%2$s» + تشغيل + إضافة إلى قائمة الانتظار + تشغيل التالي + هذا الجهاز + على هذا الجهاز + جهاز بعيد + جهاز بعيد (%1$s) + على الجهاز المتصل + + + فشل الاتصال + انقطع الاتصال + فشل الإرسال + فشل الاكتشاف: %1$s + تعذّر الاتصال: %1$s + + + الأجهزة + تحديث + متصل بـ %1$s:%2$d + قطع الاتصال + جارٍ الاتصال... + جارٍ البحث عن أجهزة على الشبكة... + لم يتم العثور على أجهزة + تأكد من تفعيل \"السماح بالتحكم عن بُعد\" في إعدادات الجهاز الآخر. + جهاز غير معروف + جارٍ التحديد... + المعرّف: %1$s... + + + التحكم عن بُعد + غير متصل + خطأ في الاتصال: %1$s + غلاف الألبوم + مقطع غير معروف + فنان غير معروف + التقديم والترجيع + ترتيب عشوائي + السابق + التالي + وضع التكرار + مستوى الصوت + إزالة من قائمة الانتظار + خيارات إضافية + + + أين تريد التشغيل؟ + لديك جلسة جام نشطة. اختر أين تريد تشغيل هذه المجموعة. + التشغيل هنا + اقتراح للجام + + + لم يتم ضبط وسيط للجام + مضيف الوسيط فارغ + فشل الاتصال + انقطع الاتصال + + + الوسيط + مجهول + + + تمت الإضافة إلى قائمة انتظار الجام + + تمت إضافة %1$d مقطع إلى قائمة انتظار الجام + تمت إضافة مقطع واحد إلى قائمة انتظار الجام + تمت إضافة %1$d مقطعين إلى قائمة انتظار الجام + تمت إضافة %1$d مقاطع إلى قائمة انتظار الجام + تمت إضافة %1$d مقطعًا إلى قائمة انتظار الجام + تمت إضافة %1$d مقطع إلى قائمة انتظار الجام + + + + طلب تحكم عن بُعد + يريد \"%1$s\" التحكم في التشغيل على هذا الجهاز. + رفض + سماح + السماح دائمًا + + + نتائج البحث + الكل + المقاطع + المستخدمون + لا تتوفر أنواع بحث + ابدأ الكتابة للبحث + ابدأ الكتابة للبحث عن مقاطع + لم يتم العثور على نتائج + عرض الكل + جارٍ البحث + جارٍ تحميل المقاطع... + جارٍ تحميل الألبومات... + جارٍ تحميل الفنانين... + جارٍ تحميل قوائم التشغيل... + جارٍ تحميل المستخدمين... + لم يتم العثور على ألبومات + لم يتم العثور على فنانين + لم يتم العثور على قوائم تشغيل + لم يتم العثور على مستخدمين + نبحث بين صناديق الأسطوانات... + نبحث في أرجاء الكون... + نُحمّي مكبرات الصوت... + ننفض الغبار عن الأسطوانات... + نستشير آلهة الموسيقى... + نضبط الهوائيات... + نقلّب بين التسجيلات... + تعذّر تحميل إمكانات البحث + تعذّر البحث + تعذّر البحث عن المقاطع + تعذّر البحث عن الألبومات + تعذّر البحث عن الفنانين + تعذّر البحث عن قوائم التشغيل + تعذّر البحث عن المستخدمين + + + تصفّح + + + متزامنة + عادية + لا تتوفر كلمات متزامنة + لا تتوفر كلمات عادية + + + %1$s في %2$s + %1$d kbps + %1$s kHz • %2$d قناة + لم يتم تحديد إضافة بيانات وصفية + + + رجوع + إعادة المحاولة + إغلاق مربع الحوار + جارٍ التحميل... + غير معروف + خطأ غير معروف + + + مستخدم + قائمة تشغيل + + + نعتذر بشدة + هناك خلل ما في الوقت الحالي. تجربتك تهمنا، ونبذل قصارى جهدنا لإصلاح ذلك. يُرجى المحاولة مرة أخرى أو مراجعة تفاصيل الخطأ أدناه. + تفاصيل الخطأ + نسخ إلى الحافظة + عرض السجل الكامل + سجل الخطأ الكامل + + + ترتيب عشوائي + تشغيل عشوائي + متابعة + إلغاء المتابعة + تعديل قائمة التشغيل + بواسطة %1$s + + + خيارات المقطع + إضافة إلى الجام + بدء الراديو + تشغيل التالي + نقل إلى التالي + إزالة من قائمة الانتظار + إزالة من المفضلة + حفظ في المفضلة + إضافة إلى قائمة تشغيل + تنزيل + مشاركة + + + الأصلي + العنوان + الفنان + الألبوم + المدة + ترتيب + لم يتم العثور على مقاطع + تصفية + إجراءات جماعية + تنزيل الكل + تنزيل %1$d + إضافة الكل إلى قائمة الانتظار + إضافة %1$d إلى قائمة الانتظار + تشغيل الكل تاليًا + تشغيل %1$d تاليًا + إضافة الكل إلى قائمة تشغيل + إضافة %1$d إلى قائمة تشغيل + إضافة الكل إلى الجام + إضافة %1$d إلى الجام + +%1$d آخرين + + + لا يوجد URL لتحميله + الرجوع في سجل المتصفح + التقدم في سجل المتصفح + + + تصفية قائمة الحظر... + لا توجد عناصر محظورة + لا توجد عناصر محظورة تطابق \'%1$s\' + المقاطع + مقطع + إضافة إلى قائمة الحظر + إزالة من قائمة الحظر + + + لا توجد بيانات وصفية للمقطع + تعذّر تحديد URL البث + تعذّر تحديد URL البث. جرّب تشغيل المقطع أولًا. + + + المقاطع المحفوظة + أنت + + + تتابعه + أفضل المقاطع + تشغيل أفضل المقاطع + إضافة أفضل المقاطع إلى قائمة الانتظار + فنانون مشابهون + قوائم تشغيل مميزة + تعذّر تحميل نظرة عامة على الفنان + عدد المتابعين غير متاح + %1$s ألف متابع + %1$s مليون متابع + %1$s مليار متابع + + %1$d متابع + متابع واحد + %1$d متابعان + %1$d متابعين + %1$d متابعًا + %1$d متابع + + + + إعجاب + إلغاء الإعجاب + عنوان مقطع افتراضي + فنانون افتراضيون + ألبوم غير معروف + التقديم والترجيع + مستوى الصوت + تفعيل الترتيب العشوائي + إيقاف الترتيب العشوائي + السابق + التالي + تشغيل أو إيقاف مؤقت + التكرار متوقف + تكرار مقطع واحد + تكرار الكل + كتم الصوت + إلغاء كتم الصوت + خيارات إضافية + خيارات المشغّل + طي المشغّل + توسيع كلمات الأغنية + قيد التشغيل الآن + كلمات الأغنية + مصدر بديل + مصدر بديل + تنزيل + تم التنزيل + جارٍ التنزيل... + تم التنزيل مسبقًا + جارٍ إعادة محاولة التنزيل... + جارٍ تنزيل %1$s + سيتوقف التشغيل مؤقتًا بعد %1$s. + إيقاف التشغيل مؤقتًا بعد عدد الدقائق المحدد. + الدقائق + اختر بين 1 و600 دقيقة + إلغاء المؤقت + بدء + تم تحديد URL التشغيل بإعدادات بث مختلفة. يطابق البث المميز تفضيلك الحالي؛ وسيتم تحديث URL النشط عند طلب البث التالي. + مصدر الصوت المطابق + جارٍ تحميل المصدر المطابق… + لا حاجة إلى مصدر بعيد مطابق أثناء تشغيل الملف المخزن مؤقتًا محليًا. + لا يتوفر مصدر مطابق. + لا يوجد مصدر صوت مطابق مخزن مؤقتًا لهذا المقطع. + لم يتم تخزين تفاصيل البث مؤقتًا بعد. شغّل المقطع أولًا لتحميلها. + تعذّر تحميل تفاصيل المقطع. + التدفقات المتاحة + يتم تقديم هذا المقطع من ذاكرة التخزين المؤقت المحلية. لا تُستخدم متغيرات البث لهذا التشغيل. + جارٍ تحميل التدفقات… + لا تتوفر تدفقات لهذا المصدر. + البث المحدد حاليًا + يطابق تفضيلات صيغة البث وجودته + URL التشغيل النشط + مُقدَّم من ذاكرة التخزين المؤقت المحلية + مقطع البيانات الوصفية + العنوان + المعرّف + الألبوم + درجة الثقة + URI خارجي + معرّف الألبوم + نوع الألبوم + تاريخ إصدار الألبوم + عدد مقاطع الألبوم + أنواع الألبوم الموسيقية + المدة + رقم المقطع + رقم القرص + محتوى صريح + الشعبية + ISRC + عناوين URL للصور المصغرة + + + إظهار/إخفاء الشريط الجانبي + + + مصادر بديلة + لا يوجد مقطع قيد التشغيل حاليًا + جارٍ البحث عن مصادر بديلة... + لم يتم العثور على مصادر بديلة + درجة الثقة: %1$d% + المصدر النشط + + + طرد + إزالته من الجلسة + طرده ومنعه من الانضمام مجددًا + إزالة الاقتراحات + إزالة كل المقاطع التي أضافها إلى قائمة الانتظار + إعادة الترتيب + إزالة من قائمة الانتظار + %1$s • أضافه %2$s + + + غير معروف + جارٍ تحميل قائمة التشغيل... + إضافة مقاطع + إضافة مقاطع إلى قائمة التشغيل + ابحث عن مقاطع... + بحث + لم يتم العثور على مقاطع + ابحث عن مقاطع لإضافتها + تم + إضافة + تمت الإضافة + + + جارٍ تحميل الألبوم... + + + إضافة إلى قائمة تشغيل خاصة بك + بحث + ابحث في قوائم التشغيل الخاصة بك... + جارٍ تحميل قوائم التشغيل الخاصة بك... + لا تملك أي قوائم تشغيل بعد.\nأنشئ واحدة لإضافة المقاطع. + إنشاء قائمة تشغيل + تعديل قائمة التشغيل + إنشاء + الاسم + اسم قائمة التشغيل + الوصف + الوصف (اختياري) + عامة + يمكن للجميع رؤية قائمة التشغيل هذه + تعاونية + يمكن للآخرين إضافة مقاطع إلى قائمة التشغيل هذه + صورة الغلاف + غلاف قائمة التشغيل + لا توجد صورة + تغيير الصورة + اختيار صورة + + + تثبيت + استبدال + لم يتم العثور على plugin.json في ملف zip + لم يتم العثور على manifest.zipline.json في ملف zip + تنسيق plugin.json غير صالح: %1$s + تعذّرت قراءة الإضافة: %1$s + تعذّر تثبيت الإضافة: %1$s + يتوفر إصدار أحدث من %1$s. أكّد التحديث قبل التثبيت. + %1$s مثبتة بالفعل. أكّد استبدالها قبل التثبيت. + الإضافات المدمجة مضمّنة بالفعل ولا يمكن إضافتها مرة أخرى. + لا يمكن إزالة الإضافات المدمجة. + تعلن %1$s عن قيمة apiVersion غير صالحة (%2$s). المتوقع إصدار دلالي متوافق مع %3$s. + إصدار API الإضافات في Spotube %1$s غير صالح. + تستهدف %1$s إصدار API الإضافات %2$s، لكن Spotube يدعم إصدار API الإضافات الرئيسي %3$s. ثبّت إضافة مبنية لنفس الإصدار الرئيسي من API. + + + API الإضافة غير متوافق + تثبيت الإضافة؟ + ستتم إضافة %1$s إلى إضافاتك المثبتة. + تحديث الإضافة؟ + إصدار أقدم مثبت (%1$s). هل تريد التحديث إلى %2$s؟ + استبدال الإضافة المثبتة؟ + تستخدم %1$s إصدارًا دلاليًا غير صالح (%2$s) ولا يمكن مقارنتها كتحديث. هل تريد استبدال الإضافة المثبتة على أي حال؟ + لا يمكن مقارنة الإصدار المثبت (%1$s) باستخدام الإصدار الدلالي. هل تريد استبداله بـ %2$s؟ + الإصدار %1$s مثبت بالفعل. هل تريد استبدال الإضافة الحالية بالنسخة المقدَّمة؟ + الإصدار المثبت %1$s أحدث من الإصدار المقدَّم %2$s. هل تريد استبداله على أي حال؟ + هل تريد استبدال الإضافة المثبتة بالنسخة المقدَّمة؟ + + + %1$s: + الإصدار + إصدار API + المؤلف + الترخيص + الإمكانات + القدرات + المستودع + التواصل + الإبلاغ عن الأخطاء + ادعم %1$s + + + رسمية + موثّقة + نجوم GitHub + URL مستودع GitHub + + + لم يتم العثور على إصدارات + اختيار الإصدار + اختر إصدارًا + إصدار تجريبي + ملاحظات الإصدار + + + الرئيسية + بحث + على الجهاز + الاتصال + الأجهزة + + + إخفاء النافذة + إظهار النافذة + المقطع التالي + المقطع السابق + ترتيب عشوائي + تكرار + التكرار: متوقف + التكرار: مقطع واحد + التكرار: الكل + مستوى الصوت: %1$d% + رفع الصوت + + خفض الصوت - + كتم الصوت + إلغاء كتم الصوت + الإعجاب بالمقطع + إلغاء الإعجاب بالمقطع + خروج + + + تصغير + تكبير + استعادة + إغلاق + + + خدمة التشغيل + التشغيل + عناصر التحكم في تشغيل الوسائط + + + تصفّح + قيد التشغيل الآن + مميز + المقاطع المحفوظة + أفضل المقاطع + diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings.xml b/composeApp/src/commonMain/composeResources/values-bn/strings.xml new file mode 100644 index 00000000..03b55163 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-bn/strings.xml @@ -0,0 +1,938 @@ + + + + কিউ + কিউ খালি করুন + কিউ ফিল্টার করুন... + কিউতে কিছু নেই + + গান, শিল্পী, অ্যালবাম খুঁজুন... + সব হিস্ট্রি মুছুন + খুঁজুন... + + + আপনার লাইব্রেরি + লাইব্রেরি + প্লেলিস্ট + অ্যালবাম + শিল্পী + লোকাল ট্র্যাক + ডাউনলোড + সেভ করা প্লেলিস্ট খুঁজুন… + সেভ করা অ্যালবাম খুঁজুন… + সেভ করা শিল্পী খুঁজুন… + ফোল্ডার বা ট্র্যাক খুঁজুন… + লাইব্রেরি ফিল্টার করুন + ফিল্টার বন্ধ করুন + প্লেলিস্ট তৈরি করুন + লোকাল ট্র্যাক রিফ্রেশ করুন + গ্রিড ভিউতে যান + লিস্ট ভিউতে যান + কোনো সেভ করা প্লেলিস্ট নেই + “%1$s”-এর সাথে মেলে এমন কোনো প্লেলিস্ট নেই + কোনো সেভ করা অ্যালবাম নেই + “%1$s”-এর সাথে মেলে এমন কোনো অ্যালবাম নেই + কোনো সেভ করা শিল্পী নেই + “%1$s”-এর সাথে মেলে এমন কোনো শিল্পী নেই + + + ফোল্ডার + ফোল্ডার + %1$s • %2$s + + %1$dটি ট্র্যাক + %1$dটি ট্র্যাক + + + %1$dটি ফোল্ডার + %1$dটি ফোল্ডার + + ফোল্ডারে ফিরে যান + ফোল্ডার প্লে করুন + অডিও অ্যাক্সেস প্রয়োজন + আপনার ডিভাইসের গান দেখতে অডিও অ্যাক্সেসের অনুমতি দিন। + অনুমতি দিন + কোনো লোকাল ফোল্ডার পাওয়া যায়নি + এই ফোল্ডারে কোনো ট্র্যাক নেই + মেলে এমন কোনো ফোল্ডার বা ট্র্যাক নেই + লোকাল মিডিয়া রিফ্রেশ করা যায়নি + + + %1$dটি চলমান • %2$dটি সম্পন্ন + সম্পন্নগুলো মুছুন + এখনো কোনো ডাউনলোড নেই + ডাউনলোড হচ্ছে %1$d% + অপেক্ষমাণ + সম্পন্ন + বাতিল + ব্যর্থ + ব্যর্থ: %1$s + ডাউনলোড বাতিল করুন + আবার ডাউনলোড করুন + ডাউনলোড সরান + + + শিল্পী + ব্ল্যাকলিস্টেড + পছন্দের ট্র্যাক + আপনার সেভ করা গান + অজানা শিল্পী + ট্র্যাকের নাম + শিল্পীর নাম + + নির্বাচিত + + সেটিংস + + প্লাগইন + ভাষা ও অঞ্চল + অ্যাপিয়ারেন্স + প্লেব্যাক + ডাউনলোড + ক্যাশিং + ডেস্কটপ + আপডেট + সাপোর্ট + সম্পর্কে + + বন্ধ করুন + সেভ করুন + বাতিল + + ভাষা + অ্যাপ ইন্টারফেসের ভাষা। বর্তমান: %1$s + সিস্টেম ডিফল্ট + দেশ + কনটেন্ট ও প্রাপ্যতার জন্য দেশ। বর্তমান: %1$s + %1$s (%2$s) + + থিম + Spotube আপনার সিস্টেমের অ্যাপিয়ারেন্সের সাথে কীভাবে মিলবে তা বেছে নিন। বর্তমান: %1$s + লাইট + ডার্ক + সিস্টেম + + সারফেস + অ্যাপের ব্যাকগ্রাউন্ড ম্যাটেরিয়াল। বর্তমান: %1$s + জলীয় + বরফি + ক্রোম + অ্যাক্রিলিক + কাচ + + অ্যাকসেন্ট রং + বর্তমান প্যালেট: %1$s + অ্যাকসেন্ট রং + একটি অ্যাকসেন্ট প্যালেট বেছে নিন এবং লাইট ও ডার্ক দুই থিমেই প্রিভিউ দেখুন। + লাইট + ডার্ক + প্রিভিউ + বাটন + গ্রিন গবলিন + ইলেকট্রিক ভায়োলেট + ওশেনিক সায়ান + সানসেট অরেঞ্জ + রোজ গার্ডেন + মিডনাইট ব্লু + মেটালিক স্লেট + + লুক ও ফিল + সারফেস ম্যাটেরিয়াল ও অ্যাকসেন্ট। বর্তমান: %1$s · %2$s + লুক ও ফিল + একটি সারফেস ম্যাটেরিয়াল বেছে নিন, তারপর তার সাথে মানানসই একটি অ্যাকসেন্ট। প্রতিটি অপশনই বাছাই করা কম্বিনেশন। + সারফেস + অ্যাকসেন্ট + সেকেন্ডারি + + প্যালেট + Millennium অ্যাকসেন্ট প্যালেট। বর্তমান: %1$s + প্যালেট + একটি Millennium অ্যাকসেন্ট প্যালেট বেছে নিন এবং লাইট ও ডার্কে প্রিভিউ দেখুন। + প্যালেট + Millennium প্যালেট ও অ্যাকসেন্ট। বর্তমান: %1$s + নীল + কমলা + লাইম + + ডাউনলোড ফোল্ডার পরিবর্তন + বর্তমান ফোল্ডার: %1$s + বর্তমান ফোল্ডার: ডিফল্ট মিউজিক ফোল্ডার + ডিফল্ট মিউজিক ফোল্ডার ব্যবহার করতে এটি ফাঁকা রাখুন। + C:\Music\Spotube + লোকাল মিডিয়া ফোল্ডার + ডেস্কটপে স্ক্যান করার জন্য কাস্টম ফোল্ডার। যোগ করা হয়েছে: %1$d + এখনো কোনো কাস্টম ফোল্ডার যোগ করা হয়নি + আপনার লোকাল মিউজিক লাইব্রেরির জন্য কাস্টম ফোল্ডার। + ম্যানেজ করুন + ফোল্ডার যোগ করুন + ফোল্ডার সরান + কোনো কাস্টম ফোল্ডার যোগ করা হয়নি। + স্ক্যান তালিকা থেকে %1$s সরান + ডাউনলোড ও মিউজিক ক্যাশ ফোল্ডার স্বয়ংক্রিয়ভাবে অন্তর্ভুক্ত হয়। + আপনার ডাউনলোড ফোল্ডার স্বয়ংক্রিয়ভাবে অন্তর্ভুক্ত হয়। + মিউজিক ক্যাশ ফোল্ডার স্বয়ংক্রিয়ভাবে অন্তর্ভুক্ত হয়। + সেভ হচ্ছে… + ফোল্ডার সেভ করা যায়নি। আপনার পরিবর্তনগুলো রয়ে গেছে; আবার চেষ্টা করুন। + + %1$dটি ফোল্ডার + %1$dটি ফোল্ডার + + ডাউনলোডের মিউজিক ফরম্যাট + ডাউনলোডের জন্য পছন্দের কোডেক ও কন্টেইনার। বর্তমান: %1$s + ডাউনলোডের মিউজিক কোয়ালিটি + + স্ট্রিমিংয়ের মিউজিক ফরম্যাট + মিউজিক স্ট্রিমিংয়ের জন্য পছন্দের কোডেক ও কন্টেইনার। বর্তমান: %1$s + স্ট্রিমিংয়ের মিউজিক কোয়ালিটি + বর্তমান: %1$s + মিউজিক ক্যাশিং চালু করুন + বারবার প্লে দ্রুত করতে স্ট্রিম করা অডিও লোকালি সংরক্ষণ করুন। + ক্যাশ ফোল্ডার + বর্তমান: %1$s + বর্তমান: ডিফল্ট অ্যাপ্লিকেশন ক্যাশ + ক্যাশ সাইজের সীমা + কোনো সীমা নেই + বর্তমান সীমা: %1$d MB + MB-তে সর্বোচ্চ ক্যাশ সাইজ দিন। সীমাহীন রাখতে 0 দিন বা ফাঁকা রাখুন। + 500 + সাইজ 0 বা একটি ধনাত্মক সংখ্যা হতে হবে। + অবিরাম প্লেব্যাক চালু করুন + কিউ শেষ হলে সাজেস্ট করা ট্র্যাক দিয়ে প্লেব্যাক চালু রাখুন। + Connect চালু করুন + Spotube Connect ফিচারের মাধ্যমে রিমোট প্লেব্যাক কন্ট্রোল উপলব্ধ করুন। + প্লেব্যাক প্রক্সি সার্ভার পোর্ট + রিমোট কন্ট্রোলের অনুমতি দিন + একই নেটওয়ার্কের অন্য ডিভাইস কানেক্ট হলে প্লেব্যাক নিয়ন্ত্রণ করতে দিন। + রিমোট কন্ট্রোল ডিভাইসের নাম + অন্য ডিভাইস এই ডিভাইসটি খুঁজে পেলে যে নাম দেখবে। বর্তমান: %1$s + আপনার নেটওয়ার্কের অন্য ডিভাইস এই Spotube খুঁজে পেলে দেখানোর জন্য একটি সহজ নাম বেছে নিন। + যেমন: বসার ঘরের Spotube + ডিভাইসের হোস্টনেম + প্লেব্যাক প্রক্সি সার্ভারের ব্যবহৃত পোর্ট। বর্তমান: %1$d + 1 থেকে 65535-এর মধ্যে একটি পোর্ট বেছে নিন। + 14769 + একটি সঠিক পূর্ণসংখ্যা দিন। + পোর্ট 1 থেকে 65535-এর মধ্যে হতে হবে। + + ব্ল্যাকলিস্ট + ব্ল্যাকলিস্ট করা ট্র্যাক ও শিল্পী ম্যানেজ করুন + + ট্রেতে মিনিমাইজ করুন + উইন্ডো মিনিমাইজ করলে Spotube সিস্টেম ট্রেতে চালু রাখুন। + Discord রিচ প্রেজেন্স + ডেস্কটপে Discord-এ আপনার বর্তমান প্লেব্যাক স্ট্যাটাস দেখান। + + স্বয়ংক্রিয়ভাবে আপডেট চেক করুন + ব্যাকগ্রাউন্ডে Spotube-এর নতুন রিলিজ খুঁজুন। + + প্লাগইন ম্যানেজ করুন + আপনার প্লাগইনগুলো ম্যানেজ করুন + ডিফল্ট %1$s প্লাগইন + মেটাডেটা + অডিও + লিরিক্স + স্ক্রবল + কোনো প্লাগইন নির্বাচিত নয় + পরিবর্তন করুন + নির্বাচন করুন + নির্বাচিত + নির্বাচন মুছুন + কোনো প্লাগইন উপলব্ধ নেই + %1$s প্লাগইন + মুছুন + + কনফিগার করুন + প্লাগইন ম্যানেজার + অনুগ্রহ করে একটি URL দিন + URL অবশ্যই http:// বা https:// দিয়ে শুরু হতে হবে + প্লাগইন ডাউনলোড করা যায়নি + এখনো কোনো প্লাগইন ইনস্টল করা হয়নি + একটি ডাউনলোড URL পেস্ট করুন বা ওপর থেকে একটি ফাইল বেছে নিন। + + %1$dটি প্লাগইন ইনস্টল করা আছে + %1$dটি প্লাগইন ইনস্টল করা আছে + + প্লাগইন ইনস্টল করুন + https://example.com/plugin.smplug + ডাউনলোড + ফাইল থেকে ইনস্টল করুন (.smplug) + সক্রিয় + v%1$s + বিল্ট-ইন + প্লাগইন সরান + লগইন + লগআউট + URL থেকে ডাউনলোড করুন + ফাইল থেকে ইনস্টল করুন + ইনস্টল করুন + + • %1$s + অনুরোধ করা পারমিশন + এই প্লাগইনের কোনো বিশেষ পারমিশন প্রয়োজন নেই। + ইনস্টল করা বনাম প্রদত্ত + ইনস্টল করা + প্রদত্ত + API %1$s → %2$s + স্থায়ী স্টোরেজ + ডিস্কে ডেটা পড়া ও লেখা + নেটওয়ার্ক রিকোয়েস্ট + ইন্টারনেটে ডেটা পাঠানো ও গ্রহণ করা + WebView + অ্যাপের ভেতরে ওয়েব কনটেন্ট দেখানো + গ্রুপ জ্যাম + MQTT ব্রোকার + ব্রোকার হোস্ট + %1$s:%2$d + ব্রোকার পোর্ট + TLS ব্যবহার করুন + ইউজারনেম (ঐচ্ছিক) + পাসওয়ার্ড (ঐচ্ছিক) + ক্লায়েন্ট ID প্রিফিক্স + এই Spotube শনাক্ত করতে ব্যবহৃত হয় + ব্রোকার কানেকশন এনক্রিপ্ট করুন + রুম শুরু করার আগে বর্তমান ব্রোকার সেটিংস যাচাই করুন। + কানেকশন পরীক্ষা করুন + পরীক্ষা চলছে… + %1$d ms-এ কানেক্ট হয়েছে + ব্যর্থ: %1$s + প্লেসহোল্ডার ব্রোকার — সেলফ-হোস্ট করতে নিজেরটি কনফিগার করুন + কানেকশন + ক্রেডেনশিয়াল + পরিচিতি + ডায়াগনস্টিকস + + + গ্রুপ জ্যাম + তৈরি করুন + যোগ দিন + একটি রুম তৈরি করুন, তারপর এর কোড বন্ধুদের সাথে শেয়ার করুন। + রুম তৈরি করুন + রুমে যোগ দিন + কানেক্ট হচ্ছে… + রুম কোড + ABC123 + ব্রোকার: %1$s + রুম হোস্ট করতে বা যোগ দিতে সেটিংসে একটি ব্রোকার সেট আপ করুন। + ব্রোকার সেটিংস + কানেক্টেড + আবার কানেক্ট হচ্ছে… + কোড কপি করুন + কপি হয়েছে! + শেয়ার করুন + অংশগ্রহণকারী (%1$d) + হোস্ট + শ্রোতা + শ্রোতাদের জন্য অপেক্ষা করা হচ্ছে… + সরান + ব্যান করুন + ম্যানেজ করুন + রুম ছেড়ে দিন + কানেক্ট করা যায়নি + বাদ দিন + জ্যাম সেশন তৈরি করা যায়নি + জ্যাম সেশনে যোগ দেওয়া যায়নি + অক্ষর ও সংখ্যা দিয়ে ছয় অক্ষরের একটি রুম কোড দিন। + এই কোড দিয়ে আমার Spotube জ্যামে যোগ দিন: %1$s + + অ্যালবামে যান + স্লিপ টাইমার + ট্র্যাকের বিস্তারিত + প্লে + পজ + কিছুই বাজছে না + কিউতে যোগ করুন + + উপেক্ষা করুন + আপডেট করুন + Spotube %1$s উপলব্ধ + এই রিলিজে নতুন কী আছে তা Spotube ডাউনলোড পেজে দেখুন। + %1$s %2$s উপলব্ধ + বর্তমানে ইনস্টল করা: %1$s + এই আপডেটে নতুন কী আছে তা প্লাগইনের পেজে দেখুন। + + এড়িয়ে যান + পরবর্তী + পেছনে + শুরু করুন + Spotube + মিউজিক, কোনো বাঁধন ছাড়াই। + মিউজিকের জগতের জন্য একটি ওপেন-সোর্স প্লেয়ার।\nআপনার মতো করে ঘুরে দেখুন। নিজের মতো করে সাজিয়ে নিন। + এই প্রজেক্টটি বাঁচিয়ে রাখুন। + Spotube মানুষের তৈরি, কোনো সাবস্ক্রিপশন ব্যবসার নয়। এটি যদি আপনার প্রতিদিনের জীবনের অংশ হয়ে ওঠে, তাহলে একটু সাপোর্ট আমাদের এর প্রাপ্য যত্ন নিতে সাহায্য করে।\n\nস্বাধীন সফটওয়্যারকে জায়গা দেওয়ার জন্য ধন্যবাদ। + গান ও প্লেলিস্ট খুঁজতে একটি ক্যাটালগ প্লাগইন দিয়ে শুরু করুন। সেগুলো বাজাতে একটি অডিও প্লাগইন বেছে নিন। লিরিক্স ও শোনার হিস্ট্রি ঐচ্ছিক। নিচে একটি প্লাগইন খুঁজুন, এর পারমিশন দেখে নিন, ইনস্টল করুন, তারপর এখানে নির্বাচন করুন। + প্লাগইন খুঁজুন + আপনার সোর্স + আবার চেষ্টা করুন + আরও প্লাগইন + নতুন কোনো প্লাগইন পাওয়া যায়নি। ওপরের লিংক বা .smplug ফাইল থেকেও ইনস্টল করতে পারেন। + এই মুহূর্তে প্লাগইন খোঁজা যাচ্ছে না। আবার চেষ্টা করুন, অথবা লিংক বা ফাইল থেকে ইনস্টল করুন। + স্বাগতম + সাপোর্ট + প্লাগইন + গাইড + + Spotube যেভাবে ব্যবহার করবেন + চারটি সহজ ধাপ, তারপরই গান শুনুন। + সাইন ইন + আপনার Spotify অ্যাকাউন্ট কানেক্ট করুন, অথবা নিজের পছন্দের ক্যাটালগ প্লাগইন বেছে নিন। + খুঁজুন ও প্লে করুন + যেকোনো গান, অ্যালবাম বা শিল্পী খুঁজুন, তারপর প্লে চাপুন। + অফলাইনে শুনুন + কানেকশন ছাড়াই শুনতে ট্র্যাক ডাউনলোড করুন। + লিরিক্সের সাথে গান + ট্র্যাক বাজার সময় প্লেয়ার থেকে সিঙ্ক করা লিরিক্স খুলুন। + + + আপনার মিউজিক। আপনার সোর্স। আপনার নিয়ম। + একটি ফ্রি, ওপেন-সোর্স মিউজিক প্লেয়ার, যা তৈরি করেছে মিউজিক ভালোবাসা একটি কমিউনিটি। + ওপেন সোর্স + বিজ্ঞাপন নেই + টেলিমেট্রি নেই + প্লাগইন চালিত + চলুন শুরু করি + + + সমর্থকরাই আমাদের এগিয়ে নেন + ভালোবাসায় তৈরি। আপনার কারণেই টিকে আছে। + Spotube ১০০% ফ্রি, লিব্রে ও ওপেন সোর্স — কোনো বিজ্ঞাপন, সাবস্ক্রিপশন বা ট্র্যাকিং নেই। প্রতিটি ফিচার আছে কারণ আপনার মতো মানুষ কোড চালু রাখতে সাহায্য করেছেন। + Patreon + মাসিক স্পনসর + নিয়মিত ডেভেলপমেন্ট ও ইনফ্রাস্ট্রাকচারে সাপোর্ট করুন + Buy Me a Coffee + এককালীন কফি + একটি ছোট্ট টিপ দিয়ে রাত জাগা কোডিং সেশনে শক্তি জোগান + Open Collective + কমিউনিটি ফান্ড + স্বচ্ছ, কমিউনিটি-পরিচালিত আর্থিক সহায়তা + ওপেন মিউজিকে বিশ্বাস রাখার জন্য অন্তরের অন্তস্তল থেকে ধন্যবাদ। + সাপোর্ট করা সম্পূর্ণ ঐচ্ছিক — যেকোনো অবস্থাতেই Spotube ফ্রি থাকবে। + স্পনসর করুন + %1$s খুলুন + + + Spotube-কে সাপোর্ট করুন + Spotube-কে সাপোর্ট করুন + Spotube ফ্রি, লিব্রে ও ওপেন সোর্স — কোনো বিজ্ঞাপন, সাবস্ক্রিপশন বা ট্র্যাকিং নেই। এটি যদি আপনাকে আনন্দ দেয়, তাহলে প্রজেক্টটিকে সাপোর্ট করার কথা ভাবুন। + এই রিমাইন্ডারগুলো সেটিংস → সাপোর্ট থেকে বন্ধ করতে পারেন। + পরে দেখব + সাপোর্ট রিমাইন্ডার + প্রতি ৭ দিনে প্রজেক্টটিকে সাপোর্ট করার একটি ছোট রিমাইন্ডার দেখান। + + + মডুলার আর্কিটেকচার + আপনার মিউজিক কোথা থেকে আসবে তা বেছে নিন। + Spotube ক্যাটালগ খোঁজা, অডিও স্ট্রিমিং ও সিঙ্ক করা লিরিক্স আলাদা রাখে। নিচে আপনার ইঞ্জিনগুলো কনফিগার করুন বা যেকোনো সময় বদলে নিন। + মেটাডেটা ইঞ্জিন + মিউজিক খোঁজা, সার্চ, ট্র্যাক তালিকা ও প্লেলিস্ট চালায়। + অডিও ইঞ্জিন + যে অডিও স্ট্রিম বাজে তা আনে ও ডিকোড করে। + লিরিক্স ইঞ্জিন + রিয়েল টাইমে সময়-সিঙ্ক করা কারাওকে লিরিক্স দেখায়। + স্ক্রবল ইঞ্জিন + Last.fm বা ListenBrainz-এ শোনার হিস্ট্রি রেকর্ড করে। + সক্রিয় + বিল্ট-ইন + প্রস্তুত + ঐচ্ছিক + একটি ইঞ্জিন বেছে নিন + ইনস্টল করা ইঞ্জিন + ডিফল্ট হিসেবে সেট করুন + NewPipe YouTube ও LRCLib Lyrics বিল্ট-ইন এবং শুরু থেকেই স্ট্রিম করার জন্য প্রস্তুত। + সেটিংস → প্লাগইন থেকে যেকোনো সময় কমিউনিটি প্লাগইন ম্যানেজ বা যোগ করুন। + + + রেট্রো-মডার্ন আর্কিটেকচার + খাঁটি Hi-Fi, কোনো মধ্যস্থতাকারী নেই + Spotube কীভাবে সরাসরি আপনার মেশিনে বিকেন্দ্রীভূত প্লেব্যাক পরিচালনা করে, দেখুন। + সরাসরি পিয়ার-টু-পিয়ার • 256 KBPS OPUS + নীল প্রহর + Spotube · প্লেয়ার ডেমো + মাঝের মুহূর্তগুলোর জন্য একটু মিউজিক। + ১. বিকেন্দ্রীভূত খোঁজ + সার্চ সরাসরি আপনার বেছে নেওয়া ক্যাটালগ প্লাগইনে যায়। Spotube কখনো আপনার রুচির প্রোফাইল তৈরি করে না। + ২. সরাসরি স্ট্রিম রেজোলিউশন + অডিও কনটেন্ট সার্ভার থেকে সরাসরি আপনার মেশিনে স্ট্রিম হয়। কোনো প্রক্সি বা রিমোট ডেটা সংগ্রহ নেই। + ৩. লোকাল DSP ও প্লেব্যাক + ইকুয়ালাইজার কন্ট্রোল ও ঐচ্ছিক অফলাইন ডাউনলোডসহ লোকালি ডিকোড ও ক্যাশ করা হয়। + ৪. সিঙ্ক করা লিরিক্স ও জ্যাম + কারাওকে লিরিক্স প্লেব্যাকের সময়ের সাথে মিলে চলে, সাথে আছে LAN রিমোট কন্ট্রোল ও গ্রুপ জ্যাম। + কোনো মধ্যস্থতাকারী সার্ভার নেই। প্লেব্যাক হয় সরাসরি আপনার ডিভাইসে — ডিজাইনেই প্রাইভেট। + + + একটি ডিভাইস বেছে নিন + %1$s: “%2$s” + প্লে + কিউতে যোগ + পরবর্তীতে প্লে + এই ডিভাইস + এই ডিভাইসে + রিমোট ডিভাইস + রিমোট ডিভাইস (%1$s) + সংযুক্ত ডিভাইসে + + + কানেকশন ব্যর্থ হয়েছে + কানেকশন বিচ্ছিন্ন হয়েছে + পাঠানো যায়নি + খোঁজা ব্যর্থ হয়েছে: %1$s + কানেক্ট করা যায়নি: %1$s + + + ডিভাইস + রিফ্রেশ + %1$s:%2$d-এ কানেক্টেড + ডিসকানেক্ট + কানেক্ট হচ্ছে... + নেটওয়ার্কে ডিভাইস খোঁজা হচ্ছে... + কোনো ডিভাইস পাওয়া যায়নি + অন্য ডিভাইসের সেটিংসে \"রিমোট কন্ট্রোলের অনুমতি দিন\" চালু আছে কিনা নিশ্চিত করুন। + অজানা ডিভাইস + খোঁজা হচ্ছে... + ID: %1$s... + + + রিমোট কন্ট্রোল + ডিসকানেক্টেড + কানেকশন ত্রুটি: %1$s + অ্যালবাম কভার + অজানা ট্র্যাক + অজানা শিল্পী + সিক + শাফল + আগের + পরের + লুপ মোড + ভলিউম + কিউ থেকে সরান + আরও অপশন + + + কোথায় প্লে করবেন? + আপনার একটি জ্যাম সেশন চালু আছে। এই কালেকশন কোথায় প্লে করবেন বেছে নিন। + এখানে প্লে করুন + জ্যামে সাজেস্ট করুন + + + কোনো জ্যাম ব্রোকার কনফিগার করা নেই + ব্রোকার হোস্ট ফাঁকা + কানেকশন ব্যর্থ হয়েছে + কানেকশন বিচ্ছিন্ন হয়েছে + + + ব্রোকার + anonymous + + + জ্যাম কিউতে যোগ করা হয়েছে + + জ্যাম কিউতে %1$dটি যোগ করা হয়েছে + জ্যাম কিউতে %1$dটি যোগ করা হয়েছে + + + + রিমোট কন্ট্রোলের অনুরোধ + \"%1$s\" এই ডিভাইসের প্লেব্যাক নিয়ন্ত্রণ করতে চায়। + প্রত্যাখ্যান + অনুমতি দিন + সবসময় অনুমতি দিন + + + সার্চের ফলাফল + সব + ট্র্যাক + ইউজার + কোনো সার্চ টাইপ উপলব্ধ নেই + খুঁজতে টাইপ করা শুরু করুন + ট্র্যাক খুঁজতে টাইপ করা শুরু করুন + কোনো ফলাফল পাওয়া যায়নি + সব দেখুন + খোঁজা হচ্ছে + ট্র্যাক লোড হচ্ছে... + অ্যালবাম লোড হচ্ছে... + শিল্পী লোড হচ্ছে... + প্লেলিস্ট লোড হচ্ছে... + ইউজার লোড হচ্ছে... + কোনো অ্যালবাম পাওয়া যায়নি + কোনো শিল্পী পাওয়া যায়নি + কোনো প্লেলিস্ট পাওয়া যায়নি + কোনো ইউজার পাওয়া যায়নি + রেকর্ডের বাক্স ঘাঁটা হচ্ছে... + মহাবিশ্বে খোঁজা হচ্ছে... + স্পিকার গরম করা হচ্ছে... + ভাইনিলের ধুলো ঝাড়া হচ্ছে... + সংগীতের দেবতাদের জিজ্ঞেস করা হচ্ছে... + অ্যান্টেনা টিউন করা হচ্ছে... + রেকর্ড উল্টে দেখা হচ্ছে... + সার্চের সক্ষমতা লোড করা যায়নি + সার্চ করা যায়নি + ট্র্যাক খোঁজা যায়নি + অ্যালবাম খোঁজা যায়নি + শিল্পী খোঁজা যায়নি + প্লেলিস্ট খোঁজা যায়নি + ইউজার খোঁজা যায়নি + + + ব্রাউজ + + + সিঙ্কড + সাধারণ + কোনো সিঙ্ক করা লিরিক্স নেই + কোনো সাধারণ লিরিক্স নেই + + + %2$s-এ %1$s + %1$d kbps + %1$s kHz • %2$d ch + কোনো মেটাডেটা প্লাগইন নির্বাচিত নয় + + + পেছনে + আবার চেষ্টা করুন + ডায়ালগ বন্ধ করুন + লোড হচ্ছে... + অজানা + অজানা ত্রুটি + + + ইউজার + প্লেলিস্ট + + + আমরা সত্যিই দুঃখিত + এই মুহূর্তে কিছু একটা ঠিকমতো কাজ করছে না। আপনার অভিজ্ঞতা আমাদের কাছে গুরুত্বপূর্ণ, এবং আমরা এটি ঠিক করার সর্বোচ্চ চেষ্টা করছি। অনুগ্রহ করে আবার চেষ্টা করুন অথবা নিচে ত্রুটির বিস্তারিত দেখুন। + ত্রুটির বিস্তারিত + ক্লিপবোর্ডে কপি করুন + সম্পূর্ণ লগ দেখুন + সম্পূর্ণ ত্রুটি লগ + + + শাফল + শাফল প্লে + ফলো করুন + আনফলো করুন + প্লেলিস্ট এডিট করুন + %1$s-এর + + + ট্র্যাক অপশন + জ্যামে যোগ করুন + রেডিও শুরু করুন + পরবর্তীতে প্লে করুন + পরবর্তীতে সরান + কিউ থেকে সরান + পছন্দের তালিকা থেকে সরান + পছন্দের তালিকায় সেভ করুন + প্লেলিস্টে যোগ করুন + ডাউনলোড + শেয়ার করুন + + + মূল ক্রম + নাম + শিল্পী + অ্যালবাম + দৈর্ঘ্য + সাজান + কোনো ট্র্যাক পাওয়া যায়নি + ফিল্টার + একসাথে অ্যাকশন + সব ডাউনলোড করুন + %1$dটি ডাউনলোড করুন + সব কিউতে যোগ করুন + %1$dটি কিউতে যোগ করুন + সব পরবর্তীতে প্লে করুন + %1$dটি পরবর্তীতে প্লে করুন + সব প্লেলিস্টে যোগ করুন + %1$dটি প্লেলিস্টে যোগ করুন + সব জ্যামে যোগ করুন + %1$dটি জ্যামে যোগ করুন + +আরও %1$d জন + + + লোড করার মতো কোনো URL নেই + ব্রাউজার হিস্ট্রিতে পেছনে যান + ব্রাউজার হিস্ট্রিতে সামনে যান + + + ব্ল্যাকলিস্ট ফিল্টার করুন... + ব্ল্যাকলিস্টে কিছু নেই + \'%1$s\'-এর সাথে মেলে এমন কিছু ব্ল্যাকলিস্টে নেই + ট্র্যাক + ট্র্যাক + ব্ল্যাকলিস্টে যোগ করুন + ব্ল্যাকলিস্ট থেকে সরান + + + ট্র্যাকের কোনো মেটাডেটা নেই + স্ট্রিম URL পাওয়া যায়নি + স্ট্রিম URL পাওয়া যায়নি। প্রথমে ট্র্যাকটি প্লে করে দেখুন। + + + সেভ করা ট্র্যাক + আপনি + + + ফলো করছেন + জনপ্রিয় ট্র্যাক + জনপ্রিয় ট্র্যাক প্লে করুন + জনপ্রিয় ট্র্যাক কিউতে যোগ করুন + সম্পর্কিত শিল্পী + ফিচার্ড প্লেলিস্ট + শিল্পীর ওভারভিউ লোড করা যায়নি + ফলোয়ার সংখ্যা পাওয়া যাচ্ছে না + %1$sK ফলোয়ার + %1$sM ফলোয়ার + %1$sB ফলোয়ার + + %1$d জন ফলোয়ার + %1$d জন ফলোয়ার + + + + লাইক + আনলাইক + No-op ট্র্যাকের নাম + No-op শিল্পী + অজানা অ্যালবাম + সিক + ভলিউম + শাফল চালু করুন + শাফল বন্ধ করুন + আগের + পরের + প্লে বা পজ + লুপ মোড বন্ধ + লুপ মোড: একটি + লুপ মোড: সব + মিউট + আনমিউট + আরও অপশন + প্লেয়ার অপশন + প্লেয়ার ছোট করুন + লিরিক্স বড় করুন + এখন বাজছে + লিরিক্স + বিকল্প সোর্স + বিকল্প সোর্স + ডাউনলোড + ডাউনলোড হয়েছে + ডাউনলোড হচ্ছে... + আগেই ডাউনলোড করা হয়েছে + আবার ডাউনলোডের চেষ্টা হচ্ছে... + %1$s ডাউনলোড হচ্ছে + %1$s পর প্লেব্যাক পজ হবে। + নির্বাচিত মিনিট পরে প্লেব্যাক পজ করুন। + মিনিট + 1 থেকে 600 মিনিটের মধ্যে বেছে নিন + টাইমার বাতিল করুন + শুরু করুন + প্লেব্যাক URL ভিন্ন স্ট্রিমিং সেটিংসে পাওয়া গেছে। হাইলাইট করা স্ট্রিমটি আপনার বর্তমান পছন্দের সাথে মেলে; পরবর্তী স্ট্রিম রিকোয়েস্টে সক্রিয় URL রিফ্রেশ হবে। + মিলে যাওয়া অডিও সোর্স + মিলে যাওয়া সোর্স লোড হচ্ছে… + লোকাল ক্যাশ ফাইল প্লে করার সময় মিলে যাওয়া রিমোট সোর্সের প্রয়োজন নেই। + মিলে যাওয়া কোনো সোর্স উপলব্ধ নেই। + এই ট্র্যাকের জন্য মিলে যাওয়া কোনো অডিও সোর্স ক্যাশ করা নেই। + স্ট্রিমের বিস্তারিত এখনো ক্যাশ করা হয়নি। লোড করতে প্রথমে ট্র্যাকটি প্লে করুন। + ট্র্যাকের বিস্তারিত লোড করা যায়নি। + উপলব্ধ স্ট্রিম + এই ট্র্যাকটি লোকাল ক্যাশ থেকে চালানো হচ্ছে। এই প্লেব্যাকে স্ট্রিম ভ্যারিয়েন্ট ব্যবহৃত হয় না। + স্ট্রিম লোড হচ্ছে… + এই সোর্সের জন্য কোনো স্ট্রিম উপলব্ধ নেই। + বর্তমানে নির্বাচিত স্ট্রিম + আপনার স্ট্রিমিং ফরম্যাট ও কোয়ালিটি পছন্দের সাথে মেলে + সক্রিয় প্লেব্যাক URL + লোকাল ক্যাশ থেকে চালানো হচ্ছে + মেটাডেটা ট্র্যাক + নাম + ID + অ্যালবাম + নির্ভরযোগ্যতা + এক্সটার্নাল URI + অ্যালবাম ID + অ্যালবামের ধরন + অ্যালবাম প্রকাশের তারিখ + অ্যালবামের ট্র্যাক সংখ্যা + অ্যালবামের জনরা + দৈর্ঘ্য + ট্র্যাক নম্বর + ডিস্ক নম্বর + এক্সপ্লিসিট + জনপ্রিয়তা + ISRC + থাম্বনেইল URL + + + সাইডবার টগল করুন + + + বিকল্প সোর্স + এখন কোনো ট্র্যাক বাজছে না + বিকল্প সোর্স খোঁজা হচ্ছে... + কোনো বিকল্প সোর্স পাওয়া যায়নি + নির্ভরযোগ্যতা: %1$d% + সক্রিয় সোর্স + + + বের করে দিন + তাকে সেশন থেকে সরিয়ে দিন + বের করে দিন এবং আবার যোগ দেওয়া আটকান + সাজেশন সরান + তার কিউতে যোগ করা সব ট্র্যাক সরিয়ে দিন + ক্রম বদলান + কিউ থেকে সরান + %1$s • যোগ করেছেন %2$s + + + অজানা + প্লেলিস্ট লোড হচ্ছে... + ট্র্যাক যোগ করুন + প্লেলিস্টে ট্র্যাক যোগ করুন + ট্র্যাক খুঁজুন... + খুঁজুন + কোনো ট্র্যাক পাওয়া যায়নি + যোগ করার জন্য ট্র্যাক খুঁজুন + সম্পন্ন + যোগ করুন + যোগ করা হয়েছে + + + অ্যালবাম লোড হচ্ছে... + + + আপনার প্লেলিস্টে যোগ করুন + খুঁজুন + আপনার প্লেলিস্ট খুঁজুন... + আপনার প্লেলিস্ট লোড হচ্ছে... + আপনার এখনো কোনো প্লেলিস্ট নেই।\nট্র্যাক যোগ করতে একটি তৈরি করুন। + প্লেলিস্ট তৈরি করুন + প্লেলিস্ট এডিট করুন + তৈরি করুন + নাম + প্লেলিস্টের নাম + বিবরণ + বিবরণ (ঐচ্ছিক) + পাবলিক + যে কেউ এই প্লেলিস্ট দেখতে পারবে + কোলাবোরেটিভ + অন্যরা এই প্লেলিস্টে ট্র্যাক যোগ করতে পারবে + কভার ছবি + প্লেলিস্ট কভার + কোনো ছবি নেই + ছবি পরিবর্তন করুন + ছবি বেছে নিন + + + ইনস্টল করুন + প্রতিস্থাপন করুন + zip ফাইলে plugin.json পাওয়া যায়নি + zip ফাইলে manifest.zipline.json পাওয়া যায়নি + plugin.json-এর ফরম্যাট সঠিক নয়: %1$s + প্লাগইন পড়া যায়নি: %1$s + প্লাগইন ইনস্টল করা যায়নি: %1$s + %1$s-এর একটি নতুন ভার্সন উপলব্ধ। ইনস্টল করার আগে আপডেট নিশ্চিত করুন। + %1$s আগেই ইনস্টল করা আছে। ইনস্টল করার আগে প্রতিস্থাপন নিশ্চিত করুন। + বিল্ট-ইন প্লাগইনগুলো আগে থেকেই অন্তর্ভুক্ত, আবার যোগ করা যাবে না। + বিল্ট-ইন প্লাগইন সরানো যাবে না। + %1$s একটি অবৈধ apiVersion (%2$s) ঘোষণা করেছে। %3$s-এর সাথে সামঞ্জস্যপূর্ণ সিমান্টিক ভার্সনিং প্রত্যাশিত। + Spotube প্লাগইন API ভার্সন %1$s অবৈধ। + %1$s প্লাগইন API %2$s-এর জন্য তৈরি, কিন্তু Spotube ব্রেকিং প্লাগইন API %3$s সাপোর্ট করে। একই ব্রেকিং API ভার্সনের জন্য তৈরি প্লাগইন ইনস্টল করুন। + + + প্লাগইন API সামঞ্জস্যপূর্ণ নয় + প্লাগইন ইনস্টল করবেন? + %1$s আপনার ইনস্টল করা প্লাগইনে যোগ হবে। + প্লাগইন আপডেট করবেন? + একটি পুরোনো ভার্সন (%1$s) ইনস্টল করা আছে। %2$s-এ আপডেট করবেন? + ইনস্টল করা প্লাগইন প্রতিস্থাপন করবেন? + %1$s একটি অবৈধ সিমান্টিক ভার্সন (%2$s) ব্যবহার করে, তাই আপডেট হিসেবে তুলনা করা যাচ্ছে না। তবুও ইনস্টল করা প্লাগইন প্রতিস্থাপন করবেন? + ইনস্টল করা ভার্সন (%1$s) সিমান্টিক ভার্সনিং দিয়ে তুলনা করা যাচ্ছে না। এটিকে %2$s দিয়ে প্রতিস্থাপন করবেন? + ভার্সন %1$s আগেই ইনস্টল করা আছে। বিদ্যমান প্লাগইনটি প্রদত্ত কপি দিয়ে প্রতিস্থাপন করবেন? + ইনস্টল করা ভার্সন %1$s প্রদত্ত ভার্সন %2$s-এর চেয়ে নতুন। তবুও প্রতিস্থাপন করবেন? + ইনস্টল করা প্লাগইনটি প্রদত্ত কপি দিয়ে প্রতিস্থাপন করবেন? + + + %1$s: + ভার্সন + API ভার্সন + লেখক + লাইসেন্স + সক্ষমতা + ক্ষমতা + রিপোজিটরি + যোগাযোগ + বাগ রিপোর্ট করুন + %1$s-কে সাপোর্ট করুন + + + অফিশিয়াল + ভেরিফায়েড + GitHub স্টার + GitHub রিপোজিটরি URL + + + কোনো রিলিজ পাওয়া যায়নি + রিলিজ নির্বাচন করুন + একটি রিলিজ বেছে নিন + প্রি-রিলিজ + রিলিজ নোট + + + হোম + সার্চ + এই ডিভাইসে + Connect + ডিভাইস + + + উইন্ডো লুকান + উইন্ডো দেখান + পরের ট্র্যাক + আগের ট্র্যাক + শাফল + লুপ + লুপ: বন্ধ + লুপ: একটি + লুপ: সব + ভলিউম: %1$d% + ভলিউম + + ভলিউম - + মিউট + আনমিউট + ট্র্যাক লাইক করুন + ট্র্যাক আনলাইক করুন + বের হন + + + মিনিমাইজ + ম্যাক্সিমাইজ + রিস্টোর + বন্ধ করুন + + + প্লেব্যাক সার্ভিস + প্লেব্যাক + মিডিয়া প্লেব্যাক কন্ট্রোল + + + ব্রাউজ + এখন বাজছে + ফিচার্ড + সেভ করা ট্র্যাক + জনপ্রিয় ট্র্যাক + diff --git a/composeApp/src/commonMain/composeResources/values-de/strings.xml b/composeApp/src/commonMain/composeResources/values-de/strings.xml new file mode 100644 index 00000000..71f1278a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-de/strings.xml @@ -0,0 +1,938 @@ + + + + Warteschlange + Warteschlange leeren + Warteschlange filtern... + Keine Einträge in der Warteschlange + + Songs, Künstler, Alben suchen... + Verlauf löschen + Suchen... + + + Deine Bibliothek + Bibliothek + Playlists + Alben + Künstler + Lokale Titel + Downloads + Gespeicherte Playlists durchsuchen… + Gespeicherte Alben durchsuchen… + Gespeicherte Künstler durchsuchen… + Ordner oder Titel durchsuchen… + Bibliothek filtern + Filter schließen + Playlist erstellen + Lokale Titel aktualisieren + Zur Rasteransicht wechseln + Zur Listenansicht wechseln + Keine gespeicherten Playlists + Keine Playlists für „%1$s“ + Keine gespeicherten Alben + Keine Alben für „%1$s“ + Keine gespeicherten Künstler + Keine Künstler für „%1$s“ + + + Ordner + Ordner + %1$s • %2$s + + %1$d Titel + %1$d Titel + + + %1$d Ordner + %1$d Ordner + + Zurück zu den Ordnern + Ordner abspielen + Audiozugriff erforderlich + Erlaube den Audiozugriff, um Musik auf deinem Gerät zu durchsuchen. + Zugriff erlauben + Keine lokalen Ordner gefunden + Keine Titel in diesem Ordner + Keine passenden Ordner oder Titel + Lokale Medien konnten nicht aktualisiert werden + + + %1$d aktiv • %2$d abgeschlossen + Abgeschlossene entfernen + Noch keine Downloads + Wird heruntergeladen: %1$d% + In Warteschlange + Abgeschlossen + Abgebrochen + Fehlgeschlagen + Fehlgeschlagen: %1$s + Download abbrechen + Download wiederholen + Download entfernen + + + Künstler + Auf der Sperrliste + Lieblingssongs + Deine gespeicherten Songs + Unbekannter Künstler + Titelname + Künstlername + + Ausgewählt + + Einstellungen + + Plugins + Sprache und Region + Darstellung + Wiedergabe + Downloads + Zwischenspeicher + Desktop + Updates + Unterstützen + Über + + Schließen + Speichern + Abbrechen + + Sprache + Sprache der App-Oberfläche. Aktuell: %1$s + Systemstandard + Land + Land für Inhalte und Verfügbarkeit. Aktuell: %1$s + %1$s (%2$s) + + Design + Lege fest, wie sich Spotube an das Erscheinungsbild deines Systems anpasst. Aktuell: %1$s + Hell + Dunkel + System + + Oberfläche + Hintergrundmaterial der App. Aktuell: %1$s + Wasser + Eis + Chrom + Acryl + Glas + + Akzentfarbe + Aktuelle Palette: %1$s + Akzentfarbe + Wähle eine Akzentpalette und sieh sie dir im hellen und dunklen Design an. + Hell + Dunkel + Vorschau + Schaltfläche + Koboldgrün + Elektroviolett + Ozeancyan + Abendrot + Rosengarten + Mitternachtsblau + Schiefermetallic + + Look & Feel + Oberflächenmaterial und Akzent. Aktuell: %1$s · %2$s + Look & Feel + Wähle ein Oberflächenmaterial und dann einen passenden Akzent. Jede Option ist eine sorgfältig abgestimmte Kombination. + Oberfläche + Akzent + Sekundär + + Palette + Millennium-Akzentpalette. Aktuell: %1$s + Palette + Wähle eine Millennium-Akzentpalette und sieh sie dir hell und dunkel an. + Palette + Millennium-Palette und Akzent. Aktuell: %1$s + Blau + Orange + Limette + + Eigener Download-Ordner + Aktueller Ordner: %1$s + Aktueller Ordner: Standard-Musikordner + Leer lassen, um den Standard-Musikordner zu verwenden. + C:\Music\Spotube + Lokale Medienordner + Eigene Ordner, die auf dem Desktop durchsucht werden. Hinzugefügt: %1$d + Noch kein eigener Ordner hinzugefügt + Eigene Ordner für deine lokale Musikbibliothek. + Verwalten + Ordner hinzufügen + Ordner entfernen + Keine eigenen Ordner hinzugefügt. + %1$s aus der Suchliste entfernen + Download- und Musik-Cache-Ordner werden automatisch einbezogen. + Dein Download-Ordner wird automatisch einbezogen. + Der Musik-Cache-Ordner wird automatisch einbezogen. + Wird gespeichert… + Ordner konnten nicht gespeichert werden. Deine Änderungen sind noch da – versuche es erneut. + + %1$d Ordner + %1$d Ordner + + Download-Format + Bevorzugter Codec und Container für Downloads. Aktuell: %1$s + Download-Qualität + + Streaming-Format + Bevorzugter Codec und Container für Musik-Streaming. Aktuell: %1$s + Streaming-Qualität + Aktuell: %1$s + Musik zwischenspeichern + Gestreamte Audiodaten lokal speichern, damit sie beim erneuten Abspielen schneller laden. + Cache-Ordner + Aktuell: %1$s + Aktuell: Standard-App-Cache + Cache-Größenlimit + Kein Limit + Aktuelles Limit: %1$d MB + Lege die maximale Cache-Größe in MB fest. Gib 0 ein oder lass das Feld leer für kein Limit. + 500 + Die Größe muss 0 oder eine positive Zahl sein. + Endlose Wiedergabe aktivieren + Wenn deine Warteschlange endet, geht es mit empfohlenen Titeln weiter. + Connect aktivieren + Fernsteuerung der Wiedergabe über Spotube Connect bereitstellen. + Port des Wiedergabe-Proxyservers + Fernsteuerung erlauben + Andere Geräte im selben Netzwerk dürfen die Wiedergabe steuern, wenn sie sich verbinden. + Gerätename für die Fernsteuerung + Der Name, den andere Geräte bei der Suche sehen. Aktuell: %1$s + Wähle einen gut erkennbaren Namen, den andere Geräte in deinem Netzwerk sehen, wenn sie diese Spotube-Instanz finden. + z. B. Spotube Wohnzimmer + Hostname des Geräts + Vom Wiedergabe-Proxyserver verwendeter Port. Aktuell: %1$d + Wähle einen Port zwischen 1 und 65535. + 14769 + Gib eine gültige ganze Zahl ein. + Der Port muss zwischen 1 und 65535 liegen. + + Sperrliste + Gesperrte Titel und Künstler verwalten + + In den Infobereich minimieren + Spotube läuft im Infobereich weiter, wenn das Fenster minimiert ist. + Discord Rich Presence + Zeige auf dem Desktop deinen aktuellen Wiedergabestatus in Discord an. + + Automatisch nach Updates suchen + Im Hintergrund nach neuen Spotube-Versionen suchen. + + Plugins verwalten + Verwalte deine Plugins + Standard-Plugin für %1$s + Metadaten + Audio + Songtexte + Scrobbling + Kein Plugin ausgewählt + Ändern + Auswählen + Ausgewählt + Auswahl aufheben + Keine Plugins verfügbar + Plugin %1$s + Löschen + + Konfigurieren + Plugin-Verwaltung + Bitte gib eine URL ein + Die URL muss mit http:// oder https:// beginnen + Plugin konnte nicht heruntergeladen werden + Noch keine Plugins installiert + Füge oben eine Download-URL ein oder wähle eine Datei aus. + + %1$d Plugin installiert + %1$d Plugins installiert + + Plugin installieren + https://example.com/plugin.smplug + Herunterladen + Aus Datei installieren (.smplug) + Aktiv + v%1$s + Integriert + Plugin entfernen + Anmelden + Abmelden + Von URL herunterladen + Aus Datei installieren + Installieren + + • %1$s + Angeforderte Berechtigungen + Dieses Plugin benötigt keine besonderen Berechtigungen. + Installiert vs. neu + Installiert + Neu + API %1$s → %2$s + Dauerhafter Speicher + Daten auf dem Datenträger lesen und schreiben + Netzwerkanfragen + Daten über das Internet senden und empfangen + WebView + Webinhalte in der App anzeigen + Gruppen-Jam + MQTT-Broker + Broker-Host + %1$s:%2$d + Broker-Port + TLS verwenden + Benutzername (optional) + Passwort (optional) + Client-ID-Präfix + Dient zur Identifizierung dieser Spotube-Instanz + Verbindung zum Broker verschlüsseln + Prüfe die aktuellen Broker-Einstellungen, bevor du einen Raum startest. + Verbindung testen + Wird getestet… + Verbunden in %1$d ms + Fehlgeschlagen: %1$s + Platzhalter-Broker – richte deinen eigenen ein, um selbst zu hosten + Verbindung + Zugangsdaten + Identität + Diagnose + + + Gruppen-Jam + Erstellen + Beitreten + Erstelle einen Raum und teile den Code mit deinen Freunden. + Raum erstellen + Raum beitreten + Verbinden… + Raumcode + ABC123 + Broker: %1$s + Richte in den Einstellungen einen Broker ein, um einen Raum zu hosten oder beizutreten. + Broker-Einstellungen + Verbunden + Verbindung wird wiederhergestellt… + Code kopieren + Kopiert! + Teilen + Teilnehmer (%1$d) + Host + Zuhörer + Warte auf Zuhörer… + Entfernen + Sperren + Verwalten + Raum verlassen + Verbindung fehlgeschlagen + Schließen + Jam-Session konnte nicht erstellt werden + Beitritt zur Jam-Session fehlgeschlagen + Gib einen sechsstelligen Raumcode aus Buchstaben und Ziffern ein. + Tritt meiner Spotube-Jam bei, Code: %1$s + + Zum Album + Sleep-Timer + Titeldetails + Abspielen + Pause + Keine Wiedergabe + Zur Warteschlange hinzufügen + + Ignorieren + Aktualisieren + Spotube %1$s ist verfügbar + Die Neuerungen dieser Version findest du auf der Spotube-Downloadseite. + %1$s %2$s ist verfügbar + Aktuell installiert: %1$s + Die Neuerungen dieses Updates findest du auf der Seite des Plugins. + + Überspringen + Weiter + Zurück + Los geht\'s + Spotube + Musik ohne Fesseln. + Ein Open-Source-Player für eine ganze Welt voller Musik.\nZum Entdecken. Zum Selbstgestalten. + Halte dieses Projekt am Leben. + Spotube wird von Menschen gemacht, nicht von einem Abo-Unternehmen. Wenn es Teil deines Alltags wird, hilft uns schon eine kleine Unterstützung, ihm die Pflege zu geben, die es verdient.\n\nDanke, dass du unabhängiger Software einen Platz gibst. + Beginne mit einem Katalog-Plugin, um Songs und Playlists zu finden. Wähle ein Audio-Plugin, um sie abzuspielen. Songtexte und Hörverlauf sind optional. Entdecke unten ein Plugin, prüfe seine Berechtigungen, installiere es und wähle es dann hier aus. + Plugins entdecken + Deine Quellen + Erneut versuchen + Weitere Plugins + Keine neuen Plugins gefunden. Du kannst auch oben eines über einen Link oder eine .smplug-Datei installieren. + Die Plugin-Suche ist gerade nicht verfügbar. Versuche es erneut oder installiere über einen Link oder eine Datei. + Willkommen + Unterstützen + Plugins + Anleitung + + So nutzt du Spotube + Vier schnelle Schritte und du hörst Musik. + Anmelden + Verbinde dein Spotify-Konto oder wähle ein eigenes Katalog-Plugin. + Suchen und abspielen + Finde einen Song, ein Album oder einen Künstler und tippe auf Abspielen. + Offline hören + Lade Titel herunter, um auch ohne Verbindung weiterzuhören. + Songtexte mitlesen + Öffne im Player synchronisierte Songtexte, während ein Titel läuft. + + + Deine Musik. Deine Quellen. Deine Regeln. + Ein kostenloser Open-Source-Musikplayer, entwickelt von einer Community, die Musik einfach liebt. + Open Source + Keine Werbung + Keine Telemetrie + Mit Plugins + Los geht\'s + + + Unterstützer halten uns am Laufen + Mit Leidenschaft gebaut. Von dir am Leben gehalten. + Spotube ist zu 100 % kostenlos, frei und Open Source – keine Werbung, keine Abos, kein Tracking. Jede Funktion gibt es nur, weil Menschen wie du geholfen haben, die Entwicklung am Laufen zu halten. + Patreon + Monatlicher Sponsor + Unterstütze laufende Entwicklung und Infrastruktur + Buy Me a Coffee + Einmaliger Kaffee + Gib einer nächtlichen Coding-Session mit einem kleinen Trinkgeld Energie + Open Collective + Community-Fonds + Transparente, von der Community verwaltete Finanzierung + Von Herzen danke, dass du an offene Musik glaubst. + Die Unterstützung ist völlig freiwillig – Spotube bleibt so oder so kostenlos. + Sponsern + %1$s öffnen + + + Spotube unterstützen + Spotube unterstützen + Spotube ist kostenlos, frei und Open Source – keine Werbung, keine Abos, kein Tracking. Wenn es dir Freude macht, unterstütze doch das Projekt. + Du kannst diese Erinnerungen unter Einstellungen → Unterstützen deaktivieren. + Vielleicht später + Unterstützungs-Erinnerungen + Alle 7 Tage eine dezente Erinnerung anzeigen, das Projekt zu unterstützen. + + + Modulare Architektur + Bestimme, woher deine Musik kommt. + Spotube trennt Katalogsuche, Audio-Streaming und synchronisierte Songtexte. Konfiguriere unten deine Engines oder wechsle sie jederzeit. + Metadaten-Engine + Sorgt für Musikentdeckung, Suche, Titellisten und Playlists. + Audio-Engine + Lädt und dekodiert die abgespielten Audiostreams. + Songtext-Engine + Liefert zeitsynchrone Karaoke-Songtexte in Echtzeit. + Scrobble-Engine + Zeichnet deinen Hörverlauf bei Last.fm oder ListenBrainz auf. + Aktiv + Integriert + Bereit + Optional + Engine auswählen + Installierte Engines + Als Standard festlegen + NewPipe YouTube und LRCLib Lyrics sind integriert und sofort einsatzbereit. + Unter Einstellungen → Plugins kannst du jederzeit Community-Plugins verwalten oder hinzufügen. + + + Retro-moderne Architektur + Reines Hi-Fi, ohne Zwischenhändler + So steuert Spotube die dezentrale Wiedergabe direkt auf deinem Gerät. + DIREKT PEER-TO-PEER • 256 KBPS OPUS + Blaue Stunde + Spotube · Player-Demo + Ein bisschen Musik für die Momente dazwischen. + 1. Dezentrale Entdeckung + Suchanfragen gehen direkt an dein gewähltes Katalog-Plugin. Spotube erstellt nie ein Profil deines Geschmacks. + 2. Direkte Stream-Auflösung + Audio wird direkt von den Content-Servern auf dein Gerät gestreamt. Keine Proxy-Umwege, kein Datensammeln aus der Ferne. + 3. Lokales DSP & Wiedergabe + Lokal dekodiert und zwischengespeichert, mit Equalizer und optionalem Offline-Download. + 4. Synchronisierte Songtexte & Jam + Karaoke-Songtexte folgen der Wiedergabeposition – dazu LAN-Fernsteuerung und Gruppen-Jam. + Keine Zwischenserver. Die Wiedergabe läuft direkt auf deinem Gerät – privat aus Prinzip. + + + Gerät auswählen + %1$s: „%2$s“ + Abspielen + Zur Warteschlange hinzufügen + Als Nächstes abspielen + Dieses Gerät + Auf diesem Gerät + Remote-Gerät + Remote-Gerät (%1$s) + Auf dem verbundenen Gerät + + + Verbindung fehlgeschlagen + Verbindung unterbrochen + Senden fehlgeschlagen + Gerätesuche fehlgeschlagen: %1$s + Verbindung fehlgeschlagen: %1$s + + + Geräte + Aktualisieren + Verbunden mit %1$s:%2$d + Trennen + Verbinden... + Suche nach Geräten im Netzwerk... + Keine Geräte gefunden + Stelle sicher, dass auf dem anderen Gerät „Fernsteuerung erlauben“ in den Einstellungen aktiviert ist. + Unbekanntes Gerät + Wird aufgelöst... + ID: %1$s... + + + Fernsteuerung + Getrennt + Verbindungsfehler: %1$s + Albumcover + Unbekannter Titel + Unbekannter Künstler + Spulen + Zufallswiedergabe + Zurück + Weiter + Wiederholungsmodus + Lautstärke + Aus Warteschlange entfernen + Weitere Optionen + + + Wo abspielen? + Du hast eine aktive Jam-Session. Wähle, wo diese Sammlung abgespielt werden soll. + Hier abspielen + Für Jam vorschlagen + + + Kein Jam-Broker konfiguriert + Broker-Host ist leer + Verbindung fehlgeschlagen + Verbindung unterbrochen + + + Broker + anonym + + + Zur Jam-Warteschlange hinzugefügt + + %1$d Titel zur Jam-Warteschlange hinzugefügt + %1$d Titel zur Jam-Warteschlange hinzugefügt + + + + Fernsteuerungsanfrage + „%1$s“ möchte die Wiedergabe auf diesem Gerät steuern. + Ablehnen + Erlauben + Immer erlauben + + + Suchergebnisse + Alle + Titel + Nutzer + Keine Suchtypen verfügbar + Tippe, um zu suchen + Tippe, um nach Titeln zu suchen + Keine Ergebnisse gefunden + Alle anzeigen + Suche läuft + Titel werden geladen... + Alben werden geladen... + Künstler werden geladen... + Playlists werden geladen... + Nutzer werden geladen... + Keine Alben gefunden + Keine Künstler gefunden + Keine Playlists gefunden + Keine Nutzer gefunden + Wir wühlen in den Plattenkisten... + Wir durchsuchen den Kosmos... + Die Lautsprecher wärmen sich auf... + Wir entstauben das Vinyl... + Wir befragen die Musikgötter... + Wir richten die Antennen aus... + Wir blättern durch die Platten... + Suchfunktionen konnten nicht geladen werden + Suche fehlgeschlagen + Titelsuche fehlgeschlagen + Albumsuche fehlgeschlagen + Künstlersuche fehlgeschlagen + Playlist-Suche fehlgeschlagen + Nutzersuche fehlgeschlagen + + + Entdecken + + + Synchron + Nur Text + Keine synchronisierten Songtexte verfügbar + Keine Songtexte verfügbar + + + %1$s in %2$s + %1$d kbps + %1$s kHz • %2$d Kanäle + Kein Metadaten-Plugin ausgewählt + + + Zurück + Erneut versuchen + Dialog schließen + Wird geladen... + Unbekannt + Unbekannter Fehler + + + Nutzer + Playlist + + + Das tut uns wirklich leid + Gerade funktioniert etwas nicht richtig. Dein Erlebnis ist uns wichtig und wir tun unser Bestes, um das zu beheben. Versuche es bitte erneut oder sieh dir die Fehlerdetails unten an. + Fehlerdetails + In die Zwischenablage kopieren + Vollständiges Protokoll anzeigen + Vollständiges Fehlerprotokoll + + + Zufallswiedergabe + Zufällig abspielen + Folgen + Nicht mehr folgen + Playlist bearbeiten + Von %1$s + + + Titeloptionen + Zur Jam hinzufügen + Radio starten + Als Nächstes abspielen + Als Nächstes einreihen + Aus Warteschlange entfernen + Aus Favoriten entfernen + Als Favorit speichern + Zur Playlist hinzufügen + Herunterladen + Teilen + + + Original + Titel + Künstler + Album + Dauer + Sortieren + Keine Titel gefunden + Filtern + Sammelaktionen + Alle herunterladen + %1$d herunterladen + Alle zur Warteschlange hinzufügen + %1$d zur Warteschlange hinzufügen + Alle als Nächstes abspielen + %1$d als Nächstes abspielen + Alle zur Playlist hinzufügen + %1$d zur Playlist hinzufügen + Alle zur Jam hinzufügen + %1$d zur Jam hinzufügen + +%1$d weitere + + + Keine URL zum Laden + Im Browserverlauf zurück + Im Browserverlauf vorwärts + + + Sperrliste filtern... + Keine gesperrten Einträge + Keine gesperrten Einträge für „%1$s“ + Titel + Titel + Zur Sperrliste hinzufügen + Von der Sperrliste entfernen + + + Keine Titel-Metadaten + Stream-URL konnte nicht ermittelt werden + Stream-URL konnte nicht ermittelt werden. Spiele den Titel zuerst ab. + + + Gespeicherte Titel + Du + + + Gefolgt + Top-Titel + Top-Titel abspielen + Top-Titel zur Warteschlange hinzufügen + Ähnliche Künstler + Empfohlene Playlists + Künstlerübersicht konnte nicht geladen werden + Follower nicht verfügbar + %1$s Tsd. Follower + %1$s Mio. Follower + %1$s Mrd. Follower + + %1$d Follower + %1$d Follower + + + + Gefällt mir + Gefällt mir nicht mehr + Platzhalter-Titel + Platzhalter-Künstler + Unbekanntes Album + Spulen + Lautstärke + Zufallswiedergabe aktivieren + Zufallswiedergabe deaktivieren + Zurück + Weiter + Abspielen oder pausieren + Wiederholen aus + Titel wiederholen + Alle wiederholen + Stummschalten + Stummschaltung aufheben + Weitere Optionen + Player-Optionen + Player minimieren + Songtexte erweitern + Aktuelle Wiedergabe + Songtext + Alternative Quelle + Alternative Quelle + Herunterladen + Heruntergeladen + Wird heruntergeladen... + Bereits heruntergeladen + Download wird wiederholt... + %1$s wird heruntergeladen + Die Wiedergabe wird in %1$s pausiert. + Wiedergabe nach der ausgewählten Anzahl an Minuten pausieren. + Minuten + Zwischen 1 und 600 Minuten wählen + Timer abbrechen + Starten + Die Wiedergabe-URL wurde mit anderen Streaming-Einstellungen ermittelt. Der markierte Stream entspricht deiner aktuellen Einstellung; die aktive URL wird bei der nächsten Stream-Anfrage aktualisiert. + Zugeordnete Audioquelle + Zugeordnete Quelle wird geladen… + Beim Abspielen der lokal zwischengespeicherten Datei wird keine zugeordnete Remote-Quelle benötigt. + Keine zugeordnete Quelle verfügbar. + Für diesen Titel ist keine zugeordnete Audioquelle zwischengespeichert. + Stream-Details sind noch nicht zwischengespeichert. Spiele den Titel zuerst ab, um sie zu laden. + Titeldetails konnten nicht geladen werden. + Verfügbare Streams + Dieser Titel wird aus dem lokalen Cache abgespielt. Stream-Varianten werden für diese Wiedergabe nicht verwendet. + Streams werden geladen… + Für diese Quelle sind keine Streams verfügbar. + Aktuell ausgewählter Stream + Entspricht deinen Einstellungen für Streaming-Format & -Qualität + Aktive Wiedergabe-URL + Aus lokalem Cache abgespielt + Metadaten-Titel + Titel + ID + Album + Übereinstimmung + Externe URI + Album-ID + Albumtyp + Erscheinungsdatum des Albums + Anzahl der Titel im Album + Album-Genres + Dauer + Titelnummer + CD-Nummer + Explizit + Beliebtheit + ISRC + Vorschaubild-URLs + + + Seitenleiste ein-/ausblenden + + + Alternative Quellen + Aktuell wird kein Titel abgespielt + Suche nach alternativen Quellen... + Keine alternativen Quellen gefunden + Übereinstimmung: %1$d% + Aktive Quelle + + + Rauswerfen + Aus der Session entfernen + Rauswerfen und erneuten Beitritt verhindern + Vorschläge entfernen + Alle Titel entfernen, die diese Person zur Warteschlange hinzugefügt hat + Neu anordnen + Aus Warteschlange entfernen + %1$s • Hinzugefügt von %2$s + + + Unbekannt + Playlist wird geladen... + Titel hinzufügen + Titel zur Playlist hinzufügen + Titel suchen... + Suchen + Keine Titel gefunden + Suche nach Titeln zum Hinzufügen + Fertig + Hinzufügen + Hinzugefügt + + + Album wird geladen... + + + Zu deiner Playlist hinzufügen + Suchen + Deine Playlists durchsuchen... + Deine Playlists werden geladen... + Du hast noch keine eigenen Playlists.\nErstelle eine, um Titel hinzuzufügen. + Playlist erstellen + Playlist bearbeiten + Erstellen + Name + Name der Playlist + Beschreibung + Beschreibung (optional) + Öffentlich + Alle können diese Playlist sehen + Gemeinsam + Andere können Titel zu dieser Playlist hinzufügen + Coverbild + Playlist-Cover + Kein Bild + Bild ändern + Bild auswählen + + + Installieren + Ersetzen + plugin.json wurde in der ZIP-Datei nicht gefunden + manifest.zipline.json wurde in der ZIP-Datei nicht gefunden + Ungültiges plugin.json-Format: %1$s + Plugin konnte nicht gelesen werden: %1$s + Plugin konnte nicht installiert werden: %1$s + Eine neuere Version von %1$s ist verfügbar. Bestätige das Update vor der Installation. + %1$s ist bereits installiert. Bestätige das Ersetzen vor der Installation. + Integrierte Plugins sind bereits enthalten und können nicht erneut hinzugefügt werden. + Integrierte Plugins können nicht entfernt werden. + %1$s gibt eine ungültige apiVersion an (%2$s). Erwartet wird semantische Versionierung, kompatibel mit %3$s. + Die Spotube-Plugin-API-Version %1$s ist ungültig. + %1$s ist für Plugin-API %2$s gebaut, Spotube unterstützt jedoch die Breaking-API-Version %3$s. Installiere ein Plugin, das für dieselbe Breaking-API-Version gebaut wurde. + + + Plugin-API nicht kompatibel + Plugin installieren? + %1$s wird zu deinen installierten Plugins hinzugefügt. + Plugin aktualisieren? + Eine ältere Version ist installiert (%1$s). Auf %2$s aktualisieren? + Installiertes Plugin ersetzen? + %1$s verwendet eine ungültige semantische Version (%2$s) und kann nicht als Update verglichen werden. Installiertes Plugin trotzdem ersetzen? + Die installierte Version (%1$s) lässt sich nicht per semantischer Versionierung vergleichen. Durch %2$s ersetzen? + Version %1$s ist bereits installiert. Vorhandenes Plugin durch die neue Kopie ersetzen? + Die installierte Version %1$s ist neuer als die bereitgestellte Version %2$s. Trotzdem ersetzen? + Installiertes Plugin durch die neue Kopie ersetzen? + + + %1$s: + Version + API-Version + Autor + Lizenz + Berechtigungen + Funktionen + Repository + Kontakt + Fehler melden + %1$s unterstützen + + + Offiziell + Verifiziert + GitHub-Sterne + URL des GitHub-Repositorys + + + Keine Releases gefunden + Release auswählen + Wähle ein Release + Vorabversion + Versionshinweise + + + Startseite + Suche + Auf dem Gerät + Verbinden + Geräte + + + Fenster ausblenden + Fenster anzeigen + Nächster Titel + Vorheriger Titel + Zufallswiedergabe + Wiederholen + Wiederholen: AUS + Wiederholen: TITEL + Wiederholen: ALLE + Lautstärke: %1$d% + Lautstärke + + Lautstärke - + Stummschalten + Stummschaltung aufheben + Titel liken + Titel nicht mehr liken + Beenden + + + Minimieren + Maximieren + Wiederherstellen + Schließen + + + Wiedergabedienst + Wiedergabe + Steuerung der Medienwiedergabe + + + Entdecken + Aktuelle Wiedergabe + Empfohlen + Gespeicherte Titel + Top-Titel + diff --git a/composeApp/src/commonMain/composeResources/values-es/strings.xml b/composeApp/src/commonMain/composeResources/values-es/strings.xml new file mode 100644 index 00000000..f6322e76 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-es/strings.xml @@ -0,0 +1,944 @@ + + + + Cola + Vaciar cola + Filtrar cola... + La cola está vacía + + Busca canciones, artistas, álbumes... + Borrar todo el historial + Buscar... + + + Tu biblioteca + Biblioteca + Playlists + Álbumes + Artistas + Canciones locales + Descargas + Buscar playlists guardadas… + Buscar álbumes guardados… + Buscar artistas guardados… + Buscar carpetas o canciones… + Filtrar biblioteca + Cerrar filtro + Crear playlist + Actualizar canciones locales + Cambiar a vista de cuadrícula + Cambiar a vista de lista + No hay playlists guardadas + Ninguna playlist coincide con “%1$s” + No hay álbumes guardados + Ningún álbum coincide con “%1$s” + No hay artistas guardados + Ningún artista coincide con “%1$s” + + + Carpetas + Carpeta + %1$s • %2$s + + %1$d canción + %1$d canciones + %1$d canciones + + + %1$d carpeta + %1$d carpetas + %1$d carpetas + + Volver a carpetas + Reproducir carpeta + Se necesita acceso al audio + Permite el acceso al audio para explorar la música de tu dispositivo. + Permitir acceso + No se encontraron carpetas locales + No hay canciones en esta carpeta + No hay carpetas ni canciones que coincidan + No se pudo actualizar el contenido local + + + %1$d activas • %2$d completadas + Borrar completadas + Aún no hay descargas + Descargando %1$d% + En cola + Completada + Cancelada + Error + Error: %1$s + Cancelar descarga + Reintentar descarga + Quitar descarga + + + Artista + En lista negra + Canciones que te gustan + Tus canciones guardadas + Artista desconocido + Título de la canción + Nombre del artista + + Seleccionado + + Ajustes + + Plugins + Idioma y región + Apariencia + Reproducción + Descargas + Caché + Escritorio + Actualizaciones + Apoyo + Acerca de + + Cerrar + Guardar + Cancelar + + Idioma + Idioma de la interfaz de la app. Actual: %1$s + Predeterminado del sistema + País + País de referencia para el contenido y la disponibilidad. Actual: %1$s + %1$s (%2$s) + + Tema + Elige cómo se adapta Spotube a la apariencia del sistema. Actual: %1$s + Claro + Oscuro + Sistema + + Superficie + Material de fondo de la app. Actual: %1$s + Acuoso + Helado + Cromo + Acrílico + Cristal + + Color de acento + Paleta actual: %1$s + Color de acento + Elige una paleta de acento y previsualízala en los temas claro y oscuro. + Claro + Oscuro + Vista previa + Botón + Duende verde + Violeta eléctrico + Cian oceánico + Naranja atardecer + Jardín de rosas + Azul medianoche + Pizarra metálica + + Aspecto y estilo + Material de superficie y acento. Actual: %1$s · %2$s + Aspecto y estilo + Elige un material de superficie y luego un acento que combine con él. Cada opción es una combinación seleccionada. + Superficie + Acento + Secundario + + Paleta + Paleta de acento Millennium. Actual: %1$s + Paleta + Elige una paleta de acento Millennium y previsualízala en claro y oscuro. + Paleta + Paleta Millennium y acento. Actual: %1$s + Azul + Naranja + Lima + + Carpeta de descargas personalizada + Carpeta actual: %1$s + Carpeta actual: carpeta de música predeterminada + Déjalo vacío para usar la carpeta de música predeterminada. + C:\Music\Spotube + Carpetas de contenido local + Carpetas personalizadas que se analizan en el escritorio. Añadidas: %1$d + Aún no has añadido ninguna carpeta personalizada + Carpetas personalizadas para tu biblioteca de música local. + Gestionar + Añadir carpeta + Quitar carpeta + No se han añadido carpetas personalizadas. + Quitar %1$s de la lista de análisis + Las carpetas de descargas y de caché de música se incluyen automáticamente. + Tu carpeta de descargas se incluye automáticamente. + La carpeta de caché de música se incluye automáticamente. + Guardando… + No se pudieron guardar las carpetas. Tus cambios siguen aquí; inténtalo de nuevo. + + %1$d carpeta + %1$d carpetas + %1$d carpetas + + Formato de descarga + Códec y contenedor preferidos para las descargas. Actual: %1$s + Calidad de descarga + + Formato de streaming + Códec y contenedor preferidos para el streaming de música. Actual: %1$s + Calidad de streaming + Actual: %1$s + Activar caché de música + Guarda el audio reproducido en local para acelerar las siguientes reproducciones. + Carpeta de caché + Actual: %1$s + Actual: caché predeterminada de la app + Límite de tamaño de la caché + Sin límite + Límite actual: %1$d MB + Define el tamaño máximo de la caché en MB. Escribe 0 o déjalo vacío para no tener límite. + 500 + El tamaño debe ser 0 o un número positivo. + Activar reproducción infinita + Sigue reproduciendo canciones recomendadas cuando termine la cola. + Activar Connect + Ofrece controles de reproducción remota mediante la función Spotube Connect. + Puerto del servidor proxy de reproducción + Permitir control remoto + Permite que otros dispositivos de la misma red controlen la reproducción al conectarse. + Nombre del dispositivo para control remoto + El nombre que ven otros dispositivos al detectar este. Actual: %1$s + Elige un nombre reconocible que verán otros dispositivos de tu red al detectar esta instancia de Spotube. + p. ej., Spotube del salón + nombre de host del dispositivo + Puerto que usa el servidor proxy de reproducción. Actual: %1$d + Elige un puerto entre 1 y 65535. + 14769 + Introduce un número entero válido. + El puerto debe estar entre 1 y 65535. + + Lista negra + Gestiona las canciones y artistas de la lista negra + + Minimizar a la bandeja + Mantén Spotube en ejecución en la bandeja del sistema al minimizar la ventana. + Rich Presence de Discord + Muestra lo que estás escuchando en Discord desde el escritorio. + + Buscar actualizaciones automáticamente + Busca nuevas versiones de Spotube en segundo plano. + + Gestionar plugins + Gestiona tus plugins + Plugin de %1$s predeterminado + Metadatos + Audio + Letras + Scrobbling + Ningún plugin seleccionado + Cambiar + Seleccionar + Seleccionado + Borrar selección + No hay plugins disponibles + Plugin %1$s + Borrar + + Configurar + Gestor de plugins + Introduce una URL + La URL debe empezar por http:// o https:// + No se pudo descargar el plugin + Aún no hay plugins instalados + Pega una URL de descarga o elige un archivo arriba. + + %1$d plugin instalado + %1$d de plugins instalados + %1$d plugins instalados + + Instalar un plugin + https://example.com/plugin.smplug + Descargar + Instalar desde archivo (.smplug) + Activo + v%1$s + Integrado + Quitar plugin + Iniciar sesión + Cerrar sesión + Descargar desde URL + Instalar desde archivo + Instalar + + • %1$s + Permisos solicitados + Este plugin no necesita permisos especiales. + Instalado frente a proporcionado + Instalado + Proporcionado + API %1$s → %2$s + Almacenamiento persistente + Leer y escribir datos en el disco + Solicitudes de red + Enviar y recibir datos por internet + WebView + Mostrar contenido web dentro de la app + Jam en grupo + Broker MQTT + Host del broker + %1$s:%2$d + Puerto del broker + Usar TLS + Usuario (opcional) + Contraseña (opcional) + Prefijo de ID de cliente + Se usa para identificar esta instancia de Spotube + Cifra la conexión con el broker + Comprueba los ajustes actuales del broker antes de crear una sala. + Probar conexión + Probando… + Conectado en %1$d ms + Error: %1$s + Broker de ejemplo: configura el tuyo para alojarlo tú mismo + Conexión + Credenciales + Identidad + Diagnóstico + + + Jam en grupo + Crear + Unirse + Crea una sala y comparte su código con tus amigos. + Crear sala + Unirse a la sala + Conectando… + Código de la sala + ABC123 + Broker: %1$s + Configura un broker en Ajustes para crear una sala o unirte a una. + Ajustes del broker + Conectado + Reconectando… + Copiar código + ¡Copiado! + Compartir + Participantes (%1$d) + Anfitrión + Oyente + Esperando a los oyentes… + Quitar + Bloquear + Gestionar + Salir de la sala + No se pudo conectar + Cerrar + No se pudo crear la sesión de Jam + No se pudo unir a la sesión de Jam + Introduce un código de sala de seis caracteres con letras y números. + Únete a mi Jam de Spotube con el código: %1$s + + Ir al álbum + Temporizador + Detalles de la canción + Reproducir + Pausar + No se está reproduciendo nada + Añadir a la cola + + Ignorar + Actualizar + Spotube %1$s está disponible + Descubre las novedades de esta versión en la página de descargas de Spotube. + %1$s %2$s está disponible + Versión instalada: %1$s + Descubre las novedades de esta actualización en la página del plugin. + + Omitir + Siguiente + Atrás + Empezar + Spotube + Música, sin ataduras. + Un reproductor de código abierto para todo un mundo de música.\nPara que lo explores. Para que lo hagas tuyo. + Mantén vivo este proyecto. + Spotube lo hacen personas, no un negocio de suscripciones. Si se convierte en parte de tu día a día, un pequeño apoyo nos ayuda a darle el cuidado que merece.\n\nGracias por hacerle un hueco al software independiente. + Empieza con un plugin de catálogo para encontrar canciones y playlists. Elige un plugin de audio para reproducirlas. Las letras y el historial de escucha son opcionales. Descubre un plugin abajo, revisa sus permisos, instálalo y selecciónalo aquí. + Descubrir plugins + Tus fuentes + Reintentar + Más plugins + No se encontraron plugins nuevos. También puedes instalar uno desde un enlace o un archivo .smplug arriba. + La búsqueda de plugins no está disponible ahora. Inténtalo de nuevo o instala uno desde un enlace o archivo. + Bienvenida + Apoyo + Plugins + Guía + + Cómo usar Spotube + Cuatro pasos rápidos y a escuchar. + Inicia sesión + Conecta tu cuenta de Spotify o elige tu propio plugin de catálogo. + Busca y reproduce + Encuentra cualquier canción, álbum o artista y dale al play. + Escucha sin conexión + Descarga canciones para seguir escuchando sin conexión. + Sigue la letra + Abre la letra sincronizada desde el reproductor mientras suena una canción. + + + Tu música. Tus fuentes. Tus reglas. + Un reproductor de música gratuito y de código abierto creado por una comunidad que simplemente ama la música. + Código abierto + Sin anuncios + Sin telemetría + Basado en plugins + ¡Vamos! + + + Nuestros mecenas nos mantienen en marcha + Hecho con pasión. Mantenido gracias a ti. + Spotube es 100 % gratuito, libre y de código abierto: sin anuncios, sin suscripciones, sin rastreo. Cada función existe porque personas como tú decidieron ayudar a que el código siga fluyendo. + Patreon + Mecenas mensual + Apoya el desarrollo continuo y la infraestructura + Buy Me a Coffee + Un café puntual + Da energía a una sesión de programación nocturna con una pequeña propina + Open Collective + Fondo comunitario + Financiación transparente y gestionada por la comunidad + Gracias de todo corazón por creer en la música abierta. + Apoyar es totalmente opcional: Spotube seguirá siendo gratis de todos modos. + Patrocinar + Abrir %1$s + + + Apoyar a Spotube + Apoyar a Spotube + Spotube es gratuito, libre y de código abierto: sin anuncios, sin suscripciones, sin rastreo. Si te alegra el día, plantéate apoyar el proyecto. + Puedes desactivar estos recordatorios en Ajustes → Apoyo. + Quizás más tarde + Recordatorios de apoyo + Muestra un recordatorio amable para apoyar el proyecto cada 7 días. + + + Arquitectura modular + Elige de dónde viene tu música. + Spotube separa el catálogo, el streaming de audio y las letras sincronizadas. Configura tus motores abajo o cámbialos cuando quieras. + Motor de metadatos + Se encarga del descubrimiento de música, la búsqueda, las listas de canciones y las playlists. + Motor de audio + Obtiene y decodifica las secuencias de audio que se reproducen. + Motor de letras + Muestra letras de karaoke sincronizadas en tiempo real. + Motor de scrobbling + Registra tu historial de escucha en Last.fm o ListenBrainz. + Activo + Integrado + Listo + Opcional + Selecciona un motor + Motores instalados + Establecer como predeterminado + NewPipe YouTube y LRCLib Lyrics vienen integrados y listos para usar desde el primer momento. + Gestiona o añade nuevos plugins de la comunidad cuando quieras en Ajustes → Plugins. + + + Arquitectura retromoderna + Alta fidelidad pura, sin intermediarios + Así es como Spotube orquesta la reproducción descentralizada directamente en tu equipo. + P2P DIRECTO • OPUS A 256 KBPS + Hora azul + Spotube · demostración del reproductor + Un poco de música para los momentos intermedios. + 1. Descubrimiento descentralizado + Las búsquedas van directamente al plugin de catálogo que elijas. Spotube nunca analiza tus gustos. + 2. Resolución directa del stream + El audio llega directamente de los servidores de contenido a tu equipo. Sin saltos por proxies ni recopilación remota de datos. + 3. DSP y reproducción en local + Decodificado y almacenado en caché localmente, con ecualizador y descarga opcional para escuchar sin conexión. + 4. Letras sincronizadas y Jam + Las letras de karaoke siguen la reproducción al segundo, con control remoto por LAN y Jam en grupo. + Sin servidores intermediarios. La reproducción ocurre en tu dispositivo: privada por diseño. + + + Elige un dispositivo + %1$s: «%2$s» + Reproducir + Añadir a la cola + Reproducir a continuación + Este dispositivo + En este dispositivo + Dispositivo remoto + Dispositivo remoto (%1$s) + En el dispositivo conectado + + + Error de conexión + Se perdió la conexión + Error al enviar + Error al buscar dispositivos: %1$s + No se pudo conectar: %1$s + + + Dispositivos + Actualizar + Conectado a %1$s:%2$d + Desconectar + Conectando... + Buscando dispositivos en la red... + No se encontraron dispositivos + Asegúrate de que el otro dispositivo tenga activado \"Permitir control remoto\" en los ajustes. + Dispositivo desconocido + Resolviendo... + ID: %1$s... + + + Control remoto + Desconectado + Error de conexión: %1$s + Portada del álbum + Canción desconocida + Artista desconocido + Avanzar + Aleatorio + Anterior + Siguiente + Modo de repetición + Volumen + Quitar de la cola + Más opciones + + + ¿Dónde reproducir? + Tienes una sesión de Jam activa. Elige dónde reproducir esta colección. + Reproducir aquí + Sugerir al Jam + + + No hay ningún broker de Jam configurado + El host del broker está vacío + Error de conexión + Se perdió la conexión + + + Broker + anónimo + + + Añadida a la cola del Jam + + %1$d añadida a la cola del Jam + %1$d añadidas a la cola del Jam + %1$d añadidas a la cola del Jam + + + + Solicitud de control remoto + \"%1$s\" quiere controlar la reproducción en este dispositivo. + Denegar + Permitir + Permitir siempre + + + Resultados de búsqueda + Todo + Canciones + Usuarios + No hay tipos de búsqueda disponibles + Empieza a escribir para buscar + Empieza a escribir para buscar canciones + No se encontraron resultados + Ver todo + Buscando + Cargando canciones... + Cargando álbumes... + Cargando artistas... + Cargando playlists... + Cargando usuarios... + No se encontraron álbumes + No se encontraron artistas + No se encontraron playlists + No se encontraron usuarios + Rebuscando entre los discos... + Buscando por el cosmos... + Calentando los altavoces... + Quitando el polvo a los vinilos... + Consultando a los dioses de la música... + Sintonizando las antenas... + Pasando discos... + No se pudieron cargar las opciones de búsqueda + No se pudo realizar la búsqueda + No se pudieron buscar canciones + No se pudieron buscar álbumes + No se pudieron buscar artistas + No se pudieron buscar playlists + No se pudieron buscar usuarios + + + Explorar + + + Sincronizada + Texto + No hay letra sincronizada disponible + No hay letra disponible + + + %1$s en %2$s + %1$d kbps + %1$s kHz • %2$d canales + No hay ningún plugin de metadatos seleccionado + + + Atrás + Reintentar + Cerrar diálogo + Cargando... + Desconocido + Error desconocido + + + Usuario + Playlist + + + Lo sentimos de verdad + Algo no funciona bien en este momento. Tu experiencia nos importa y estamos haciendo todo lo posible para solucionarlo. Inténtalo de nuevo o consulta los detalles del error a continuación. + Detalles del error + Copiar al portapapeles + Ver registro completo + Registro de errores completo + + + Aleatorio + Reproducción aleatoria + Seguir + Dejar de seguir + Editar playlist + De %1$s + + + Opciones de la canción + Añadir al Jam + Iniciar radio + Reproducir a continuación + Mover a siguiente + Quitar de la cola + Quitar de favoritos + Guardar en favoritos + Añadir a playlist + Descargar + Compartir + + + Original + Título + Artista + Álbum + Duración + Ordenar + No se encontraron canciones + Filtrar + Acciones en bloque + Descargar todo + Descargar %1$d + Añadir todo a la cola + Añadir %1$d a la cola + Reproducir todo a continuación + Reproducir %1$d a continuación + Añadir todo a una playlist + Añadir %1$d a una playlist + Añadir todo al Jam + Añadir %1$d al Jam + +%1$d más + + + No hay ninguna URL que cargar + Retroceder en el historial del navegador + Avanzar en el historial del navegador + + + Filtrar lista negra... + No hay elementos en la lista negra + Ningún elemento de la lista negra coincide con “%1$s” + Canciones + Canción + Añadir a la lista negra + Quitar de la lista negra + + + No hay metadatos de la canción + No se pudo obtener la URL del stream + No se pudo obtener la URL del stream. Prueba a reproducir la canción primero. + + + Canciones guardadas + Tú + + + Siguiendo + Canciones populares + Reproducir canciones populares + Añadir canciones populares a la cola + Artistas similares + Playlists destacadas + No se pudo cargar la información del artista + Seguidores no disponibles + %1$s mil seguidores + %1$s M de seguidores + %1$s mil M de seguidores + + %1$d seguidor + %1$d de seguidores + %1$d seguidores + + + + Me gusta + Ya no me gusta + Título de canción no disponible + Artistas no disponibles + Álbum desconocido + Avanzar + Volumen + Activar aleatorio + Desactivar aleatorio + Anterior + Siguiente + Reproducir o pausar + Repetición desactivada + Repetir una + Repetir todo + Silenciar + Activar sonido + Más opciones + Opciones del reproductor + Contraer reproductor + Ampliar letra + Reproduciendo ahora + Letra + Fuente alternativa + Fuente alternativa + Descargar + Descargada + Descargando... + Ya descargada + Reintentando la descarga... + Descargando %1$s + La reproducción se pausará en %1$s. + Pausa la reproducción tras el número de minutos seleccionado. + Minutos + Elige entre 1 y 600 minutos + Cancelar temporizador + Iniciar + La URL de reproducción se obtuvo con otros ajustes de streaming. El stream resaltado coincide con tu preferencia actual; la URL activa se actualizará en la próxima solicitud de stream. + Fuente de audio asociada + Cargando fuente asociada… + No se necesita una fuente remota asociada mientras se reproduce el archivo local en caché. + No hay ninguna fuente asociada disponible. + No hay ninguna fuente de audio asociada en caché para esta canción. + Los detalles del stream aún no están en caché. Reproduce primero la canción para cargarlos. + No se pudieron cargar los detalles de la canción. + Streams disponibles + Esta canción se está reproduciendo desde la caché local. Las variantes de stream no se usan en esta reproducción. + Cargando streams… + No hay streams disponibles para esta fuente. + Stream seleccionado actualmente + Coincide con tu preferencia de formato y calidad de streaming + URL de reproducción activa + Reproducido desde la caché local + Canción de metadatos + Título + ID + Álbum + Fiabilidad + URI externa + ID del álbum + Tipo de álbum + Fecha de lanzamiento del álbum + Número de canciones del álbum + Géneros del álbum + Duración + Número de pista + Número de disco + Explícito + Popularidad + ISRC + URL de miniaturas + + + Mostrar u ocultar barra lateral + + + Fuentes alternativas + No se está reproduciendo ninguna canción + Buscando fuentes alternativas... + No se encontraron fuentes alternativas + Fiabilidad: %1$d% + Fuente activa + + + Expulsar + Sacarlo de la sesión + Expulsarlo e impedir que vuelva a unirse + Quitar sugerencias + Quitar todas las canciones que añadió a la cola + Reordenar + Quitar de la cola + %1$s • Añadida por %2$s + + + Desconocido + Cargando playlist... + Añadir canciones + Añadir canciones a la playlist + Buscar canciones... + Buscar + No se encontraron canciones + Busca canciones para añadir + Listo + Añadir + Añadida + + + Cargando álbum... + + + Añadir a tu playlist + Buscar + Busca en tus playlists... + Cargando tus playlists... + Aún no tienes ninguna playlist.\nCrea una para añadir canciones. + Crear playlist + Editar playlist + Crear + Nombre + Nombre de la playlist + Descripción + Descripción (opcional) + Pública + Cualquiera puede ver esta playlist + Colaborativa + Otras personas pueden añadir canciones a esta playlist + Imagen de portada + Portada de la playlist + Sin imagen + Cambiar imagen + Elegir imagen + + + Instalar + Reemplazar + No se encontró plugin.json en el archivo zip + No se encontró manifest.zipline.json en el archivo zip + Formato de plugin.json no válido: %1$s + No se pudo leer el plugin: %1$s + No se pudo instalar el plugin: %1$s + Hay una versión más reciente de %1$s. Confirma la actualización antes de instalarla. + %1$s ya está instalado. Confirma que quieres reemplazarlo antes de instalarlo. + Los plugins integrados ya están incluidos y no se pueden volver a añadir. + Los plugins integrados no se pueden quitar. + %1$s declara una apiVersion no válida (%2$s). Se esperaba un versionado semántico compatible con %3$s. + La versión %1$s de la API de plugins de Spotube no es válida. + %1$s está diseñado para la API de plugins %2$s, pero Spotube es compatible con la API de plugins %3$s, que incluye cambios incompatibles. Instala un plugin creado para la misma versión principal de la API. + + + API de plugin no compatible + ¿Instalar plugin? + %1$s se añadirá a tus plugins instalados. + ¿Actualizar plugin? + Hay una versión anterior instalada (%1$s). ¿Actualizar a %2$s? + ¿Reemplazar el plugin instalado? + %1$s usa una versión semántica no válida (%2$s) y no se puede comparar como actualización. ¿Reemplazar el plugin instalado de todos modos? + La versión instalada (%1$s) no se puede comparar con el versionado semántico. ¿Reemplazarla por %2$s? + La versión %1$s ya está instalada. ¿Reemplazar el plugin existente por la copia proporcionada? + La versión instalada %1$s es más reciente que la versión proporcionada %2$s. ¿Reemplazarla de todos modos? + ¿Reemplazar el plugin instalado por la copia proporcionada? + + + %1$s: + Versión + Versión de la API + Autor + Licencia + Capacidades + Funciones + Repositorio + Contacto + Informar de errores + Apoyar a %1$s + + + Oficial + Verificado + Estrellas en GitHub + URL del repositorio de GitHub + + + No se encontraron versiones + Seleccionar versión + Selecciona una versión + Versión preliminar + Notas de la versión + + + Inicio + Buscar + En el dispositivo + Connect + Dispositivos + + + Ocultar ventana + Mostrar ventana + Siguiente canción + Canción anterior + Aleatorio + Repetir + Repetir: NO + Repetir: UNA + Repetir: TODO + Volumen: %1$d% + Volumen + + Volumen - + Silenciar + Activar sonido + Me gusta + Ya no me gusta + Salir + + + Minimizar + Maximizar + Restaurar + Cerrar + + + Servicio de reproducción + Reproducción + Controles de reproducción multimedia + + + Explorar + Reproduciendo ahora + Destacados + Canciones guardadas + Canciones populares + diff --git a/composeApp/src/commonMain/composeResources/values-fa/strings.xml b/composeApp/src/commonMain/composeResources/values-fa/strings.xml new file mode 100644 index 00000000..89556d91 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-fa/strings.xml @@ -0,0 +1,938 @@ + + + + صف پخش + پاک کردن صف + فیلتر صف... + صف پخش خالی است + + جستجوی آهنگ‌ها، هنرمندان، آلبوم‌ها... + پاک کردن کل تاریخچه + جستجو... + + + کتابخانهٔ شما + کتابخانه + پلی‌لیست‌ها + آلبوم‌ها + هنرمندان + آهنگ‌های محلی + دانلودها + جستجو در پلی‌لیست‌های ذخیره‌شده… + جستجو در آلبوم‌های ذخیره‌شده… + جستجو در هنرمندان ذخیره‌شده… + جستجوی پوشه‌ها یا آهنگ‌ها… + فیلتر کتابخانه + بستن فیلتر + ساخت پلی‌لیست + به‌روزرسانی آهنگ‌های محلی + نمایش شبکه‌ای + نمایش فهرستی + هیچ پلی‌لیست ذخیره‌شده‌ای ندارید + هیچ پلی‌لیستی با «%1$s» مطابقت ندارد + هیچ آلبوم ذخیره‌شده‌ای ندارید + هیچ آلبومی با «%1$s» مطابقت ندارد + هیچ هنرمند ذخیره‌شده‌ای ندارید + هیچ هنرمندی با «%1$s» مطابقت ندارد + + + پوشه‌ها + پوشه + %1$s • %2$s + + %1$d آهنگ + %1$d آهنگ + + + %1$d پوشه + %1$d پوشه + + بازگشت به پوشه‌ها + پخش پوشه + دسترسی به صدا لازم است + برای مرور موسیقی‌های دستگاهتان، اجازهٔ دسترسی به فایل‌های صوتی را بدهید. + اعطای دسترسی + هیچ پوشهٔ محلی‌ای پیدا نشد + این پوشه آهنگی ندارد + هیچ پوشه یا آهنگ مطابقی پیدا نشد + به‌روزرسانی رسانه‌های محلی ناموفق بود + + + %1$d در حال انجام • %2$d تکمیل‌شده + پاک کردن تکمیل‌شده‌ها + هنوز دانلودی ندارید + در حال دانلود %1$d% + در صف + تکمیل شد + لغو شد + ناموفق + ناموفق: %1$s + لغو دانلود + تلاش دوباره برای دانلود + حذف دانلود + + + هنرمند + در فهرست سیاه + آهنگ‌های پسندیده + آهنگ‌های ذخیره‌شدهٔ شما + هنرمند ناشناس + عنوان آهنگ + نام هنرمند + + انتخاب‌شده + + تنظیمات + + افزونه‌ها + زبان و منطقه + ظاهر + پخش + دانلودها + حافظهٔ پنهان + دسکتاپ + به‌روزرسانی‌ها + حمایت + درباره + + بستن + ذخیره + لغو + + زبان + زبان رابط کاربری برنامه. فعلی: %1$s + پیش‌فرض سیستم + کشور + کشور منطقه برای محتوا و دسترس‌پذیری. فعلی: %1$s + %1$s (%2$s) + + پوسته + انتخاب کنید Spotube چگونه با ظاهر سیستم شما هماهنگ شود. فعلی: %1$s + روشن + تیره + سیستم + + سطح + جنس پس‌زمینهٔ برنامه. فعلی: %1$s + آبی‌گون + یخی + کروم + آکریلیک + شیشه + + رنگ تأکیدی + پالت فعلی: %1$s + رنگ تأکیدی + یک پالت رنگ تأکیدی انتخاب کنید و پیش‌نمایش آن را در پوستهٔ روشن و تیره ببینید. + روشن + تیره + پیش‌نمایش + دکمه + گابلین سبز + بنفش الکتریکی + فیروزه‌ای اقیانوسی + نارنجی غروب + باغ رز + آبی نیمه‌شب + خاکستری فلزی + + ظاهر و حس + جنس سطح و رنگ تأکیدی. فعلی: %1$s · %2$s + ظاهر و حس + یک جنس سطح انتخاب کنید، سپس رنگ تأکیدی‌ای که با آن جور باشد. هر گزینه ترکیبی دست‌چین‌شده است. + سطح + رنگ تأکیدی + ثانویه + + پالت + پالت رنگ تأکیدی Millennium. فعلی: %1$s + پالت + یک پالت رنگ تأکیدی Millennium انتخاب کنید و پیش‌نمایش آن را در حالت روشن و تیره ببینید. + پالت + پالت Millennium و رنگ تأکیدی. فعلی: %1$s + آبی + نارنجی + لیمویی + + پوشهٔ دانلود سفارشی + پوشهٔ فعلی: %1$s + پوشهٔ فعلی: پوشهٔ پیش‌فرض موسیقی + برای استفاده از پوشهٔ پیش‌فرض موسیقی، این قسمت را خالی بگذارید. + C:\Music\Spotube + پوشه‌های رسانهٔ محلی + پوشه‌های سفارشی برای اسکن در دسکتاپ. افزوده‌شده: %1$d + هنوز پوشهٔ سفارشی‌ای اضافه نشده است + پوشه‌های سفارشی برای کتابخانهٔ موسیقی محلی شما. + مدیریت + افزودن پوشه + حذف پوشه + هیچ پوشهٔ سفارشی‌ای اضافه نشده است. + حذف %1$s از فهرست اسکن + پوشه‌های دانلود و حافظهٔ پنهان موسیقی به‌طور خودکار اضافه می‌شوند. + پوشهٔ دانلود شما به‌طور خودکار اضافه می‌شود. + پوشهٔ حافظهٔ پنهان موسیقی به‌طور خودکار اضافه می‌شود. + در حال ذخیره… + ذخیرهٔ پوشه‌ها ممکن نشد. تغییرات شما هنوز اینجاست؛ دوباره تلاش کنید. + + %1$d پوشه + %1$d پوشه + + قالب دانلود موسیقی + کدک و قالب ترجیحی برای دانلودها. فعلی: %1$s + کیفیت دانلود موسیقی + + قالب پخش آنلاین موسیقی + کدک و قالب ترجیحی برای پخش آنلاین موسیقی. فعلی: %1$s + کیفیت پخش آنلاین موسیقی + فعلی: %1$s + فعال‌سازی حافظهٔ پنهان موسیقی + صدای پخش‌شده را به‌صورت محلی ذخیره کنید تا پخش دوباره سریع‌تر شود. + پوشهٔ حافظهٔ پنهان + فعلی: %1$s + فعلی: حافظهٔ پنهان پیش‌فرض برنامه + محدودیت حجم حافظهٔ پنهان + بدون محدودیت + محدودیت فعلی: %1$d MB + حداکثر حجم حافظهٔ پنهان را به MB تعیین کنید. برای نامحدود، 0 وارد کنید یا خالی بگذارید. + 500 + حجم باید 0 یا یک عدد مثبت باشد. + فعال‌سازی پخش بی‌پایان + وقتی صف پخش تمام شد، پخش با آهنگ‌های پیشنهادی ادامه پیدا کند. + فعال‌سازی Connect + کنترل‌های پخش از راه دور را از طریق قابلیت Spotube Connect در دسترس قرار دهید. + درگاه سرور پراکسی پخش + اجازهٔ کنترل از راه دور + به دستگاه‌های دیگر در همان شبکه اجازه دهید هنگام اتصال، پخش را کنترل کنند. + نام دستگاه برای کنترل از راه دور + نامی که دستگاه‌های دیگر هنگام یافتن این دستگاه می‌بینند. فعلی: %1$s + یک نام دوستانه انتخاب کنید تا دستگاه‌های دیگر شبکه هنگام یافتن این نسخه از Spotube آن را ببینند. + مثلاً Spotube اتاق نشیمن + نام میزبان دستگاه + درگاه مورد استفادهٔ سرور پراکسی پخش. فعلی: %1$d + درگاهی بین 1 و 65535 انتخاب کنید. + 14769 + یک عدد صحیح معتبر وارد کنید. + درگاه باید بین 1 و 65535 باشد. + + فهرست سیاه + مدیریت آهنگ‌ها و هنرمندان فهرست سیاه + + کوچک‌سازی به سینی سیستم + وقتی پنجره کوچک می‌شود، Spotube در سینی سیستم در حال اجرا بماند. + وضعیت Discord + وضعیت پخش فعلی خود را در Discord روی دسکتاپ نمایش دهید. + + بررسی خودکار به‌روزرسانی‌ها + نسخه‌های جدید Spotube را در پس‌زمینه بررسی کنید. + + مدیریت افزونه‌ها + افزونه‌های خود را مدیریت کنید + افزونهٔ پیش‌فرض %1$s + فراداده + صدا + متن آهنگ + اسکراب + هیچ افزونه‌ای انتخاب نشده است + تغییر + انتخاب + انتخاب‌شده + پاک کردن انتخاب + هیچ افزونه‌ای در دسترس نیست + افزونهٔ %1$s + پاک کردن + + پیکربندی + مدیریت افزونه‌ها + لطفاً یک URL وارد کنید + URL باید با http:// یا https:// شروع شود + دانلود افزونه ناموفق بود + هنوز افزونه‌ای نصب نشده است + یک URL دانلود وارد کنید یا از بالا فایلی انتخاب کنید. + + %1$d افزونه نصب‌شده + %1$d افزونه نصب‌شده + + نصب افزونه + https://example.com/plugin.smplug + دانلود + نصب از فایل (.smplug) + فعال + v%1$s + داخلی + حذف افزونه + ورود + خروج + دانلود از URL + نصب از فایل + نصب + + • %1$s + مجوزهای درخواستی + این افزونه به مجوز خاصی نیاز ندارد. + نصب‌شده در برابر ارائه‌شده + نصب‌شده + ارائه‌شده + API %1$s → %2$s + ذخیره‌سازی دائمی + خواندن و نوشتن داده روی دیسک + درخواست‌های شبکه + ارسال و دریافت داده از طریق اینترنت + WebView + نمایش محتوای وب درون برنامه + جم گروهی + بروکر MQTT + میزبان بروکر + %1$s:%2$d + درگاه بروکر + استفاده از TLS + نام کاربری (اختیاری) + گذرواژه (اختیاری) + پیشوند شناسهٔ کلاینت + برای شناسایی این نسخه از Spotube استفاده می‌شود + رمزگذاری اتصال به بروکر + پیش از شروع اتاق، تنظیمات فعلی بروکر را بررسی کنید. + آزمایش اتصال + در حال آزمایش… + در %1$d میلی‌ثانیه متصل شد + ناموفق: %1$s + بروکر نمونه — برای میزبانی شخصی، بروکر خودتان را پیکربندی کنید + اتصال + اطلاعات ورود + هویت + عیب‌یابی + + + جم گروهی + ساختن + پیوستن + یک اتاق بسازید و کد آن را با دوستانتان به اشتراک بگذارید. + ساخت اتاق + پیوستن به اتاق + در حال اتصال… + کد اتاق + ABC123 + بروکر: %1$s + برای میزبانی یا پیوستن به اتاق، یک بروکر در تنظیمات راه‌اندازی کنید. + تنظیمات بروکر + متصل + در حال اتصال دوباره… + کپی کد + کپی شد! + اشتراک‌گذاری + شرکت‌کنندگان (%1$d) + میزبان + شنونده + در انتظار شنوندگان… + حذف + مسدود کردن + مدیریت + ترک اتاق + اتصال برقرار نشد + بستن + ساخت جلسهٔ جم ناموفق بود + پیوستن به جلسهٔ جم ناموفق بود + یک کد اتاق شش‌نویسه‌ای شامل حروف و اعداد وارد کنید. + با این کد به جم Spotube من بپیوندید: %1$s + + رفتن به آلبوم + زمان‌سنج خواب + جزئیات آهنگ + پخش + توقف + چیزی در حال پخش نیست + افزودن به صف + + نادیده گرفتن + به‌روزرسانی + Spotube %1$s در دسترس است + تازه‌های این نسخه را در صفحهٔ دانلود Spotube ببینید. + %1$s %2$s در دسترس است + نسخهٔ نصب‌شدهٔ فعلی: %1$s + تازه‌های این به‌روزرسانی را در صفحهٔ افزونه ببینید. + + رد شدن + بعدی + بازگشت + شروع کنید + Spotube + موسیقی، بی‌هیچ قید و بندی. + پخش‌کننده‌ای متن‌باز برای دنیایی از موسیقی.\nبرای کشف کردن. برای از آنِ خود کردن. + این پروژه را زنده نگه دارید. + Spotube را آدم‌ها ساخته‌اند، نه یک کسب‌وکار اشتراکی. اگر بخشی از زندگی روزمره‌تان شده، کمی حمایت به ما کمک می‌کند آن‌طور که شایسته است از آن مراقبت کنیم.\n\nسپاس که برای نرم‌افزار مستقل جا باز می‌کنید. + با یک افزونهٔ کاتالوگ شروع کنید تا آهنگ‌ها و پلی‌لیست‌ها را پیدا کنید. یک افزونهٔ صوتی برای پخش آن‌ها انتخاب کنید. متن آهنگ و تاریخچهٔ شنیدن اختیاری‌اند. افزونه‌ای را در پایین پیدا کنید، مجوزهایش را بررسی کنید، نصبش کنید و سپس آن را اینجا انتخاب کنید. + کشف افزونه‌ها + منابع شما + تلاش دوباره + افزونه‌های بیشتر + افزونهٔ جدیدی پیدا نشد. می‌توانید از طریق پیوند یا فایل .smplug در بالا هم افزونه نصب کنید. + کشف افزونه‌ها در حال حاضر در دسترس نیست. دوباره تلاش کنید یا از پیوند یا فایل نصب کنید. + خوش آمدید + حمایت + افزونه‌ها + راهنما + + نحوهٔ استفاده از Spotube + چهار گام سریع و مشغول شنیدن هستید. + ورود + حساب Spotify خود را متصل کنید یا افزونهٔ کاتالوگ دلخواهتان را انتخاب کنید. + جستجو و پخش + هر آهنگ، آلبوم یا هنرمندی را پیدا کنید و پخش را بزنید. + گوش دادن آفلاین + آهنگ‌ها را دانلود کنید تا بدون اینترنت هم پخش شوند. + همراهی با متن آهنگ + هنگام پخش آهنگ، متن همگام‌شده را از پخش‌کننده باز کنید. + + + موسیقی شما. منابع شما. قوانین شما. + پخش‌کننده‌ای رایگان و متن‌باز، ساختهٔ جامعه‌ای که فقط عاشق موسیقی است. + متن‌باز + بدون تبلیغات + بدون ردیابی + مبتنی بر افزونه + برویم + + + حامیان ما را سرپا نگه می‌دارند + ساخته‌شده با عشق. زنده به لطف شما. + Spotube صددرصد رایگان، آزاد و متن‌باز است — بدون تبلیغات، بدون اشتراک، بدون ردیابی. هر قابلیتی وجود دارد چون کسانی مثل شما تصمیم گرفتند به ادامهٔ توسعه کمک کنند. + Patreon + حامی ماهانه + از توسعه و زیرساخت مستمر حمایت کنید + Buy Me a Coffee + یک قهوهٔ یک‌باره + با یک انعام کوچک، به یک جلسهٔ کدنویسی شبانه انرژی بدهید + Open Collective + صندوق جامعه + پشتیبانی مالی شفاف و تحت مدیریت جامعه + از صمیم قلب سپاسگزاریم که به موسیقی آزاد باور دارید. + حمایت کاملاً اختیاری است — Spotube در هر صورت رایگان می‌ماند. + حمایت + باز کردن %1$s + + + حمایت از Spotube + حمایت از Spotube + Spotube رایگان، آزاد و متن‌باز است — بدون تبلیغات، بدون اشتراک، بدون ردیابی. اگر برایتان شادی می‌آورد، حمایت از پروژه را در نظر بگیرید. + می‌توانید این یادآوری‌ها را در تنظیمات → حمایت خاموش کنید. + شاید بعداً + یادآوری‌های حمایت + هر ۷ روز یک یادآوری ملایم برای حمایت از پروژه نمایش داده شود. + + + معماری ماژولار + انتخاب کنید موسیقی‌تان از کجا بیاید. + Spotube کشف کاتالوگ، پخش صدا و متن همگام‌شده را از هم جدا می‌کند. موتورهای خود را در پایین پیکربندی کنید یا هر زمان عوضشان کنید. + موتور فراداده + کشف موسیقی، جستجو، فهرست آهنگ‌ها و پلی‌لیست‌ها را ممکن می‌کند. + موتور صدا + جریان‌های صوتی در حال پخش را دریافت و رمزگشایی می‌کند. + موتور متن آهنگ + متن کارائوکهٔ همگام با زمان را به‌صورت زنده نمایش می‌دهد. + موتور اسکراب + تاریخچهٔ شنیدن را در Last.fm یا ListenBrainz ثبت می‌کند. + فعال + داخلی + آماده + اختیاری + یک موتور انتخاب کنید + موتورهای نصب‌شده + تنظیم به‌عنوان پیش‌فرض + NewPipe YouTube و LRCLib Lyrics داخلی هستند و از همان ابتدا آمادهٔ پخش‌اند. + هر زمان در تنظیمات → افزونه‌ها، افزونه‌های جامعه را مدیریت یا اضافه کنید. + + + معماری رترو-مدرن + Hi-Fi خالص، بدون واسطه + این‌گونه Spotube پخش غیرمتمرکز را مستقیماً روی دستگاه شما هماهنگ می‌کند. + نظیربه‌نظیر مستقیم • 256 KBPS OPUS + ساعت آبی + Spotube · نمایش پخش‌کننده + کمی موسیقی برای لحظه‌های میان‌راه. + ۱. کشف غیرمتمرکز + جستجوها مستقیماً به افزونهٔ کاتالوگ انتخابی شما می‌روند. Spotube هرگز از سلیقهٔ شما نمایه نمی‌سازد. + ۲. دریافت مستقیم جریان + صدا مستقیماً از سرورهای محتوا به دستگاه شما می‌رسد. بدون پراکسی میانی یا جمع‌آوری داده از راه دور. + ۳. پردازش صدا و پخش محلی + رمزگشایی و ذخیرهٔ محلی، همراه با اکولایزر و دانلود آفلاین اختیاری. + ۴. متن همگام و جم + متن کارائوکه با زمان پخش همگام می‌شود، همراه با کنترل از راه دور در LAN و جم گروهی. + بدون سرور واسطه. پخش درست روی دستگاه شما انجام می‌شود — خصوصی از پایه. + + + یک دستگاه انتخاب کنید + %1$s: «%2$s» + پخش + افزودن به صف + پخش بعدی + همین دستگاه + روی همین دستگاه + دستگاه راه دور + دستگاه راه دور (%1$s) + روی دستگاه متصل + + + اتصال ناموفق بود + اتصال قطع شد + ارسال ناموفق بود + یافتن دستگاه‌ها ناموفق بود: %1$s + اتصال ناموفق بود: %1$s + + + دستگاه‌ها + به‌روزرسانی + متصل به %1$s:%2$d + قطع اتصال + در حال اتصال... + در حال جستجوی دستگاه‌ها در شبکه... + هیچ دستگاهی پیدا نشد + مطمئن شوید گزینهٔ \"اجازهٔ کنترل از راه دور\" در تنظیمات دستگاه دیگر فعال است. + دستگاه ناشناس + در حال شناسایی... + شناسه: %1$s... + + + کنترل از راه دور + قطع شد + خطای اتصال: %1$s + کاور آلبوم + آهنگ ناشناس + هنرمند ناشناس + جابه‌جایی + پخش تصادفی + قبلی + بعدی + حالت تکرار + صدا + حذف از صف + گزینه‌های بیشتر + + + کجا پخش شود؟ + یک جلسهٔ جم فعال دارید. انتخاب کنید این مجموعه کجا پخش شود. + پخش در اینجا + پیشنهاد به جم + + + هیچ بروکر جمی پیکربندی نشده است + میزبان بروکر خالی است + اتصال ناموفق بود + اتصال قطع شد + + + بروکر + ناشناس + + + به صف جم اضافه شد + + %1$d آهنگ به صف جم اضافه شد + %1$d آهنگ به صف جم اضافه شد + + + + درخواست کنترل از راه دور + \"%1$s\" می‌خواهد پخش را روی این دستگاه کنترل کند. + رد کردن + اجازه دادن + همیشه اجازه بده + + + نتایج جستجو + همه + آهنگ‌ها + کاربران + هیچ نوع جستجویی در دسترس نیست + برای جستجو شروع به تایپ کنید + برای جستجوی آهنگ‌ها شروع به تایپ کنید + نتیجه‌ای پیدا نشد + مشاهدهٔ همه + در حال جستجو + در حال بارگیری آهنگ‌ها... + در حال بارگیری آلبوم‌ها... + در حال بارگیری هنرمندان... + در حال بارگیری پلی‌لیست‌ها... + در حال بارگیری کاربران... + هیچ آلبومی پیدا نشد + هیچ هنرمندی پیدا نشد + هیچ پلی‌لیستی پیدا نشد + هیچ کاربری پیدا نشد + در حال گشتن میان صفحه‌ها... + در حال جستجوی کیهان... + در حال گرم کردن بلندگوها... + در حال گردگیری صفحه‌های گرامافون... + در حال مشورت با خدایان موسیقی... + در حال تنظیم آنتن‌ها... + در حال ورق زدن آلبوم‌ها... + بارگیری قابلیت‌های جستجو ناموفق بود + جستجو ناموفق بود + جستجوی آهنگ‌ها ناموفق بود + جستجوی آلبوم‌ها ناموفق بود + جستجوی هنرمندان ناموفق بود + جستجوی پلی‌لیست‌ها ناموفق بود + جستجوی کاربران ناموفق بود + + + مرور + + + همگام + ساده + متن همگام‌شده‌ای در دسترس نیست + متن ساده‌ای در دسترس نیست + + + %1$s در %2$s + %1$d kbps + %1$s kHz • %2$d کانال + هیچ افزونهٔ فراداده‌ای انتخاب نشده است + + + بازگشت + تلاش دوباره + بستن پنجره + در حال بارگیری... + ناشناس + خطای ناشناخته + + + کاربر + پلی‌لیست + + + واقعاً متأسفیم + در حال حاضر چیزی درست کار نمی‌کند. تجربهٔ شما برای ما مهم است و تمام تلاشمان را برای رفع آن می‌کنیم. لطفاً دوباره تلاش کنید یا جزئیات خطا را در پایین بررسی کنید. + جزئیات خطا + کپی در کلیپ‌بورد + مشاهدهٔ گزارش کامل + گزارش کامل خطا + + + پخش تصادفی + پخش تصادفی + دنبال کردن + لغو دنبال کردن + ویرایش پلی‌لیست + از %1$s + + + گزینه‌های آهنگ + افزودن به جم + شروع رادیو + پخش بعدی + انتقال به بعدی + حذف از صف + حذف از علاقه‌مندی‌ها + افزودن به علاقه‌مندی‌ها + افزودن به پلی‌لیست + دانلود + اشتراک‌گذاری + + + اصلی + عنوان + هنرمند + آلبوم + مدت + مرتب‌سازی + هیچ آهنگی پیدا نشد + فیلتر + کارهای گروهی + دانلود همه + دانلود %1$d + افزودن همه به صف + افزودن %1$d به صف + پخش همه در ادامه + پخش %1$d در ادامه + افزودن همه به پلی‌لیست + افزودن %1$d به پلی‌لیست + افزودن همه به جم + افزودن %1$d به جم + +%1$d دیگر + + + URL برای بارگیری وجود ندارد + بازگشت در تاریخچهٔ مرورگر + رفتن به جلو در تاریخچهٔ مرورگر + + + فیلتر فهرست سیاه... + هیچ موردی در فهرست سیاه نیست + هیچ مورد فهرست سیاهی با «%1$s» مطابقت ندارد + آهنگ‌ها + آهنگ + افزودن به فهرست سیاه + حذف از فهرست سیاه + + + فرادادهٔ آهنگ موجود نیست + URL جریان پیدا نشد + URL جریان پیدا نشد. ابتدا آهنگ را پخش کنید. + + + آهنگ‌های ذخیره‌شده + شما + + + دنبال می‌کنید + آهنگ‌های برتر + پخش آهنگ‌های برتر + افزودن آهنگ‌های برتر به صف + هنرمندان مرتبط + پلی‌لیست‌های ویژه + بارگیری نمای کلی هنرمند ناموفق بود + تعداد دنبال‌کنندگان در دسترس نیست + %1$sK دنبال‌کننده + %1$sM دنبال‌کننده + %1$sB دنبال‌کننده + + %1$d دنبال‌کننده + %1$d دنبال‌کننده + + + + پسندیدن + لغو پسند + عنوان آهنگ خالی + هنرمندان خالی + آلبوم ناشناس + جابه‌جایی + صدا + فعال کردن پخش تصادفی + غیرفعال کردن پخش تصادفی + قبلی + بعدی + پخش یا توقف + تکرار خاموش + تکرار یک آهنگ + تکرار همه + بی‌صدا + باصدا + گزینه‌های بیشتر + گزینه‌های پخش‌کننده + جمع کردن پخش‌کننده + باز کردن متن آهنگ + در حال پخش + متن آهنگ + منبع جایگزین + منبع جایگزین + دانلود + دانلود شد + در حال دانلود... + قبلاً دانلود شده است + تلاش دوباره برای دانلود... + در حال دانلود %1$s + پخش تا %1$s دیگر متوقف می‌شود. + پخش پس از تعداد دقیقهٔ انتخاب‌شده متوقف شود. + دقیقه + عددی بین 1 تا 600 دقیقه انتخاب کنید + لغو زمان‌سنج + شروع + URL پخش با تنظیمات پخش آنلاین متفاوتی به دست آمده است. جریان برجسته‌شده با ترجیح فعلی شما مطابقت دارد؛ URL فعال در درخواست بعدی جریان به‌روز می‌شود. + منبع صوتی منطبق + در حال بارگیری منبع منطبق… + هنگام پخش فایل ذخیره‌شدهٔ محلی، به منبع راه دور منطبق نیازی نیست. + هیچ منبع منطبقی در دسترس نیست. + هیچ منبع صوتی منطبقی برای این آهنگ در حافظهٔ پنهان نیست. + جزئیات جریان هنوز در حافظهٔ پنهان نیست. برای بارگیری آن‌ها ابتدا آهنگ را پخش کنید. + بارگیری جزئیات آهنگ ناموفق بود. + جریان‌های در دسترس + این آهنگ از حافظهٔ پنهان محلی پخش می‌شود. نسخه‌های جریان برای این پخش استفاده نمی‌شوند. + در حال بارگیری جریان‌ها… + هیچ جریانی برای این منبع در دسترس نیست. + جریان انتخاب‌شدهٔ فعلی + مطابق با قالب و کیفیت پخش آنلاین ترجیحی شما + URL پخش فعال + پخش از حافظهٔ پنهان محلی + آهنگ فراداده + عنوان + شناسه + آلبوم + اطمینان + URI خارجی + شناسهٔ آلبوم + نوع آلبوم + تاریخ انتشار آلبوم + تعداد آهنگ‌های آلبوم + ژانرهای آلبوم + مدت + شمارهٔ آهنگ + شمارهٔ دیسک + صریح + محبوبیت + ISRC + URLهای تصویر بندانگشتی + + + نمایش/پنهان کردن نوار کناری + + + منابع جایگزین + هیچ آهنگی در حال پخش نیست + در حال جستجوی منابع جایگزین... + هیچ منبع جایگزینی پیدا نشد + اطمینان: %1$d% + منبع فعال + + + بیرون کردن + او را از جلسه حذف کنید + بیرون کردن و جلوگیری از پیوستن دوباره + حذف پیشنهادها + حذف همهٔ آهنگ‌هایی که او به صف اضافه کرده است + تغییر ترتیب + حذف از صف + %1$s • افزوده‌شده توسط %2$s + + + ناشناس + در حال بارگیری پلی‌لیست... + افزودن آهنگ + افزودن آهنگ به پلی‌لیست + جستجوی آهنگ‌ها... + جستجو + هیچ آهنگی پیدا نشد + آهنگ‌هایی را برای افزودن جستجو کنید + تمام + افزودن + اضافه شد + + + در حال بارگیری آلبوم... + + + افزودن به پلی‌لیست شما + جستجو + جستجو در پلی‌لیست‌های شما... + در حال بارگیری پلی‌لیست‌های شما... + هنوز پلی‌لیستی ندارید.\nبرای افزودن آهنگ، یکی بسازید. + ساخت پلی‌لیست + ویرایش پلی‌لیست + ساختن + نام + نام پلی‌لیست + توضیحات + توضیحات (اختیاری) + عمومی + همه می‌توانند این پلی‌لیست را ببینند + مشارکتی + دیگران می‌توانند به این پلی‌لیست آهنگ اضافه کنند + تصویر کاور + کاور پلی‌لیست + بدون تصویر + تغییر تصویر + انتخاب تصویر + + + نصب + جایگزینی + plugin.json در فایل zip پیدا نشد + manifest.zipline.json در فایل zip پیدا نشد + قالب plugin.json نامعتبر است: %1$s + خواندن افزونه ناموفق بود: %1$s + نصب افزونه ناموفق بود: %1$s + نسخهٔ جدیدتری از %1$s در دسترس است. پیش از نصب، به‌روزرسانی را تأیید کنید. + %1$s از قبل نصب شده است. پیش از نصب، جایگزینی آن را تأیید کنید. + افزونه‌های داخلی از قبل وجود دارند و نمی‌توان دوباره اضافه‌شان کرد. + افزونه‌های داخلی را نمی‌توان حذف کرد. + %1$s یک apiVersion نامعتبر (%2$s) اعلام کرده است. نسخه‌بندی معنایی سازگار با %3$s انتظار می‌رود. + نسخهٔ API افزونهٔ Spotube (%1$s) نامعتبر است. + %1$s برای API افزونهٔ %2$s ساخته شده، اما Spotube از نسخهٔ اصلی API افزونهٔ %3$s پشتیبانی می‌کند. افزونه‌ای نصب کنید که برای همان نسخهٔ اصلی API ساخته شده باشد. + + + API افزونه سازگار نیست + افزونه نصب شود؟ + %1$s به افزونه‌های نصب‌شدهٔ شما اضافه می‌شود. + افزونه به‌روز شود؟ + نسخهٔ قدیمی‌تری نصب است (%1$s). به %2$s به‌روز شود؟ + افزونهٔ نصب‌شده جایگزین شود؟ + %1$s از یک نسخهٔ معنایی نامعتبر (%2$s) استفاده می‌کند و نمی‌توان آن را به‌عنوان به‌روزرسانی مقایسه کرد. با این حال افزونهٔ نصب‌شده جایگزین شود؟ + نسخهٔ نصب‌شده (%1$s) را نمی‌توان با نسخه‌بندی معنایی مقایسه کرد. با %2$s جایگزین شود؟ + نسخهٔ %1$s از قبل نصب است. افزونهٔ موجود با نسخهٔ ارائه‌شده جایگزین شود؟ + نسخهٔ نصب‌شدهٔ %1$s از نسخهٔ ارائه‌شدهٔ %2$s جدیدتر است. با این حال جایگزین شود؟ + افزونهٔ نصب‌شده با نسخهٔ ارائه‌شده جایگزین شود؟ + + + %1$s: + نسخه + نسخهٔ API + سازنده + مجوز + قابلیت‌ها + توانایی‌ها + مخزن + تماس + گزارش اشکال + حمایت از %1$s + + + رسمی + تأییدشده + ستاره‌های GitHub + URL مخزن GitHub + + + هیچ نسخه‌ای پیدا نشد + انتخاب نسخه + یک نسخه انتخاب کنید + پیش‌انتشار + یادداشت‌های انتشار + + + خانه + جستجو + روی دستگاه + اتصال + دستگاه‌ها + + + پنهان کردن پنجره + نمایش پنجره + آهنگ بعدی + آهنگ قبلی + پخش تصادفی + تکرار + تکرار: خاموش + تکرار: یک آهنگ + تکرار: همه + صدا: %1$d% + صدا + + صدا - + بی‌صدا + باصدا + پسندیدن آهنگ + لغو پسند آهنگ + خروج + + + کوچک کردن + بزرگ کردن + بازگرداندن + بستن + + + سرویس پخش + پخش + کنترل‌های پخش رسانه + + + مرور + در حال پخش + ویژه + آهنگ‌های ذخیره‌شده + آهنگ‌های برتر + diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml new file mode 100644 index 00000000..9c4bf680 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -0,0 +1,944 @@ + + + + File d\'attente + Vider la file d\'attente + Filtrer la file d\'attente... + La file d\'attente est vide + + Rechercher des titres, artistes, albums... + Effacer tout l\'historique + Rechercher... + + + Bibliothèque + Bibliothèque + Playlists + Albums + Artistes + Titres locaux + Téléchargements + Rechercher dans les playlists enregistrées… + Rechercher dans les albums enregistrés… + Rechercher dans les artistes enregistrés… + Rechercher des dossiers ou des titres… + Filtrer la bibliothèque + Fermer le filtre + Créer une playlist + Actualiser les titres locaux + Afficher en grille + Afficher en liste + Aucune playlist enregistrée + Aucune playlist ne correspond à « %1$s » + Aucun album enregistré + Aucun album ne correspond à « %1$s » + Aucun artiste enregistré + Aucun artiste ne correspond à « %1$s » + + + Dossiers + Dossier + %1$s • %2$s + + %1$d titre + %1$d titres + %1$d titres + + + %1$d dossier + %1$d dossiers + %1$d dossiers + + Retour aux dossiers + Lire le dossier + Accès à l\'audio requis + Autorisez l\'accès à l\'audio pour parcourir la musique de votre appareil. + Autoriser l\'accès + Aucun dossier local trouvé + Aucun titre dans ce dossier + Aucun dossier ou titre correspondant + Échec de l\'actualisation des médias locaux + + + %1$d en cours • %2$d terminé(s) + Effacer les terminés + Aucun téléchargement pour l\'instant + Téléchargement %1$d% + En attente + Terminé + Annulé + Échec + Échec : %1$s + Annuler le téléchargement + Relancer le téléchargement + Supprimer le téléchargement + + + Artiste + Sur liste noire + Titres likés + Vos titres enregistrés + Artiste inconnu + Titre du morceau + Nom de l\'artiste + + Sélectionné + + Paramètres + + Plugins + Langue et région + Apparence + Lecture + Téléchargements + Cache + Bureau + Mises à jour + Soutien + À propos + + Fermer + Enregistrer + Annuler + + Langue + Langue de l\'interface. Actuelle : %1$s + Langue du système + Pays + Pays utilisé pour le contenu et la disponibilité. Actuel : %1$s + %1$s (%2$s) + + Thème + Choisissez comment Spotube s\'adapte à l\'apparence du système. Actuel : %1$s + Clair + Sombre + Système + + Surface + Matériau d\'arrière-plan de l\'application. Actuel : %1$s + Aqueux + Givré + Chrome + Acrylique + Verre + + Couleur d\'accentuation + Palette actuelle : %1$s + Couleur d\'accentuation + Choisissez une palette d\'accentuation et prévisualisez-la en thème clair et sombre. + Clair + Sombre + Aperçu + Bouton + Bouffon vert + Violet électrique + Cyan océan + Orange couchant + Roseraie + Bleu nuit + Ardoise métallique + + Style et rendu + Matériau de surface et accentuation. Actuel : %1$s · %2$s + Style et rendu + Choisissez un matériau de surface, puis une couleur d\'accentuation assortie. Chaque option est une combinaison soigneusement choisie. + Surface + Accentuation + Secondaire + + Palette + Palette d\'accentuation Millennium. Actuelle : %1$s + Palette + Choisissez une palette d\'accentuation Millennium et prévisualisez-la en clair et en sombre. + Palette + Palette Millennium et accentuation. Actuelle : %1$s + Bleu + Orange + Citron vert + + Dossier de téléchargement personnalisé + Dossier actuel : %1$s + Dossier actuel : dossier Musique par défaut + Laissez vide pour utiliser le dossier Musique par défaut. + C:\Music\Spotube + Dossiers de médias locaux + Dossiers personnalisés à analyser sur ordinateur. Ajoutés : %1$d + Aucun dossier personnalisé ajouté + Dossiers personnalisés pour votre bibliothèque musicale locale. + Gérer + Ajouter un dossier + Supprimer le dossier + Aucun dossier personnalisé ajouté. + Retirer %1$s de la liste d\'analyse + Les dossiers de téléchargement et de cache musical sont inclus automatiquement. + Votre dossier de téléchargement est inclus automatiquement. + Le dossier de cache musical est inclus automatiquement. + Enregistrement… + Impossible d\'enregistrer les dossiers. Vos modifications sont conservées ; réessayez. + + %1$d dossier + %1$d dossiers + %1$d dossiers + + Format de téléchargement + Codec et conteneur préférés pour les téléchargements. Actuel : %1$s + Qualité de téléchargement + + Format de streaming + Codec et conteneur préférés pour le streaming. Actuel : %1$s + Qualité de streaming + Actuel : %1$s + Activer le cache musical + Stocker l\'audio diffusé en local pour accélérer les écoutes suivantes. + Dossier du cache + Actuel : %1$s + Actuel : cache par défaut de l\'application + Taille maximale du cache + Aucune limite + Limite actuelle : %1$d MB + Définissez la taille maximale du cache en MB. Saisissez 0 ou laissez vide pour aucune limite. + 500 + La taille doit être 0 ou un nombre positif. + Activer la lecture continue + Poursuivre la lecture avec des titres recommandés à la fin de la file d\'attente. + Activer Connect + Proposer des commandes de lecture à distance via la fonctionnalité Spotube Connect. + Port du serveur proxy de lecture + Autoriser le contrôle à distance + Permettre aux autres appareils du même réseau de contrôler la lecture lorsqu\'ils se connectent. + Nom de l\'appareil pour le contrôle à distance + Le nom que voient les autres appareils lorsqu\'ils détectent celui-ci. Actuel : %1$s + Choisissez un nom convivial à afficher aux autres appareils de votre réseau lorsqu\'ils détectent cette instance de Spotube. + p. ex. Spotube du salon + nom d\'hôte de l\'appareil + Port utilisé par le serveur proxy de lecture. Actuel : %1$d + Choisissez un port entre 1 et 65535. + 14769 + Saisissez un nombre entier valide. + Le port doit être compris entre 1 et 65535. + + Liste noire + Gérer les titres et artistes sur liste noire + + Réduire dans la zone de notification + Garder Spotube actif dans la zone de notification lorsque la fenêtre est réduite. + Statut enrichi Discord + Afficher votre lecture en cours dans Discord sur ordinateur. + + Rechercher automatiquement les mises à jour + Rechercher les nouvelles versions de Spotube en arrière-plan. + + Gérer les plugins + Gérez vos plugins + Plugin %1$s par défaut + Métadonnées + Audio + Paroles + Scrobbling + Aucun plugin sélectionné + Modifier + Sélectionner + Sélectionné + Effacer la sélection + Aucun plugin disponible + Plugin %1$s + Effacer + + Configurer + Gestionnaire de plugins + Veuillez saisir une URL + L\'URL doit commencer par http:// ou https:// + Échec du téléchargement du plugin + Aucun plugin installé pour l\'instant + Collez une URL de téléchargement ou choisissez un fichier ci-dessus. + + %1$d plugin installé + %1$d de plugins installés + %1$d plugins installés + + Installer un plugin + https://example.com/plugin.smplug + Télécharger + Installer depuis un fichier (.smplug) + Actif + v%1$s + Intégré + Supprimer le plugin + Se connecter + Se déconnecter + Télécharger depuis une URL + Installer depuis un fichier + Installer + + • %1$s + Autorisations demandées + Ce plugin ne nécessite aucune autorisation particulière. + Installé ou fourni + Installé + Fourni + API %1$s → %2$s + Stockage persistant + Lire et écrire des données sur le disque + Requêtes réseau + Envoyer et recevoir des données sur Internet + WebView + Afficher du contenu web dans l\'application + Jam de groupe + Broker MQTT + Hôte du broker + %1$s:%2$d + Port du broker + Utiliser TLS + Nom d\'utilisateur (facultatif) + Mot de passe (facultatif) + Préfixe de l\'ID client + Sert à identifier cette instance de Spotube + Chiffrer la connexion au broker + Vérifiez les paramètres actuels du broker avant de créer une salle. + Tester la connexion + Test en cours… + Connecté en %1$d ms + Échec : %1$s + Broker temporaire : configurez le vôtre pour l\'auto-héberger + Connexion + Identifiants + Identité + Diagnostic + + + Jam de groupe + Créer + Rejoindre + Créez une salle, puis partagez son code avec vos amis. + Créer une salle + Rejoindre la salle + Connexion… + Code de la salle + ABC123 + Broker : %1$s + Configurez un broker dans les Paramètres pour créer ou rejoindre une salle. + Paramètres du broker + Connecté + Reconnexion… + Copier le code + Copié ! + Partager + Participants (%1$d) + Hôte + Auditeur + En attente d\'auditeurs… + Retirer + Bannir + Gérer + Quitter la salle + Connexion impossible + Ignorer + Échec de la création de la session Jam + Impossible de rejoindre la session Jam + Saisissez un code de salle de six caractères (lettres et chiffres). + Rejoignez ma Jam Spotube avec le code : %1$s + + Accéder à l\'album + Minuteur de veille + Détails du titre + Lecture + Pause + Aucune lecture en cours + Ajouter à la file d\'attente + + Ignorer + Mettre à jour + Spotube %1$s est disponible + Découvrez les nouveautés de cette version sur la page de téléchargement de Spotube. + %1$s %2$s est disponible + Version installée : %1$s + Découvrez les nouveautés de cette mise à jour sur la page du plugin. + + Passer + Suivant + Retour + Commencer + Spotube + La musique, sans contraintes. + Un lecteur open source pour tout un monde de musique.\nÀ vous de l\'explorer. À vous de vous l\'approprier. + Faites vivre ce projet. + Spotube est créé par des personnes, pas par une entreprise d\'abonnements. S\'il fait partie de votre quotidien, un petit soutien nous aide à lui accorder l\'attention qu\'il mérite.\n\nMerci de faire une place aux logiciels indépendants. + Commencez par un plugin de catalogue pour trouver des titres et des playlists. Choisissez un plugin audio pour les écouter. Les paroles et l\'historique d\'écoute sont facultatifs. Découvrez un plugin ci-dessous, vérifiez ses autorisations, installez-le, puis sélectionnez-le ici. + Découvrir des plugins + Vos sources + Réessayer + Plus de plugins + Aucun nouveau plugin trouvé. Vous pouvez aussi en installer un depuis un lien ou un fichier .smplug ci-dessus. + La découverte de plugins est indisponible pour le moment. Réessayez, ou installez-en un depuis un lien ou un fichier. + Bienvenue + Soutien + Plugins + Guide + + Comment utiliser Spotube + Quatre étapes rapides et c\'est parti. + Se connecter + Connectez votre compte Spotify ou choisissez votre propre plugin de catalogue. + Rechercher et écouter + Trouvez n\'importe quel titre, album ou artiste, puis lancez la lecture. + Écouter hors connexion + Téléchargez des titres pour continuer à écouter sans connexion. + Suivre les paroles + Ouvrez les paroles synchronisées depuis le lecteur pendant la lecture d\'un titre. + + + Votre musique. Vos sources. Vos règles. + Un lecteur de musique gratuit et open source, créé par une communauté qui aime tout simplement la musique. + Open source + Sans publicité + Sans télémétrie + Basé sur des plugins + C\'est parti + + + Nos soutiens nous font avancer + Créé avec passion. Maintenu en vie grâce à vous. + Spotube est 100 % gratuit, libre et open source — sans publicité, sans abonnement, sans pistage. Chaque fonctionnalité existe parce que des personnes comme vous ont choisi d\'aider à faire avancer le code. + Patreon + Soutien mensuel + Soutenez le développement et l\'infrastructure dans la durée + Buy Me a Coffee + Un café ponctuel + Alimentez une session de code nocturne avec un petit pourboire + Open Collective + Fonds communautaire + Un financement transparent, géré par la communauté + Merci du fond du cœur de croire en la musique libre. + Le soutien est entièrement facultatif — Spotube reste gratuit dans tous les cas. + Soutenir + Ouvrir %1$s + + + Soutenir Spotube + Soutenir Spotube + Spotube est gratuit, libre et open source — sans publicité, sans abonnement, sans pistage. S\'il vous apporte de la joie, pensez à soutenir le projet. + Vous pouvez désactiver ces rappels dans Paramètres → Soutien. + Plus tard + Rappels de soutien + Afficher un petit rappel pour soutenir le projet tous les 7 jours. + + + Architecture modulaire + Choisissez d\'où vient votre musique. + Spotube sépare la découverte du catalogue, le streaming audio et les paroles synchronisées. Configurez vos moteurs ci-dessous ou changez-les à tout moment. + Moteur de métadonnées + Gère la découverte musicale, la recherche, les listes de titres et les playlists. + Moteur audio + Récupère et décode les flux audio lus. + Moteur de paroles + Diffuse des paroles karaoké synchronisées en temps réel. + Moteur de scrobbling + Enregistre l\'historique d\'écoute sur Last.fm ou ListenBrainz. + Actif + Intégré + Prêt + Facultatif + Sélectionnez un moteur + Moteurs installés + Définir par défaut + NewPipe YouTube et LRCLib Lyrics sont intégrés et prêts à l\'emploi. + Gérez ou ajoutez des plugins de la communauté à tout moment dans Paramètres → Plugins. + + + Architecture rétro-moderne + Hi-Fi pure, zéro intermédiaire + Voici comment Spotube orchestre une lecture décentralisée directement sur votre machine. + PAIR-À-PAIR DIRECT • OPUS 256 KBPS + L\'heure bleue + Spotube · démonstration du lecteur + Un peu de musique pour les moments entre deux. + 1. Découverte décentralisée + Les requêtes vont directement au plugin de catalogue choisi. Spotube ne profile jamais vos goûts. + 2. Résolution directe des flux + L\'audio est diffusé directement des serveurs de contenu vers votre machine. Aucun proxy ni collecte de données à distance. + 3. DSP et lecture en local + Décodage et mise en cache en local, avec égaliseur et téléchargement hors connexion facultatif. + 4. Paroles synchronisées et Jam + Les paroles karaoké suivent la position de lecture, avec contrôle à distance en LAN et Jam de groupe. + Aucun serveur intermédiaire. La lecture se fait directement sur votre appareil — confidentielle par conception. + + + Choisir un appareil + %1$s : « %2$s » + Lire + Ajouter à la file d\'attente + Lire ensuite + Cet appareil + Sur cet appareil + Appareil distant + Appareil distant (%1$s) + Sur l\'appareil connecté + + + Échec de la connexion + Connexion perdue + Échec de l\'envoi + Échec de la détection : %1$s + Échec de la connexion : %1$s + + + Appareils + Actualiser + Connecté à %1$s:%2$d + Se déconnecter + Connexion... + Recherche d\'appareils sur le réseau... + Aucun appareil trouvé + Assurez-vous que l\'option \"Autoriser le contrôle à distance\" est activée dans les paramètres de l\'autre appareil. + Appareil inconnu + Résolution... + ID : %1$s... + + + Contrôle à distance + Déconnecté + Erreur de connexion : %1$s + Pochette d\'album + Titre inconnu + Artiste inconnu + Avancer/reculer + Lecture aléatoire + Précédent + Suivant + Mode de répétition + Volume + Retirer de la file d\'attente + Plus d\'options + + + Où écouter ? + Une session Jam est en cours. Choisissez où lire cette collection. + Lire ici + Proposer à la Jam + + + Aucun broker Jam configuré + L\'hôte du broker est vide + Échec de la connexion + Connexion perdue + + + Broker + anonyme + + + Ajouté à la file d\'attente de la Jam + + %1$d titre ajouté à la file d\'attente de la Jam + %1$d titres ajoutés à la file d\'attente de la Jam + %1$d titres ajoutés à la file d\'attente de la Jam + + + + Demande de contrôle à distance + \"%1$s\" souhaite contrôler la lecture sur cet appareil. + Refuser + Autoriser + Toujours autoriser + + + Résultats de recherche + Tout + Titres + Utilisateurs + Aucun type de recherche disponible + Commencez à taper pour rechercher + Commencez à taper pour rechercher des titres + Aucun résultat + Tout afficher + Recherche en cours + Chargement des titres... + Chargement des albums... + Chargement des artistes... + Chargement des playlists... + Chargement des utilisateurs... + Aucun album trouvé + Aucun artiste trouvé + Aucune playlist trouvée + Aucun utilisateur trouvé + On fouille dans les bacs... + On explore le cosmos... + On fait chauffer les enceintes... + On dépoussière les vinyles... + On consulte les dieux de la musique... + On règle les antennes... + On feuillette les disques... + Impossible de charger les fonctions de recherche + Échec de la recherche + Échec de la recherche de titres + Échec de la recherche d\'albums + Échec de la recherche d\'artistes + Échec de la recherche de playlists + Échec de la recherche d\'utilisateurs + + + Parcourir + + + Synchronisées + Texte brut + Aucunes paroles synchronisées disponibles + Aucunes paroles disponibles + + + %1$s dans %2$s + %1$d kbps + %1$s kHz • %2$d canaux + Aucun plugin de métadonnées sélectionné + + + Retour + Réessayer + Fermer la boîte de dialogue + Chargement... + Inconnu + Erreur inconnue + + + Utilisateur + Playlist + + + Nous sommes vraiment désolés + Quelque chose ne fonctionne pas pour le moment. Votre expérience compte pour nous et nous faisons de notre mieux pour corriger le problème. Veuillez réessayer ou consulter les détails de l\'erreur ci-dessous. + Détails de l\'erreur + Copier dans le presse-papiers + Voir le journal complet + Journal d\'erreurs complet + + + Lecture aléatoire + Lecture aléatoire + Suivre + Ne plus suivre + Modifier la playlist + Par %1$s + + + Options du titre + Ajouter à la Jam + Lancer la radio + Lire ensuite + Placer en suivant + Retirer de la file d\'attente + Retirer des favoris + Ajouter aux favoris + Ajouter à une playlist + Télécharger + Partager + + + Ordre d\'origine + Titre + Artiste + Album + Durée + Trier + Aucun titre trouvé + Filtrer + Actions groupées + Tout télécharger + Télécharger %1$d + Tout ajouter à la file d\'attente + Ajouter %1$d à la file d\'attente + Tout lire ensuite + Lire %1$d ensuite + Tout ajouter à une playlist + Ajouter %1$d à une playlist + Tout ajouter à la Jam + Ajouter %1$d à la Jam + +%1$d autres + + + Aucune URL à charger + Page précédente + Page suivante + + + Filtrer la liste noire... + Aucun élément sur liste noire + Aucun élément sur liste noire ne correspond à « %1$s » + Titres + Titre + Ajouter à la liste noire + Retirer de la liste noire + + + Aucune métadonnée pour ce titre + Impossible de résoudre l\'URL du flux + Impossible de résoudre l\'URL du flux. Essayez d\'abord de lire le titre. + + + Titres enregistrés + Vous + + + Abonné + Titres populaires + Lire les titres populaires + Ajouter les titres populaires à la file d\'attente + Artistes similaires + Playlists à la une + Impossible de charger la présentation de l\'artiste + Abonnés indisponibles + %1$s k abonnés + %1$s M d\'abonnés + %1$s Md d\'abonnés + + %1$d abonné + %1$d d\'abonnés + %1$d abonnés + + + + J\'aime + Je n\'aime plus + Titre du morceau indisponible + Artistes indisponibles + Album inconnu + Position de lecture + Volume + Activer la lecture aléatoire + Désactiver la lecture aléatoire + Précédent + Suivant + Lecture ou pause + Répétition désactivée + Répéter le titre + Tout répéter + Couper le son + Réactiver le son + Plus d\'options + Options du lecteur + Réduire le lecteur + Agrandir les paroles + En cours de lecture + Paroles + Source alternative + Source alternative + Télécharger + Téléchargé + Téléchargement... + Déjà téléchargé + Nouvelle tentative de téléchargement... + Téléchargement de %1$s + La lecture sera mise en pause dans %1$s. + Mettre la lecture en pause après le nombre de minutes choisi. + Minutes + Entre 1 et 600 minutes + Annuler le minuteur + Démarrer + L\'URL de lecture a été résolue avec d\'autres paramètres de streaming. Le flux mis en évidence correspond à votre préférence actuelle ; l\'URL active sera actualisée à la prochaine demande de flux. + Source audio associée + Chargement de la source associée… + Aucune source distante associée n\'est nécessaire pendant la lecture du fichier en cache local. + Aucune source associée disponible. + Aucune source audio associée n\'est en cache pour ce titre. + Les détails du flux ne sont pas encore en cache. Lisez d\'abord le titre pour les charger. + Impossible de charger les détails du titre. + Flux disponibles + Ce titre est lu depuis le cache local. Les variantes de flux ne sont pas utilisées pour cette lecture. + Chargement des flux… + Aucun flux disponible pour cette source. + Flux actuellement sélectionné + Correspond à vos préférences de format et de qualité de streaming + URL de lecture active + Lu depuis le cache local + Titre (métadonnées) + Titre + ID + Album + Fiabilité + URI externe + ID de l\'album + Type d\'album + Date de sortie de l\'album + Nombre de titres de l\'album + Genres de l\'album + Durée + Numéro de piste + Numéro de disque + Explicite + Popularité + ISRC + URL des miniatures + + + Afficher/masquer la barre latérale + + + Sources alternatives + Aucun titre en cours de lecture + Recherche de sources alternatives... + Aucune source alternative trouvée + Fiabilité : %1$d% + Source active + + + Exclure + Le retirer de la session + L\'exclure et l\'empêcher de revenir + Retirer ses suggestions + Retirer tous les titres qu\'il a ajoutés à la file d\'attente + Réorganiser + Retirer de la file d\'attente + %1$s • Ajouté par %2$s + + + Inconnu + Chargement de la playlist... + Ajouter des titres + Ajouter des titres à la playlist + Rechercher des titres... + Rechercher + Aucun titre trouvé + Recherchez des titres à ajouter + OK + Ajouter + Ajouté + + + Chargement de l\'album... + + + Ajouter à une de vos playlists + Rechercher + Rechercher dans vos playlists... + Chargement de vos playlists... + Vous n\'avez encore aucune playlist.\nCréez-en une pour y ajouter des titres. + Créer une playlist + Modifier la playlist + Créer + Nom + Nom de la playlist + Description + Description (facultatif) + Publique + Tout le monde peut voir cette playlist + Collaborative + D\'autres personnes peuvent ajouter des titres à cette playlist + Image de couverture + Couverture de la playlist + Aucune image + Changer l\'image + Choisir une image + + + Installer + Remplacer + plugin.json introuvable dans le fichier zip + manifest.zipline.json introuvable dans le fichier zip + Format de plugin.json non valide : %1$s + Impossible de lire le plugin : %1$s + Impossible d\'installer le plugin : %1$s + Une version plus récente de %1$s est disponible. Confirmez la mise à jour avant l\'installation. + %1$s est déjà installé. Confirmez le remplacement avant l\'installation. + Les plugins intégrés sont déjà inclus et ne peuvent pas être ajoutés à nouveau. + Les plugins intégrés ne peuvent pas être supprimés. + %1$s déclare une apiVersion non valide (%2$s). Une version sémantique compatible avec %3$s est attendue. + La version %1$s de l\'API de plugins de Spotube n\'est pas valide. + %1$s cible l\'API de plugins %2$s, mais Spotube prend en charge la version majeure %3$s de l\'API de plugins. Installez un plugin conçu pour la même version majeure de l\'API. + + + API de plugin incompatible + Installer le plugin ? + %1$s sera ajouté à vos plugins installés. + Mettre à jour le plugin ? + Une version plus ancienne est installée (%1$s). Mettre à jour vers la version %2$s ? + Remplacer le plugin installé ? + %1$s utilise une version sémantique non valide (%2$s) et ne peut pas être comparé comme une mise à jour. Remplacer quand même le plugin installé ? + La version installée (%1$s) ne peut pas être comparée selon la gestion sémantique de version. La remplacer par %2$s ? + La version %1$s est déjà installée. Remplacer le plugin existant par la copie fournie ? + La version installée %1$s est plus récente que la version fournie %2$s. La remplacer quand même ? + Remplacer le plugin installé par la copie fournie ? + + + %1$s : + Version + Version de l\'API + Auteur + Licence + Capacités + Fonctions + Dépôt + Contact + Signaler un bug + Soutenir %1$s + + + Officiel + Vérifié + Étoiles GitHub + URL du dépôt GitHub + + + Aucune version trouvée + Choisir une version + Sélectionnez une version + Préversion + Notes de version + + + Accueil + Rechercher + Sur l\'appareil + Connect + Appareils + + + Masquer la fenêtre + Afficher la fenêtre + Titre suivant + Titre précédent + Lecture aléatoire + Répétition + Répétition : DÉSACTIVÉE + Répétition : TITRE + Répétition : TOUT + Volume : %1$d% + Volume + + Volume - + Couper le son + Réactiver le son + Aimer le titre + Ne plus aimer le titre + Quitter + + + Réduire + Agrandir + Restaurer + Fermer + + + Service de lecture + Lecture + Commandes de lecture multimédia + + + Parcourir + En cours de lecture + À la une + Titres enregistrés + Titres populaires + diff --git a/composeApp/src/commonMain/composeResources/values-hi/strings.xml b/composeApp/src/commonMain/composeResources/values-hi/strings.xml new file mode 100644 index 00000000..569467ed --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-hi/strings.xml @@ -0,0 +1,937 @@ + + + कतार + कतार साफ़ करें + कतार फ़िल्टर करें... + कतार में कुछ नहीं है + + गाने, कलाकार, एल्बम खोजें... + पूरा इतिहास साफ़ करें + खोजें... + + + आपकी लाइब्रेरी + लाइब्रेरी + प्लेलिस्ट + एल्बम + कलाकार + लोकल ट्रैक + डाउनलोड + सेव की गई प्लेलिस्ट खोजें… + सेव किए गए एल्बम खोजें… + सेव किए गए कलाकार खोजें… + फ़ोल्डर या ट्रैक खोजें… + लाइब्रेरी फ़िल्टर करें + फ़िल्टर बंद करें + प्लेलिस्ट बनाएं + लोकल ट्रैक रीफ़्रेश करें + ग्रिड व्यू पर जाएं + लिस्ट व्यू पर जाएं + कोई सेव की गई प्लेलिस्ट नहीं + “%1$s” से मेल खाती कोई प्लेलिस्ट नहीं + कोई सेव किया गया एल्बम नहीं + “%1$s” से मेल खाता कोई एल्बम नहीं + कोई सेव किया गया कलाकार नहीं + “%1$s” से मेल खाता कोई कलाकार नहीं + + + फ़ोल्डर + फ़ोल्डर + %1$s • %2$s + + %1$d ट्रैक + %1$d ट्रैक + + + %1$d फ़ोल्डर + %1$d फ़ोल्डर + + फ़ोल्डर पर वापस जाएं + फ़ोल्डर चलाएं + ऑडियो एक्सेस ज़रूरी है + अपने डिवाइस का संगीत देखने के लिए ऑडियो एक्सेस की अनुमति दें। + एक्सेस दें + कोई लोकल फ़ोल्डर नहीं मिला + इस फ़ोल्डर में कोई ट्रैक नहीं है + कोई मेल खाता फ़ोल्डर या ट्रैक नहीं + लोकल मीडिया रीफ़्रेश नहीं हो सका + + + %1$d जारी • %2$d पूरे + पूरे हुए हटाएं + अभी कोई डाउनलोड नहीं + डाउनलोड हो रहा है %1$d% + कतार में + पूरा हुआ + रद्द किया गया + विफल + विफल: %1$s + डाउनलोड रद्द करें + फिर से डाउनलोड करें + डाउनलोड हटाएं + + + कलाकार + ब्लैकलिस्ट में + पसंद किए गए ट्रैक + आपके सेव किए गए गाने + अज्ञात कलाकार + ट्रैक का नाम + कलाकार का नाम + + चुना गया + + सेटिंग + + प्लगइन + भाषा और क्षेत्र + दिखावट + प्लेबैक + डाउनलोड + कैशिंग + डेस्कटॉप + अपडेट + सहयोग + जानकारी + + बंद करें + सेव करें + रद्द करें + + भाषा + ऐप इंटरफ़ेस की भाषा। मौजूदा: %1$s + सिस्टम डिफ़ॉल्ट + देश + कंटेंट और उपलब्धता के लिए क्षेत्रीय देश। मौजूदा: %1$s + %1$s (%2$s) + + थीम + चुनें कि Spotube आपके सिस्टम की दिखावट से कैसे मेल खाए। मौजूदा: %1$s + लाइट + डार्क + सिस्टम + + सरफ़ेस + ऐप का बैकग्राउंड मटीरियल। मौजूदा: %1$s + जलीय + बर्फ़ीला + क्रोम + एक्रिलिक + ग्लास + + एक्सेंट रंग + मौजूदा पैलेट: %1$s + एक्सेंट रंग + एक्सेंट पैलेट चुनें और उसे लाइट और डार्क दोनों थीम में देखें। + लाइट + डार्क + प्रीव्यू + बटन + ग्रीन गॉब्लिन + इलेक्ट्रिक वायलेट + ओशनिक सियान + सनसेट ऑरेंज + रोज़ गार्डन + मिडनाइट ब्लू + मेटैलिक स्लेट + + लुक और फ़ील + सरफ़ेस मटीरियल और एक्सेंट। मौजूदा: %1$s · %2$s + लुक और फ़ील + एक सरफ़ेस मटीरियल चुनें, फिर उसके साथ जंचने वाला एक्सेंट। हर विकल्प एक चुना हुआ कॉम्बिनेशन है। + सरफ़ेस + एक्सेंट + सेकेंडरी + + पैलेट + Millennium एक्सेंट पैलेट। मौजूदा: %1$s + पैलेट + Millennium एक्सेंट पैलेट चुनें और उसे लाइट और डार्क में देखें। + पैलेट + Millennium पैलेट और एक्सेंट। मौजूदा: %1$s + नीला + नारंगी + लाइम + + डाउनलोड फ़ोल्डर बदलें + मौजूदा फ़ोल्डर: %1$s + मौजूदा फ़ोल्डर: डिफ़ॉल्ट म्यूज़िक फ़ोल्डर + डिफ़ॉल्ट म्यूज़िक फ़ोल्डर इस्तेमाल करने के लिए इसे खाली छोड़ें। + C:\Music\Spotube + लोकल मीडिया फ़ोल्डर + डेस्कटॉप पर स्कैन करने के लिए कस्टम फ़ोल्डर। जोड़े गए: %1$d + अभी तक कोई कस्टम फ़ोल्डर नहीं जोड़ा गया + आपकी लोकल म्यूज़िक लाइब्रेरी के लिए कस्टम फ़ोल्डर। + मैनेज करें + फ़ोल्डर जोड़ें + फ़ोल्डर हटाएं + कोई कस्टम फ़ोल्डर नहीं जोड़ा गया। + %1$s को स्कैन सूची से हटाएं + डाउनलोड और म्यूज़िक कैश फ़ोल्डर अपने-आप शामिल होते हैं। + आपका डाउनलोड फ़ोल्डर अपने-आप शामिल होता है। + म्यूज़िक कैश फ़ोल्डर अपने-आप शामिल होता है। + सेव हो रहा है… + फ़ोल्डर सेव नहीं हो सके। आपके बदलाव अभी भी यहीं हैं; फिर से कोशिश करें। + + %1$d फ़ोल्डर + %1$d फ़ोल्डर + + डाउनलोड म्यूज़िक फ़ॉर्मैट + डाउनलोड के लिए पसंदीदा कोडेक और कंटेनर। मौजूदा: %1$s + डाउनलोड म्यूज़िक क्वालिटी + + स्ट्रीमिंग म्यूज़िक फ़ॉर्मैट + म्यूज़िक स्ट्रीमिंग के लिए पसंदीदा कोडेक और कंटेनर। मौजूदा: %1$s + स्ट्रीमिंग म्यूज़िक क्वालिटी + मौजूदा: %1$s + म्यूज़िक कैशिंग चालू करें + दोबारा तेज़ी से चलाने के लिए स्ट्रीम किया गया ऑडियो डिवाइस पर सेव करें। + कैश फ़ोल्डर + मौजूदा: %1$s + मौजूदा: डिफ़ॉल्ट ऐप्लिकेशन कैश + कैश साइज़ सीमा + कोई सीमा नहीं + मौजूदा सीमा: %1$d MB + MB में अधिकतम कैश साइज़ सेट करें। कोई सीमा न रखने के लिए 0 डालें या खाली छोड़ें। + 500 + साइज़ 0 या कोई पॉज़िटिव संख्या होनी चाहिए। + लगातार प्लेबैक चालू करें + कतार खत्म होने पर सुझाए गए ट्रैक के साथ प्लेबैक जारी रखें। + Connect चालू करें + Spotube Connect फ़ीचर के ज़रिए रिमोट प्लेबैक कंट्रोल उपलब्ध कराएं। + प्लेबैक प्रॉक्सी सर्वर पोर्ट + रिमोट कंट्रोल की अनुमति दें + एक ही नेटवर्क के दूसरे डिवाइस कनेक्ट होने पर प्लेबैक कंट्रोल कर सकें। + रिमोट कंट्रोल डिवाइस का नाम + यह नाम दूसरे डिवाइस को इस डिवाइस को खोजते समय दिखता है। मौजूदा: %1$s + एक आसान नाम चुनें, जो आपके नेटवर्क के दूसरे डिवाइस को यह Spotube खोजते समय दिखे। + जैसे लिविंग रूम Spotube + डिवाइस होस्टनेम + प्लेबैक प्रॉक्सी सर्वर का पोर्ट। मौजूदा: %1$d + 1 से 65535 के बीच कोई पोर्ट चुनें। + 14769 + एक मान्य पूर्ण संख्या डालें। + पोर्ट 1 से 65535 के बीच होना चाहिए। + + ब्लैकलिस्ट + ब्लैकलिस्ट किए गए ट्रैक और कलाकार मैनेज करें + + ट्रे में छोटा करें + विंडो छोटी करने पर Spotube को सिस्टम ट्रे में चलता रहने दें। + Discord रिच प्रेज़ेंस + डेस्कटॉप पर Discord में आपका मौजूदा प्लेबैक स्टेटस दिखाएं। + + अपडेट अपने-आप जांचें + बैकग्राउंड में Spotube के नए रिलीज़ देखें। + + प्लगइन मैनेज करें + अपने प्लगइन मैनेज करें + डिफ़ॉल्ट %1$s प्लगइन + मेटाडेटा + ऑडियो + लिरिक्स + स्क्रॉबल + कोई प्लगइन नहीं चुना गया + बदलें + चुनें + चुना गया + चुनाव हटाएं + कोई प्लगइन उपलब्ध नहीं + %1$s प्लगइन + हटाएं + + कॉन्फ़िगर करें + प्लगइन मैनेजर + कृपया URL डालें + URL http:// या https:// से शुरू होना चाहिए + प्लगइन डाउनलोड नहीं हो सका + अभी तक कोई प्लगइन इंस्टॉल नहीं है + डाउनलोड URL पेस्ट करें या ऊपर से कोई फ़ाइल चुनें। + + %1$d प्लगइन इंस्टॉल है + %1$d प्लगइन इंस्टॉल हैं + + प्लगइन इंस्टॉल करें + https://example.com/plugin.smplug + डाउनलोड करें + फ़ाइल से इंस्टॉल करें (.smplug) + सक्रिय + v%1$s + बिल्ट-इन + प्लगइन हटाएं + लॉग इन करें + लॉग आउट करें + URL से डाउनलोड करें + फ़ाइल से इंस्टॉल करें + इंस्टॉल करें + + • %1$s + मांगी गई अनुमतियां + इस प्लगइन को किसी खास अनुमति की ज़रूरत नहीं है। + इंस्टॉल बनाम दिया गया + इंस्टॉल किया गया + दिया गया + API %1$s → %2$s + स्थायी स्टोरेज + डिस्क पर डेटा पढ़ें और लिखें + नेटवर्क अनुरोध + इंटरनेट पर डेटा भेजें और पाएं + WebView + ऐप के अंदर वेब कंटेंट दिखाएं + ग्रुप जैम + MQTT ब्रोकर + ब्रोकर होस्ट + %1$s:%2$d + ब्रोकर पोर्ट + TLS इस्तेमाल करें + यूज़रनेम (वैकल्पिक) + पासवर्ड (वैकल्पिक) + क्लाइंट ID प्रीफ़िक्स + इस Spotube की पहचान के लिए इस्तेमाल होता है + ब्रोकर कनेक्शन एन्क्रिप्ट करें + रूम शुरू करने से पहले मौजूदा ब्रोकर सेटिंग जांचें। + कनेक्शन टेस्ट करें + टेस्ट हो रहा है… + %1$d ms में कनेक्ट हुआ + विफल: %1$s + प्लेसहोल्डर ब्रोकर — खुद होस्ट करने के लिए अपना ब्रोकर सेट करें + कनेक्शन + क्रेडेंशियल + पहचान + डायग्नोस्टिक्स + + + ग्रुप जैम + बनाएं + जुड़ें + एक रूम बनाएं, फिर उसका कोड अपने दोस्तों के साथ शेयर करें। + रूम बनाएं + रूम से जुड़ें + कनेक्ट हो रहा है… + रूम कोड + ABC123 + ब्रोकर: %1$s + रूम होस्ट करने या उससे जुड़ने के लिए सेटिंग में ब्रोकर सेट करें। + ब्रोकर सेटिंग + कनेक्ट है + फिर से कनेक्ट हो रहा है… + कोड कॉपी करें + कॉपी हो गया! + शेयर करें + सदस्य (%1$d) + होस्ट + श्रोता + श्रोताओं का इंतज़ार है… + हटाएं + बैन करें + मैनेज करें + रूम छोड़ें + कनेक्ट नहीं हो सका + ख़ारिज करें + जैम सेशन नहीं बन सका + जैम सेशन से नहीं जुड़ सके + अक्षरों और अंकों वाला छह अक्षर का रूम कोड डालें। + मेरे Spotube जैम से जुड़ें, कोड: %1$s + + एल्बम पर जाएं + स्लीप टाइमर + ट्रैक की जानकारी + चलाएं + रोकें + कुछ नहीं चल रहा + कतार में जोड़ें + + अनदेखा करें + अपडेट करें + Spotube %1$s उपलब्ध है + इस रिलीज़ में क्या नया है, यह Spotube डाउनलोड पेज पर देखें। + %1$s %2$s उपलब्ध है + अभी इंस्टॉल है: %1$s + इस अपडेट में क्या नया है, यह प्लगइन के पेज पर देखें। + + छोड़ें + आगे + पीछे + शुरू करें + Spotube + संगीत, बिना किसी बंधन के। + संगीत की दुनिया के लिए एक ओपन-सोर्स प्लेयर।\nखोजिए अपने तरीके से। बनाइए इसे अपना। + इस प्रोजेक्ट को चलते रहने दें। + Spotube को लोग बनाते हैं, कोई सब्सक्रिप्शन बिज़नेस नहीं। अगर यह आपकी रोज़ की ज़िंदगी का हिस्सा बन जाए, तो थोड़ा सा सहयोग हमें इसकी वह देखभाल करने में मदद करता है जिसका यह हकदार है।\n\nस्वतंत्र सॉफ़्टवेयर को जगह देने के लिए धन्यवाद। + गाने और प्लेलिस्ट ढूंढने के लिए किसी कैटलॉग प्लगइन से शुरुआत करें। उन्हें चलाने के लिए एक ऑडियो प्लगइन चुनें। लिरिक्स और सुनने का इतिहास वैकल्पिक हैं। नीचे कोई प्लगइन खोजें, उसकी अनुमतियां देखें, इंस्टॉल करें, फिर उसे यहां चुनें। + प्लगइन खोजें + आपके सोर्स + फिर से कोशिश करें + और प्लगइन + कोई नया प्लगइन नहीं मिला। आप ऊपर दिए लिंक या .smplug फ़ाइल से भी इंस्टॉल कर सकते हैं। + प्लगइन खोज अभी उपलब्ध नहीं है। फिर से कोशिश करें, या लिंक या फ़ाइल से इंस्टॉल करें। + स्वागत + सहयोग + प्लगइन + गाइड + + Spotube कैसे इस्तेमाल करें + चार आसान स्टेप और आप सुनना शुरू कर सकते हैं। + साइन इन करें + अपना Spotify अकाउंट कनेक्ट करें, या अपना कैटलॉग प्लगइन चुनें। + खोजें और चलाएं + कोई भी गाना, एल्बम या कलाकार खोजें, फिर प्ले दबाएं। + ऑफ़लाइन सुनें + बिना कनेक्शन के सुनते रहने के लिए ट्रैक डाउनलोड करें। + लिरिक्स के साथ गाएं + ट्रैक चलते समय प्लेयर से सिंक्ड लिरिक्स खोलें। + + + आपका संगीत। आपके सोर्स। आपके नियम। + एक मुफ़्त, ओपन-सोर्स म्यूज़िक प्लेयर, जिसे संगीत से प्यार करने वाली कम्युनिटी ने बनाया है। + ओपन सोर्स + कोई विज्ञापन नहीं + कोई टेलीमेट्री नहीं + प्लगइन से चलने वाला + चलिए शुरू करें + + + सहयोगियों से हम चलते हैं + जुनून से बना। आपसे ज़िंदा। + Spotube 100% मुफ़्त, स्वतंत्र और ओपन सोर्स है — न विज्ञापन, न सब्सक्रिप्शन, न ट्रैकिंग। हर फ़ीचर इसलिए है क्योंकि आप जैसे लोगों ने कोड को आगे बढ़ाने में मदद करना चुना। + Patreon + मासिक स्पॉन्सर + लगातार डेवलपमेंट और इंफ्रास्ट्रक्चर में सहयोग करें + Buy Me a Coffee + एक बार की कॉफ़ी + एक छोटी सी टिप से देर रात की कोडिंग को ऊर्जा दें + Open Collective + कम्युनिटी फ़ंड + पारदर्शी, कम्युनिटी द्वारा संचालित आर्थिक सहयोग + ओपन म्यूज़िक पर भरोसा करने के लिए दिल से धन्यवाद। + सहयोग करना पूरी तरह वैकल्पिक है — Spotube हर हाल में मुफ़्त रहेगा। + स्पॉन्सर करें + %1$s खोलें + + + Spotube को सहयोग दें + Spotube को सहयोग दें + Spotube मुफ़्त, स्वतंत्र और ओपन सोर्स है — न विज्ञापन, न सब्सक्रिप्शन, न ट्रैकिंग। अगर यह आपको खुशी देता है, तो प्रोजेक्ट को सहयोग देने के बारे में सोचें। + आप ये रिमाइंडर सेटिंग → सहयोग में बंद कर सकते हैं। + बाद में + सहयोग रिमाइंडर + हर 7 दिन में प्रोजेक्ट को सहयोग देने का एक छोटा सा रिमाइंडर दिखाएं। + + + मॉड्यूलर आर्किटेक्चर + चुनें कि आपका संगीत कहां से आए। + Spotube कैटलॉग खोज, ऑडियो स्ट्रीमिंग और सिंक्ड लिरिक्स को अलग रखता है। नीचे अपने इंजन सेट करें या उन्हें कभी भी बदलें। + मेटाडेटा इंजन + म्यूज़िक खोज, सर्च, ट्रैक लिस्टिंग और प्लेलिस्ट को चलाता है। + ऑडियो इंजन + चलने वाली ऑडियो स्ट्रीम लाता और डिकोड करता है। + लिरिक्स इंजन + समय के साथ सिंक कराओके लिरिक्स रियल टाइम में दिखाता है। + स्क्रॉबल इंजन + सुनने का इतिहास Last.fm या ListenBrainz पर दर्ज करता है। + सक्रिय + बिल्ट-इन + तैयार + वैकल्पिक + एक इंजन चुनें + इंस्टॉल किए गए इंजन + डिफ़ॉल्ट बनाएं + NewPipe YouTube और LRCLib लिरिक्स बिल्ट-इन हैं और शुरू से ही स्ट्रीम करने के लिए तैयार हैं। + सेटिंग → प्लगइन में कभी भी कम्युनिटी प्लगइन मैनेज करें या नए जोड़ें। + + + रेट्रो-मॉडर्न आर्किटेक्चर + शुद्ध Hi-Fi, कोई बिचौलिया नहीं + देखिए Spotube कैसे आपकी मशीन पर सीधे डीसेंट्रलाइज़्ड प्लेबैक चलाता है। + सीधा पीयर-टू-पीयर • 256 KBPS OPUS + ब्लू आवर + Spotube · प्लेयर डेमो + बीच के पलों के लिए थोड़ा सा संगीत। + 1. डीसेंट्रलाइज़्ड खोज + सर्च सीधे आपके चुने हुए कैटलॉग प्लगइन तक जाती है। Spotube कभी आपकी पसंद की प्रोफ़ाइल नहीं बनाता। + 2. सीधा स्ट्रीम रिज़ॉल्यूशन + ऑडियो सीधे कंटेंट सर्वर से आपकी मशीन तक स्ट्रीम होता है। न कोई प्रॉक्सी, न रिमोट डेटा संग्रह। + 3. लोकल DSP और प्लेबैक + इक्वलाइज़र कंट्रोल और वैकल्पिक ऑफ़लाइन डाउनलोड के साथ लोकल रूप से डिकोड और कैश होता है। + 4. सिंक्ड लिरिक्स और जैम + कराओके लिरिक्स प्लेबैक के समय के साथ चलते हैं, साथ में LAN रिमोट कंट्रोल और ग्रुप जैम। + कोई बिचौलिया सर्वर नहीं। प्लेबैक सीधे आपके डिवाइस पर होता है — शुरू से ही निजी। + + + डिवाइस चुनें + %1$s: “%2$s” + चलाएं + कतार में जोड़ें + अगला चलाएं + यह डिवाइस + इस डिवाइस पर + रिमोट डिवाइस + रिमोट डिवाइस (%1$s) + कनेक्ट किए गए डिवाइस पर + + + कनेक्शन विफल + कनेक्शन टूट गया + भेजना विफल + खोज विफल: %1$s + कनेक्ट नहीं हो सका: %1$s + + + डिवाइस + रीफ़्रेश करें + %1$s:%2$d से कनेक्ट है + डिस्कनेक्ट करें + कनेक्ट हो रहा है... + नेटवर्क पर डिवाइस खोजे जा रहे हैं... + कोई डिवाइस नहीं मिला + पक्का करें कि दूसरे डिवाइस की सेटिंग में \"रिमोट कंट्रोल की अनुमति दें\" चालू है। + अज्ञात डिवाइस + पता लगाया जा रहा है... + ID: %1$s... + + + रिमोट कंट्रोल + डिस्कनेक्ट हो गया + कनेक्शन में गड़बड़ी: %1$s + एल्बम कवर + अज्ञात ट्रैक + अज्ञात कलाकार + आगे-पीछे करें + शफ़ल + पिछला + अगला + रिपीट मोड + वॉल्यूम + कतार से हटाएं + और विकल्प + + + कहां चलाएं? + आपका एक जैम सेशन चालू है। चुनें कि यह कलेक्शन कहां चलाना है। + यहां चलाएं + जैम में सुझाएं + + + कोई जैम ब्रोकर सेट नहीं है + ब्रोकर होस्ट खाली है + कनेक्शन विफल + कनेक्शन टूट गया + + + ब्रोकर + anonymous + + + जैम कतार में जोड़ा गया + + %1$d ट्रैक जैम कतार में जोड़ा गया + %1$d ट्रैक जैम कतार में जोड़े गए + + + + रिमोट कंट्रोल अनुरोध + \"%1$s\" इस डिवाइस पर प्लेबैक कंट्रोल करना चाहता है। + मना करें + अनुमति दें + हमेशा अनुमति दें + + + खोज के नतीजे + सभी + ट्रैक + यूज़र + कोई खोज प्रकार उपलब्ध नहीं + खोजने के लिए टाइप करें + ट्रैक खोजने के लिए टाइप करें + कोई नतीजा नहीं मिला + सभी देखें + खोज रहे हैं + ट्रैक लोड हो रहे हैं... + एल्बम लोड हो रहे हैं... + कलाकार लोड हो रहे हैं... + प्लेलिस्ट लोड हो रही हैं... + यूज़र लोड हो रहे हैं... + कोई एल्बम नहीं मिला + कोई कलाकार नहीं मिला + कोई प्लेलिस्ट नहीं मिली + कोई यूज़र नहीं मिला + रिकॉर्ड के ढेर खंगाले जा रहे हैं... + ब्रह्मांड में खोज रहे हैं... + स्पीकर गर्म हो रहे हैं... + विनाइल की धूल झाड़ रहे हैं... + संगीत के देवताओं से सलाह ले रहे हैं... + एंटीना ट्यून कर रहे हैं... + रिकॉर्ड पलट रहे हैं... + खोज की सुविधाएं लोड नहीं हो सकीं + खोज नहीं हो सकी + ट्रैक नहीं खोजे जा सके + एल्बम नहीं खोजे जा सके + कलाकार नहीं खोजे जा सके + प्लेलिस्ट नहीं खोजी जा सकीं + यूज़र नहीं खोजे जा सके + + + ब्राउज़ करें + + + सिंक्ड + सादा + सिंक्ड लिरिक्स उपलब्ध नहीं हैं + सादे लिरिक्स उपलब्ध नहीं हैं + + + %2$s में %1$s + %1$d kbps + %1$s kHz • %2$d ch + कोई मेटाडेटा प्लगइन नहीं चुना गया + + + वापस + फिर से कोशिश करें + डायलॉग बंद करें + लोड हो रहा है... + अज्ञात + अज्ञात गड़बड़ी + + + यूज़र + प्लेलिस्ट + + + हमें सच में खेद है + अभी कुछ ठीक से काम नहीं कर रहा है। आपका अनुभव हमारे लिए मायने रखता है, और हम इसे ठीक करने की पूरी कोशिश कर रहे हैं। कृपया फिर से कोशिश करें या नीचे गड़बड़ी की जानकारी देखें। + गड़बड़ी की जानकारी + क्लिपबोर्ड पर कॉपी करें + पूरा लॉग देखें + पूरा एरर लॉग + + + शफ़ल + शफ़ल करके चलाएं + फ़ॉलो करें + अनफ़ॉलो करें + प्लेलिस्ट एडिट करें + %1$s द्वारा + + + ट्रैक विकल्प + जैम में जोड़ें + रेडियो शुरू करें + अगला चलाएं + अगले पर ले जाएं + कतार से हटाएं + पसंदीदा से हटाएं + पसंदीदा में सेव करें + प्लेलिस्ट में जोड़ें + डाउनलोड करें + शेयर करें + + + मूल क्रम + नाम + कलाकार + एल्बम + अवधि + क्रम से लगाएं + कोई ट्रैक नहीं मिला + फ़िल्टर + एक साथ कार्रवाई + सभी डाउनलोड करें + %1$d डाउनलोड करें + सभी को कतार में जोड़ें + %1$d को कतार में जोड़ें + सभी को अगला चलाएं + %1$d को अगला चलाएं + सभी को प्लेलिस्ट में जोड़ें + %1$d को प्लेलिस्ट में जोड़ें + सभी को जैम में जोड़ें + %1$d को जैम में जोड़ें + +%1$d और + + + लोड करने के लिए कोई URL नहीं + ब्राउज़र इतिहास में पीछे जाएं + ब्राउज़र इतिहास में आगे जाएं + + + ब्लैकलिस्ट फ़िल्टर करें... + ब्लैकलिस्ट में कुछ नहीं है + \'%1$s\' से मेल खाता कोई ब्लैकलिस्ट आइटम नहीं + ट्रैक + ट्रैक + ब्लैकलिस्ट में जोड़ें + ब्लैकलिस्ट से हटाएं + + + ट्रैक का मेटाडेटा नहीं है + स्ट्रीम URL नहीं मिल सका + स्ट्रीम URL नहीं मिल सका। पहले ट्रैक चलाकर देखें। + + + सेव किए गए ट्रैक + आप + + + फ़ॉलो कर रहे हैं + टॉप ट्रैक + टॉप ट्रैक चलाएं + टॉप ट्रैक कतार में जोड़ें + मिलते-जुलते कलाकार + फ़ीचर्ड प्लेलिस्ट + कलाकार की जानकारी लोड नहीं हो सकी + फ़ॉलोअर उपलब्ध नहीं + %1$sK फ़ॉलोअर + %1$sM फ़ॉलोअर + %1$sB फ़ॉलोअर + + %1$d फ़ॉलोअर + %1$d फ़ॉलोअर + + + + पसंद करें + पसंद से हटाएं + No-op ट्रैक का नाम + No-op कलाकार + अज्ञात एल्बम + आगे-पीछे करें + वॉल्यूम + शफ़ल चालू करें + शफ़ल बंद करें + पिछला + अगला + चलाएं या रोकें + रिपीट बंद + एक को रिपीट करें + सभी को रिपीट करें + म्यूट करें + अनम्यूट करें + और विकल्प + प्लेयर विकल्प + प्लेयर छोटा करें + लिरिक्स बड़ा करें + अभी चल रहा है + लिरिक्स + वैकल्पिक सोर्स + वैकल्पिक सोर्स + डाउनलोड करें + डाउनलोड हो गया + डाउनलोड हो रहा है... + पहले से डाउनलोड है + डाउनलोड फिर से शुरू हो रहा है... + %1$s डाउनलोड हो रहा है + प्लेबैक %1$s में रुक जाएगा। + चुने गए मिनटों के बाद प्लेबैक रोकें। + मिनट + 1 से 600 मिनट के बीच चुनें + टाइमर रद्द करें + शुरू करें + प्लेबैक URL अलग स्ट्रीमिंग सेटिंग के साथ तय किया गया था। हाइलाइट की गई स्ट्रीम आपकी मौजूदा पसंद से मेल खाती है; सक्रिय URL अगली स्ट्रीम रिक्वेस्ट पर रीफ़्रेश होगा। + मेल खाता ऑडियो सोर्स + मेल खाता सोर्स लोड हो रहा है… + लोकल कैश फ़ाइल चलाते समय मेल खाते रिमोट सोर्स की ज़रूरत नहीं है। + कोई मेल खाता सोर्स उपलब्ध नहीं है। + इस ट्रैक के लिए कोई मेल खाता ऑडियो सोर्स कैश नहीं है। + स्ट्रीम की जानकारी अभी कैश नहीं है। उसे लोड करने के लिए पहले ट्रैक चलाएं। + ट्रैक की जानकारी लोड नहीं हो सकी। + उपलब्ध स्ट्रीम + यह ट्रैक लोकल कैश से चल रहा है। इस प्लेबैक के लिए स्ट्रीम वेरिएंट इस्तेमाल नहीं होते। + स्ट्रीम लोड हो रही हैं… + इस सोर्स के लिए कोई स्ट्रीम उपलब्ध नहीं है। + अभी चुनी गई स्ट्रीम + आपकी स्ट्रीमिंग फ़ॉर्मैट और क्वालिटी पसंद से मेल खाती है + सक्रिय प्लेबैक URL + लोकल कैश से चल रहा है + मेटाडेटा ट्रैक + नाम + ID + एल्बम + विश्वसनीयता + बाहरी URI + एल्बम ID + एल्बम प्रकार + एल्बम रिलीज़ की तारीख + एल्बम में ट्रैक की संख्या + एल्बम की शैलियां + अवधि + ट्रैक नंबर + डिस्क नंबर + एक्सप्लिसिट + लोकप्रियता + ISRC + थंबनेल URL + + + साइडबार दिखाएं/छिपाएं + + + वैकल्पिक सोर्स + अभी कोई ट्रैक नहीं चल रहा + वैकल्पिक सोर्स खोजे जा रहे हैं... + कोई वैकल्पिक सोर्स नहीं मिला + विश्वसनीयता: %1$d% + सक्रिय सोर्स + + + निकालें + उन्हें सेशन से हटाएं + निकालें और दोबारा जुड़ने से रोकें + सुझाव हटाएं + उनके कतार में जोड़े गए सभी ट्रैक हटाएं + क्रम बदलें + कतार से हटाएं + %1$s • %2$s ने जोड़ा + + + अज्ञात + प्लेलिस्ट लोड हो रही है... + ट्रैक जोड़ें + प्लेलिस्ट में ट्रैक जोड़ें + ट्रैक खोजें... + खोजें + कोई ट्रैक नहीं मिला + जोड़ने के लिए ट्रैक खोजें + हो गया + जोड़ें + जोड़ा गया + + + एल्बम लोड हो रहा है... + + + अपनी प्लेलिस्ट में जोड़ें + खोजें + अपनी प्लेलिस्ट खोजें... + आपकी प्लेलिस्ट लोड हो रही हैं... + अभी तक आपकी कोई प्लेलिस्ट नहीं है।\nट्रैक जोड़ने के लिए एक प्लेलिस्ट बनाएं। + प्लेलिस्ट बनाएं + प्लेलिस्ट एडिट करें + बनाएं + नाम + प्लेलिस्ट का नाम + विवरण + विवरण (वैकल्पिक) + सार्वजनिक + कोई भी यह प्लेलिस्ट देख सकता है + साझा + दूसरे लोग इस प्लेलिस्ट में ट्रैक जोड़ सकते हैं + कवर इमेज + प्लेलिस्ट कवर + कोई इमेज नहीं + इमेज बदलें + इमेज चुनें + + + इंस्टॉल करें + बदलें + zip फ़ाइल में plugin.json नहीं मिला + zip फ़ाइल में manifest.zipline.json नहीं मिला + plugin.json का फ़ॉर्मैट अमान्य है: %1$s + प्लगइन पढ़ा नहीं जा सका: %1$s + प्लगइन इंस्टॉल नहीं हो सका: %1$s + %1$s का नया वर्शन उपलब्ध है। इंस्टॉल करने से पहले अपडेट की पुष्टि करें। + %1$s पहले से इंस्टॉल है। इंस्टॉल करने से पहले उसे बदलने की पुष्टि करें। + बिल्ट-इन प्लगइन पहले से शामिल हैं और दोबारा नहीं जोड़े जा सकते। + बिल्ट-इन प्लगइन हटाए नहीं जा सकते। + %1$s ने अमान्य apiVersion (%2$s) बताया है। %3$s के साथ संगत सिमैंटिक वर्शनिंग अपेक्षित है। + Spotube प्लगइन API वर्शन %1$s अमान्य है। + %1$s प्लगइन API %2$s के लिए बना है, लेकिन Spotube ब्रेकिंग प्लगइन API %3$s सपोर्ट करता है। उसी ब्रेकिंग API वर्शन के लिए बना प्लगइन इंस्टॉल करें। + + + प्लगइन API संगत नहीं है + प्लगइन इंस्टॉल करें? + %1$s आपके इंस्टॉल किए गए प्लगइन में जोड़ा जाएगा। + प्लगइन अपडेट करें? + एक पुराना वर्शन (%1$s) इंस्टॉल है। %2$s पर अपडेट करें? + इंस्टॉल किया गया प्लगइन बदलें? + %1$s अमान्य सिमैंटिक वर्शन (%2$s) इस्तेमाल करता है और अपडेट के रूप में इसकी तुलना नहीं हो सकती। फिर भी इंस्टॉल किया गया प्लगइन बदलें? + इंस्टॉल किए गए वर्शन (%1$s) की सिमैंटिक वर्शनिंग से तुलना नहीं हो सकती। इसे %2$s से बदलें? + वर्शन %1$s पहले से इंस्टॉल है। मौजूदा प्लगइन को दी गई कॉपी से बदलें? + इंस्टॉल किया गया वर्शन %1$s, दिए गए वर्शन %2$s से नया है। फिर भी बदलें? + इंस्टॉल किए गए प्लगइन को दी गई कॉपी से बदलें? + + + %1$s: + वर्शन + API वर्शन + लेखक + लाइसेंस + क्षमताएं + सुविधाएं + रिपॉज़िटरी + संपर्क + बग रिपोर्ट करें + %1$s को सहयोग दें + + + आधिकारिक + सत्यापित + GitHub स्टार + GitHub रिपॉज़िटरी URL + + + कोई रिलीज़ नहीं मिली + रिलीज़ चुनें + एक रिलीज़ चुनें + प्री-रिलीज़ + रिलीज़ नोट्स + + + होम + खोजें + डिवाइस पर + कनेक्ट + डिवाइस + + + विंडो छिपाएं + विंडो दिखाएं + अगला ट्रैक + पिछला ट्रैक + शफ़ल + रिपीट + रिपीट: बंद + रिपीट: एक + रिपीट: सभी + वॉल्यूम: %1$d% + वॉल्यूम + + वॉल्यूम - + म्यूट करें + अनम्यूट करें + ट्रैक पसंद करें + ट्रैक को पसंद से हटाएं + बाहर निकलें + + + छोटा करें + बड़ा करें + पहले जैसा करें + बंद करें + + + प्लेबैक सेवा + प्लेबैक + मीडिया प्लेबैक कंट्रोल + + + ब्राउज़ करें + अभी चल रहा है + फ़ीचर्ड + सेव किए गए ट्रैक + टॉप ट्रैक + diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml new file mode 100644 index 00000000..fbc02ba5 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml @@ -0,0 +1,932 @@ + + + + Antrean + Kosongkan Antrean + Filter antrean... + Antrean kosong + + Cari lagu, artis, album... + Hapus semua riwayat + Cari... + + + Koleksi Kamu + Koleksi + Playlist + Album + Artis + Lagu Lokal + Unduhan + Cari playlist tersimpan… + Cari album tersimpan… + Cari artis tersimpan… + Cari folder atau lagu… + Filter koleksi + Tutup filter + Buat playlist + Muat ulang lagu lokal + Beralih ke tampilan petak + Beralih ke tampilan daftar + Belum ada playlist tersimpan + Tidak ada playlist yang cocok dengan “%1$s” + Belum ada album tersimpan + Tidak ada album yang cocok dengan “%1$s” + Belum ada artis tersimpan + Tidak ada artis yang cocok dengan “%1$s” + + + Folder + Folder + %1$s • %2$s + + %1$d lagu + + + %1$d folder + + Kembali ke folder + Putar folder + Akses audio diperlukan + Izinkan akses audio untuk menjelajahi musik di perangkatmu. + Beri akses + Tidak ada folder lokal + Tidak ada lagu di folder ini + Tidak ada folder atau lagu yang cocok + Gagal memuat ulang media lokal + + + %1$d aktif • %2$d selesai + Hapus yang selesai + Belum ada unduhan + Mengunduh %1$d% + Dalam antrean + Selesai + Dibatalkan + Gagal + Gagal: %1$s + Batalkan unduhan + Coba unduh lagi + Hapus unduhan + + + Artis + Diblokir + Lagu yang Disukai + Lagu tersimpanmu + Artis tidak dikenal + Judul lagu + Nama artis + + Dipilih + + Pengaturan + + Plugin + Bahasa dan Wilayah + Tampilan + Pemutaran + Unduhan + Cache + Desktop + Pembaruan + Dukungan + Tentang + + Tutup + Simpan + Batal + + Bahasa + Bahasa antarmuka aplikasi. Saat ini: %1$s + Bawaan sistem + Negara + Negara untuk konten dan ketersediaan. Saat ini: %1$s + %1$s (%2$s) + + Tema + Pilih cara Spotube menyesuaikan dengan tampilan sistem. Saat ini: %1$s + Terang + Gelap + Sistem + + Permukaan + Material latar belakang aplikasi. Saat ini: %1$s + Air + Es + Krom + Akrilik + Kaca + + Warna aksen + Palet saat ini: %1$s + Warna aksen + Pilih palet aksen dan pratinjau dalam tema terang maupun gelap. + Terang + Gelap + Pratinjau + Tombol + Green Goblin + Ungu Elektrik + Sian Samudra + Jingga Senja + Taman Mawar + Biru Tengah Malam + Batu Metalik + + Tampilan & nuansa + Material permukaan dan aksen. Saat ini: %1$s · %2$s + Tampilan & nuansa + Pilih material permukaan, lalu aksen yang serasi. Setiap opsi adalah kombinasi pilihan. + Permukaan + Aksen + Sekunder + + Palet + Palet aksen Millennium. Saat ini: %1$s + Palet + Pilih palet aksen Millennium dan pratinjau dalam mode terang dan gelap. + Palet + Palet dan aksen Millennium. Saat ini: %1$s + Biru + Jingga + Hijau Limau + + Ganti folder unduhan + Folder saat ini: %1$s + Folder saat ini: folder musik bawaan + Biarkan kosong untuk memakai folder musik bawaan. + C:\Music\Spotube + Folder media lokal + Folder kustom untuk dipindai di desktop. Ditambahkan: %1$d + Belum ada folder kustom + Folder kustom untuk koleksi musik lokalmu. + Kelola + Tambah folder + Hapus folder + Belum ada folder kustom yang ditambahkan. + Hapus %1$s dari daftar pindai + Folder unduhan dan cache musik disertakan secara otomatis. + Folder unduhanmu disertakan secara otomatis. + Folder cache musik disertakan secara otomatis. + Menyimpan… + Gagal menyimpan folder. Perubahanmu masih ada; coba lagi. + + %1$d folder + + Format unduhan musik + Codec dan kontainer pilihan untuk unduhan. Saat ini: %1$s + Kualitas unduhan musik + + Format streaming musik + Codec dan kontainer pilihan untuk streaming musik. Saat ini: %1$s + Kualitas streaming musik + Saat ini: %1$s + Aktifkan cache musik + Simpan audio yang di-streaming secara lokal agar pemutaran ulang lebih cepat. + Folder cache + Saat ini: %1$s + Saat ini: cache aplikasi bawaan + Batas ukuran cache + Tanpa batas + Batas saat ini: %1$d MB + Atur ukuran cache maksimum dalam MB. Masukkan 0 atau kosongkan untuk tanpa batas. + 500 + Ukuran harus 0 atau bilangan positif. + Aktifkan pemutaran tanpa henti + Lanjutkan pemutaran dengan lagu rekomendasi saat antrean habis. + Aktifkan Connect + Tampilkan kontrol pemutaran jarak jauh melalui fitur Spotube Connect. + Port server proxy pemutaran + Izinkan kontrol jarak jauh + Izinkan perangkat lain di jaringan yang sama mengontrol pemutaran saat terhubung. + Nama perangkat kontrol jarak jauh + Nama yang dilihat perangkat lain saat menemukan perangkat ini. Saat ini: %1$s + Pilih nama yang mudah dikenali untuk ditampilkan ke perangkat lain di jaringanmu saat mereka menemukan Spotube ini. + mis. Spotube Ruang Tamu + nama host perangkat + Port yang dipakai server proxy pemutaran. Saat ini: %1$d + Pilih port antara 1 dan 65535. + 14769 + Masukkan bilangan bulat yang valid. + Port harus antara 1 dan 65535. + + Daftar blokir + Kelola lagu dan artis yang diblokir + + Minimalkan ke tray + Biarkan Spotube tetap berjalan di system tray saat jendela diminimalkan. + Discord Rich Presence + Tampilkan status pemutaran saat ini di Discord pada desktop. + + Periksa pembaruan otomatis + Cari rilis Spotube baru di latar belakang. + + Kelola Plugin + Kelola plugin kamu + Plugin %1$s Bawaan + Metadata + Audio + Lirik + Scrobble + Belum ada plugin dipilih + Ubah + Pilih + Dipilih + Hapus pilihan + Tidak ada plugin tersedia + Plugin %1$s + Hapus + + Konfigurasi + Pengelola Plugin + Masukkan URL + URL harus diawali http:// atau https:// + Gagal mengunduh plugin + Belum ada plugin terpasang + Tempel URL unduhan atau pilih file di atas. + + %1$d plugin terpasang + + Pasang Plugin + https://example.com/plugin.smplug + Unduh + Pasang dari file (.smplug) + Aktif + v%1$s + Bawaan + Hapus plugin + Masuk + Keluar + Unduh dari URL + Pasang dari file + Pasang + + • %1$s + Izin yang diminta + Plugin ini tidak memerlukan izin khusus. + Terpasang vs diberikan + Terpasang + Diberikan + API %1$s → %2$s + Penyimpanan Persisten + Membaca dan menulis data ke disk + Permintaan Jaringan + Mengirim dan menerima data melalui internet + WebView + Menampilkan konten web di dalam aplikasi + Jam Grup + Broker MQTT + Host broker + %1$s:%2$d + Port broker + Gunakan TLS + Nama pengguna (opsional) + Kata sandi (opsional) + Prefiks ID klien + Dipakai untuk mengidentifikasi Spotube ini + Enkripsi koneksi broker + Periksa pengaturan broker saat ini sebelum memulai ruang. + Uji koneksi + Menguji… + Terhubung dalam %1$d ms + Gagal: %1$s + Broker sementara — konfigurasikan milikmu sendiri untuk self-host + Koneksi + Kredensial + Identitas + Diagnostik + + + Jam Grup + Buat + Gabung + Buat ruang, lalu bagikan kodenya ke teman-temanmu. + Buat ruang + Gabung ke ruang + Menghubungkan… + Kode ruang + ABC123 + Broker: %1$s + Atur broker di Pengaturan untuk membuat atau bergabung ke ruang. + Pengaturan broker + Terhubung + Menghubungkan ulang… + Salin kode + Disalin! + Bagikan + Peserta (%1$d) + Host + Pendengar + Menunggu pendengar… + Keluarkan + Blokir + Kelola + Tinggalkan ruang + Gagal terhubung + Tutup + Gagal membuat sesi Jam + Gagal bergabung ke sesi Jam + Masukkan kode ruang enam karakter berisi huruf dan angka. + Gabung ke Jam Spotube-ku dengan kode: %1$s + + Buka album + Timer tidur + Detail lagu + Putar + Jeda + Tidak ada yang diputar + Tambahkan ke antrean + + Abaikan + Perbarui + Spotube %1$s telah tersedia + Lihat yang baru di rilis ini pada halaman unduhan Spotube. + %1$s %2$s telah tersedia + Terpasang saat ini: %1$s + Lihat yang baru di pembaruan ini pada halaman plugin. + + Lewati + Berikutnya + Kembali + Mulai + Spotube + Musik, tanpa ikatan. + Pemutar open source untuk dunia musik.\nBebas kamu jelajahi. Bebas kamu jadikan milikmu. + Bantu proyek ini tetap hidup. + Spotube dibuat oleh orang-orang, bukan bisnis langganan. Jika Spotube menjadi bagian dari keseharianmu, sedikit dukungan membantu kami merawatnya sebagaimana mestinya.\n\nTerima kasih telah memberi ruang bagi perangkat lunak independen. + Mulailah dengan plugin katalog untuk menemukan lagu dan playlist. Pilih plugin audio untuk memutarnya. Lirik dan riwayat dengar bersifat opsional. Temukan plugin di bawah, tinjau izinnya, pasang, lalu pilih di sini. + Temukan plugin + Sumbermu + Coba lagi + Plugin lainnya + Tidak ada plugin baru. Kamu juga bisa memasang plugin dari tautan atau file .smplug di atas. + Penemuan plugin sedang tidak tersedia. Coba lagi, atau pasang dari tautan atau file. + Selamat datang + Dukungan + Plugin + Panduan + + Cara menggunakan Spotube + Empat langkah singkat dan kamu siap mendengarkan. + Masuk + Hubungkan akun Spotify-mu, atau pilih plugin katalogmu sendiri. + Cari dan putar + Temukan lagu, album, atau artis apa pun, lalu tekan putar. + Dengarkan offline + Unduh lagu agar tetap bisa diputar tanpa koneksi. + Ikuti liriknya + Buka lirik tersinkron dari pemutar saat lagu diputar. + + + Musikmu. Sumbermu. Aturanmu. + Pemutar musik gratis dan open source yang dibuat oleh komunitas pencinta musik. + Open source + Tanpa iklan + Tanpa telemetri + Berbasis plugin + Ayo Mulai + + + Pendukung membuat kami terus berjalan + Dibuat dengan semangat. Dihidupkan olehmu. + Spotube 100% gratis, bebas, dan open source — tanpa iklan, tanpa langganan, tanpa pelacakan. Setiap fitur ada karena orang sepertimu memilih untuk membantu kode terus berkembang. + Patreon + Sponsor Bulanan + Dukung pengembangan dan infrastruktur secara rutin + Buy Me a Coffee + Kopi Sekali Saja + Beri semangat sesi ngoding larut malam dengan tip singkat + Open Collective + Dana Komunitas + Dukungan finansial yang transparan dan dikelola komunitas + Terima kasih dari lubuk hati kami karena telah percaya pada musik terbuka. + Dukungan sepenuhnya opsional — Spotube tetap gratis apa pun pilihanmu. + Sponsori + Buka %1$s + + + Dukung Spotube + Dukung Spotube + Spotube gratis, bebas, dan open source — tanpa iklan, tanpa langganan, tanpa pelacakan. Jika Spotube membuatmu senang, pertimbangkan untuk mendukung proyek ini. + Kamu bisa mematikan pengingat ini di Pengaturan → Dukungan. + Nanti saja + Pengingat dukungan + Tampilkan pengingat ringan untuk mendukung proyek setiap 7 hari. + + + Arsitektur Modular + Pilih dari mana musikmu berasal. + Spotube memisahkan penemuan katalog, streaming audio, dan lirik tersinkron. Atur mesinmu di bawah atau ganti kapan saja. + Mesin Metadata + Mendukung penemuan musik, pencarian, daftar lagu, dan playlist. + Mesin Audio + Mengambil dan mendekode stream audio yang diputar. + Mesin Lirik + Menampilkan lirik karaoke tersinkron secara real-time. + Mesin Scrobble + Mencatat riwayat dengar ke Last.fm atau ListenBrainz. + Aktif + Bawaan + Siap + Opsional + Pilih mesin + Mesin Terpasang + Jadikan bawaan + NewPipe YouTube dan LRCLib Lyrics sudah bawaan dan siap diputar langsung. + Kelola atau tambahkan plugin komunitas kapan saja di Pengaturan → Plugin. + + + Arsitektur Retro-Modern + Hi-Fi murni, tanpa perantara + Begini cara Spotube mengatur pemutaran terdesentralisasi langsung di perangkatmu. + PEER-TO-PEER LANGSUNG • 256 KBPS OPUS + Blue hour + Spotube · demonstrasi pemutar + Sedikit musik untuk momen-momen di antaranya. + 1. Penemuan Terdesentralisasi + Pencarian langsung dikirim ke plugin katalog pilihanmu. Spotube tidak pernah memprofilkan selera musikmu. + 2. Resolusi Stream Langsung + Audio di-streaming langsung dari server konten ke perangkatmu. Tanpa lompatan proxy atau pengumpulan data jarak jauh. + 3. DSP & Pemutaran Lokal + Didekode dan di-cache secara lokal dengan kontrol equalizer dan unduhan offline opsional. + 4. Lirik Tersinkron & Jam + Lirik karaoke mengikuti waktu pemutaran, dengan kontrol jarak jauh via LAN dan Jam Grup. + Tanpa server perantara. Pemutaran terjadi langsung di perangkatmu — privat sejak awal. + + + Pilih perangkat + %1$s: “%2$s” + Putar + Tambahkan ke antrean + Putar berikutnya + Perangkat Ini + Di perangkat ini + Perangkat Jarak Jauh + Perangkat Jarak Jauh (%1$s) + Di perangkat yang terhubung + + + Koneksi gagal + Koneksi terputus + Gagal mengirim + Pencarian gagal: %1$s + Gagal terhubung: %1$s + + + Perangkat + Muat ulang + Terhubung ke %1$s:%2$d + Putuskan + Menghubungkan... + Mencari perangkat di jaringan... + Tidak ada perangkat ditemukan + Pastikan perangkat lain telah mengaktifkan \"Izinkan kontrol jarak jauh\" di pengaturan. + Perangkat Tidak Dikenal + Menyelesaikan... + ID: %1$s... + + + Kontrol Jarak Jauh + Terputus + Kesalahan koneksi: %1$s + Sampul album + Lagu Tidak Dikenal + Artis Tidak Dikenal + Cari posisi + Acak + Sebelumnya + Berikutnya + Mode ulang + Volume + Hapus dari antrean + Opsi lainnya + + + Putar di Mana? + Kamu sedang dalam sesi Jam. Pilih tempat untuk memutar koleksi ini. + Putar di sini + Sarankan ke Jam + + + Belum ada broker Jam yang dikonfigurasi + Host broker kosong + Koneksi gagal + Koneksi terputus + + + Broker + anonim + + + Ditambahkan ke antrean Jam + + %1$d ditambahkan ke antrean Jam + + + + Permintaan Kontrol Jarak Jauh + \"%1$s\" ingin mengontrol pemutaran di perangkat ini. + Tolak + Izinkan + Selalu Izinkan + + + Hasil pencarian + Semua + Lagu + Pengguna + Tidak ada jenis pencarian tersedia + Mulai mengetik untuk mencari + Mulai mengetik untuk mencari lagu + Tidak ada hasil + Lihat semua + Mencari + Memuat lagu... + Memuat album... + Memuat artis... + Memuat playlist... + Memuat pengguna... + Tidak ada album ditemukan + Tidak ada artis ditemukan + Tidak ada playlist ditemukan + Tidak ada pengguna ditemukan + Membongkar tumpukan piringan... + Menjelajahi alam semesta... + Memanaskan speaker... + Membersihkan debu piringan hitam... + Bertanya pada dewa musik... + Menyetel antena... + Membolak-balik koleksi piringan... + Gagal memuat kemampuan pencarian + Gagal mencari + Gagal mencari lagu + Gagal mencari album + Gagal mencari artis + Gagal mencari playlist + Gagal mencari pengguna + + + Jelajahi + + + Tersinkron + Biasa + Lirik tersinkron tidak tersedia + Lirik biasa tidak tersedia + + + %1$s dalam %2$s + %1$d kbps + %1$s kHz • %2$d ch + Belum ada plugin metadata dipilih + + + Kembali + Coba lagi + Tutup dialog + Memuat... + Tidak diketahui + Kesalahan tidak diketahui + + + Pengguna + Playlist + + + Kami Mohon Maaf + Ada yang tidak berfungsi saat ini. Pengalamanmu penting bagi kami, dan kami berusaha sebaik mungkin untuk memperbaikinya. Coba lagi atau periksa detail kesalahan di bawah. + Detail Kesalahan + Salin ke papan klip + Lihat log lengkap + Log Kesalahan Lengkap + + + Acak + Putar acak + Ikuti + Berhenti mengikuti + Edit playlist + Oleh %1$s + + + Opsi lagu + Tambahkan ke Jam + Mulai radio + Putar berikutnya + Pindahkan ke berikutnya + Hapus dari antrean + Hapus dari favorit + Simpan sebagai favorit + Tambahkan ke playlist + Unduh + Bagikan + + + Asli + Judul + Artis + Album + Durasi + Urutkan + Tidak ada lagu ditemukan + Filter + Tindakan massal + Unduh Semua + Unduh %1$d + Tambahkan Semua ke Antrean + Tambahkan %1$d ke Antrean + Putar Semua Berikutnya + Putar %1$d Berikutnya + Tambahkan Semua ke Playlist + Tambahkan %1$d ke Playlist + Tambahkan Semua ke Jam + Tambahkan %1$d ke Jam + +%1$d lainnya + + + Tidak ada URL untuk dimuat + Kembali di riwayat browser + Maju di riwayat browser + + + Filter daftar blokir... + Tidak ada item yang diblokir + Tidak ada item diblokir yang cocok dengan \'%1$s\' + Lagu + Lagu + Tambahkan ke daftar blokir + Hapus dari daftar blokir + + + Tidak ada metadata lagu + Tidak dapat menemukan URL stream + Tidak dapat menemukan URL stream. Coba putar lagunya terlebih dahulu. + + + Lagu Tersimpan + Kamu + + + Mengikuti + Lagu Teratas + Putar lagu teratas + Tambahkan lagu teratas ke antrean + Artis Terkait + Playlist Unggulan + Gagal memuat ringkasan artis + Jumlah pengikut tidak tersedia + %1$s rb pengikut + %1$s jt pengikut + %1$s M pengikut + + %1$d pengikut + + + + Suka + Batal suka + Judul lagu kosong + Artis kosong + Album Tidak Dikenal + Cari posisi + Volume + Aktifkan acak + Nonaktifkan acak + Sebelumnya + Berikutnya + Putar atau jeda + Mode ulang nonaktif + Ulang satu lagu + Ulang semua + Bisukan + Bunyikan + Opsi lainnya + Opsi pemutar + Ciutkan pemutar + Perluas lirik + Sedang Diputar + Lirik + Sumber Alternatif + Sumber alternatif + Unduh + Diunduh + Mengunduh... + Sudah diunduh + Mencoba mengunduh lagi... + Mengunduh %1$s + Pemutaran akan dijeda dalam %1$s. + Jeda pemutaran setelah jumlah menit yang dipilih. + Menit + Pilih antara 1 dan 600 menit + Batalkan timer + Mulai + URL pemutaran ditentukan dengan pengaturan streaming yang berbeda. Stream yang disorot sesuai dengan preferensimu saat ini; URL aktif akan diperbarui pada permintaan stream berikutnya. + Sumber audio yang cocok + Memuat sumber yang cocok… + Sumber jarak jauh yang cocok tidak diperlukan saat memutar file cache lokal. + Tidak ada sumber yang cocok. + Tidak ada sumber audio cocok yang di-cache untuk lagu ini. + Detail stream belum di-cache. Putar lagunya terlebih dahulu untuk memuatnya. + Gagal memuat detail lagu. + Stream yang tersedia + Lagu ini diputar dari cache lokal. Varian stream tidak digunakan untuk pemutaran ini. + Memuat stream… + Tidak ada stream tersedia untuk sumber ini. + Stream yang sedang dipilih + Sesuai dengan preferensi format & kualitas streaming-mu + URL pemutaran aktif + Diputar dari cache lokal + Metadata lagu + Judul + ID + Album + Tingkat kecocokan + URI Eksternal + ID Album + Jenis album + Tanggal rilis album + Jumlah lagu album + Genre album + Durasi + Nomor lagu + Nomor disk + Eksplisit + Popularitas + ISRC + URL thumbnail + + + Tampilkan/Sembunyikan Sidebar + + + Sumber Alternatif + Tidak ada lagu yang sedang diputar + Mencari sumber alternatif... + Tidak ada sumber alternatif ditemukan + Kecocokan: %1$d% + Sumber aktif + + + Keluarkan + Keluarkan mereka dari sesi + Keluarkan dan cegah mereka bergabung kembali + Hapus saran + Hapus semua lagu yang mereka tambahkan ke antrean + Ubah urutan + Hapus dari antrean + %1$s • Ditambahkan oleh %2$s + + + Tidak diketahui + Memuat playlist... + Tambah Lagu + Tambahkan Lagu ke Playlist + Cari lagu... + Cari + Tidak ada lagu ditemukan + Cari lagu untuk ditambahkan + Selesai + Tambah + Ditambahkan + + + Memuat album... + + + Tambahkan ke Playlist Kamu + Cari + Cari playlist kamu... + Memuat playlist kamu... + Kamu belum punya playlist.\nBuat satu untuk menambahkan lagu. + Buat Playlist + Edit Playlist + Buat + Nama + Nama playlist + Deskripsi + Deskripsi (opsional) + Publik + Siapa saja dapat melihat playlist ini + Kolaboratif + Orang lain dapat menambahkan lagu ke playlist ini + Gambar Sampul + Sampul playlist + Tidak ada gambar + Ganti Gambar + Pilih Gambar + + + Pasang + Ganti + plugin.json tidak ditemukan di file zip + manifest.zipline.json tidak ditemukan di file zip + Format plugin.json tidak valid: %1$s + Gagal membaca plugin: %1$s + Gagal memasang plugin: %1$s + Versi lebih baru dari %1$s tersedia. Konfirmasi pembaruan sebelum memasang. + %1$s sudah terpasang. Konfirmasi penggantian sebelum memasang. + Plugin bawaan sudah disertakan dan tidak dapat ditambahkan lagi. + Plugin bawaan tidak dapat dihapus. + %1$s mendeklarasikan apiVersion yang tidak valid (%2$s). Diharapkan semantic versioning yang kompatibel dengan %3$s. + Versi API plugin Spotube %1$s tidak valid. + %1$s menargetkan API plugin %2$s, tetapi Spotube mendukung API plugin breaking %3$s. Pasang plugin yang dibuat untuk versi API breaking yang sama. + + + API plugin tidak kompatibel + Pasang plugin? + %1$s akan ditambahkan ke plugin terpasang. + Perbarui plugin? + Versi lama terpasang (%1$s). Perbarui ke %2$s? + Ganti plugin terpasang? + %1$s menggunakan versi semantik yang tidak valid (%2$s) dan tidak dapat dibandingkan sebagai pembaruan. Tetap ganti plugin terpasang? + Versi terpasang (%1$s) tidak dapat dibandingkan menggunakan semantic versioning. Ganti dengan %2$s? + Versi %1$s sudah terpasang. Ganti plugin yang ada dengan salinan yang diberikan? + Versi terpasang %1$s lebih baru daripada versi yang diberikan %2$s. Tetap ganti? + Ganti plugin terpasang dengan salinan yang diberikan? + + + %1$s: + Versi + Versi API + Pembuat + Lisensi + Kapabilitas + Kemampuan + Repositori + Kontak + Laporkan Bug + Dukung %1$s + + + Resmi + Terverifikasi + Bintang GitHub + URL repositori GitHub + + + Tidak ada rilis ditemukan + Pilih Rilis + Pilih rilis + Pra-rilis + Catatan Rilis + + + Beranda + Cari + Di Perangkat + Connect + Perangkat + + + Sembunyikan Jendela + Tampilkan Jendela + Lagu Berikutnya + Lagu Sebelumnya + Acak + Ulang + Ulang: MATI + Ulang: SATU + Ulang: SEMUA + Volume: %1$d% + Volume + + Volume - + Bisukan + Bunyikan + Sukai Lagu + Batal Sukai Lagu + Keluar + + + Minimalkan + Maksimalkan + Pulihkan + Tutup + + + Layanan pemutaran + Pemutaran + Kontrol pemutaran media + + + Jelajahi + Sedang Diputar + Unggulan + Lagu Tersimpan + Lagu Teratas + diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml new file mode 100644 index 00000000..4a3475b6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml @@ -0,0 +1,944 @@ + + + + Coda + Svuota coda + Filtra coda... + La coda è vuota + + Cerca brani, artisti, album... + Cancella tutta la cronologia + Cerca... + + + La tua libreria + Libreria + Playlist + Album + Artisti + Brani locali + Download + Cerca nelle playlist salvate… + Cerca negli album salvati… + Cerca negli artisti salvati… + Cerca cartelle o brani… + Filtra libreria + Chiudi filtro + Crea playlist + Aggiorna brani locali + Passa alla vista a griglia + Passa alla vista a elenco + Nessuna playlist salvata + Nessuna playlist corrisponde a “%1$s” + Nessun album salvato + Nessun album corrisponde a “%1$s” + Nessun artista salvato + Nessun artista corrisponde a “%1$s” + + + Cartelle + Cartella + %1$s • %2$s + + %1$d brano + %1$d brani + %1$d brani + + + %1$d cartella + %1$d cartelle + %1$d cartelle + + Torna alle cartelle + Riproduci cartella + Accesso all\'audio necessario + Consenti l\'accesso all\'audio per sfogliare la musica sul tuo dispositivo. + Consenti accesso + Nessuna cartella locale trovata + Nessun brano in questa cartella + Nessuna cartella o brano corrispondente + Impossibile aggiornare i contenuti locali + + + %1$d attivi • %2$d completati + Rimuovi completati + Ancora nessun download + Download in corso %1$d% + In coda + Completato + Annullato + Non riuscito + Non riuscito: %1$s + Annulla download + Riprova download + Rimuovi download + + + Artista + In blacklist + Brani che ti piacciono + I tuoi brani salvati + Artista sconosciuto + Titolo del brano + Nome dell\'artista + + Selezionato + + Impostazioni + + Plugin + Lingua e area geografica + Aspetto + Riproduzione + Download + Cache + Desktop + Aggiornamenti + Supporto + Informazioni + + Chiudi + Salva + Annulla + + Lingua + Lingua dell\'interfaccia dell\'app. Attuale: %1$s + Predefinita di sistema + Paese + Paese per contenuti e disponibilità. Attuale: %1$s + %1$s (%2$s) + + Tema + Scegli come Spotube si adatta all\'aspetto del sistema. Attuale: %1$s + Chiaro + Scuro + Sistema + + Superficie + Materiale di sfondo dell\'app. Attuale: %1$s + Acqua + Ghiaccio + Cromo + Acrilico + Vetro + + Colore di accento + Palette attuale: %1$s + Colore di accento + Scegli una palette di accento e visualizzane l\'anteprima nei temi chiaro e scuro. + Chiaro + Scuro + Anteprima + Pulsante + Verde Goblin + Viola elettrico + Ciano oceano + Arancio tramonto + Giardino di rose + Blu notte + Ardesia metallica + + Aspetto e stile + Materiale della superficie e accento. Attuale: %1$s · %2$s + Aspetto e stile + Scegli un materiale per la superficie, poi un accento abbinato. Ogni opzione è una combinazione curata. + Superficie + Accento + Secondario + + Palette + Palette di accento Millennium. Attuale: %1$s + Palette + Scegli una palette di accento Millennium e visualizzane l\'anteprima in chiaro e scuro. + Palette + Palette e accento Millennium. Attuale: %1$s + Blu + Arancione + Lime + + Cartella di download personalizzata + Cartella attuale: %1$s + Cartella attuale: cartella Musica predefinita + Lascia vuoto per usare la cartella Musica predefinita. + C:\Music\Spotube + Cartelle multimediali locali + Cartelle personalizzate da analizzare sul desktop. Aggiunte: %1$d + Nessuna cartella personalizzata aggiunta + Cartelle personalizzate per la tua libreria musicale locale. + Gestisci + Aggiungi cartella + Rimuovi cartella + Nessuna cartella personalizzata aggiunta. + Rimuovi %1$s dall\'elenco di analisi + Le cartelle di download e della cache musicale sono incluse automaticamente. + La tua cartella di download è inclusa automaticamente. + La cartella della cache musicale è inclusa automaticamente. + Salvataggio… + Impossibile salvare le cartelle. Le modifiche sono ancora qui; riprova. + + %1$d cartella + %1$d cartelle + %1$d cartelle + + Formato musica scaricata + Codec e contenitore preferiti per i download. Attuale: %1$s + Qualità musica scaricata + + Formato musica in streaming + Codec e contenitore preferiti per lo streaming musicale. Attuale: %1$s + Qualità musica in streaming + Attuale: %1$s + Attiva cache musicale + Salva l\'audio in streaming in locale per velocizzare gli ascolti successivi. + Cartella cache + Attuale: %1$s + Attuale: cache predefinita dell\'app + Limite dimensione cache + Nessun limite + Limite attuale: %1$d MB + Imposta la dimensione massima della cache in MB. Inserisci 0 o lascia vuoto per nessun limite. + 500 + La dimensione deve essere 0 o un numero positivo. + Attiva riproduzione continua + Continua la riproduzione con brani consigliati quando la coda finisce. + Attiva Connect + Rendi disponibili i controlli di riproduzione remota tramite la funzione Spotube Connect. + Porta del server proxy di riproduzione + Consenti controllo remoto + Consenti agli altri dispositivi sulla stessa rete di controllare la riproduzione quando si connettono. + Nome del dispositivo per il controllo remoto + Il nome che gli altri dispositivi vedono quando trovano questo. Attuale: %1$s + Scegli un nome riconoscibile da mostrare agli altri dispositivi della tua rete quando trovano questa istanza di Spotube. + es. Spotube del soggiorno + nome host del dispositivo + Porta usata dal server proxy di riproduzione. Attuale: %1$d + Scegli una porta tra 1 e 65535. + 14769 + Inserisci un numero intero valido. + La porta deve essere compresa tra 1 e 65535. + + Blacklist + Gestisci brani e artisti in blacklist + + Riduci nell\'area di notifica + Mantieni Spotube in esecuzione nell\'area di notifica quando la finestra è ridotta a icona. + Rich Presence di Discord + Mostra lo stato di riproduzione attuale su Discord dal desktop. + + Controlla automaticamente gli aggiornamenti + Cerca nuove versioni di Spotube in background. + + Gestisci plugin + Gestisci i tuoi plugin + Plugin %1$s predefinito + Metadati + Audio + Testi + Scrobbling + Nessun plugin selezionato + Cambia + Seleziona + Selezionato + Cancella selezione + Nessun plugin disponibile + Plugin %1$s + Cancella + + Configura + Gestione plugin + Inserisci un URL + L\'URL deve iniziare con http:// o https:// + Impossibile scaricare il plugin + Nessun plugin installato + Incolla un URL di download o scegli un file qui sopra. + + %1$d plugin installato + %1$d di plugin installati + %1$d plugin installati + + Installa un plugin + https://example.com/plugin.smplug + Scarica + Installa da file (.smplug) + Attivo + v%1$s + Integrato + Rimuovi plugin + Accedi + Esci + Scarica da URL + Installa da file + Installa + + • %1$s + Autorizzazioni richieste + Questo plugin non richiede autorizzazioni speciali. + Installato e fornito + Installato + Fornito + API %1$s → %2$s + Archiviazione permanente + Leggere e scrivere dati su disco + Richieste di rete + Inviare e ricevere dati tramite Internet + WebView + Mostrare contenuti web all\'interno dell\'app + Jam di gruppo + Broker MQTT + Host del broker + %1$s:%2$d + Porta del broker + Usa TLS + Nome utente (facoltativo) + Password (facoltativa) + Prefisso ID client + Usato per identificare questa istanza di Spotube + Crittografa la connessione al broker + Verifica le impostazioni attuali del broker prima di avviare una stanza. + Verifica connessione + Verifica in corso… + Connesso in %1$d ms + Non riuscito: %1$s + Broker di esempio: configurane uno tuo per l\'hosting autonomo + Connessione + Credenziali + Identità + Diagnostica + + + Jam di gruppo + Crea + Partecipa + Crea una stanza, poi condividi il codice con i tuoi amici. + Crea stanza + Entra nella stanza + Connessione… + Codice stanza + ABC123 + Broker: %1$s + Configura un broker nelle Impostazioni per ospitare una stanza o parteciparvi. + Impostazioni broker + Connesso + Riconnessione… + Copia codice + Copiato! + Condividi + Partecipanti (%1$d) + Host + Ascoltatore + In attesa di ascoltatori… + Rimuovi + Banna + Gestisci + Esci dalla stanza + Impossibile connettersi + Ignora + Impossibile creare la sessione Jam + Impossibile partecipare alla sessione Jam + Inserisci un codice stanza di sei caratteri composto da lettere e cifre. + Unisciti alla mia Jam su Spotube con il codice: %1$s + + Vai all\'album + Timer di spegnimento + Dettagli brano + Riproduci + Pausa + Nessuna riproduzione in corso + Aggiungi alla coda + + Ignora + Aggiorna + Spotube %1$s è disponibile + Scopri le novità di questa versione nella pagina dei download di Spotube. + %1$s %2$s è disponibile + Attualmente installata: %1$s + Scopri le novità di questo aggiornamento nella pagina del plugin. + + Salta + Avanti + Indietro + Inizia + Spotube + Musica, senza vincoli. + Un player open source per un mondo di musica.\nTutto da esplorare. Tutto da rendere tuo. + Aiutaci a mantenere vivo questo progetto. + Spotube è creato da persone, non da un\'azienda in abbonamento. Se diventa parte della tua quotidianità, un piccolo contributo ci aiuta a dargli la cura che merita.\n\nGrazie per fare spazio al software indipendente. + Inizia con un plugin di catalogo per trovare brani e playlist. Scegli un plugin audio per riprodurli. Testi e cronologia di ascolto sono facoltativi. Scopri un plugin qui sotto, controlla le sue autorizzazioni, installalo e poi selezionalo qui. + Scopri plugin + Le tue fonti + Riprova + Altri plugin + Nessun nuovo plugin trovato. Puoi anche installarne uno da un link o da un file .smplug qui sopra. + La ricerca dei plugin non è disponibile al momento. Riprova oppure installa da un link o da un file. + Benvenuto + Supporto + Plugin + Guida + + Come usare Spotube + Quattro rapidi passaggi e sei pronto ad ascoltare. + Accedi + Collega il tuo account Spotify o scegli il tuo plugin di catalogo. + Cerca e riproduci + Trova qualsiasi brano, album o artista, poi premi play. + Ascolta offline + Scarica i brani per continuare ad ascoltarli senza connessione. + Segui il testo + Apri i testi sincronizzati dal player mentre ascolti un brano. + + + La tua musica. Le tue fonti. Le tue regole. + Un player musicale gratuito e open source, creato da una community che ama semplicemente la musica. + Open source + Nessuna pubblicità + Nessuna telemetria + Basato su plugin + Iniziamo + + + I sostenitori ci fanno andare avanti + Creato con passione. Tenuto in vita da te. + Spotube è gratuito al 100%, libero e open source: niente pubblicità, niente abbonamenti, nessun tracciamento. Ogni funzione esiste perché persone come te hanno scelto di contribuire allo sviluppo. + Patreon + Sostenitore mensile + Sostieni lo sviluppo continuo e l\'infrastruttura + Buy Me a Coffee + Un caffè una tantum + Alimenta una sessione di programmazione notturna con una piccola mancia + Open Collective + Fondo della community + Sostegno finanziario trasparente e gestito dalla community + Grazie di cuore per credere nella musica libera. + Il sostegno è del tutto facoltativo: Spotube resta comunque gratuito. + Sostieni + Apri %1$s + + + Sostieni Spotube + Sostieni Spotube + Spotube è gratuito, libero e open source: niente pubblicità, niente abbonamenti, nessun tracciamento. Se ti rende felice, valuta di sostenere il progetto. + Puoi disattivare questi promemoria in Impostazioni → Supporto. + Forse più tardi + Promemoria di sostegno + Mostra un discreto promemoria per sostenere il progetto ogni 7 giorni. + + + Architettura modulare + Scegli da dove arriva la tua musica. + Spotube separa la ricerca nel catalogo, lo streaming audio e i testi sincronizzati. Configura i tuoi motori qui sotto o cambiali in qualsiasi momento. + Motore metadati + Gestisce scoperta musicale, ricerca, elenchi di brani e playlist. + Motore audio + Recupera e decodifica i flussi audio da riprodurre. + Motore testi + Trasmette testi karaoke sincronizzati in tempo reale. + Motore scrobbling + Registra la cronologia di ascolto su Last.fm o ListenBrainz. + Attivo + Integrato + Pronto + Facoltativo + Seleziona un motore + Motori installati + Imposta come predefinito + NewPipe YouTube e LRCLib Lyrics sono integrati e pronti per lo streaming fin da subito. + Gestisci o aggiungi nuovi plugin della community in qualsiasi momento in Impostazioni → Plugin. + + + Architettura retro-moderna + Puro Hi-Fi, zero intermediari + Ecco come Spotube gestisce la riproduzione decentralizzata direttamente sul tuo dispositivo. + PEER-TO-PEER DIRETTO • OPUS 256 KBPS + Ora blu + Spotube · dimostrazione del player + Un po\' di musica per i momenti di mezzo. + 1. Ricerca decentralizzata + Le ricerche vanno direttamente al plugin di catalogo scelto. Spotube non profila mai i tuoi gusti. + 2. Risoluzione diretta degli stream + L\'audio arriva direttamente dai server dei contenuti al tuo dispositivo. Nessun proxy intermedio né raccolta remota di dati. + 3. DSP e riproduzione in locale + Decodificato e salvato in cache in locale, con equalizzatore e download offline facoltativo. + 4. Testi sincronizzati e Jam + I testi karaoke seguono il tempo di riproduzione, con controllo remoto via LAN e Jam di gruppo. + Nessun server intermediario. La riproduzione avviene direttamente sul tuo dispositivo: privacy by design. + + + Scegli un dispositivo + %1$s: «%2$s» + Riproduci + Aggiungi alla coda + Riproduci dopo + Questo dispositivo + Su questo dispositivo + Dispositivo remoto + Dispositivo remoto (%1$s) + Sul dispositivo connesso + + + Connessione non riuscita + Connessione persa + Invio non riuscito + Ricerca non riuscita: %1$s + Impossibile connettersi: %1$s + + + Dispositivi + Aggiorna + Connesso a %1$s:%2$d + Disconnetti + Connessione... + Ricerca di dispositivi sulla rete... + Nessun dispositivo trovato + Assicurati che sull\'altro dispositivo sia attivo \"Consenti controllo remoto\" nelle impostazioni. + Dispositivo sconosciuto + Risoluzione... + ID: %1$s... + + + Controllo remoto + Disconnesso + Errore di connessione: %1$s + Copertina dell\'album + Brano sconosciuto + Artista sconosciuto + Posizione + Riproduzione casuale + Precedente + Successivo + Modalità ripetizione + Volume + Rimuovi dalla coda + Altre opzioni + + + Dove riprodurre? + Hai una sessione Jam attiva. Scegli dove riprodurre questa raccolta. + Riproduci qui + Suggerisci alla Jam + + + Nessun broker Jam configurato + L\'host del broker è vuoto + Connessione non riuscita + Connessione persa + + + Broker + anonimo + + + Aggiunto alla coda della Jam + + %1$d brano aggiunto alla coda della Jam + %1$d brani aggiunti alla coda della Jam + %1$d brani aggiunti alla coda della Jam + + + + Richiesta di controllo remoto + \"%1$s\" vuole controllare la riproduzione su questo dispositivo. + Rifiuta + Consenti + Consenti sempre + + + Risultati della ricerca + Tutto + Brani + Utenti + Nessun tipo di ricerca disponibile + Inizia a digitare per cercare + Inizia a digitare per cercare brani + Nessun risultato trovato + Mostra tutto + Ricerca in corso + Caricamento brani... + Caricamento album... + Caricamento artisti... + Caricamento playlist... + Caricamento utenti... + Nessun album trovato + Nessun artista trovato + Nessuna playlist trovata + Nessun utente trovato + Frugando tra le casse di dischi... + Esplorando il cosmo... + Scaldando le casse... + Spolverando i vinili... + Consultando gli dei della musica... + Sintonizzando le antenne... + Sfogliando i dischi... + Impossibile caricare le funzionalità di ricerca + Ricerca non riuscita + Impossibile cercare i brani + Impossibile cercare gli album + Impossibile cercare gli artisti + Impossibile cercare le playlist + Impossibile cercare gli utenti + + + Sfoglia + + + Sincronizzati + Semplici + Nessun testo sincronizzato disponibile + Nessun testo semplice disponibile + + + %1$s in %2$s + %1$d kbps + %1$s kHz • %2$d can. + Nessun plugin di metadati selezionato + + + Indietro + Riprova + Chiudi finestra di dialogo + Caricamento... + Sconosciuto + Errore sconosciuto + + + Utente + Playlist + + + Ci dispiace davvero + Qualcosa non sta funzionando in questo momento. La tua esperienza è importante per noi e stiamo facendo del nostro meglio per risolvere il problema. Riprova o controlla i dettagli dell\'errore qui sotto. + Dettagli dell\'errore + Copia negli appunti + Visualizza log completo + Log completo dell\'errore + + + Riproduzione casuale + Riproduci in ordine casuale + Segui + Non seguire più + Modifica playlist + Di %1$s + + + Opzioni brano + Aggiungi alla Jam + Avvia radio + Riproduci dopo + Sposta come successivo + Rimuovi dalla coda + Rimuovi dai preferiti + Salva nei preferiti + Aggiungi alla playlist + Scarica + Condividi + + + Originale + Titolo + Artista + Album + Durata + Ordina + Nessun brano trovato + Filtra + Azioni multiple + Scarica tutto + Scarica %1$d + Aggiungi tutto alla coda + Aggiungi %1$d alla coda + Riproduci tutto dopo + Riproduci %1$d dopo + Aggiungi tutto alla playlist + Aggiungi %1$d alla playlist + Aggiungi tutto alla Jam + Aggiungi %1$d alla Jam + +%1$d altri + + + Nessun URL da caricare + Torna indietro nella cronologia del browser + Vai avanti nella cronologia del browser + + + Filtra blacklist... + Nessun elemento in blacklist + Nessun elemento in blacklist corrisponde a \'%1$s\' + Brani + Brano + Aggiungi alla blacklist + Rimuovi dalla blacklist + + + Nessun metadato del brano + Impossibile risolvere l\'URL dello stream + Impossibile risolvere l\'URL dello stream. Prova prima a riprodurre il brano. + + + Brani salvati + Tu + + + Segui già + Brani più popolari + Riproduci i brani più popolari + Aggiungi i brani più popolari alla coda + Artisti simili + Playlist in evidenza + Impossibile caricare la panoramica dell\'artista + Follower non disponibili + %1$s K follower + %1$s Mln follower + %1$s Mld follower + + %1$d follower + %1$d follower + %1$d follower + + + + Mi piace + Non mi piace più + Titolo brano segnaposto + Artisti segnaposto + Album sconosciuto + Posizione + Volume + Attiva riproduzione casuale + Disattiva riproduzione casuale + Precedente + Successivo + Riproduci o metti in pausa + Ripetizione disattivata + Ripeti brano + Ripeti tutto + Disattiva audio + Riattiva audio + Altre opzioni + Opzioni del player + Comprimi player + Espandi testo + In riproduzione + Testo + Fonte alternativa + Fonte alternativa + Scarica + Scaricato + Download in corso... + Già scaricato + Nuovo tentativo di download... + Download di %1$s + La riproduzione si interromperà tra %1$s. + Metti in pausa la riproduzione dopo il numero di minuti selezionato. + Minuti + Scegli tra 1 e 600 minuti + Annulla timer + Avvia + L\'URL di riproduzione è stato risolto con impostazioni di streaming diverse. Lo stream evidenziato corrisponde alle tue preferenze attuali; l\'URL attivo verrà aggiornato alla prossima richiesta di stream. + Fonte audio abbinata + Caricamento della fonte abbinata… + Una fonte remota abbinata non è necessaria durante la riproduzione del file locale in cache. + Nessuna fonte abbinata disponibile. + Nessuna fonte audio abbinata in cache per questo brano. + I dettagli dello stream non sono ancora in cache. Riproduci prima il brano per caricarli. + Impossibile caricare i dettagli del brano. + Stream disponibili + Questo brano viene riprodotto dalla cache locale. Le varianti di stream non vengono usate per questa riproduzione. + Caricamento degli stream… + Nessuno stream disponibile per questa fonte. + Stream attualmente selezionato + Corrisponde alle tue preferenze di formato e qualità dello streaming + URL di riproduzione attivo + Riprodotto dalla cache locale + Brano dei metadati + Titolo + ID + Album + Affidabilità + URI esterno + ID album + Tipo di album + Data di uscita dell\'album + Numero di brani dell\'album + Generi dell\'album + Durata + Numero del brano + Numero del disco + Esplicito + Popolarità + ISRC + URL delle miniature + + + Mostra/nascondi barra laterale + + + Fonti alternative + Nessun brano in riproduzione + Ricerca di fonti alternative... + Nessuna fonte alternativa trovata + Affidabilità: %1$d% + Fonte attiva + + + Espelli + Rimuovilo dalla sessione + Espellilo e impediscigli di rientrare + Rimuovi suggerimenti + Rimuovi tutti i brani che ha aggiunto alla coda + Riordina + Rimuovi dalla coda + %1$s • Aggiunto da %2$s + + + Sconosciuto + Caricamento playlist... + Aggiungi brani + Aggiungi brani alla playlist + Cerca brani... + Cerca + Nessun brano trovato + Cerca brani da aggiungere + Fine + Aggiungi + Aggiunto + + + Caricamento album... + + + Aggiungi a una tua playlist + Cerca + Cerca nelle tue playlist... + Caricamento delle tue playlist... + Non hai ancora nessuna playlist.\nCreane una per aggiungere brani. + Crea playlist + Modifica playlist + Crea + Nome + Nome della playlist + Descrizione + Descrizione (facoltativa) + Pubblica + Chiunque può vedere questa playlist + Collaborativa + Altre persone possono aggiungere brani a questa playlist + Immagine di copertina + Copertina della playlist + Nessuna immagine + Cambia immagine + Scegli immagine + + + Installa + Sostituisci + plugin.json non trovato nel file zip + manifest.zipline.json non trovato nel file zip + Formato di plugin.json non valido: %1$s + Impossibile leggere il plugin: %1$s + Impossibile installare il plugin: %1$s + È disponibile una versione più recente di %1$s. Conferma l\'aggiornamento prima di installarla. + %1$s è già installato. Conferma la sostituzione prima di installarlo. + I plugin integrati sono già inclusi e non possono essere aggiunti di nuovo. + I plugin integrati non possono essere rimossi. + %1$s dichiara un apiVersion non valido (%2$s). È previsto un versionamento semantico compatibile con %3$s. + La versione API dei plugin di Spotube %1$s non è valida. + %1$s è destinato all\'API plugin %2$s, ma Spotube supporta l\'API plugin %3$s con modifiche incompatibili. Installa un plugin creato per la stessa versione principale dell\'API. + + + API del plugin non compatibile + Installare il plugin? + %1$s verrà aggiunto ai plugin installati. + Aggiornare il plugin? + È installata una versione precedente (%1$s). Aggiornare a %2$s? + Sostituire il plugin installato? + %1$s usa una versione semantica non valida (%2$s) e non può essere confrontato come aggiornamento. Sostituire comunque il plugin installato? + La versione installata (%1$s) non può essere confrontata tramite versionamento semantico. Sostituirla con %2$s? + La versione %1$s è già installata. Sostituire il plugin esistente con la copia fornita? + La versione installata %1$s è più recente della versione fornita %2$s. Sostituirla comunque? + Sostituire il plugin installato con la copia fornita? + + + %1$s: + Versione + Versione API + Autore + Licenza + Funzionalità + Abilità + Repository + Contatto + Segnala bug + Sostieni %1$s + + + Ufficiale + Verificato + Stelle su GitHub + URL del repository GitHub + + + Nessuna versione trovata + Seleziona versione + Seleziona una versione + Pre-release + Note di rilascio + + + Home + Cerca + Sul dispositivo + Connect + Dispositivi + + + Nascondi finestra + Mostra finestra + Brano successivo + Brano precedente + Riproduzione casuale + Ripetizione + Ripetizione: NO + Ripetizione: UNO + Ripetizione: TUTTI + Volume: %1$d% + Volume + + Volume - + Disattiva audio + Riattiva audio + Metti Mi piace al brano + Togli Mi piace al brano + Esci + + + Riduci a icona + Ingrandisci + Ripristina + Chiudi + + + Servizio di riproduzione + Riproduzione + Controlli di riproduzione multimediale + + + Sfoglia + In riproduzione + In evidenza + Brani salvati + Brani più popolari + diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml new file mode 100644 index 00000000..9976483c --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml @@ -0,0 +1,932 @@ + + + + 再生キュー + キューをクリア + キューを絞り込み... + キューに曲がありません + + 曲、アーティスト、アルバムを検索... + 履歴をすべて消去 + 検索... + + + ライブラリ + ライブラリ + プレイリスト + アルバム + アーティスト + ローカルの曲 + ダウンロード + 保存したプレイリストを検索… + 保存したアルバムを検索… + 保存したアーティストを検索… + フォルダや曲を検索… + ライブラリを絞り込み + 絞り込みを閉じる + プレイリストを作成 + ローカルの曲を更新 + グリッド表示に切り替え + リスト表示に切り替え + 保存したプレイリストはありません + 「%1$s」に一致するプレイリストはありません + 保存したアルバムはありません + 「%1$s」に一致するアルバムはありません + 保存したアーティストはありません + 「%1$s」に一致するアーティストはありません + + + フォルダ + フォルダ + %1$s • %2$s + + %1$d曲 + + + %1$d個のフォルダ + + フォルダ一覧に戻る + フォルダを再生 + オーディオへのアクセスが必要です + デバイス内の音楽を表示するには、オーディオへのアクセスを許可してください。 + アクセスを許可 + ローカルフォルダが見つかりません + このフォルダには曲がありません + 一致するフォルダや曲はありません + ローカルメディアを更新できませんでした + + + %1$d件進行中 • %2$d件完了 + 完了済みを消去 + ダウンロードはまだありません + ダウンロード中 %1$d% + 待機中 + 完了 + キャンセル済み + 失敗 + 失敗: %1$s + ダウンロードをキャンセル + ダウンロードを再試行 + ダウンロードを削除 + + + アーティスト + ブラックリスト登録済み + お気に入りの曲 + 保存した曲 + 不明なアーティスト + 曲名 + アーティスト名 + + 選択済み + + 設定 + + プラグイン + 言語と地域 + 外観 + 再生 + ダウンロード + キャッシュ + デスクトップ + アップデート + サポート + このアプリについて + + 閉じる + 保存 + キャンセル + + 言語 + アプリの表示言語。現在: %1$s + システムのデフォルト + 国 + コンテンツと利用可否に使用する国・地域。現在: %1$s + %1$s (%2$s) + + テーマ + Spotubeをシステムの外観にどう合わせるかを選択します。現在: %1$s + ライト + ダーク + システム + + サーフェス + アプリの背景素材。現在: %1$s + ウォーター + アイス + クローム + アクリル + ガラス + + アクセントカラー + 現在のパレット: %1$s + アクセントカラー + アクセントパレットを選び、ライトとダークの両テーマでプレビューできます。 + ライト + ダーク + プレビュー + ボタン + グリーンゴブリン + エレクトリックバイオレット + オーシャンシアン + サンセットオレンジ + ローズガーデン + ミッドナイトブルー + メタリックスレート + + ルック&フィール + サーフェス素材とアクセント。現在: %1$s · %2$s + ルック&フィール + サーフェス素材を選び、それに合うアクセントを選択してください。どの選択肢も厳選された組み合わせです。 + サーフェス + アクセント + セカンダリ + + パレット + Millenniumアクセントパレット。現在: %1$s + パレット + Millenniumアクセントパレットを選び、ライトとダークでプレビューできます。 + パレット + Millenniumパレットとアクセント。現在: %1$s + ブルー + オレンジ + ライム + + ダウンロードフォルダの変更 + 現在のフォルダ: %1$s + 現在のフォルダ: デフォルトの音楽フォルダ + 空欄にするとデフォルトの音楽フォルダを使用します。 + C:\Music\Spotube + ローカルメディアフォルダ + デスクトップでスキャンするカスタムフォルダ。追加済み: %1$d + カスタムフォルダはまだ追加されていません + ローカル音楽ライブラリ用のカスタムフォルダ。 + 管理 + フォルダを追加 + フォルダを削除 + カスタムフォルダは追加されていません。 + %1$sをスキャン対象から削除 + ダウンロードフォルダと音楽キャッシュフォルダは自動的に含まれます。 + ダウンロードフォルダは自動的に含まれます。 + 音楽キャッシュフォルダは自動的に含まれます。 + 保存中… + フォルダを保存できませんでした。変更内容は保持されていますので、もう一度お試しください。 + + %1$d個のフォルダ + + ダウンロードの音楽フォーマット + ダウンロードに使用するコーデックとコンテナ。現在: %1$s + ダウンロードの音質 + + ストリーミングの音楽フォーマット + ストリーミング再生に使用するコーデックとコンテナ。現在: %1$s + ストリーミングの音質 + 現在: %1$s + 音楽のキャッシュを有効化 + ストリーミングした音声をローカルに保存し、再度の再生を高速化します。 + キャッシュフォルダ + 現在: %1$s + 現在: アプリのデフォルトキャッシュ + キャッシュサイズの上限 + 上限なし + 現在の上限: %1$d MB + キャッシュの最大サイズをMB単位で設定します。0を入力するか空欄にすると上限なしになります。 + 500 + サイズは0または正の数で指定してください。 + エンドレス再生を有効化 + キューが終わったら、おすすめの曲で再生を続けます。 + Connectを有効化 + Spotube Connect機能でリモート再生コントロールを公開します。 + 再生プロキシサーバーのポート + リモート操作を許可 + 同じネットワーク上の他のデバイスが接続したときに再生を操作できるようにします。 + リモート操作用のデバイス名 + 他のデバイスがこのデバイスを検出したときに表示される名前。現在: %1$s + ネットワーク上の他のデバイスがこのSpotubeを検出したときに表示される、わかりやすい名前を付けてください。 + 例: リビングのSpotube + デバイスのホスト名 + 再生プロキシサーバーが使用するポート。現在: %1$d + 1〜65535のポートを選択してください。 + 14769 + 有効な整数を入力してください。 + ポートは1〜65535の範囲で指定してください。 + + ブラックリスト + ブラックリストに登録した曲とアーティストを管理 + + トレイに最小化 + ウィンドウを最小化したとき、Spotubeをシステムトレイで実行し続けます。 + Discordリッチプレゼンス + デスクトップで現在の再生状況をDiscordに表示します。 + + アップデートを自動確認 + Spotubeの新しいリリースをバックグラウンドで確認します。 + + プラグインを管理 + プラグインを管理します + デフォルトの%1$sプラグイン + メタデータ + オーディオ + 歌詞 + Scrobble + プラグインが選択されていません + 変更 + 選択 + 選択済み + 選択を解除 + 利用できるプラグインはありません + %1$sプラグイン + クリア + + 構成 + プラグインマネージャー + URLを入力してください + URLは http:// または https:// で始まる必要があります + プラグインをダウンロードできませんでした + プラグインはまだインストールされていません + 上でダウンロードURLを貼り付けるか、ファイルを選択してください。 + + %1$d個のプラグインをインストール済み + + プラグインをインストール + https://example.com/plugin.smplug + ダウンロード + ファイルからインストール (.smplug) + 有効 + v%1$s + 内蔵 + プラグインを削除 + ログイン + ログアウト + URLからダウンロード + ファイルからインストール + インストール + + • %1$s + 要求される権限 + このプラグインに特別な権限は必要ありません。 + インストール済み vs 提供版 + インストール済み + 提供版 + API %1$s → %2$s + 永続ストレージ + ディスクへのデータの読み書き + ネットワークリクエスト + インターネット経由でのデータの送受信 + WebView + アプリ内でのWebコンテンツの表示 + グループジャム + MQTTブローカー + ブローカーのホスト + %1$s:%2$d + ブローカーのポート + TLSを使用 + ユーザー名 (任意) + パスワード (任意) + クライアントIDのプレフィックス + このSpotubeを識別するために使用されます + ブローカーとの接続を暗号化します + ルームを開始する前に、現在のブローカー設定を確認します。 + 接続をテスト + テスト中… + %1$d msで接続しました + 失敗: %1$s + 仮のブローカーです。セルフホストするには独自のブローカーを設定してください + 接続 + 認証情報 + 識別情報 + 診断 + + + グループジャム + 作成 + 参加 + ルームを作成して、コードを友達に共有しましょう。 + ルームを作成 + ルームに参加 + 接続中… + ルームコード + ABC123 + ブローカー: %1$s + ルームを主催または参加するには、設定でブローカーを設定してください。 + ブローカー設定 + 接続済み + 再接続中… + コードをコピー + コピーしました! + 共有 + 参加者 (%1$d) + ホスト + リスナー + リスナーを待っています… + 削除 + BAN + 管理 + ルームから退出 + 接続できませんでした + 閉じる + ジャムセッションを作成できませんでした + ジャムセッションに参加できませんでした + 英数字6文字のルームコードを入力してください。 + コード %1$s で私のSpotubeジャムに参加しよう + + アルバムへ移動 + スリープタイマー + 曲の詳細 + 再生 + 一時停止 + 再生中の曲はありません + キューに追加 + + 無視 + アップデート + Spotube %1$s が利用可能です + このリリースの新機能はSpotubeのダウンロードページでご確認ください。 + %1$s %2$s が利用可能です + インストール済み: %1$s + このアップデートの新機能はプラグインのページでご確認ください。 + + スキップ + 次へ + 戻る + はじめる + Spotube + 音楽を、しがらみなく。 + 音楽の世界へ広がる、オープンソースのプレーヤー。\n自由に探して、自分らしく楽しもう。 + このプロジェクトを支えてください。 + Spotubeはサブスクリプションビジネスではなく、人の手でつくられています。日々の暮らしの一部になったなら、少しのご支援が、このアプリにふさわしい手入れを続ける力になります。\n\n独立したソフトウェアに居場所をつくってくださり、ありがとうございます。 + まずはカタログプラグインで曲やプレイリストを探せるようにしましょう。次に、再生用のオーディオプラグインを選びます。歌詞と再生履歴は任意です。下からプラグインを見つけて権限を確認し、インストールしてからここで選択してください。 + プラグインを探す + ソース + 再試行 + その他のプラグイン + 新しいプラグインは見つかりませんでした。上のリンクや.smplugファイルからインストールすることもできます。 + 現在プラグインを検索できません。再試行するか、リンクまたはファイルからインストールしてください。 + ようこそ + サポート + プラグイン + ガイド + + Spotubeの使い方 + 簡単な4ステップで、すぐに聴けます。 + サインイン + Spotifyアカウントを連携するか、お好みのカタログプラグインを選んでください。 + 検索して再生 + 曲、アルバム、アーティストを探して再生しましょう。 + オフラインで聴く + 曲をダウンロードすれば、接続がなくても再生できます。 + 歌詞を表示 + 曲の再生中に、プレーヤーから同期歌詞を開けます。 + + + あなたの音楽。あなたのソース。あなたのルール。 + 音楽を愛するコミュニティがつくった、無料でオープンソースの音楽プレーヤー。 + オープンソース + 広告なし + テレメトリなし + プラグインで拡張 + はじめよう + + + サポーターの皆さんに支えられています + 情熱でつくり、あなたが支える。 + Spotubeは100%無料の自由なオープンソースソフトウェアです。広告も、サブスクリプションも、トラッキングもありません。すべての機能は、あなたのような人たちが開発を支えてくれたからこそ存在しています。 + Patreon + 毎月のスポンサー + 継続的な開発とインフラを支援 + Buy Me a Coffee + 1回限りのコーヒー + ちょっとしたチップで深夜のコーディングを応援 + Open Collective + コミュニティ基金 + 透明性が高く、コミュニティが運営する資金支援 + オープンな音楽を信じてくださり、心から感謝します。 + 支援はまったくの任意です。どちらでもSpotubeは無料のままです。 + 支援する + %1$sを開く + + + Spotubeを支援 + Spotubeを支援 + Spotubeは無料の自由なオープンソースソフトウェアです。広告も、サブスクリプションも、トラッキングもありません。気に入っていただけたら、プロジェクトへのご支援をご検討ください。 + このリマインダーは「設定 → サポート」でオフにできます。 + あとで + 支援のリマインダー + 7日ごとに、プロジェクトへの支援をさりげなくお知らせします。 + + + モジュール式アーキテクチャ + 音楽をどこから届けるか、選びましょう。 + Spotubeはカタログの検索、オーディオのストリーミング、同期歌詞をそれぞれ分離しています。下でエンジンを設定でき、いつでも切り替えられます。 + メタデータエンジン + 音楽の発見、検索、曲リスト、プレイリストを担います。 + オーディオエンジン + 再生するオーディオストリームを取得してデコードします。 + 歌詞エンジン + 時間同期されたカラオケ歌詞をリアルタイムで表示します。 + Scrobbleエンジン + 再生履歴をLast.fmやListenBrainzに記録します。 + 有効 + 内蔵 + 準備完了 + 任意 + エンジンを選択 + インストール済みのエンジン + デフォルトに設定 + NewPipe YouTubeとLRCLib Lyricsは内蔵されており、すぐにストリーミングできます。 + コミュニティプラグインの管理や追加は、いつでも「設定 → プラグイン」から行えます。 + + + レトロモダンなアーキテクチャ + 純粋なHi-Fi、仲介者ゼロ + Spotubeがあなたのマシン上で分散型の再生を直接制御する仕組みをご紹介します。 + ダイレクトP2P • 256 KBPS OPUS + ブルーアワー + Spotube · プレーヤーのデモ + 合間のひとときに、少しの音楽を。 + 1. 分散型の検索 + 検索クエリは選択したカタログプラグインに直接送られます。Spotubeがあなたの好みをプロファイリングすることはありません。 + 2. ストリームを直接解決 + オーディオはコンテンツサーバーからあなたのマシンへ直接ストリーミングされます。プロキシの経由やリモートでのデータ収集はありません。 + 3. ローカルDSPと再生 + ローカルでデコード・キャッシュされ、イコライザーやオフライン用ダウンロード (任意) も使えます。 + 4. 同期歌詞とジャム + カラオケ歌詞が再生位置に合わせて流れ、LANでのリモート操作やグループジャムにも対応します。 + 仲介サーバーはありません。再生はすべてあなたのデバイス上で行われ、設計段階からプライバシーを守ります。 + + + デバイスを選択 + %1$s:「%2$s」 + 再生 + キューに追加 + 次に再生 + このデバイス + このデバイスで + リモートデバイス + リモートデバイス (%1$s) + 接続中のデバイスで + + + 接続に失敗しました + 接続が切断されました + 送信に失敗しました + 検出に失敗しました: %1$s + 接続できませんでした: %1$s + + + デバイス + 更新 + %1$s:%2$d に接続済み + 切断 + 接続中... + ネットワーク上のデバイスを検索中... + デバイスが見つかりません + 相手のデバイスの設定で「リモート操作を許可」が有効になっていることを確認してください。 + 不明なデバイス + 解決中... + ID: %1$s... + + + リモート操作 + 切断されました + 接続エラー: %1$s + アルバムカバー + 不明な曲 + 不明なアーティスト + シーク + シャッフル + 前へ + 次へ + リピートモード + 音量 + キューから削除 + その他のオプション + + + どこで再生しますか? + ジャムセッションが進行中です。このコレクションを再生する場所を選んでください。 + ここで再生 + ジャムに提案 + + + ジャム用ブローカーが設定されていません + ブローカーのホストが空です + 接続に失敗しました + 接続が切断されました + + + ブローカー + anonymous + + + ジャムのキューに追加しました + + %1$d曲をジャムのキューに追加しました + + + + リモート操作のリクエスト + 「%1$s」がこのデバイスの再生を操作しようとしています。 + 拒否 + 許可 + 常に許可 + + + 検索結果 + すべて + 曲 + ユーザー + 利用できる検索タイプはありません + 入力して検索 + 入力して曲を検索 + 結果が見つかりません + すべて表示 + 検索中 + 曲を読み込み中... + アルバムを読み込み中... + アーティストを読み込み中... + プレイリストを読み込み中... + ユーザーを読み込み中... + アルバムが見つかりません + アーティストが見つかりません + プレイリストが見つかりません + ユーザーが見つかりません + レコード棚を掘り返しています... + 宇宙を探索しています... + スピーカーをウォームアップしています... + レコードのほこりを払っています... + 音楽の神様にお伺いを立てています... + アンテナを調整しています... + レコードをめくっています... + 検索機能を読み込めませんでした + 検索できませんでした + 曲を検索できませんでした + アルバムを検索できませんでした + アーティストを検索できませんでした + プレイリストを検索できませんでした + ユーザーを検索できませんでした + + + ブラウズ + + + 同期 + テキスト + 同期歌詞はありません + テキスト歌詞はありません + + + %1$s (%2$s) + %1$d kbps + %1$s kHz • %2$d ch + メタデータプラグインが選択されていません + + + 戻る + 再試行 + ダイアログを閉じる + 読み込み中... + 不明 + 不明なエラー + + + ユーザー + プレイリスト + + + 申し訳ありません + 現在、一部の機能が正しく動作していません。ご不便をおかけして申し訳ありません。解決に全力で取り組んでいます。もう一度お試しいただくか、下のエラー詳細をご確認ください。 + エラーの詳細 + クリップボードにコピー + ログ全体を表示 + エラーログ全体 + + + シャッフル + シャッフル再生 + フォロー + フォロー解除 + プレイリストを編集 + 作成者: %1$s + + + 曲のオプション + ジャムに追加 + ラジオを開始 + 次に再生 + 次に移動 + キューから削除 + お気に入りから削除 + お気に入りに保存 + プレイリストに追加 + ダウンロード + 共有 + + + 元の順序 + タイトル + アーティスト + アルバム + 再生時間 + 並べ替え + 曲が見つかりません + 絞り込み + 一括操作 + すべてダウンロード + %1$d曲をダウンロード + すべてキューに追加 + %1$d曲をキューに追加 + すべて次に再生 + %1$d曲を次に再生 + すべてプレイリストに追加 + %1$d曲をプレイリストに追加 + すべてジャムに追加 + %1$d曲をジャムに追加 + 他%1$d組 + + + 読み込むURLがありません + ブラウザの履歴を戻る + ブラウザの履歴を進む + + + ブラックリストを絞り込み... + ブラックリストに登録された項目はありません + 「%1$s」に一致するブラックリスト項目はありません + 曲 + 曲 + ブラックリストに追加 + ブラックリストから削除 + + + 曲のメタデータがありません + ストリームURLを解決できませんでした + ストリームURLを解決できませんでした。先に曲を再生してみてください。 + + + 保存した曲 + あなた + + + フォロー中 + 人気曲 + 人気曲を再生 + 人気曲をキューに追加 + 関連アーティスト + 注目のプレイリスト + アーティストの概要を読み込めませんでした + フォロワー数を取得できません + フォロワー %1$sK人 + フォロワー %1$sM人 + フォロワー %1$sB人 + + フォロワー %1$d人 + + + + お気に入りに追加 + お気に入りから削除 + No-op の曲名 + No-op のアーティスト + 不明なアルバム + シーク + 音量 + シャッフルをオン + シャッフルをオフ + 前へ + 次へ + 再生/一時停止 + リピート: オフ + リピート: 1曲 + リピート: すべて + ミュート + ミュート解除 + その他のオプション + プレーヤーのオプション + プレーヤーを折りたたむ + 歌詞を展開 + 再生中 + 歌詞 + 代替ソース + 代替ソース + ダウンロード + ダウンロード済み + ダウンロード中... + ダウンロード済みです + ダウンロードを再試行中... + %1$sをダウンロード中 + %1$s後に再生を一時停止します。 + 選択した時間 (分) が経過したら再生を一時停止します。 + 分 + 1〜600分の間で選択 + タイマーをキャンセル + 開始 + 再生URLは異なるストリーミング設定で解決されました。ハイライトされたストリームが現在の設定に一致しています。有効なURLは次回のストリームリクエスト時に更新されます。 + 一致したオーディオソース + 一致したソースを読み込み中… + ローカルのキャッシュファイルを再生中は、一致するリモートソースは不要です。 + 一致するソースはありません。 + この曲に一致するオーディオソースはキャッシュされていません。 + ストリームの詳細はまだキャッシュされていません。読み込むには先に曲を再生してください。 + 曲の詳細を読み込めませんでした。 + 利用可能なストリーム + この曲はローカルキャッシュから再生されています。この再生ではストリームのバリエーションは使用されません。 + ストリームを読み込み中… + このソースで利用できるストリームはありません。 + 現在選択中のストリーム + ストリーミングのフォーマットと音質の設定に一致 + 有効な再生URL + ローカルキャッシュから再生 + メタデータの曲 + タイトル + ID + アルバム + 一致度 + 外部URI + アルバムID + アルバムの種類 + アルバムのリリース日 + アルバムの曲数 + アルバムのジャンル + 再生時間 + トラック番号 + ディスク番号 + Explicit + 人気度 + ISRC + サムネイルURL + + + サイドバーの表示切り替え + + + 代替ソース + 再生中の曲はありません + 代替ソースを検索中... + 代替ソースが見つかりません + 一致度: %1$d% + 使用中のソース + + + キック + セッションから退出させます + キックして再参加できないようにします + 提案を削除 + このユーザーがキューに追加した曲をすべて削除します + 並べ替え + キューから削除 + %1$s • %2$sが追加 + + + 不明 + プレイリストを読み込み中... + 曲を追加 + プレイリストに曲を追加 + 曲を検索... + 検索 + 曲が見つかりません + 追加する曲を検索 + 完了 + 追加 + 追加済み + + + アルバムを読み込み中... + + + プレイリストに追加 + 検索 + プレイリストを検索... + プレイリストを読み込み中... + 所有しているプレイリストはまだありません。\nプレイリストを作成して曲を追加しましょう。 + プレイリストを作成 + プレイリストを編集 + 作成 + 名前 + プレイリスト名 + 説明 + 説明 (任意) + 公開 + 誰でもこのプレイリストを見られます + 共同編集 + 他のユーザーもこのプレイリストに曲を追加できます + カバー画像 + プレイリストのカバー + 画像なし + 画像を変更 + 画像を選択 + + + インストール + 置き換え + zipファイル内にplugin.jsonが見つかりません + zipファイル内にmanifest.zipline.jsonが見つかりません + plugin.jsonの形式が無効です: %1$s + プラグインを読み込めませんでした: %1$s + プラグインをインストールできませんでした: %1$s + %1$sの新しいバージョンがあります。インストールする前にアップデートを確認してください。 + %1$sはすでにインストールされています。インストールする前に置き換えを確認してください。 + 内蔵プラグインはすでに含まれているため、再度追加することはできません。 + 内蔵プラグインは削除できません。 + %1$sで宣言されたapiVersion (%2$s) が無効です。%3$sと互換性のあるセマンティックバージョニングが必要です。 + SpotubeのプラグインAPIバージョン %1$s が無効です。 + %1$sはプラグインAPI %2$s を対象としていますが、SpotubeがサポートするのはプラグインAPI %3$s (破壊的変更を含むバージョン) です。同じメジャーAPIバージョン向けにビルドされたプラグインをインストールしてください。 + + + プラグインAPIに互換性がありません + プラグインをインストールしますか? + %1$sがインストール済みプラグインに追加されます。 + プラグインをアップデートしますか? + 古いバージョン (%1$s) がインストールされています。%2$sにアップデートしますか? + インストール済みのプラグインを置き換えますか? + %1$sは無効なセマンティックバージョン (%2$s) を使用しているため、アップデートとして比較できません。それでもインストール済みのプラグインを置き換えますか? + インストール済みのバージョン (%1$s) はセマンティックバージョニングで比較できません。%2$sで置き換えますか? + バージョン %1$s はすでにインストールされています。既存のプラグインを提供されたものに置き換えますか? + インストール済みのバージョン %1$s は提供されたバージョン %2$s より新しいです。それでも置き換えますか? + インストール済みのプラグインを提供されたものに置き換えますか? + + + %1$s: + バージョン + APIバージョン + 作者 + ライセンス + 権限 + 機能 + リポジトリ + 連絡先 + バグを報告 + %1$sを支援 + + + 公式 + 認証済み + GitHubのスター + GitHubリポジトリのURL + + + リリースが見つかりません + リリースを選択 + リリースを選択してください + プレリリース + リリースノート + + + ホーム + 検索 + デバイス内 + Connect + デバイス + + + ウィンドウを隠す + ウィンドウを表示 + 次の曲 + 前の曲 + シャッフル + リピート + リピート: オフ + リピート: 1曲 + リピート: すべて + 音量: %1$d% + 音量 + + 音量 - + ミュート + ミュート解除 + お気に入りに追加 + お気に入りから削除 + 終了 + + + 最小化 + 最大化 + 元に戻す + 閉じる + + + 再生サービス + 再生 + メディア再生コントロール + + + ブラウズ + 再生中 + 注目 + 保存した曲 + 人気曲 + diff --git a/composeApp/src/commonMain/composeResources/values-ko/strings.xml b/composeApp/src/commonMain/composeResources/values-ko/strings.xml new file mode 100644 index 00000000..f0b8409e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-ko/strings.xml @@ -0,0 +1,932 @@ + + + + 대기열 + 대기열 비우기 + 대기열 필터... + 대기열이 비어 있어요 + + 노래, 아티스트, 앨범 검색... + 기록 모두 지우기 + 검색... + + + 내 라이브러리 + 라이브러리 + 플레이리스트 + 앨범 + 아티스트 + 로컬 트랙 + 다운로드 + 저장한 플레이리스트 검색… + 저장한 앨범 검색… + 저장한 아티스트 검색… + 폴더 또는 트랙 검색… + 라이브러리 필터 + 필터 닫기 + 플레이리스트 만들기 + 로컬 트랙 새로고침 + 그리드 보기로 전환 + 목록 보기로 전환 + 저장한 플레이리스트가 없어요 + “%1$s”와(과) 일치하는 플레이리스트가 없어요 + 저장한 앨범이 없어요 + “%1$s”와(과) 일치하는 앨범이 없어요 + 저장한 아티스트가 없어요 + “%1$s”와(과) 일치하는 아티스트가 없어요 + + + 폴더 + 폴더 + %1$s • %2$s + + 트랙 %1$d개 + + + 폴더 %1$d개 + + 폴더로 돌아가기 + 폴더 재생 + 오디오 접근 권한 필요 + 기기의 음악을 둘러보려면 오디오 접근을 허용하세요. + 접근 허용 + 로컬 폴더를 찾을 수 없어요 + 이 폴더에 트랙이 없어요 + 일치하는 폴더나 트랙이 없어요 + 로컬 미디어를 새로고침하지 못했어요 + + + 진행 중 %1$d개 • 완료 %2$d개 + 완료된 항목 지우기 + 아직 다운로드한 항목이 없어요 + 다운로드 중 %1$d% + 대기 중 + 완료됨 + 취소됨 + 실패 + 실패: %1$s + 다운로드 취소 + 다운로드 다시 시도 + 다운로드 삭제 + + + 아티스트 + 차단됨 + 좋아요 표시한 곡 + 저장한 노래 + 알 수 없는 아티스트 + 트랙 제목 + 아티스트 이름 + + 선택됨 + + 설정 + + 플러그인 + 언어 및 지역 + 화면 스타일 + 재생 + 다운로드 + 캐시 + 데스크톱 + 업데이트 + 후원 + 정보 + + 닫기 + 저장 + 취소 + + 언어 + 앱 인터페이스 언어입니다. 현재: %1$s + 시스템 기본값 + 국가 + 콘텐츠 및 이용 가능 여부에 적용되는 국가입니다. 현재: %1$s + %1$s (%2$s) + + 테마 + Spotube가 시스템 화면 모드를 따르는 방식을 선택하세요. 현재: %1$s + 라이트 + 다크 + 시스템 + + 표면 + 앱의 배경 재질입니다. 현재: %1$s + 물결 + 얼음 + 크롬 + 아크릴 + 유리 + + 강조 색상 + 현재 팔레트: %1$s + 강조 색상 + 강조 팔레트를 선택하고 라이트 및 다크 테마에서 미리 확인하세요. + 라이트 + 다크 + 미리보기 + 버튼 + 그린 고블린 + 일렉트릭 바이올렛 + 오셔닉 시안 + 선셋 오렌지 + 로즈 가든 + 미드나이트 블루 + 메탈릭 슬레이트 + + 디자인 & 스타일 + 표면 재질과 강조 색상. 현재: %1$s · %2$s + 디자인 & 스타일 + 표면 재질을 고른 다음 어울리는 강조 색상을 선택하세요. 모든 옵션은 엄선된 조합이에요. + 표면 + 강조 색상 + 보조 + + 팔레트 + Millennium 강조 팔레트. 현재: %1$s + 팔레트 + Millennium 강조 팔레트를 선택하고 라이트 및 다크 모드에서 미리 확인하세요. + 팔레트 + Millennium 팔레트 및 강조 색상. 현재: %1$s + 블루 + 오렌지 + 라임 + + 다운로드 폴더 지정 + 현재 폴더: %1$s + 현재 폴더: 기본 음악 폴더 + 기본 음악 폴더를 사용하려면 비워 두세요. + C:\Music\Spotube + 로컬 미디어 폴더 + 데스크톱에서 검색할 사용자 지정 폴더. 추가됨: %1$d + 아직 추가한 사용자 지정 폴더가 없어요 + 로컬 음악 라이브러리용 사용자 지정 폴더입니다. + 관리 + 폴더 추가 + 폴더 삭제 + 추가한 사용자 지정 폴더가 없습니다. + 검색 목록에서 %1$s 삭제 + 다운로드 폴더와 음악 캐시 폴더는 자동으로 포함돼요. + 다운로드 폴더는 자동으로 포함돼요. + 음악 캐시 폴더는 자동으로 포함돼요. + 저장 중… + 폴더를 저장하지 못했어요. 변경 사항은 그대로 남아 있으니 다시 시도하세요. + + 폴더 %1$d개 + + 다운로드 음악 형식 + 다운로드에 사용할 코덱 및 컨테이너. 현재: %1$s + 다운로드 음질 + + 스트리밍 음악 형식 + 음악 스트리밍에 사용할 코덱 및 컨테이너. 현재: %1$s + 스트리밍 음질 + 현재: %1$s + 음악 캐시 사용 + 스트리밍한 오디오를 기기에 저장해 다시 재생할 때 더 빠르게 불러와요. + 캐시 폴더 + 현재: %1$s + 현재: 기본 앱 캐시 + 캐시 용량 제한 + 제한 없음 + 현재 제한: %1$d MB + 최대 캐시 용량을 MB 단위로 설정하세요. 제한을 두지 않으려면 0을 입력하거나 비워 두세요. + 500 + 용량은 0 또는 양수여야 해요. + 끝없는 재생 사용 + 대기열이 끝나면 추천 트랙으로 계속 재생해요. + Connect 사용 + Spotube Connect 기능을 통해 원격 재생 컨트롤을 제공해요. + 재생 프록시 서버 포트 + 원격 제어 허용 + 같은 네트워크의 다른 기기가 연결되면 재생을 제어할 수 있도록 허용해요. + 원격 제어 기기 이름 + 다른 기기가 이 기기를 찾을 때 표시되는 이름입니다. 현재: %1$s + 네트워크의 다른 기기가 이 Spotube를 찾을 때 표시할 이름을 정하세요. + 예: 거실 Spotube + 기기 호스트 이름 + 재생 프록시 서버에서 사용하는 포트입니다. 현재: %1$d + 1에서 65535 사이의 포트를 선택하세요. + 14769 + 올바른 정수를 입력하세요. + 포트는 1에서 65535 사이여야 해요. + + 차단 목록 + 차단한 트랙 및 아티스트 관리 + + 트레이로 최소화 + 창을 최소화하면 Spotube를 시스템 트레이에서 계속 실행해요. + Discord Rich Presence + 데스크톱에서 현재 재생 상태를 Discord에 표시해요. + + 자동으로 업데이트 확인 + 백그라운드에서 Spotube 새 버전을 확인해요. + + 플러그인 관리 + 플러그인을 관리하세요 + 기본 %1$s 플러그인 + 메타데이터 + 오디오 + 가사 + 스크로블 + 선택한 플러그인 없음 + 변경 + 선택 + 선택됨 + 선택 해제 + 사용 가능한 플러그인이 없어요 + %1$s 플러그인 + 지우기 + + 구성 + 플러그인 관리자 + URL을 입력하세요 + URL은 http:// 또는 https://로 시작해야 해요 + 플러그인을 다운로드하지 못했어요 + 아직 설치한 플러그인이 없어요 + 위에 다운로드 URL을 붙여넣거나 파일을 선택하세요. + + 플러그인 %1$d개 설치됨 + + 플러그인 설치 + https://example.com/plugin.smplug + 다운로드 + 파일에서 설치(.smplug) + 사용 중 + v%1$s + 기본 제공 + 플러그인 삭제 + 로그인 + 로그아웃 + URL에서 다운로드 + 파일에서 설치 + 설치 + + • %1$s + 요청된 권한 + 이 플러그인에는 특별한 권한이 필요하지 않아요. + 설치된 버전과 제공된 버전 비교 + 설치됨 + 제공됨 + API %1$s → %2$s + 영구 저장소 + 디스크에 데이터 읽기 및 쓰기 + 네트워크 요청 + 인터넷으로 데이터 송수신 + WebView + 앱 안에 웹 콘텐츠 표시 + 그룹 잼 + MQTT 브로커 + 브로커 호스트 + %1$s:%2$d + 브로커 포트 + TLS 사용 + 사용자 이름(선택 사항) + 비밀번호(선택 사항) + 클라이언트 ID 접두사 + 이 Spotube 인스턴스를 식별하는 데 사용돼요 + 브로커 연결 암호화 + 방을 시작하기 전에 현재 브로커 설정을 확인하세요. + 연결 테스트 + 테스트 중… + %1$d ms 만에 연결됨 + 실패: %1$s + 임시 브로커입니다 — 직접 호스팅하려면 브로커를 설정하세요 + 연결 + 인증 정보 + 식별 정보 + 진단 + + + 그룹 잼 + 만들기 + 참여 + 방을 만든 다음 친구들에게 코드를 공유하세요. + 방 만들기 + 방 참여 + 연결 중… + 방 코드 + ABC123 + 브로커: %1$s + 방을 만들거나 참여하려면 설정에서 브로커를 설정하세요. + 브로커 설정 + 연결됨 + 다시 연결 중… + 코드 복사 + 복사됨! + 공유 + 참여자(%1$d) + 호스트 + 리스너 + 리스너를 기다리는 중… + 내보내기 + 차단 + 관리 + 방 나가기 + 연결할 수 없어요 + 닫기 + 잼 세션을 만들지 못했어요 + 잼 세션에 참여하지 못했어요 + 영문자와 숫자로 된 6자리 방 코드를 입력하세요. + 내 Spotube 잼에 참여하세요. 코드: %1$s + + 앨범으로 이동 + 취침 타이머 + 트랙 세부정보 + 재생 + 일시정지 + 재생 중인 항목 없음 + 대기열에 추가 + + 무시 + 업데이트 + Spotube %1$s 버전을 사용할 수 있어요 + 이번 릴리스의 새로운 기능은 Spotube 다운로드 페이지에서 확인하세요. + %1$s %2$s 버전을 사용할 수 있어요 + 현재 설치된 버전: %1$s + 이번 업데이트의 새로운 기능은 플러그인 페이지에서 확인하세요. + + 건너뛰기 + 다음 + 뒤로 + 시작하기 + Spotube + 아무런 조건 없는 음악. + 음악의 세계를 위한 오픈 소스 플레이어.\n마음껏 탐색하고, 나만의 방식으로 꾸며 보세요. + 이 프로젝트가 계속될 수 있도록 도와주세요. + Spotube는 구독 비즈니스가 아닌 사람들이 만들어요. Spotube가 일상의 일부가 되었다면, 작은 후원이 Spotube를 더 정성껏 가꾸는 데 큰 힘이 돼요.\n\n독립 소프트웨어에 자리를 내어 주셔서 감사합니다. + 먼저 카탈로그 플러그인으로 노래와 플레이리스트를 찾으세요. 재생하려면 오디오 플러그인을 선택하세요. 가사와 감상 기록은 선택 사항이에요. 아래에서 플러그인을 찾아 권한을 확인하고 설치한 다음 여기서 선택하세요. + 플러그인 둘러보기 + 내 소스 + 다시 시도 + 더 많은 플러그인 + 새 플러그인을 찾지 못했어요. 위에서 링크나 .smplug 파일로 설치할 수도 있어요. + 지금은 플러그인 둘러보기를 사용할 수 없어요. 다시 시도하거나 링크 또는 파일로 설치하세요. + 환영합니다 + 후원 + 플러그인 + 가이드 + + Spotube 사용 방법 + 간단한 네 단계면 바로 감상할 수 있어요. + 로그인 + Spotify 계정을 연결하거나 원하는 카탈로그 플러그인을 선택하세요. + 검색하고 재생하기 + 노래, 앨범, 아티스트를 찾은 다음 재생을 누르세요. + 오프라인으로 감상하기 + 트랙을 다운로드하면 인터넷 연결 없이도 계속 재생할 수 있어요. + 가사 따라가기 + 트랙이 재생되는 동안 플레이어에서 싱크 가사를 열어 보세요. + + + 나의 음악. 나의 소스. 나의 규칙. + 음악을 사랑하는 커뮤니티가 만든 무료 오픈 소스 음악 플레이어. + 오픈 소스 + 광고 없음 + 원격 측정 없음 + 플러그인 기반 + 시작해요 + + + 후원자 덕분에 계속할 수 있어요 + 열정으로 만들고, 여러분 덕분에 이어 갑니다. + Spotube는 100% 무료이자 자유 오픈 소스 소프트웨어예요 — 광고도, 구독도, 추적도 없어요. 모든 기능은 여러분 같은 분들이 코드가 계속 이어지도록 도와주신 덕분에 존재해요. + Patreon + 월간 후원 + 지속적인 개발과 인프라를 지원해 주세요 + Buy Me a Coffee + 커피 한 잔 후원 + 간단한 팁으로 늦은 밤 코딩에 힘을 보태 주세요 + Open Collective + 커뮤니티 기금 + 투명하게 커뮤니티가 운영하는 재정 후원 + 열린 음악을 믿어 주셔서 진심으로 감사합니다. + 후원은 전적으로 선택 사항이에요 — 어떤 경우든 Spotube는 계속 무료예요. + 후원하기 + %1$s 열기 + + + Spotube 후원하기 + Spotube 후원하기 + Spotube는 무료이자 자유 오픈 소스 소프트웨어예요 — 광고도, 구독도, 추적도 없어요. Spotube가 즐거움을 준다면 프로젝트 후원을 고려해 주세요. + 이 알림은 설정 → 후원에서 끌 수 있어요. + 나중에 + 후원 알림 + 7일마다 프로젝트 후원 알림을 가볍게 표시해요. + + + 모듈식 구조 + 음악을 어디서 가져올지 선택하세요. + Spotube는 카탈로그 탐색, 오디오 스트리밍, 싱크 가사를 분리해서 처리해요. 아래에서 엔진을 구성하거나 언제든지 바꿀 수 있어요. + 메타데이터 엔진 + 음악 탐색, 검색, 트랙 목록, 플레이리스트를 담당해요. + 오디오 엔진 + 재생할 오디오 스트림을 가져오고 디코딩해요. + 가사 엔진 + 시간에 맞춘 노래방 가사를 실시간으로 보여 줘요. + 스크로블 엔진 + 감상 기록을 Last.fm 또는 ListenBrainz에 기록해요. + 사용 중 + 기본 제공 + 준비됨 + 선택 사항 + 엔진 선택 + 설치된 엔진 + 기본값으로 설정 + NewPipe YouTube와 LRCLib Lyrics는 기본 제공되며 바로 스트리밍할 수 있어요. + 설정 → 플러그인에서 언제든지 커뮤니티 플러그인을 관리하거나 추가할 수 있어요. + + + 레트로 모던 아키텍처 + 순수한 하이파이, 중개자는 없어요 + Spotube가 기기에서 직접 분산형 재생을 처리하는 방법은 다음과 같아요. + P2P 직접 연결 • 256 KBPS OPUS + 블루 아워 + Spotube · 플레이어 데모 + 그 사이의 순간들을 위한 작은 음악. + 1. 분산형 탐색 + 검색어는 선택한 카탈로그 플러그인으로 바로 전달돼요. Spotube는 취향을 분석하지 않아요. + 2. 직접 스트림 확인 + 오디오는 콘텐츠 서버에서 기기로 바로 스트리밍돼요. 프록시 경유나 원격 데이터 수집은 없어요. + 3. 로컬 DSP & 재생 + 이퀄라이저 컨트롤과 선택적 오프라인 다운로드를 지원하며 기기에서 디코딩하고 캐시해요. + 4. 싱크 가사 & 잼 + 노래방 가사가 재생 시간에 맞춰 흐르고, LAN 원격 제어와 그룹 잼도 지원해요. + 중개 서버가 없어요. 재생은 기기에서 직접 이루어져요 — 설계부터 개인정보를 보호해요. + + + 기기 선택 + %1$s: “%2$s” + 재생 + 대기열에 추가 + 다음에 재생 + 이 기기 + 이 기기에서 + 원격 기기 + 원격 기기(%1$s) + 연결된 기기에서 + + + 연결 실패 + 연결 끊김 + 전송 실패 + 기기 검색 실패: %1$s + 연결하지 못했어요: %1$s + + + 기기 + 새로고침 + %1$s:%2$d에 연결됨 + 연결 해제 + 연결 중... + 네트워크에서 기기를 찾는 중... + 기기를 찾을 수 없어요 + 상대 기기의 설정에서 “원격 제어 허용”이 켜져 있는지 확인하세요. + 알 수 없는 기기 + 확인 중... + ID: %1$s... + + + 원격 제어 + 연결 끊김 + 연결 오류: %1$s + 앨범 커버 + 알 수 없는 트랙 + 알 수 없는 아티스트 + 탐색 + 셔플 + 이전 + 다음 + 반복 모드 + 볼륨 + 대기열에서 삭제 + 옵션 더보기 + + + 어디에서 재생할까요? + 진행 중인 잼 세션이 있어요. 이 컬렉션을 재생할 위치를 선택하세요. + 여기에서 재생 + 잼에 추천하기 + + + 구성된 잼 브로커가 없어요 + 브로커 호스트가 비어 있어요 + 연결 실패 + 연결 끊김 + + + 브로커 + 익명 + + + 잼 대기열에 추가했어요 + + 잼 대기열에 %1$d곡을 추가했어요 + + + + 원격 제어 요청 + “%1$s”에서 이 기기의 재생을 제어하려고 해요. + 거부 + 허용 + 항상 허용 + + + 검색 결과 + 전체 + 트랙 + 사용자 + 사용 가능한 검색 유형이 없어요 + 입력하여 검색하세요 + 입력하여 트랙을 검색하세요 + 검색 결과가 없어요 + 모두 보기 + 검색 중 + 트랙 불러오는 중... + 앨범 불러오는 중... + 아티스트 불러오는 중... + 플레이리스트 불러오는 중... + 사용자 불러오는 중... + 앨범을 찾을 수 없어요 + 아티스트를 찾을 수 없어요 + 플레이리스트를 찾을 수 없어요 + 사용자를 찾을 수 없어요 + 레코드 상자를 뒤지는 중... + 우주를 탐색하는 중... + 스피커 예열하는 중... + 바이닐 먼지 터는 중... + 음악의 신께 여쭤보는 중... + 안테나 맞추는 중... + 레코드를 넘겨보는 중... + 검색 기능을 불러오지 못했어요 + 검색하지 못했어요 + 트랙을 검색하지 못했어요 + 앨범을 검색하지 못했어요 + 아티스트를 검색하지 못했어요 + 플레이리스트를 검색하지 못했어요 + 사용자를 검색하지 못했어요 + + + 둘러보기 + + + 싱크 + 일반 + 싱크 가사가 없어요 + 일반 가사가 없어요 + + + %2$s 컨테이너의 %1$s + %1$d kbps + %1$s kHz • %2$d ch + 선택한 메타데이터 플러그인이 없어요 + + + 뒤로 + 다시 시도 + 대화상자 닫기 + 불러오는 중... + 알 수 없음 + 알 수 없는 오류 + + + 사용자 + 플레이리스트 + + + 정말 죄송합니다 + 지금 문제가 발생했어요. 여러분의 경험은 저희에게 소중하며, 문제를 해결하기 위해 최선을 다하고 있어요. 다시 시도하거나 아래 오류 세부정보를 확인하세요. + 오류 세부정보 + 클립보드에 복사 + 전체 로그 보기 + 전체 오류 로그 + + + 셔플 + 셔플 재생 + 팔로우 + 팔로우 취소 + 플레이리스트 편집 + 만든 사람: %1$s + + + 트랙 옵션 + 잼에 추가 + 라디오 시작 + 다음에 재생 + 다음 곡으로 이동 + 대기열에서 삭제 + 즐겨찾기에서 삭제 + 즐겨찾기에 저장 + 플레이리스트에 추가 + 다운로드 + 공유 + + + 기본 순서 + 제목 + 아티스트 + 앨범 + 재생 시간 + 정렬 + 트랙을 찾을 수 없어요 + 필터 + 일괄 작업 + 모두 다운로드 + %1$d곡 다운로드 + 모두 대기열에 추가 + %1$d곡 대기열에 추가 + 모두 다음에 재생 + %1$d곡 다음에 재생 + 모두 플레이리스트에 추가 + %1$d곡 플레이리스트에 추가 + 모두 잼에 추가 + %1$d곡 잼에 추가 + 외 %1$d명 + + + 불러올 URL이 없어요 + 브라우저 기록에서 뒤로 이동 + 브라우저 기록에서 앞으로 이동 + + + 차단 목록 필터... + 차단한 항목이 없어요 + ‘%1$s’와(과) 일치하는 차단 항목이 없어요 + 트랙 + 트랙 + 차단 목록에 추가 + 차단 목록에서 삭제 + + + 트랙 메타데이터 없음 + 스트림 URL을 확인할 수 없어요 + 스트림 URL을 확인할 수 없어요. 먼저 트랙을 재생해 보세요. + + + 저장한 트랙 + 나 + + + 팔로잉 + 인기 트랙 + 인기 트랙 재생 + 인기 트랙을 대기열에 추가 + 관련 아티스트 + 추천 플레이리스트 + 아티스트 개요를 불러오지 못했어요 + 팔로워 정보 없음 + 팔로워 %1$sK명 + 팔로워 %1$sM명 + 팔로워 %1$sB명 + + 팔로워 %1$d명 + + + + 좋아요 + 좋아요 취소 + 트랙 제목 없음 + 아티스트 없음 + 알 수 없는 앨범 + 탐색 + 볼륨 + 셔플 사용 + 셔플 사용 안 함 + 이전 + 다음 + 재생 또는 일시정지 + 반복 끄기 + 한 곡 반복 + 전체 반복 + 음소거 + 음소거 해제 + 옵션 더보기 + 플레이어 옵션 + 플레이어 접기 + 가사 펼치기 + 지금 재생 중 + 가사 + 대체 소스 + 대체 소스 + 다운로드 + 다운로드 완료 + 다운로드 중... + 이미 다운로드됨 + 다운로드 다시 시도 중... + %1$s 다운로드 중 + %1$s 후 재생이 일시정지돼요. + 선택한 시간(분)이 지나면 재생을 일시정지해요. + 분 + 1~600분 사이에서 선택하세요 + 타이머 취소 + 시작 + 재생 URL이 다른 스트리밍 설정으로 확인되었어요. 강조 표시된 스트림이 현재 설정과 일치하며, 사용 중인 URL은 다음 스트림 요청 시 새로고침돼요. + 일치하는 오디오 소스 + 일치하는 소스 불러오는 중… + 로컬 캐시 파일을 재생하는 동안에는 일치하는 원격 소스가 필요하지 않아요. + 일치하는 소스가 없어요. + 이 트랙에 캐시된 일치 오디오 소스가 없어요. + 스트림 정보가 아직 캐시되지 않았어요. 먼저 트랙을 재생해 불러오세요. + 트랙 세부정보를 불러오지 못했어요. + 사용 가능한 스트림 + 이 트랙은 로컬 캐시에서 재생 중이에요. 이번 재생에는 스트림 변형이 사용되지 않아요. + 스트림 불러오는 중… + 이 소스에 사용 가능한 스트림이 없어요. + 현재 선택된 스트림 + 스트리밍 형식 & 음질 설정과 일치 + 현재 재생 URL + 로컬 캐시에서 재생 중 + 메타데이터 트랙 + 제목 + ID + 앨범 + 일치도 + 외부 URI + 앨범 ID + 앨범 유형 + 앨범 발매일 + 앨범 트랙 수 + 앨범 장르 + 재생 시간 + 트랙 번호 + 디스크 번호 + 청소년 이용불가 + 인기도 + ISRC + 썸네일 URL + + + 사이드바 전환 + + + 대체 소스 + 현재 재생 중인 트랙이 없어요 + 대체 소스를 찾는 중... + 대체 소스를 찾을 수 없어요 + 일치도: %1$d% + 현재 소스 + + + 내보내기 + 세션에서 내보내요 + 내보내고 다시 참여하지 못하게 해요 + 추천곡 삭제 + 이 참여자가 대기열에 추가한 모든 트랙을 삭제해요 + 순서 변경 + 대기열에서 삭제 + %1$s • %2$s님이 추가함 + + + 알 수 없음 + 플레이리스트 불러오는 중... + 트랙 추가 + 플레이리스트에 트랙 추가 + 트랙 검색... + 검색 + 트랙을 찾을 수 없어요 + 추가할 트랙을 검색하세요 + 완료 + 추가 + 추가됨 + + + 앨범 불러오는 중... + + + 내 플레이리스트에 추가 + 검색 + 내 플레이리스트 검색... + 내 플레이리스트 불러오는 중... + 아직 소유한 플레이리스트가 없어요.\n플레이리스트를 만들어 트랙을 추가하세요. + 플레이리스트 만들기 + 플레이리스트 편집 + 만들기 + 이름 + 플레이리스트 이름 + 설명 + 설명(선택 사항) + 공개 + 누구나 이 플레이리스트를 볼 수 있어요 + 공동 작업 + 다른 사람도 이 플레이리스트에 트랙을 추가할 수 있어요 + 커버 이미지 + 플레이리스트 커버 + 이미지 없음 + 이미지 변경 + 이미지 선택 + + + 설치 + 교체 + zip 파일에서 plugin.json을 찾을 수 없어요 + zip 파일에서 manifest.zipline.json을 찾을 수 없어요 + plugin.json 형식이 올바르지 않아요: %1$s + 플러그인을 읽지 못했어요: %1$s + 플러그인을 설치하지 못했어요: %1$s + %1$s의 새 버전을 사용할 수 있어요. 설치하기 전에 업데이트를 확인하세요. + %1$s이(가) 이미 설치되어 있어요. 설치하기 전에 교체를 확인하세요. + 기본 제공 플러그인은 이미 포함되어 있어 다시 추가할 수 없어요. + 기본 제공 플러그인은 삭제할 수 없어요. + %1$s에 잘못된 apiVersion(%2$s)이 선언되어 있어요. %3$s과(와) 호환되는 유의적 버전이 필요해요. + Spotube 플러그인 API 버전 %1$s이(가) 올바르지 않아요. + %1$s은(는) 플러그인 API %2$s용이지만, Spotube는 호환성이 깨지는 플러그인 API %3$s을(를) 지원해요. 같은 주 API 버전용으로 빌드된 플러그인을 설치하세요. + + + 호환되지 않는 플러그인 API + 플러그인을 설치할까요? + %1$s이(가) 설치된 플러그인에 추가돼요. + 플러그인을 업데이트할까요? + 이전 버전(%1$s)이 설치되어 있어요. %2$s(으)로 업데이트할까요? + 설치된 플러그인을 교체할까요? + %1$s은(는) 잘못된 유의적 버전(%2$s)을 사용하므로 업데이트로 비교할 수 없어요. 그래도 설치된 플러그인을 교체할까요? + 설치된 버전(%1$s)은 유의적 버전으로 비교할 수 없어요. %2$s(으)로 교체할까요? + 버전 %1$s이(가) 이미 설치되어 있어요. 기존 플러그인을 제공된 사본으로 교체할까요? + 설치된 버전 %1$s이(가) 제공된 버전 %2$s보다 최신이에요. 그래도 교체할까요? + 설치된 플러그인을 제공된 사본으로 교체할까요? + + + %1$s: + 버전 + API 버전 + 제작자 + 라이선스 + 권한 + 기능 + 저장소 + 연락처 + 버그 신고 + %1$s 후원하기 + + + 공식 + 인증됨 + GitHub 스타 + GitHub 저장소 URL + + + 릴리스를 찾을 수 없어요 + 릴리스 선택 + 릴리스를 선택하세요 + 사전 릴리스 + 릴리스 노트 + + + 홈 + 검색 + 이 기기 + 연결 + 기기 + + + 창 숨기기 + 창 표시 + 다음 트랙 + 이전 트랙 + 셔플 + 반복 + 반복: 끔 + 반복: 한 곡 + 반복: 전체 + 볼륨: %1$d% + 볼륨 + + 볼륨 - + 음소거 + 음소거 해제 + 트랙 좋아요 + 트랙 좋아요 취소 + 종료 + + + 최소화 + 최대화 + 이전 크기로 복원 + 닫기 + + + 재생 서비스 + 재생 + 미디어 재생 컨트롤 + + + 둘러보기 + 지금 재생 중 + 추천 + 저장한 트랙 + 인기 트랙 + diff --git a/composeApp/src/commonMain/composeResources/values-nl/strings.xml b/composeApp/src/commonMain/composeResources/values-nl/strings.xml new file mode 100644 index 00000000..2c79ad97 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-nl/strings.xml @@ -0,0 +1,938 @@ + + + + Wachtrij + Wachtrij wissen + Wachtrij filteren... + Wachtrij is leeg + + Zoek nummers, artiesten, albums... + Alle geschiedenis wissen + Zoeken... + + + Jouw bibliotheek + Bibliotheek + Playlists + Albums + Artiesten + Lokale nummers + Downloads + Zoek in opgeslagen playlists… + Zoek in opgeslagen albums… + Zoek in opgeslagen artiesten… + Zoek mappen of nummers… + Bibliotheek filteren + Filter sluiten + Playlist maken + Lokale nummers vernieuwen + Rasterweergave + Lijstweergave + Geen opgeslagen playlists + Geen playlists gevonden voor “%1$s” + Geen opgeslagen albums + Geen albums gevonden voor “%1$s” + Geen opgeslagen artiesten + Geen artiesten gevonden voor “%1$s” + + + Mappen + Map + %1$s • %2$s + + %1$d nummer + %1$d nummers + + + %1$d map + %1$d mappen + + Terug naar mappen + Map afspelen + Toegang tot audio vereist + Geef toegang tot audio om muziek op je apparaat te bekijken. + Toegang geven + Geen lokale mappen gevonden + Geen nummers in deze map + Geen overeenkomende mappen of nummers + Lokale media vernieuwen mislukt + + + %1$d actief • %2$d voltooid + Voltooide wissen + Nog geen downloads + Downloaden %1$d% + In wachtrij + Voltooid + Geannuleerd + Mislukt + Mislukt: %1$s + Download annuleren + Download opnieuw proberen + Download verwijderen + + + Artiest + Op zwarte lijst + Gelikete nummers + Je opgeslagen nummers + Onbekende artiest + Titel van nummer + Naam van artiest + + Geselecteerd + + Instellingen + + Plug-ins + Taal en regio + Weergave + Afspelen + Downloads + Cache + Desktop + Updates + Steunen + Over + + Sluiten + Opslaan + Annuleren + + Taal + Taal van de app. Huidig: %1$s + Systeemstandaard + Land + Land voor content en beschikbaarheid. Huidig: %1$s + %1$s (%2$s) + + Thema + Kies hoe Spotube aansluit op de weergave van je systeem. Huidig: %1$s + Licht + Donker + Systeem + + Oppervlak + Achtergrondmateriaal van de app. Huidig: %1$s + Waterig + IJzig + Chroom + Acryl + Glas + + Accentkleur + Huidig palet: %1$s + Accentkleur + Kies een accentpalet en bekijk het in zowel het lichte als het donkere thema. + Licht + Donker + Voorbeeld + Knop + Groene kobold + Elektrisch violet + Oceaancyaan + Zonsondergangoranje + Rozentuin + Middernachtblauw + Metaalgrijs + + Uiterlijk + Oppervlaktemateriaal en accent. Huidig: %1$s · %2$s + Uiterlijk + Kies een oppervlaktemateriaal en daarna een accent dat erbij past. Elke optie is een zorgvuldig gekozen combinatie. + Oppervlak + Accent + Secundair + + Palet + Millennium-accentpalet. Huidig: %1$s + Palet + Kies een Millennium-accentpalet en bekijk het in licht en donker. + Palet + Millennium-palet en accent. Huidig: %1$s + Blauw + Oranje + Limoen + + Andere downloadmap + Huidige map: %1$s + Huidige map: standaard muziekmap + Laat dit leeg om de standaard muziekmap te gebruiken. + C:\Music\Spotube + Lokale mediamappen + Eigen mappen om te scannen op desktop. Toegevoegd: %1$d + Nog geen eigen map toegevoegd + Eigen mappen voor je lokale muziekbibliotheek. + Beheren + Map toevoegen + Map verwijderen + Geen eigen mappen toegevoegd. + %1$s verwijderen uit de scanlijst + De download- en muziekcachemappen worden automatisch meegenomen. + Je downloadmap wordt automatisch meegenomen. + De muziekcachemap wordt automatisch meegenomen. + Opslaan… + Kan mappen niet opslaan. Je wijzigingen zijn bewaard; probeer het opnieuw. + + %1$d map + %1$d mappen + + Downloadformaat + Voorkeurscodec en -container voor downloads. Huidig: %1$s + Downloadkwaliteit + + Streamingformaat + Voorkeurscodec en -container voor het streamen van muziek. Huidig: %1$s + Streamingkwaliteit + Huidig: %1$s + Muziekcache inschakelen + Gestreamde audio lokaal opslaan zodat herhaald afspelen sneller gaat. + Cachemap + Huidig: %1$s + Huidig: standaard app-cache + Maximale cachegrootte + Geen limiet + Huidige limiet: %1$d MB + Stel de maximale cachegrootte in MB in. Vul 0 in of laat leeg voor geen limiet. + 500 + Grootte moet 0 of een positief getal zijn. + Eindeloos afspelen inschakelen + Blijf afspelen met aanbevolen nummers als je wachtrij leeg is. + Connect inschakelen + Maak bediening op afstand beschikbaar via Spotube Connect. + Poort van afspeelproxyserver + Bediening op afstand toestaan + Laat andere apparaten op hetzelfde netwerk het afspelen bedienen als ze verbinding maken. + Apparaatnaam voor bediening op afstand + De naam die andere apparaten zien als ze dit apparaat vinden. Huidig: %1$s + Kies een herkenbare naam die andere apparaten op je netwerk te zien krijgen als ze deze Spotube-installatie vinden. + bijv. Spotube woonkamer + hostnaam van apparaat + Poort die de afspeelproxyserver gebruikt. Huidig: %1$d + Kies een poort tussen 1 en 65535. + 14769 + Voer een geldig geheel getal in. + Poort moet tussen 1 en 65535 liggen. + + Zwarte lijst + Nummers en artiesten op de zwarte lijst beheren + + Minimaliseren naar systeemvak + Houd Spotube actief in het systeemvak als het venster wordt geminimaliseerd. + Discord Rich Presence + Toon wat je afspeelt in Discord op desktop. + + Automatisch controleren op updates + Op de achtergrond zoeken naar nieuwe Spotube-versies. + + Plug-ins beheren + Beheer je plug-ins + Standaardplug-in voor %1$s + Metadata + Audio + Songteksten + Scrobbelen + Geen plug-in geselecteerd + Wijzigen + Selecteren + Geselecteerd + Selectie wissen + Geen plug-ins beschikbaar + Plug-in %1$s + Wissen + + Configureren + Plug-inbeheer + Voer een URL in + URL moet beginnen met http:// of https:// + Plug-in downloaden mislukt + Nog geen plug-ins geïnstalleerd + Plak hierboven een download-URL of kies een bestand. + + %1$d plug-in geïnstalleerd + %1$d plug-ins geïnstalleerd + + Plug-in installeren + https://example.com/plugin.smplug + Downloaden + Installeren uit bestand (.smplug) + Actief + v%1$s + Ingebouwd + Plug-in verwijderen + Inloggen + Uitloggen + Downloaden via URL + Installeren uit bestand + Installeren + + • %1$s + Gevraagde machtigingen + Deze plug-in heeft geen speciale machtigingen nodig. + Geïnstalleerd vs. aangeboden + Geïnstalleerd + Aangeboden + API %1$s → %2$s + Permanente opslag + Gegevens lezen van en schrijven naar schijf + Netwerkverzoeken + Gegevens verzenden en ontvangen via internet + WebView + Webcontent weergeven in de app + Group Jam + MQTT-broker + Broker-host + %1$s:%2$d + Broker-poort + TLS gebruiken + Gebruikersnaam (optioneel) + Wachtwoord (optioneel) + Voorvoegsel client-ID + Wordt gebruikt om deze Spotube-installatie te herkennen + De verbinding met de broker versleutelen + Controleer de huidige broker-instellingen voordat je een Jam start. + Verbinding testen + Testen… + Verbonden in %1$d ms + Mislukt: %1$s + Tijdelijke broker — stel je eigen broker in om zelf te hosten + Verbinding + Inloggegevens + Identiteit + Diagnose + + + Group Jam + Maken + Deelnemen + Maak een sessie en deel de code met je vrienden. + Sessie maken + Deelnemen aan sessie + Verbinden… + Sessiecode + ABC123 + Broker: %1$s + Stel een broker in bij Instellingen om een sessie te hosten of eraan deel te nemen. + Broker-instellingen + Verbonden + Opnieuw verbinden… + Code kopiëren + Gekopieerd! + Delen + Deelnemers (%1$d) + Host + Luisteraar + Wachten op luisteraars… + Verwijderen + Blokkeren + Beheren + Sessie verlaten + Kan geen verbinding maken + Sluiten + Jam-sessie maken mislukt + Deelnemen aan Jam-sessie mislukt + Voer een sessiecode van zes tekens in met letters en cijfers. + Doe mee met mijn Spotube Jam met code: %1$s + + Naar album + Slaaptimer + Details van nummer + Afspelen + Pauzeren + Er wordt niets afgespeeld + Toevoegen aan wachtrij + + Negeren + Bijwerken + Spotube %1$s is beschikbaar + Bekijk wat er nieuw is in deze versie op de downloadpagina van Spotube. + %1$s %2$s is beschikbaar + Momenteel geïnstalleerd: %1$s + Bekijk wat er nieuw is in deze update op de pagina van de plug-in. + + Overslaan + Volgende + Terug + Aan de slag + Spotube + Muziek, zonder verplichtingen. + Een opensource-speler voor een wereld vol muziek.\nOm te ontdekken. Om helemaal van jou te maken. + Houd dit project in leven. + Spotube wordt gemaakt door mensen, niet door een abonnementsbedrijf. Als het onderdeel wordt van je dagelijks leven, helpt een beetje steun ons om het de zorg te geven die het verdient.\n\nBedankt dat je ruimte maakt voor onafhankelijke software. + Begin met een catalogusplug-in om nummers en playlists te vinden. Kies een audioplug-in om ze af te spelen. Songteksten en luistergeschiedenis zijn optioneel. Ontdek hieronder een plug-in, bekijk de machtigingen, installeer hem en selecteer hem hier. + Plug-ins ontdekken + Je bronnen + Opnieuw proberen + Meer plug-ins + Geen nieuwe plug-ins gevonden. Je kunt er ook hierboven een installeren via een link of een .smplug-bestand. + Plug-ins ontdekken is nu niet beschikbaar. Probeer het opnieuw of installeer via een link of bestand. + Welkom + Steunen + Plug-ins + Uitleg + + Zo gebruik je Spotube + Vier snelle stappen en je luistert al. + Inloggen + Koppel je Spotify-account of kies je eigen catalogusplug-in. + Zoeken en afspelen + Vind elk nummer, album of elke artiest en druk op afspelen. + Offline luisteren + Download nummers om ook zonder verbinding te luisteren. + Volg de songtekst + Open gesynchroniseerde songteksten in de speler terwijl een nummer speelt. + + + Jouw muziek. Jouw bronnen. Jouw regels. + Een gratis opensource-muziekspeler, gemaakt door een community die gewoon van muziek houdt. + Opensource + Geen advertenties + Geen telemetrie + Werkt met plug-ins + Aan de slag + + + Supporters houden ons op gang + Gemaakt met passie. In leven gehouden door jou. + Spotube is 100% gratis, vrij en opensource — geen advertenties, geen abonnementen, geen tracking. Elke functie bestaat omdat mensen zoals jij hebben geholpen de code te laten stromen. + Patreon + Maandelijkse sponsor + Steun doorlopende ontwikkeling en infrastructuur + Buy Me a Coffee + Eenmalig koffie + Geef een nachtelijke codeersessie een boost met een kleine fooi + Open Collective + Communityfonds + Transparante financiële steun, beheerd door de community + Uit de grond van ons hart bedankt dat je gelooft in open muziek. + Steunen is helemaal optioneel — Spotube blijft hoe dan ook gratis. + Sponsoren + %1$s openen + + + Spotube steunen + Spotube steunen + Spotube is gratis, vrij en opensource — geen advertenties, geen abonnementen, geen tracking. Als je er blij van wordt, overweeg dan het project te steunen. + Je kunt deze herinneringen uitzetten via Instellingen → Steunen. + Misschien later + Herinneringen om te steunen + Toon elke 7 dagen een vriendelijke herinnering om het project te steunen. + + + Modulaire architectuur + Kies waar je muziek vandaan komt. + Spotube scheidt het ontdekken van de catalogus, het streamen van audio en gesynchroniseerde songteksten. Stel je engines hieronder in of wissel ze op elk moment. + Metadata-engine + Zorgt voor muziek ontdekken, zoeken, tracklijsten en playlists. + Audio-engine + Haalt de audiostreams op die worden afgespeeld en decodeert ze. + Songtekst-engine + Streamt tijdgesynchroniseerde karaokesongteksten in realtime. + Scrobble-engine + Legt je luistergeschiedenis vast op Last.fm of ListenBrainz. + Actief + Ingebouwd + Klaar + Optioneel + Kies een engine + Geïnstalleerde engines + Instellen als standaard + NewPipe YouTube en LRCLib Lyrics zijn ingebouwd en direct klaar om te streamen. + Beheer of voeg op elk moment communityplug-ins toe via Instellingen → Plug-ins. + + + Retromoderne architectuur + Pure hi-fi, geen tussenpersonen + Zo regelt Spotube gedecentraliseerd afspelen, rechtstreeks op je apparaat. + RECHTSTREEKS PEER-TO-PEER • 256 KBPS OPUS + Blauw uur + Spotube · demonstratie van de speler + Een beetje muziek voor de momenten tussendoor. + 1. Gedecentraliseerd ontdekken + Zoekopdrachten gaan rechtstreeks naar je gekozen catalogusplug-in. Spotube maakt nooit een profiel van je smaak. + 2. Directe streams + Audio wordt rechtstreeks van de contentservers naar je apparaat gestreamd. Geen proxy\'s ertussen en geen gegevensverzameling op afstand. + 3. Lokale DSP & afspelen + Lokaal gedecodeerd en gecachet, met equalizer en optioneel offline downloaden. + 4. Gesynchroniseerde songteksten & Jam + Karaokesongteksten volgen de afspeelpositie, met bediening op afstand via LAN en Group Jam. + Geen tussenservers. Afspelen gebeurt gewoon op je apparaat — privé vanaf het ontwerp. + + + Kies een apparaat + %1$s: ‘%2$s’ + Afspelen + Toevoegen aan wachtrij + Hierna afspelen + Dit apparaat + Op dit apparaat + Extern apparaat + Extern apparaat (%1$s) + Op het verbonden apparaat + + + Verbinding mislukt + Verbinding verbroken + Verzenden mislukt + Zoeken mislukt: %1$s + Verbinden mislukt: %1$s + + + Apparaten + Vernieuwen + Verbonden met %1$s:%2$d + Verbinding verbreken + Verbinden... + Zoeken naar apparaten op het netwerk... + Geen apparaten gevonden + Zorg dat \"Bediening op afstand toestaan\" is ingeschakeld in de instellingen van het andere apparaat. + Onbekend apparaat + Bezig met ophalen... + ID: %1$s... + + + Bediening op afstand + Niet verbonden + Verbindingsfout: %1$s + Albumhoes + Onbekend nummer + Onbekende artiest + Zoeken in nummer + Shuffle + Vorige + Volgende + Herhaalmodus + Volume + Verwijderen uit wachtrij + Meer opties + + + Waar afspelen? + Je hebt een actieve Jam-sessie. Kies waar je deze collectie wilt afspelen. + Hier afspelen + Voorstellen aan Jam + + + Geen Jam-broker ingesteld + Broker-host is leeg + Verbinding mislukt + Verbinding verbroken + + + Broker + anoniem + + + Toegevoegd aan de Jam-wachtrij + + %1$d nummer toegevoegd aan de Jam-wachtrij + %1$d nummers toegevoegd aan de Jam-wachtrij + + + + Verzoek voor bediening op afstand + \"%1$s\" wil het afspelen op dit apparaat bedienen. + Weigeren + Toestaan + Altijd toestaan + + + Zoekresultaten + Alles + Nummers + Gebruikers + Geen zoektypen beschikbaar + Begin met typen om te zoeken + Begin met typen om nummers te zoeken + Geen resultaten gevonden + Alles weergeven + Zoeken + Nummers laden... + Albums laden... + Artiesten laden... + Playlists laden... + Gebruikers laden... + Geen albums gevonden + Geen artiesten gevonden + Geen playlists gevonden + Geen gebruikers gevonden + Door de platenbakken aan het spitten... + De kosmos aan het doorzoeken... + De speakers aan het opwarmen... + Het vinyl aan het afstoffen... + De muziekgoden aan het raadplegen... + De antennes aan het afstemmen... + Door de platen aan het bladeren... + Zoekmogelijkheden laden mislukt + Zoeken mislukt + Nummers zoeken mislukt + Albums zoeken mislukt + Artiesten zoeken mislukt + Playlists zoeken mislukt + Gebruikers zoeken mislukt + + + Ontdekken + + + Gesynchroniseerd + Platte tekst + Geen gesynchroniseerde songtekst beschikbaar + Geen songtekst beschikbaar + + + %1$s in %2$s + %1$d kbps + %1$s kHz • %2$d ch + Geen metadataplug-in geselecteerd + + + Terug + Opnieuw proberen + Dialoogvenster sluiten + Laden... + Onbekend + Onbekende fout + + + Gebruiker + Playlist + + + Het spijt ons echt + Er werkt momenteel iets niet goed. Jouw ervaring is belangrijk voor ons en we doen ons best om dit op te lossen. Probeer het opnieuw of bekijk de foutdetails hieronder. + Foutdetails + Kopiëren naar klembord + Volledig logboek bekijken + Volledig foutenlogboek + + + Shuffle + Shuffle afspelen + Volgen + Ontvolgen + Playlist bewerken + Van %1$s + + + Opties voor nummer + Toevoegen aan Jam + Radio starten + Hierna afspelen + Naar volgende verplaatsen + Verwijderen uit wachtrij + Verwijderen uit favorieten + Opslaan als favoriet + Toevoegen aan playlist + Downloaden + Delen + + + Oorspronkelijk + Titel + Artiest + Album + Duur + Sorteren + Geen nummers gevonden + Filteren + Bulkacties + Alles downloaden + %1$d downloaden + Alles toevoegen aan wachtrij + %1$d toevoegen aan wachtrij + Alles hierna afspelen + %1$d hierna afspelen + Alles toevoegen aan playlist + %1$d toevoegen aan playlist + Alles toevoegen aan Jam + %1$d toevoegen aan Jam + +%1$d meer + + + Geen URL om te laden + Terug in browsergeschiedenis + Vooruit in browsergeschiedenis + + + Zwarte lijst filteren... + Niets op de zwarte lijst + Niets op de zwarte lijst komt overeen met \'%1$s\' + Nummers + Nummer + Toevoegen aan zwarte lijst + Verwijderen van zwarte lijst + + + Geen metadata voor nummer + Kan stream-URL niet ophalen + Kan stream-URL niet ophalen. Speel het nummer eerst af. + + + Opgeslagen nummers + Jij + + + Volgend + Populairste nummers + Populairste nummers afspelen + Populairste nummers toevoegen aan wachtrij + Vergelijkbare artiesten + Uitgelichte playlists + Artiestoverzicht laden mislukt + Volgers niet beschikbaar + %1$sK volgers + %1$s mln. volgers + %1$s mld. volgers + + %1$d volger + %1$d volgers + + + + Liken + Niet meer liken + Titel van no-op-nummer + No-op-artiesten + Onbekend album + Zoeken in nummer + Volume + Shuffle aanzetten + Shuffle uitzetten + Vorige + Volgende + Afspelen of pauzeren + Herhalen uit + Eén nummer herhalen + Alles herhalen + Dempen + Dempen opheffen + Meer opties + Speleropties + Speler inklappen + Songtekst uitvouwen + Nu aan het afspelen + Songtekst + Alternatieve bron + Alternatieve bron + Downloaden + Gedownload + Downloaden... + Al gedownload + Download opnieuw proberen... + %1$s downloaden + Afspelen pauzeert over %1$s. + Pauzeer het afspelen na het gekozen aantal minuten. + Minuten + Kies tussen 1 en 600 minuten + Timer annuleren + Starten + De afspeel-URL is opgehaald met andere streaminginstellingen. De gemarkeerde stream komt overeen met je huidige voorkeur; de actieve URL wordt vernieuwd bij het volgende streamverzoek. + Gekoppelde audiobron + Gekoppelde bron laden… + Een gekoppelde externe bron is niet nodig zolang het lokale cachebestand wordt afgespeeld. + Er is geen gekoppelde bron beschikbaar. + Er is geen gekoppelde audiobron gecachet voor dit nummer. + Streamdetails zijn nog niet gecachet. Speel het nummer eerst af om ze te laden. + Details van nummer laden mislukt. + Beschikbare streams + Dit nummer wordt afgespeeld vanuit de lokale cache. Streamvarianten worden voor deze weergave niet gebruikt. + Streams laden… + Er zijn geen streams beschikbaar voor deze bron. + Momenteel geselecteerde stream + Komt overeen met je voorkeur voor streamingformaat & -kwaliteit + Actieve afspeel-URL + Afgespeeld vanuit lokale cache + Metadata van nummer + Titel + ID + Album + Betrouwbaarheid + Externe URI + Album-ID + Albumtype + Releasedatum album + Aantal nummers op album + Albumgenres + Duur + Nummer op album + Schijfnummer + Expliciet + Populariteit + ISRC + Miniatuur-URL\'s + + + Zijbalk tonen/verbergen + + + Alternatieve bronnen + Er wordt nu geen nummer afgespeeld + Zoeken naar alternatieve bronnen... + Geen alternatieve bronnen gevonden + Betrouwbaarheid: %1$d% + Actieve bron + + + Verwijderen + Verwijder deze persoon uit de sessie + Verwijderen en voorkomen dat deze persoon opnieuw deelneemt + Suggesties verwijderen + Verwijder alle nummers die deze persoon aan de wachtrij heeft toegevoegd + Volgorde wijzigen + Verwijderen uit wachtrij + %1$s • Toegevoegd door %2$s + + + Onbekend + Playlist laden... + Nummers toevoegen + Nummers toevoegen aan playlist + Nummers zoeken... + Zoeken + Geen nummers gevonden + Zoek nummers om toe te voegen + Klaar + Toevoegen + Toegevoegd + + + Album laden... + + + Toevoegen aan je playlist + Zoeken + Zoek in je playlists... + Je playlists laden... + Je hebt nog geen eigen playlists.\nMaak er een om nummers toe te voegen. + Playlist maken + Playlist bewerken + Maken + Naam + Naam van playlist + Beschrijving + Beschrijving (optioneel) + Openbaar + Iedereen kan deze playlist zien + Gezamenlijk + Anderen kunnen nummers aan deze playlist toevoegen + Coverafbeelding + Playlistcover + Geen afbeelding + Afbeelding wijzigen + Afbeelding kiezen + + + Installeren + Vervangen + plugin.json niet gevonden in het zip-bestand + manifest.zipline.json niet gevonden in het zip-bestand + Ongeldige indeling van plugin.json: %1$s + Plug-in lezen mislukt: %1$s + Plug-in installeren mislukt: %1$s + Er is een nieuwere versie van %1$s beschikbaar. Bevestig de update voordat je installeert. + %1$s is al geïnstalleerd. Bevestig het vervangen voordat je installeert. + Ingebouwde plug-ins zijn al inbegrepen en kunnen niet opnieuw worden toegevoegd. + Ingebouwde plug-ins kunnen niet worden verwijderd. + %1$s geeft een ongeldige apiVersion op (%2$s). Verwacht: semantische versie compatibel met %3$s. + Plug-in-API-versie %1$s van Spotube is ongeldig. + %1$s is gemaakt voor plug-in-API %2$s, maar Spotube ondersteunt de breaking plug-in-API %3$s. Installeer een plug-in die is gemaakt voor dezelfde breaking API-versie. + + + Plug-in-API niet compatibel + Plug-in installeren? + %1$s wordt toegevoegd aan je geïnstalleerde plug-ins. + Plug-in bijwerken? + Er is een oudere versie geïnstalleerd (%1$s). Bijwerken naar %2$s? + Geïnstalleerde plug-in vervangen? + %1$s gebruikt een ongeldige semantische versie (%2$s) en kan niet als update worden vergeleken. De geïnstalleerde plug-in toch vervangen? + De geïnstalleerde versie (%1$s) kan niet worden vergeleken met semantische versies. Vervangen door %2$s? + Versie %1$s is al geïnstalleerd. De bestaande plug-in vervangen door de aangeboden kopie? + De geïnstalleerde versie %1$s is nieuwer dan de aangeboden versie %2$s. Toch vervangen? + De geïnstalleerde plug-in vervangen door de aangeboden kopie? + + + %1$s: + Versie + API-versie + Auteur + Licentie + Mogelijkheden + Functies + Repository + Contact + Bugs melden + %1$s steunen + + + Officieel + Geverifieerd + GitHub-sterren + URL van GitHub-repository + + + Geen releases gevonden + Release kiezen + Kies een release + Pre-release + Release-opmerkingen + + + Home + Zoeken + Op apparaat + Verbinden + Apparaten + + + Venster verbergen + Venster tonen + Volgend nummer + Vorig nummer + Shuffle + Herhalen + Herhalen: UIT + Herhalen: ÉÉN + Herhalen: ALLES + Volume: %1$d% + Volume + + Volume - + Dempen + Dempen opheffen + Nummer liken + Nummer niet meer liken + Afsluiten + + + Minimaliseren + Maximaliseren + Herstellen + Sluiten + + + Afspeelservice + Afspelen + Bediening voor media-afspelen + + + Ontdekken + Nu aan het afspelen + Uitgelicht + Opgeslagen nummers + Populairste nummers + diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml new file mode 100644 index 00000000..ddc97d41 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -0,0 +1,950 @@ + + + + Kolejka + Wyczyść kolejkę + Filtruj kolejkę... + Kolejka jest pusta + + Szukaj utworów, wykonawców, albumów... + Wyczyść całą historię + Szukaj... + + + Twoja biblioteka + Biblioteka + Playlisty + Albumy + Wykonawcy + Lokalne utwory + Pobrane + Szukaj w zapisanych playlistach… + Szukaj w zapisanych albumach… + Szukaj wśród zapisanych wykonawców… + Szukaj folderów lub utworów… + Filtruj bibliotekę + Zamknij filtr + Utwórz playlistę + Odśwież lokalne utwory + Przełącz na widok siatki + Przełącz na widok listy + Brak zapisanych playlist + Brak playlist pasujących do „%1$s” + Brak zapisanych albumów + Brak albumów pasujących do „%1$s” + Brak zapisanych wykonawców + Brak wykonawców pasujących do „%1$s” + + + Foldery + Folder + %1$s • %2$s + + %1$d utwór + %1$d utwory + %1$d utworów + %1$d utworu + + + %1$d folder + %1$d foldery + %1$d folderów + %1$d folderu + + Wróć do folderów + Odtwórz folder + Wymagany dostęp do audio + Zezwól na dostęp do audio, aby przeglądać muzykę na urządzeniu. + Zezwól na dostęp + Nie znaleziono lokalnych folderów + Brak utworów w tym folderze + Brak pasujących folderów lub utworów + Nie udało się odświeżyć lokalnych multimediów + + + Aktywne: %1$d • Ukończone: %2$d + Wyczyść ukończone + Nic jeszcze nie pobrano + Pobieranie %1$d% + W kolejce + Ukończono + Anulowano + Niepowodzenie + Niepowodzenie: %1$s + Anuluj pobieranie + Ponów pobieranie + Usuń pobrany plik + + + Wykonawca + Na czarnej liście + Polubione utwory + Twoje zapisane utwory + Nieznany wykonawca + Tytuł utworu + Nazwa wykonawcy + + Zaznaczono + + Ustawienia + + Wtyczki + Język i region + Wygląd + Odtwarzanie + Pobieranie + Pamięć podręczna + Komputer + Aktualizacje + Wsparcie + Informacje + + Zamknij + Zapisz + Anuluj + + Język + Język interfejsu aplikacji. Obecnie: %1$s + Domyślny systemowy + Kraj + Kraj regionu dla treści i dostępności. Obecnie: %1$s + %1$s (%2$s) + + Motyw + Wybierz, jak Spotube dopasowuje się do wyglądu systemu. Obecnie: %1$s + Jasny + Ciemny + Systemowy + + Powierzchnia + Materiał tła aplikacji. Obecnie: %1$s + Wodny + Lodowy + Chrom + Akryl + Szkło + + Kolor akcentu + Obecna paleta: %1$s + Kolor akcentu + Wybierz paletę akcentu i zobacz jej podgląd w jasnym i ciemnym motywie. + Jasny + Ciemny + Podgląd + Przycisk + Zielony goblin + Elektryczny fiolet + Oceaniczny cyjan + Pomarańcz zachodu + Różany ogród + Granat północy + Metaliczny łupek + + Wygląd i styl + Materiał powierzchni i akcent. Obecnie: %1$s · %2$s + Wygląd i styl + Wybierz materiał powierzchni, a następnie pasujący do niego akcent. Każda opcja to starannie dobrane połączenie. + Powierzchnia + Akcent + Dodatkowy + + Paleta + Paleta akcentów Millennium. Obecnie: %1$s + Paleta + Wybierz paletę akcentów Millennium i zobacz jej podgląd w jasnym i ciemnym motywie. + Paleta + Paleta i akcent Millennium. Obecnie: %1$s + Niebieska + Pomarańczowa + Limonkowa + + Własny folder pobierania + Obecny folder: %1$s + Obecny folder: domyślny folder muzyki + Pozostaw puste, aby używać domyślnego folderu muzyki. + C:\Music\Spotube + Foldery lokalnych multimediów + Własne foldery skanowane na komputerze. Dodano: %1$d + Nie dodano jeszcze własnego folderu + Własne foldery dla lokalnej biblioteki muzyki. + Zarządzaj + Dodaj folder + Usuń folder + Nie dodano własnych folderów. + Usuń %1$s z listy skanowania + Foldery pobierania i pamięci podręcznej muzyki są uwzględniane automatycznie. + Folder pobierania jest uwzględniany automatycznie. + Folder pamięci podręcznej muzyki jest uwzględniany automatycznie. + Zapisywanie… + Nie udało się zapisać folderów. Twoje zmiany nadal tu są – spróbuj ponownie. + + %1$d folder + %1$d foldery + %1$d folderów + %1$d folderu + + Format pobieranej muzyki + Preferowany kodek i kontener pobierania. Obecnie: %1$s + Jakość pobieranej muzyki + + Format strumieniowanej muzyki + Preferowany kodek i kontener strumieniowania muzyki. Obecnie: %1$s + Jakość strumieniowanej muzyki + Obecnie: %1$s + Włącz pamięć podręczną muzyki + Zapisuj strumieniowany dźwięk lokalnie, aby przyspieszyć ponowne odtwarzanie. + Folder pamięci podręcznej + Obecnie: %1$s + Obecnie: domyślna pamięć podręczna aplikacji + Limit pamięci podręcznej + Bez limitu + Obecny limit: %1$d MB + Ustaw maksymalny rozmiar pamięci podręcznej w MB. Wpisz 0 lub pozostaw puste, aby nie ustawiać limitu. + 500 + Rozmiar musi wynosić 0 lub być liczbą dodatnią. + Włącz nieskończone odtwarzanie + Gdy kolejka się skończy, odtwarzaj dalej polecane utwory. + Włącz Connect + Udostępniaj sterowanie odtwarzaniem przez funkcję Spotube Connect. + Port serwera proxy odtwarzania + Zezwól na zdalne sterowanie + Pozwól innym urządzeniom w tej samej sieci sterować odtwarzaniem po połączeniu. + Nazwa urządzenia do zdalnego sterowania + Nazwa widoczna dla innych urządzeń podczas wykrywania. Obecnie: %1$s + Wybierz przyjazną nazwę, którą zobaczą inne urządzenia w Twojej sieci, gdy wykryją tę instancję Spotube. + np. Spotube w salonie + nazwa hosta urządzenia + Port używany przez serwer proxy odtwarzania. Obecnie: %1$d + Wybierz port od 1 do 65535. + 14769 + Wpisz poprawną liczbę całkowitą. + Port musi mieścić się w zakresie od 1 do 65535. + + Czarna lista + Zarządzaj utworami i wykonawcami na czarnej liście + + Minimalizuj do zasobnika + Po zminimalizowaniu okna Spotube działa dalej w zasobniku systemowym. + Status Discord (Rich Presence) + Pokazuj w Discordzie, czego teraz słuchasz (na komputerze). + + Automatycznie sprawdzaj aktualizacje + Wyszukuj nowe wersje Spotube w tle. + + Zarządzaj wtyczkami + Zarządzaj swoimi wtyczkami + Domyślna wtyczka: %1$s + Metadane + Audio + Teksty + Scrobbling + Nie wybrano wtyczki + Zmień + Wybierz + Wybrano + Wyczyść wybór + Brak dostępnych wtyczek + Wtyczka %1$s + Wyczyść + + Konfiguruj + Menedżer wtyczek + Wpisz adres URL + Adres URL musi zaczynać się od http:// lub https:// + Nie udało się pobrać wtyczki + Nie zainstalowano jeszcze żadnych wtyczek + Wklej adres URL pobierania lub wybierz plik powyżej. + + %1$d wtyczka zainstalowana + %1$d wtyczki zainstalowane + %1$d wtyczek zainstalowanych + %1$d wtyczki zainstalowanej + + Zainstaluj wtyczkę + https://example.com/plugin.smplug + Pobierz + Zainstaluj z pliku (.smplug) + Aktywna + v%1$s + Wbudowana + Usuń wtyczkę + Zaloguj się + Wyloguj się + Pobierz z adresu URL + Zainstaluj z pliku + Zainstaluj + + • %1$s + Wymagane uprawnienia + Ta wtyczka nie wymaga specjalnych uprawnień. + Zainstalowana a dostarczona + Zainstalowana + Dostarczona + API %1$s → %2$s + Trwała pamięć + Odczyt i zapis danych na dysku + Żądania sieciowe + Wysyłanie i odbieranie danych przez internet + WebView + Wyświetlanie treści internetowych w aplikacji + Grupowy Jam + Broker MQTT + Host brokera + %1$s:%2$d + Port brokera + Użyj TLS + Nazwa użytkownika (opcjonalnie) + Hasło (opcjonalnie) + Prefiks ID klienta + Służy do identyfikacji tej instancji Spotube + Szyfruj połączenie z brokerem + Sprawdź bieżące ustawienia brokera przed utworzeniem pokoju. + Testuj połączenie + Testowanie… + Połączono w %1$d ms + Niepowodzenie: %1$s + Broker zastępczy – skonfiguruj własny, aby hostować samodzielnie + Połączenie + Dane logowania + Tożsamość + Diagnostyka + + + Grupowy Jam + Utwórz + Dołącz + Utwórz pokój, a potem udostępnij jego kod znajomym. + Utwórz pokój + Dołącz do pokoju + Łączenie… + Kod pokoju + ABC123 + Broker: %1$s + Skonfiguruj brokera w Ustawieniach, aby utworzyć pokój lub do niego dołączyć. + Ustawienia brokera + Połączono + Ponowne łączenie… + Kopiuj kod + Skopiowano! + Udostępnij + Uczestnicy (%1$d) + Gospodarz + Słuchacz + Czekamy na słuchaczy… + Usuń + Zablokuj + Zarządzaj + Opuść pokój + Nie udało się połączyć + Zamknij + Nie udało się utworzyć sesji Jam + Nie udało się dołączyć do sesji Jam + Wpisz sześcioznakowy kod pokoju złożony z liter i cyfr. + Dołącz do mojego Jamu w Spotube, kod: %1$s + + Przejdź do albumu + Wyłącznik czasowy + Szczegóły utworu + Odtwórz + Wstrzymaj + Nic nie jest odtwarzane + Dodaj do kolejki + + Ignoruj + Aktualizuj + Dostępna jest wersja Spotube %1$s + Sprawdź nowości w tej wersji na stronie pobierania Spotube. + Dostępna jest wersja %2$s wtyczki %1$s + Obecnie zainstalowana: %1$s + Sprawdź nowości w tej aktualizacji na stronie wtyczki. + + Pomiń + Dalej + Wstecz + Zaczynamy + Spotube + Muzyka bez żadnych warunków. + Otwartoźródłowy odtwarzacz dla całego świata muzyki.\nOdkrywaj go. Zrób go po swojemu. + Pomóż utrzymać ten projekt. + Spotube tworzą ludzie, a nie biznes oparty na subskrypcjach. Jeśli stanie się częścią Twojej codzienności, niewielkie wsparcie pomoże nam dbać o niego tak, jak na to zasługuje.\n\nDziękujemy, że dajesz miejsce niezależnemu oprogramowaniu. + Zacznij od wtyczki katalogu, aby wyszukiwać utwory i playlisty. Wybierz wtyczkę audio, aby je odtwarzać. Teksty i historia słuchania są opcjonalne. Znajdź wtyczkę poniżej, sprawdź jej uprawnienia, zainstaluj ją, a potem wybierz ją tutaj. + Odkrywaj wtyczki + Twoje źródła + Spróbuj ponownie + Więcej wtyczek + Nie znaleziono nowych wtyczek. Możesz też zainstalować wtyczkę z linku lub pliku .smplug powyżej. + Wyszukiwanie wtyczek jest teraz niedostępne. Spróbuj ponownie lub zainstaluj z linku albo pliku. + Witaj + Wsparcie + Wtyczki + Przewodnik + + Jak korzystać ze Spotube + Cztery szybkie kroki i już słuchasz. + Zaloguj się + Połącz konto Spotify lub wybierz własną wtyczkę katalogu. + Szukaj i odtwarzaj + Znajdź dowolny utwór, album lub wykonawcę i naciśnij Odtwórz. + Słuchaj offline + Pobieraj utwory, aby słuchać ich bez połączenia. + Śledź tekst + Otwórz zsynchronizowany tekst w odtwarzaczu podczas słuchania utworu. + + + Twoja muzyka. Twoje źródła. Twoje zasady. + Darmowy, otwartoźródłowy odtwarzacz muzyki tworzony przez społeczność, która po prostu kocha muzykę. + Open source + Bez reklam + Bez telemetrii + Oparty na wtyczkach + Zaczynamy + + + Wspierający dodają nam sił + Stworzony z pasją. Utrzymywany dzięki Tobie. + Spotube jest w 100% darmowy, wolny i otwartoźródłowy — bez reklam, subskrypcji i śledzenia. Każda funkcja istnieje dzięki osobom takim jak Ty, które zdecydowały się wspierać rozwój kodu. + Patreon + Miesięczny sponsor + Wspieraj stały rozwój i infrastrukturę + Buy Me a Coffee + Jednorazowa kawa + Dodaj energii nocnej sesji kodowania drobnym napiwkiem + Open Collective + Fundusz społeczności + Przejrzyste wsparcie finansowe zarządzane przez społeczność + Dziękujemy z całego serca, że wierzysz w otwartą muzykę. + Wsparcie jest całkowicie dobrowolne — Spotube i tak pozostanie darmowy. + Wesprzyj + Otwórz %1$s + + + Wesprzyj Spotube + Wesprzyj Spotube + Spotube jest darmowy, wolny i otwartoźródłowy — bez reklam, subskrypcji i śledzenia. Jeśli sprawia Ci radość, rozważ wsparcie projektu. + Te przypomnienia możesz wyłączyć w Ustawienia → Wsparcie. + Może później + Przypomnienia o wsparciu + Co 7 dni pokazuj delikatne przypomnienie o wsparciu projektu. + + + Modułowa architektura + Wybierz, skąd pochodzi Twoja muzyka. + Spotube rozdziela odkrywanie katalogu, strumieniowanie dźwięku i zsynchronizowane teksty. Skonfiguruj silniki poniżej lub zmień je w dowolnej chwili. + Silnik metadanych + Odpowiada za odkrywanie muzyki, wyszukiwanie, listy utworów i playlisty. + Silnik audio + Pobiera i dekoduje odtwarzane strumienie audio. + Silnik tekstów + Wyświetla zsynchronizowane teksty karaoke w czasie rzeczywistym. + Silnik scrobblingu + Zapisuje historię słuchania w Last.fm lub ListenBrainz. + Aktywny + Wbudowany + Gotowy + Opcjonalny + Wybierz silnik + Zainstalowane silniki + Ustaw jako domyślny + NewPipe YouTube i LRCLib Lyrics są wbudowane i od razu gotowe do działania. + Zarządzaj wtyczkami społeczności lub dodawaj nowe w dowolnej chwili w Ustawienia → Wtyczki. + + + Retro-nowoczesna architektura + Czyste Hi-Fi, zero pośredników + Oto jak Spotube organizuje zdecentralizowane odtwarzanie bezpośrednio na Twoim komputerze. + BEZPOŚREDNIO PEER-TO-PEER • 256 KBPS OPUS + Niebieska godzina + Spotube · demonstracja odtwarzacza + Trochę muzyki na chwile pomiędzy. + 1. Zdecentralizowane odkrywanie + Zapytania trafiają prosto do wybranej wtyczki katalogu. Spotube nigdy nie profiluje Twojego gustu. + 2. Bezpośrednie pozyskiwanie strumienia + Dźwięk płynie bezpośrednio z serwerów treści na Twój komputer. Bez pośrednich serwerów proxy i zdalnego zbierania danych. + 3. Lokalne DSP i odtwarzanie + Dekodowanie i buforowanie odbywa się lokalnie, z korektorem i opcjonalnym pobieraniem offline. + 4. Zsynchronizowane teksty i Jam + Teksty karaoke podążają za odtwarzaniem, a do tego zdalne sterowanie w sieci LAN i Grupowy Jam. + Żadnych serwerów pośredniczących. Odtwarzanie odbywa się na Twoim urządzeniu — prywatność w standardzie. + + + Wybierz urządzenie + %1$s: „%2$s” + Odtwórz + Dodaj do kolejki + Odtwórz jako następny + To urządzenie + Na tym urządzeniu + Urządzenie zdalne + Urządzenie zdalne (%1$s) + Na połączonym urządzeniu + + + Połączenie nieudane + Utracono połączenie + Wysyłanie nieudane + Wykrywanie nieudane: %1$s + Nie udało się połączyć: %1$s + + + Urządzenia + Odśwież + Połączono z %1$s:%2$d + Rozłącz + Łączenie... + Wyszukiwanie urządzeń w sieci... + Nie znaleziono urządzeń + Upewnij się, że na drugim urządzeniu w ustawieniach włączono opcję „Zezwól na zdalne sterowanie”. + Nieznane urządzenie + Ustalanie adresu... + ID: %1$s... + + + Zdalne sterowanie + Rozłączono + Błąd połączenia: %1$s + Okładka albumu + Nieznany utwór + Nieznany wykonawca + Przewiń + Odtwarzanie losowe + Poprzedni + Następny + Tryb powtarzania + Głośność + Usuń z kolejki + Więcej opcji + + + Gdzie odtworzyć? + Masz aktywną sesję Jam. Wybierz, gdzie odtworzyć tę kolekcję. + Odtwórz tutaj + Zaproponuj w Jamie + + + Nie skonfigurowano brokera Jam + Host brokera jest pusty + Połączenie nieudane + Utracono połączenie + + + Broker + anonim + + + Dodano do kolejki Jamu + + Dodano %1$d utwór do kolejki Jamu + Dodano %1$d utwory do kolejki Jamu + Dodano %1$d utworów do kolejki Jamu + Dodano %1$d utworu do kolejki Jamu + + + + Prośba o zdalne sterowanie + „%1$s” chce sterować odtwarzaniem na tym urządzeniu. + Odmów + Zezwól + Zawsze zezwalaj + + + Wyniki wyszukiwania + Wszystko + Utwory + Użytkownicy + Brak dostępnych typów wyszukiwania + Zacznij pisać, aby wyszukać + Zacznij pisać, aby wyszukać utwory + Brak wyników + Pokaż wszystko + Wyszukiwanie + Wczytywanie utworów... + Wczytywanie albumów... + Wczytywanie wykonawców... + Wczytywanie playlist... + Wczytywanie użytkowników... + Nie znaleziono albumów + Nie znaleziono wykonawców + Nie znaleziono playlist + Nie znaleziono użytkowników + Przekopujemy skrzynki z płytami... + Przeszukujemy kosmos... + Rozgrzewamy głośniki... + Odkurzamy winyle... + Radzimy się bogów muzyki... + Stroimy anteny... + Przeglądamy płyty... + Nie udało się wczytać możliwości wyszukiwania + Wyszukiwanie nie powiodło się + Nie udało się wyszukać utworów + Nie udało się wyszukać albumów + Nie udało się wyszukać wykonawców + Nie udało się wyszukać playlist + Nie udało się wyszukać użytkowników + + + Przeglądaj + + + Zsynchronizowany + Zwykły + Brak zsynchronizowanego tekstu + Brak zwykłego tekstu + + + %1$s w %2$s + %1$d kbps + %1$s kHz • %2$d kan. + Nie wybrano wtyczki metadanych + + + Wstecz + Ponów + Zamknij okno + Wczytywanie... + Nieznane + Nieznany błąd + + + Użytkownik + Playlista + + + Bardzo nam przykro + Coś teraz nie działa. Zależy nam na Twoich wrażeniach i robimy wszystko, aby to naprawić. Spróbuj ponownie lub sprawdź szczegóły błędu poniżej. + Szczegóły błędu + Kopiuj do schowka + Pokaż pełny dziennik + Pełny dziennik błędu + + + Losowo + Odtwarzaj losowo + Obserwuj + Przestań obserwować + Edytuj playlistę + Autor: %1$s + + + Opcje utworu + Dodaj do Jamu + Włącz radio + Odtwórz jako następny + Przenieś na następny + Usuń z kolejki + Usuń z ulubionych + Dodaj do ulubionych + Dodaj do playlisty + Pobierz + Udostępnij + + + Oryginalna + Tytuł + Wykonawca + Album + Czas trwania + Sortuj + Nie znaleziono utworów + Filtruj + Działania zbiorcze + Pobierz wszystkie + Pobierz (%1$d) + Dodaj wszystkie do kolejki + Dodaj do kolejki (%1$d) + Odtwórz wszystkie jako następne + Odtwórz jako następne (%1$d) + Dodaj wszystkie do playlisty + Dodaj do playlisty (%1$d) + Dodaj wszystkie do Jamu + Dodaj do Jamu (%1$d) + +%1$d więcej + + + Brak adresu URL do wczytania + Wstecz w historii przeglądarki + Dalej w historii przeglądarki + + + Filtruj czarną listę... + Czarna lista jest pusta + Brak elementów czarnej listy pasujących do „%1$s” + Utwory + Utwór + Dodaj do czarnej listy + Usuń z czarnej listy + + + Brak metadanych utworu + Nie udało się ustalić adresu URL strumienia + Nie udało się ustalić adresu URL strumienia. Spróbuj najpierw odtworzyć utwór. + + + Zapisane utwory + Ty + + + Obserwujesz + Popularne utwory + Odtwórz popularne utwory + Dodaj popularne utwory do kolejki + Podobni wykonawcy + Polecane playlisty + Nie udało się wczytać przeglądu wykonawcy + Liczba obserwujących niedostępna + %1$s tys. obserwujących + %1$s mln obserwujących + %1$s mld obserwujących + + %1$d obserwujący + %1$d obserwujących + %1$d obserwujących + %1$d obserwującego + + + + Polub + Cofnij polubienie + Tytuł utworu zastępczego + Wykonawcy zastępczy + Nieznany album + Przewiń + Głośność + Włącz odtwarzanie losowe + Wyłącz odtwarzanie losowe + Poprzedni + Następny + Odtwórz lub wstrzymaj + Powtarzanie wyłączone + Powtarzaj jeden utwór + Powtarzaj wszystko + Wycisz + Wyłącz wyciszenie + Więcej opcji + Opcje odtwarzacza + Zwiń odtwarzacz + Rozwiń tekst + Teraz odtwarzane + Tekst + Alternatywne źródło + Alternatywne źródło + Pobierz + Pobrano + Pobieranie... + Już pobrano + Ponawianie pobierania... + Pobieranie: %1$s + Odtwarzanie zostanie wstrzymane za %1$s. + Wstrzymaj odtwarzanie po wybranej liczbie minut. + Minuty + Wybierz od 1 do 600 minut + Anuluj wyłącznik + Start + Adres URL odtwarzania został ustalony przy innych ustawieniach strumieniowania. Wyróżniony strumień odpowiada Twoim obecnym preferencjom; aktywny adres URL odświeży się przy następnym żądaniu strumienia. + Dopasowane źródło audio + Wczytywanie dopasowanego źródła… + Dopasowane zdalne źródło nie jest potrzebne podczas odtwarzania lokalnego pliku z pamięci podręcznej. + Brak dostępnego dopasowanego źródła. + Brak dopasowanego źródła audio w pamięci podręcznej dla tego utworu. + Szczegóły strumienia nie są jeszcze zapisane. Najpierw odtwórz utwór, aby je wczytać. + Nie udało się wczytać szczegółów utworu. + Dostępne strumienie + Ten utwór jest odtwarzany z lokalnej pamięci podręcznej. Warianty strumienia nie są używane podczas tego odtwarzania. + Wczytywanie strumieni… + Brak strumieni dla tego źródła. + Obecnie wybrany strumień + Zgodny z preferowanym formatem i jakością strumieniowania + Aktywny adres URL odtwarzania + Odtwarzane z lokalnej pamięci podręcznej + Utwór z metadanych + Tytuł + ID + Album + Pewność + Zewnętrzny URI + ID albumu + Typ albumu + Data wydania albumu + Liczba utworów w albumie + Gatunki albumu + Czas trwania + Numer utworu + Numer płyty + Treści dla dorosłych + Popularność + ISRC + Adresy URL miniatur + + + Przełącz pasek boczny + + + Alternatywne źródła + Nic nie jest obecnie odtwarzane + Wyszukiwanie alternatywnych źródeł... + Nie znaleziono alternatywnych źródeł + Pewność: %1$d% + Aktywne źródło + + + Wyrzuć + Usuń tę osobę z sesji + Wyrzuć i uniemożliw ponowne dołączenie + Usuń propozycje + Usuń wszystkie utwory dodane przez tę osobę do kolejki + Zmień kolejność + Usuń z kolejki + %1$s • Dodane przez: %2$s + + + Nieznany + Wczytywanie playlisty... + Dodaj utwory + Dodaj utwory do playlisty + Szukaj utworów... + Szukaj + Nie znaleziono utworów + Wyszukaj utwory do dodania + Gotowe + Dodaj + Dodano + + + Wczytywanie albumu... + + + Dodaj do swojej playlisty + Szukaj + Szukaj w swoich playlistach... + Wczytywanie Twoich playlist... + Nie masz jeszcze własnych playlist.\nUtwórz playlistę, aby dodać utwory. + Utwórz playlistę + Edytuj playlistę + Utwórz + Nazwa + Nazwa playlisty + Opis + Opis (opcjonalnie) + Publiczna + Każdy może zobaczyć tę playlistę + Wspólna + Inni mogą dodawać utwory do tej playlisty + Okładka + Okładka playlisty + Brak obrazu + Zmień obraz + Wybierz obraz + + + Zainstaluj + Zastąp + Nie znaleziono pliku plugin.json w archiwum zip + Nie znaleziono pliku manifest.zipline.json w archiwum zip + Nieprawidłowy format pliku plugin.json: %1$s + Nie udało się odczytać wtyczki: %1$s + Nie udało się zainstalować wtyczki: %1$s + Dostępna jest nowsza wersja %1$s. Potwierdź aktualizację przed instalacją. + %1$s jest już zainstalowana. Potwierdź zastąpienie przed instalacją. + Wbudowane wtyczki są już dołączone i nie można ich dodać ponownie. + Nie można usunąć wbudowanych wtyczek. + %1$s deklaruje nieprawidłową wartość apiVersion (%2$s). Oczekiwano wersji semantycznej zgodnej z %3$s. + Wersja API wtyczek Spotube %1$s jest nieprawidłowa. + %1$s jest przeznaczona dla API wtyczek %2$s, a Spotube obsługuje niezgodne wstecz API wtyczek %3$s. Zainstaluj wtyczkę zbudowaną dla tej samej głównej wersji API. + + + Niezgodne API wtyczki + Zainstalować wtyczkę? + %1$s zostanie dodana do zainstalowanych wtyczek. + Zaktualizować wtyczkę? + Zainstalowana jest starsza wersja (%1$s). Zaktualizować do %2$s? + Zastąpić zainstalowaną wtyczkę? + %1$s używa nieprawidłowej wersji semantycznej (%2$s) i nie można jej porównać jako aktualizacji. Mimo to zastąpić zainstalowaną wtyczkę? + Zainstalowanej wersji (%1$s) nie można porównać za pomocą wersjonowania semantycznego. Zastąpić ją wersją %2$s? + Wersja %1$s jest już zainstalowana. Zastąpić istniejącą wtyczkę dostarczoną kopią? + Zainstalowana wersja %1$s jest nowsza niż dostarczona wersja %2$s. Mimo to zastąpić? + Zastąpić zainstalowaną wtyczkę dostarczoną kopią? + + + %1$s: + Wersja + Wersja API + Autor + Licencja + Uprawnienia + Funkcje + Repozytorium + Kontakt + Zgłoś błędy + Wesprzyj %1$s + + + Oficjalna + Zweryfikowana + Gwiazdki w GitHub + Adres URL repozytorium GitHub + + + Nie znaleziono wydań + Wybierz wydanie + Wybierz wydanie + Wersja przedpremierowa + Informacje o wydaniu + + + Główna + Szukaj + Na urządzeniu + Połącz + Urządzenia + + + Ukryj okno + Pokaż okno + Następny utwór + Poprzedni utwór + Losowo + Powtarzanie + Powtarzanie: WYŁ. + Powtarzanie: JEDEN + Powtarzanie: WSZYSTKO + Głośność: %1$d% + Głośność + + Głośność - + Wycisz + Wyłącz wyciszenie + Polub utwór + Cofnij polubienie utworu + Zakończ + + + Minimalizuj + Maksymalizuj + Przywróć + Zamknij + + + Usługa odtwarzania + Odtwarzanie + Sterowanie odtwarzaniem multimediów + + + Przeglądaj + Teraz odtwarzane + Polecane + Zapisane utwory + Popularne utwory + diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml new file mode 100644 index 00000000..3694b5c2 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml @@ -0,0 +1,944 @@ + + + + Fila + Limpar fila + Filtrar fila... + A fila está vazia + + Buscar músicas, artistas, álbuns... + Limpar todo o histórico + Buscar... + + + Sua Biblioteca + Biblioteca + Playlists + Álbuns + Artistas + Arquivos locais + Downloads + Buscar playlists salvas… + Buscar álbuns salvos… + Buscar artistas salvos… + Buscar pastas ou faixas… + Filtrar biblioteca + Fechar filtro + Criar playlist + Atualizar arquivos locais + Mudar para visualização em grade + Mudar para visualização em lista + Nenhuma playlist salva + Nenhuma playlist corresponde a “%1$s” + Nenhum álbum salvo + Nenhum álbum corresponde a “%1$s” + Nenhum artista salvo + Nenhum artista corresponde a “%1$s” + + + Pastas + Pasta + %1$s • %2$s + + %1$d faixa + %1$d de faixas + %1$d faixas + + + %1$d pasta + %1$d de pastas + %1$d pastas + + Voltar para pastas + Tocar pasta + Acesso ao áudio necessário + Permita o acesso ao áudio para navegar pelas músicas do seu dispositivo. + Permitir acesso + Nenhuma pasta local encontrada + Nenhuma faixa nesta pasta + Nenhuma pasta ou faixa correspondente + Falha ao atualizar a mídia local + + + %1$d em andamento • %2$d concluídos + Limpar concluídos + Nenhum download ainda + Baixando %1$d% + Na fila + Concluído + Cancelado + Falhou + Falhou: %1$s + Cancelar download + Tentar baixar de novo + Remover download + + + Artista + Bloqueado + Músicas Curtidas + Suas músicas salvas + Artista desconhecido + Título da faixa + Nome do artista + + Selecionado + + Configurações + + Plugins + Idioma e região + Aparência + Reprodução + Downloads + Cache + Desktop + Atualizações + Apoio + Sobre + + Fechar + Salvar + Cancelar + + Idioma + Idioma da interface do app. Atual: %1$s + Padrão do sistema + País + País usado para conteúdo e disponibilidade. Atual: %1$s + %1$s (%2$s) + + Tema + Escolha como o Spotube acompanha a aparência do sistema. Atual: %1$s + Claro + Escuro + Sistema + + Superfície + Material de fundo do app. Atual: %1$s + Aquoso + Gelo + Cromado + Acrílico + Vidro + + Cor de destaque + Paleta atual: %1$s + Cor de destaque + Escolha uma paleta de destaque e veja como fica nos temas claro e escuro. + Claro + Escuro + Prévia + Botão + Duende Verde + Violeta Elétrico + Ciano Oceânico + Laranja Pôr do Sol + Jardim de Rosas + Azul Meia-Noite + Ardósia Metálica + + Visual + Material da superfície e destaque. Atual: %1$s · %2$s + Visual + Escolha um material de superfície e depois um destaque que combine com ele. Cada opção é uma combinação selecionada. + Superfície + Destaque + Secundário + + Paleta + Paleta de destaque Millennium. Atual: %1$s + Paleta + Escolha uma paleta de destaque Millennium e veja como fica no claro e no escuro. + Paleta + Paleta Millennium e destaque. Atual: %1$s + Azul + Laranja + Lima + + Pasta de download personalizada + Pasta atual: %1$s + Pasta atual: pasta de músicas padrão + Deixe em branco para usar a pasta de músicas padrão. + C:\Music\Spotube + Pastas de mídia local + Pastas personalizadas a serem verificadas no desktop. Adicionadas: %1$d + Nenhuma pasta personalizada adicionada ainda + Pastas personalizadas para sua biblioteca de músicas local. + Gerenciar + Adicionar pasta + Remover pasta + Nenhuma pasta personalizada adicionada. + Remover %1$s da lista de verificação + As pastas de download e de cache de músicas são incluídas automaticamente. + Sua pasta de download é incluída automaticamente. + A pasta de cache de músicas é incluída automaticamente. + Salvando… + Não foi possível salvar as pastas. Suas alterações continuam aqui; tente de novo. + + %1$d pasta + %1$d de pastas + %1$d pastas + + Formato de download das músicas + Codec e contêiner preferidos para downloads. Atual: %1$s + Qualidade de download das músicas + + Formato de streaming das músicas + Codec e contêiner preferidos para o streaming de música. Atual: %1$s + Qualidade de streaming das músicas + Atual: %1$s + Ativar cache de músicas + Armazena o áudio transmitido no dispositivo para acelerar as próximas reproduções. + Pasta de cache + Atual: %1$s + Atual: cache padrão do app + Limite de tamanho do cache + Sem limite + Limite atual: %1$d MB + Defina o tamanho máximo do cache em MB. Digite 0 ou deixe em branco para não ter limite. + 500 + O tamanho deve ser 0 ou um número positivo. + Ativar reprodução contínua + Continua tocando faixas recomendadas quando sua fila acabar. + Ativar Connect + Disponibiliza controles de reprodução remota pelo recurso Spotube Connect. + Porta do servidor proxy de reprodução + Permitir controle remoto + Permite que outros dispositivos na mesma rede controlem a reprodução quando se conectarem. + Nome do dispositivo para controle remoto + O nome que outros dispositivos veem ao encontrar este. Atual: %1$s + Escolha um nome amigável para exibir aos outros dispositivos da sua rede quando encontrarem esta instância do Spotube. + ex.: Spotube da Sala + nome do host do dispositivo + Porta usada pelo servidor proxy de reprodução. Atual: %1$d + Escolha uma porta entre 1 e 65535. + 14769 + Digite um número inteiro válido. + A porta deve estar entre 1 e 65535. + + Lista de bloqueio + Gerencie faixas e artistas bloqueados + + Minimizar para a bandeja + Mantém o Spotube em execução na bandeja do sistema quando a janela for minimizada. + Rich Presence do Discord + Mostra o que você está ouvindo no Discord no desktop. + + Verificar atualizações automaticamente + Procura novas versões do Spotube em segundo plano. + + Gerenciar plugins + Gerencie seus plugins + Plugin de %1$s padrão + Metadados + Áudio + Letras + Scrobble + Nenhum plugin selecionado + Alterar + Selecionar + Selecionado + Limpar seleção + Nenhum plugin disponível + Plugin %1$s + Limpar + + Configurar + Gerenciador de plugins + Digite uma URL + A URL deve começar com http:// ou https:// + Falha ao baixar o plugin + Nenhum plugin instalado ainda + Cole uma URL de download ou escolha um arquivo acima. + + %1$d plugin instalado + %1$d de plugins instalados + %1$d plugins instalados + + Instalar um plugin + https://example.com/plugin.smplug + Baixar + Instalar de arquivo (.smplug) + Ativo + v%1$s + Integrado + Remover plugin + Entrar + Sair + Baixar de URL + Instalar de arquivo + Instalar + + • %1$s + Permissões solicitadas + Este plugin não precisa de permissões especiais. + Instalado vs. fornecido + Instalado + Fornecido + API %1$s → %2$s + Armazenamento persistente + Ler e gravar dados no disco + Solicitações de rede + Enviar e receber dados pela internet + WebView + Exibir conteúdo da web dentro do app + Jam em grupo + Broker MQTT + Host do broker + %1$s:%2$d + Porta do broker + Usar TLS + Nome de usuário (opcional) + Senha (opcional) + Prefixo do ID do cliente + Usado para identificar esta instância do Spotube + Criptografa a conexão com o broker + Verifique as configurações atuais do broker antes de criar uma sala. + Testar conexão + Testando… + Conectado em %1$d ms + Falhou: %1$s + Broker provisório — configure o seu próprio para hospedar por conta própria + Conexão + Credenciais + Identidade + Diagnóstico + + + Jam em grupo + Criar + Entrar + Crie uma sala e compartilhe o código com seus amigos. + Criar sala + Entrar na sala + Conectando… + Código da sala + ABC123 + Broker: %1$s + Configure um broker nas Configurações para criar ou entrar em uma sala. + Configurações do broker + Conectado + Reconectando… + Copiar código + Copiado! + Compartilhar + Participantes (%1$d) + Anfitrião + Ouvinte + Aguardando ouvintes… + Remover + Banir + Gerenciar + Sair da sala + Não foi possível conectar + Dispensar + Falha ao criar a Jam + Falha ao entrar na Jam + Digite um código de sala com seis caracteres, usando letras e números. + Entre na minha Jam do Spotube com o código: %1$s + + Ir para o álbum + Timer + Detalhes da faixa + Tocar + Pausar + Nada tocando + Adicionar à fila + + Ignorar + Atualizar + Spotube %1$s está disponível + Veja as novidades desta versão na página de downloads do Spotube. + %1$s %2$s está disponível + Versão instalada: %1$s + Veja as novidades desta atualização na página do plugin. + + Pular + Avançar + Voltar + Começar + Spotube + Música, sem amarras. + Um player de código aberto para um mundo de música.\nPara você explorar. Para você deixar do seu jeito. + Mantenha este projeto vivo. + O Spotube é feito por pessoas, não por uma empresa de assinaturas. Se ele fizer parte do seu dia a dia, um pequeno apoio nos ajuda a dar a ele o cuidado que merece.\n\nObrigado por abrir espaço para software independente. + Comece com um plugin de catálogo para encontrar músicas e playlists. Escolha um plugin de áudio para tocá-las. Letras e histórico de reprodução são opcionais. Descubra um plugin abaixo, revise as permissões, instale-o e depois selecione-o aqui. + Descobrir plugins + Suas fontes + Tentar de novo + Mais plugins + Nenhum plugin novo encontrado. Você também pode instalar um a partir de um link ou de um arquivo .smplug acima. + A descoberta de plugins está indisponível no momento. Tente de novo ou instale a partir de um link ou arquivo. + Boas-vindas + Apoio + Plugins + Guia + + Como usar o Spotube + Quatro passos rápidos e você já está ouvindo. + Entrar + Conecte sua conta do Spotify ou escolha seu próprio plugin de catálogo. + Busque e toque + Encontre qualquer música, álbum ou artista e aperte o play. + Ouça offline + Baixe faixas para continuar ouvindo sem conexão. + Acompanhe a letra + Abra a letra sincronizada no player enquanto a faixa toca. + + + Sua música. Suas fontes. Suas regras. + Um player de música gratuito e de código aberto, feito por uma comunidade que simplesmente ama música. + Código aberto + Sem anúncios + Sem telemetria + Movido a plugins + Vamos lá + + + Apoiadores nos mantêm de pé + Feito com paixão. Mantido vivo por você. + O Spotube é 100% gratuito, livre e de código aberto — sem anúncios, sem assinaturas, sem rastreamento. Cada recurso existe porque pessoas como você decidiram ajudar a manter o código fluindo. + Patreon + Apoio mensal + Apoie o desenvolvimento contínuo e a infraestrutura + Buy Me a Coffee + Um café avulso + Dê energia a uma sessão de programação noturna com uma gorjeta rápida + Open Collective + Fundo da comunidade + Apoio financeiro transparente e gerido pela comunidade + Do fundo do coração, obrigado por acreditar na música aberta. + Apoiar é totalmente opcional — o Spotube continua gratuito de qualquer forma. + Apoiar + Abrir %1$s + + + Apoiar o Spotube + Apoie o Spotube + O Spotube é gratuito, livre e de código aberto — sem anúncios, sem assinaturas, sem rastreamento. Se ele te traz alegria, considere apoiar o projeto. + Você pode desativar esses lembretes em Configurações → Apoio. + Agora não + Lembretes de apoio + Mostra um lembrete gentil para apoiar o projeto a cada 7 dias. + + + Arquitetura modular + Escolha de onde vem a sua música. + O Spotube separa a descoberta do catálogo, o streaming de áudio e as letras sincronizadas. Configure seus motores abaixo ou troque-os quando quiser. + Motor de metadados + Alimenta a descoberta de músicas, a busca, as listas de faixas e as playlists. + Motor de áudio + Obtém e decodifica os streams de áudio que tocam. + Motor de letras + Transmite letras de karaokê sincronizadas em tempo real. + Motor de scrobble + Registra o histórico de reprodução no Last.fm ou no ListenBrainz. + Ativo + Integrado + Pronto + Opcional + Selecione um motor + Motores instalados + Definir como padrão + NewPipe YouTube e LRCLib Lyrics já vêm integrados e prontos para usar. + Gerencie ou adicione novos plugins da comunidade quando quiser em Configurações → Plugins. + + + Arquitetura retrô-moderna + Hi-Fi puro, zero intermediários + Veja como o Spotube orquestra a reprodução descentralizada diretamente na sua máquina. + PEER-TO-PEER DIRETO • OPUS 256 KBPS + Hora azul + Spotube · demonstração do player + Um pouco de música para os momentos entre um e outro. + 1. Descoberta descentralizada + As buscas vão direto para o plugin de catálogo que você escolheu. O Spotube nunca traça o perfil do seu gosto. + 2. Resolução direta do stream + O áudio vem direto dos servidores de conteúdo para a sua máquina. Sem saltos por proxy nem coleta remota de dados. + 3. DSP e reprodução locais + Decodificado e armazenado em cache localmente, com controles de equalizador e download offline opcional. + 4. Letras sincronizadas e Jam + As letras de karaokê acompanham o tempo da reprodução, com controle remoto via LAN e Jam em grupo. + Sem servidores intermediários. A reprodução acontece no seu dispositivo — privado por natureza. + + + Escolha um dispositivo + %1$s: “%2$s” + Tocar + Adicionar à fila + Tocar em seguida + Este dispositivo + Neste dispositivo + Dispositivo remoto + Dispositivo remoto (%1$s) + No dispositivo conectado + + + Falha na conexão + Conexão perdida + Falha no envio + Falha na descoberta: %1$s + Falha ao conectar: %1$s + + + Dispositivos + Atualizar + Conectado a %1$s:%2$d + Desconectar + Conectando... + Procurando dispositivos na rede... + Nenhum dispositivo encontrado + Verifique se o outro dispositivo está com \"Permitir controle remoto\" ativado nas configurações. + Dispositivo desconhecido + Resolvendo... + ID: %1$s... + + + Controle remoto + Desconectado + Erro de conexão: %1$s + Capa do álbum + Faixa desconhecida + Artista desconhecido + Avançar/retroceder + Ordem aleatória + Anterior + Próxima + Modo de repetição + Volume + Remover da fila + Mais opções + + + Tocar onde? + Você tem uma Jam ativa. Escolha onde tocar esta coleção. + Tocar aqui + Sugerir na Jam + + + Nenhum broker de Jam configurado + O host do broker está vazio + Falha na conexão + Conexão perdida + + + Broker + anônimo + + + Adicionada à fila da Jam + + %1$d faixa adicionada à fila da Jam + %1$d de faixas adicionadas à fila da Jam + %1$d faixas adicionadas à fila da Jam + + + + Solicitação de controle remoto + \"%1$s\" quer controlar a reprodução neste dispositivo. + Negar + Permitir + Sempre permitir + + + Resultados da busca + Tudo + Faixas + Usuários + Nenhum tipo de busca disponível + Comece a digitar para buscar + Comece a digitar para buscar faixas + Nenhum resultado encontrado + Ver tudo + Buscando + Carregando faixas... + Carregando álbuns... + Carregando artistas... + Carregando playlists... + Carregando usuários... + Nenhum álbum encontrado + Nenhum artista encontrado + Nenhuma playlist encontrada + Nenhum usuário encontrado + Garimpando nos caixotes de discos... + Vasculhando o cosmos... + Aquecendo as caixas de som... + Tirando o pó do vinil... + Consultando os deuses da música... + Sintonizando as antenas... + Folheando os discos... + Falha ao carregar os recursos de busca + Falha na busca + Falha ao buscar faixas + Falha ao buscar álbuns + Falha ao buscar artistas + Falha ao buscar playlists + Falha ao buscar usuários + + + Explorar + + + Sincronizada + Simples + Nenhuma letra sincronizada disponível + Nenhuma letra simples disponível + + + %1$s em %2$s + %1$d kbps + %1$s kHz • %2$d canais + Nenhum plugin de metadados selecionado + + + Voltar + Tentar de novo + Fechar caixa de diálogo + Carregando... + Desconhecido + Erro desconhecido + + + Usuário + Playlist + + + Pedimos sinceras desculpas + Algo não está funcionando agora. Sua experiência importa para nós, e estamos fazendo o possível para resolver isso. Tente de novo ou confira os detalhes do erro abaixo. + Detalhes do erro + Copiar para a área de transferência + Ver log completo + Log de erro completo + + + Aleatório + Tocar em ordem aleatória + Seguir + Deixar de seguir + Editar playlist + De %1$s + + + Opções da faixa + Adicionar à Jam + Iniciar rádio + Tocar em seguida + Mover para a próxima + Remover da fila + Remover dos favoritos + Salvar nos favoritos + Adicionar à playlist + Baixar + Compartilhar + + + Original + Título + Artista + Álbum + Duração + Ordenar + Nenhuma faixa encontrada + Filtrar + Ações em massa + Baixar tudo + Baixar %1$d + Adicionar tudo à fila + Adicionar %1$d à fila + Tocar tudo em seguida + Tocar %1$d em seguida + Adicionar tudo à playlist + Adicionar %1$d à playlist + Adicionar tudo à Jam + Adicionar %1$d à Jam + +%1$d outros + + + Nenhuma URL para carregar + Voltar no histórico do navegador + Avançar no histórico do navegador + + + Filtrar lista de bloqueio... + Nenhum item bloqueado + Nenhum item bloqueado corresponde a \'%1$s\' + Faixas + Faixa + Adicionar à lista de bloqueio + Remover da lista de bloqueio + + + Sem metadados da faixa + Não foi possível obter a URL do stream + Não foi possível obter a URL do stream. Tente tocar a faixa primeiro. + + + Faixas salvas + Você + + + Seguindo + Mais tocadas + Tocar as mais tocadas + Adicionar as mais tocadas à fila + Artistas relacionados + Playlists em destaque + Falha ao carregar a visão geral do artista + Seguidores indisponíveis + %1$s mil seguidores + %1$s mi de seguidores + %1$s bi de seguidores + + %1$d seguidor + %1$d de seguidores + %1$d seguidores + + + + Curtir + Descurtir + Título de faixa genérico + Artistas genéricos + Álbum desconhecido + Avançar/retroceder + Volume + Ativar ordem aleatória + Desativar ordem aleatória + Anterior + Próxima + Tocar ou pausar + Repetição desativada + Repetir uma + Repetir todas + Silenciar + Ativar som + Mais opções + Opções do player + Recolher player + Expandir letra + Tocando agora + Letra + Fonte alternativa + Fonte alternativa + Baixar + Baixada + Baixando... + Já baixada + Tentando baixar de novo... + Baixando %1$s + A reprodução será pausada em %1$s. + Pausa a reprodução após o número de minutos selecionado. + Minutos + Escolha entre 1 e 600 minutos + Cancelar timer + Iniciar + A URL de reprodução foi obtida com outras configurações de streaming. O stream destacado corresponde à sua preferência atual; a URL ativa será atualizada na próxima solicitação de stream. + Fonte de áudio correspondente + Carregando fonte correspondente… + Uma fonte remota correspondente não é necessária enquanto o arquivo em cache local estiver tocando. + Nenhuma fonte correspondente disponível. + Nenhuma fonte de áudio correspondente está em cache para esta faixa. + Os detalhes do stream ainda não estão em cache. Toque a faixa primeiro para carregá-los. + Falha ao carregar os detalhes da faixa. + Streams disponíveis + Esta faixa está sendo reproduzida do cache local. As variantes de stream não são usadas nesta reprodução. + Carregando streams… + Nenhum stream disponível para esta fonte. + Stream selecionado no momento + Corresponde à sua preferência de formato e qualidade de streaming + URL de reprodução ativa + Reproduzida do cache local + Faixa nos metadados + Título + ID + Álbum + Confiança + URI externo + ID do álbum + Tipo de álbum + Data de lançamento do álbum + Número de faixas do álbum + Gêneros do álbum + Duração + Número da faixa + Número do disco + Explícito + Popularidade + ISRC + URLs das miniaturas + + + Alternar barra lateral + + + Fontes alternativas + Nenhuma faixa tocando no momento + Procurando fontes alternativas... + Nenhuma fonte alternativa encontrada + Confiança: %1$d% + Fonte ativa + + + Expulsar + Remove a pessoa da sessão + Expulsa e impede que a pessoa volte a entrar + Remover sugestões + Remove todas as faixas que a pessoa adicionou à fila + Reordenar + Remover da fila + %1$s • Adicionada por %2$s + + + Desconhecido + Carregando playlist... + Adicionar faixas + Adicionar faixas à playlist + Buscar faixas... + Buscar + Nenhuma faixa encontrada + Busque faixas para adicionar + Concluído + Adicionar + Adicionada + + + Carregando álbum... + + + Adicionar à sua playlist + Buscar + Buscar nas suas playlists... + Carregando suas playlists... + Você ainda não tem nenhuma playlist.\nCrie uma para adicionar faixas. + Criar playlist + Editar playlist + Criar + Nome + Nome da playlist + Descrição + Descrição (opcional) + Pública + Qualquer pessoa pode ver esta playlist + Colaborativa + Outras pessoas podem adicionar faixas a esta playlist + Imagem de capa + Capa da playlist + Sem imagem + Alterar imagem + Escolher imagem + + + Instalar + Substituir + plugin.json não encontrado no arquivo zip + manifest.zipline.json não encontrado no arquivo zip + Formato de plugin.json inválido: %1$s + Falha ao ler o plugin: %1$s + Falha ao instalar o plugin: %1$s + Há uma versão mais recente de %1$s disponível. Confirme a atualização antes de instalar. + %1$s já está instalado. Confirme a substituição antes de instalar. + Os plugins integrados já estão incluídos e não podem ser adicionados de novo. + Os plugins integrados não podem ser removidos. + %1$s declara uma apiVersion inválida (%2$s). Era esperado um versionamento semântico compatível com %3$s. + A versão %1$s da API de plugins do Spotube é inválida. + %1$s foi feito para a API de plugins %2$s, mas o Spotube suporta a versão principal %3$s da API. Instale um plugin feito para a mesma versão principal da API. + + + API do plugin incompatível + Instalar plugin? + %1$s será adicionado aos seus plugins instalados. + Atualizar plugin? + Uma versão mais antiga está instalada (%1$s). Atualizar para %2$s? + Substituir plugin instalado? + %1$s usa uma versão semântica inválida (%2$s) e não pode ser comparado como uma atualização. Substituir o plugin instalado mesmo assim? + A versão instalada (%1$s) não pode ser comparada usando versionamento semântico. Substituí-la por %2$s? + A versão %1$s já está instalada. Substituir o plugin existente pela cópia fornecida? + A versão instalada %1$s é mais recente que a versão fornecida %2$s. Substituir mesmo assim? + Substituir o plugin instalado pela cópia fornecida? + + + %1$s: + Versão + Versão da API + Autor + Licença + Permissões + Funções + Repositório + Contato + Relatar bugs + Apoiar %1$s + + + Oficial + Verificado + Estrelas no GitHub + URL do repositório no GitHub + + + Nenhuma versão encontrada + Selecionar versão + Selecione uma versão + Pré-lançamento + Notas da versão + + + Início + Buscar + No dispositivo + Conectar + Dispositivos + + + Ocultar janela + Mostrar janela + Próxima faixa + Faixa anterior + Ordem aleatória + Repetir + Repetir: DESATIVADO + Repetir: UMA + Repetir: TODAS + Volume: %1$d% + Volume + + Volume - + Silenciar + Ativar som + Curtir faixa + Descurtir faixa + Sair + + + Minimizar + Maximizar + Restaurar + Fechar + + + Serviço de reprodução + Reprodução + Controles de reprodução de mídia + + + Explorar + Tocando agora + Em destaque + Faixas salvas + Mais tocadas + diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings.xml b/composeApp/src/commonMain/composeResources/values-ru/strings.xml new file mode 100644 index 00000000..860089e6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-ru/strings.xml @@ -0,0 +1,950 @@ + + + + Очередь + Очистить очередь + Фильтр очереди... + Очередь пуста + + Треки, исполнители, альбомы... + Очистить историю + Поиск... + + + Моя медиатека + Медиатека + Плейлисты + Альбомы + Исполнители + Локальные треки + Загрузки + Поиск по сохранённым плейлистам… + Поиск по сохранённым альбомам… + Поиск по сохранённым исполнителям… + Поиск папок или треков… + Фильтр медиатеки + Закрыть фильтр + Создать плейлист + Обновить локальные треки + Показать сеткой + Показать списком + Нет сохранённых плейлистов + Нет плейлистов по запросу «%1$s» + Нет сохранённых альбомов + Нет альбомов по запросу «%1$s» + Нет сохранённых исполнителей + Нет исполнителей по запросу «%1$s» + + + Папки + Папка + %1$s • %2$s + + %1$d трек + %1$d трека + %1$d треков + %1$d трека + + + %1$d папка + %1$d папки + %1$d папок + %1$d папки + + Назад к папкам + Слушать папку + Нужен доступ к аудио + Разрешите доступ к аудио, чтобы просматривать музыку на устройстве. + Разрешить доступ + Локальные папки не найдены + В этой папке нет треков + Подходящих папок или треков нет + Не удалось обновить локальные файлы + + + Активных: %1$d • Завершённых: %2$d + Очистить завершённые + Загрузок пока нет + Загрузка %1$d% + В очереди + Завершено + Отменено + Ошибка + Ошибка: %1$s + Отменить загрузку + Повторить загрузку + Удалить загрузку + + + Исполнитель + В чёрном списке + Любимые треки + Ваши сохранённые треки + Неизвестный исполнитель + Название трека + Имя исполнителя + + Выбрано + + Настройки + + Плагины + Язык и регион + Оформление + Воспроизведение + Загрузки + Кеширование + Компьютер + Обновления + Поддержка + О приложении + + Закрыть + Сохранить + Отмена + + Язык + Язык интерфейса приложения. Сейчас: %1$s + Как в системе + Страна + Страна для контента и доступности. Сейчас: %1$s + %1$s (%2$s) + + Тема + Как Spotube подстраивается под тему системы. Сейчас: %1$s + Светлая + Тёмная + Системная + + Поверхность + Фоновый материал приложения. Сейчас: %1$s + Вода + Лёд + Хром + Акрил + Стекло + + Акцентный цвет + Текущая палитра: %1$s + Акцентный цвет + Выберите акцентную палитру и посмотрите, как она выглядит в светлой и тёмной темах. + Светлая + Тёмная + Предпросмотр + Кнопка + Зелёный гоблин + Электрик-фиолетовый + Океанский циан + Оранжевый закат + Розовый сад + Полночный синий + Металлик-графит + + Внешний вид + Материал поверхности и акцент. Сейчас: %1$s · %2$s + Внешний вид + Выберите материал поверхности, а затем подходящий к нему акцент. Все варианты — продуманные сочетания. + Поверхность + Акцент + Вторичная + + Палитра + Акцентная палитра Millennium. Сейчас: %1$s + Палитра + Выберите акцентную палитру Millennium и посмотрите её в светлой и тёмной темах. + Палитра + Палитра Millennium и акцент. Сейчас: %1$s + Синяя + Оранжевая + Лаймовая + + Своя папка для загрузок + Текущая папка: %1$s + Текущая папка: стандартная папка «Музыка» + Оставьте пустым, чтобы использовать стандартную папку «Музыка». + C:\Music\Spotube + Папки с локальной музыкой + Дополнительные папки для сканирования на компьютере. Добавлено: %1$d + Дополнительные папки ещё не добавлены + Дополнительные папки для вашей локальной медиатеки. + Управлять + Добавить папку + Удалить папку + Дополнительные папки не добавлены. + Убрать %1$s из списка сканирования + Папки загрузок и музыкального кеша добавляются автоматически. + Папка загрузок добавляется автоматически. + Папка музыкального кеша добавляется автоматически. + Сохранение… + Не удалось сохранить папки. Изменения не потеряны — попробуйте ещё раз. + + %1$d папка + %1$d папки + %1$d папок + %1$d папки + + Формат загрузки музыки + Предпочтительный кодек и контейнер для загрузок. Сейчас: %1$s + Качество загрузки музыки + + Формат стриминга музыки + Предпочтительный кодек и контейнер для стриминга. Сейчас: %1$s + Качество стриминга музыки + Сейчас: %1$s + Кешировать музыку + Сохранять прослушанное аудио на устройстве для быстрого повторного воспроизведения. + Папка кеша + Сейчас: %1$s + Сейчас: стандартный кеш приложения + Лимит размера кеша + Без ограничений + Текущий лимит: %1$d MB + Укажите максимальный размер кеша в MB. Введите 0 или оставьте пустым, чтобы снять ограничение. + 500 + Размер должен быть 0 или положительным числом. + Бесконечное воспроизведение + Продолжать воспроизведение рекомендованными треками, когда очередь закончится. + Включить Connect + Предоставлять удалённое управление воспроизведением через Spotube Connect. + Порт прокси-сервера воспроизведения + Разрешить удалённое управление + Разрешить другим устройствам в той же сети управлять воспроизведением после подключения. + Имя устройства для удалённого управления + Имя, которое видят другие устройства при поиске. Сейчас: %1$s + Задайте понятное имя, которое будут видеть другие устройства в вашей сети при обнаружении этого Spotube. + например, Spotube в гостиной + имя хоста устройства + Порт прокси-сервера воспроизведения. Сейчас: %1$d + Укажите порт от 1 до 65535. + 14769 + Введите целое число. + Порт должен быть от 1 до 65535. + + Чёрный список + Управление заблокированными треками и исполнителями + + Сворачивать в трей + Оставлять Spotube в системном трее при сворачивании окна. + Статус в Discord + Показывать в Discord, что сейчас играет (на компьютере). + + Автоматически проверять обновления + Искать новые версии Spotube в фоновом режиме. + + Управление плагинами + Управляйте своими плагинами + Плагин по умолчанию: %1$s + Метаданные + Аудио + Тексты песен + Скробблинг + Плагин не выбран + Изменить + Выбрать + Выбран + Сбросить выбор + Нет доступных плагинов + Плагин %1$s + Сбросить + + Настроить + Менеджер плагинов + Введите URL + URL должен начинаться с http:// или https:// + Не удалось скачать плагин + Плагины ещё не установлены + Вставьте ссылку для загрузки или выберите файл выше. + + %1$d плагин установлен + %1$d плагина установлено + %1$d плагинов установлено + %1$d плагина установлено + + Установка плагина + https://example.com/plugin.smplug + Скачать + Установить из файла (.smplug) + Активен + v%1$s + Встроенный + Удалить плагин + Войти + Выйти + Скачать по URL + Установить из файла + Установить + + • %1$s + Запрашиваемые разрешения + Этому плагину не нужны особые разрешения. + Установленный и новый + Установлен + Новый + API %1$s → %2$s + Постоянное хранилище + Чтение и запись данных на диск + Сетевые запросы + Отправка и получение данных через интернет + WebView + Показ веб-контента внутри приложения + Групповой джем + MQTT-брокер + Хост брокера + %1$s:%2$d + Порт брокера + Использовать TLS + Имя пользователя (необязательно) + Пароль (необязательно) + Префикс ID клиента + Используется для идентификации этого экземпляра Spotube + Шифровать подключение к брокеру + Проверьте текущие настройки брокера перед созданием комнаты. + Проверить подключение + Проверка… + Подключено за %1$d мс + Ошибка: %1$s + Временный брокер — настройте свой, чтобы размещать джемы самостоятельно + Подключение + Учётные данные + Идентификация + Диагностика + + + Групповой джем + Создать + Присоединиться + Создайте комнату и поделитесь её кодом с друзьями. + Создать комнату + Войти в комнату + Подключение… + Код комнаты + ABC123 + Брокер: %1$s + Настройте брокер в настройках, чтобы создать комнату или присоединиться к ней. + Настройки брокера + Подключено + Переподключение… + Копировать код + Скопировано! + Поделиться + Участники (%1$d) + Организатор + Слушатель + Ждём слушателей… + Удалить + Заблокировать + Управлять + Покинуть комнату + Не удалось подключиться + Закрыть + Не удалось создать джем + Не удалось присоединиться к джему + Введите код комнаты из шести букв и цифр. + Присоединяйся к моему джему в Spotube по коду: %1$s + + Перейти к альбому + Таймер сна + Сведения о треке + Воспроизвести + Пауза + Сейчас ничего не играет + Добавить в очередь + + Пропустить + Обновить + Доступен Spotube %1$s + Узнайте, что нового в этой версии, на странице загрузок Spotube. + Доступен %1$s %2$s + Установлена версия: %1$s + Узнайте, что нового в этом обновлении, на странице плагина. + + Пропустить + Далее + Назад + Начать + Spotube + Музыка без лишних условий. + Плеер с открытым исходным кодом для целого мира музыки.\nИсследуйте его. Сделайте его своим. + Помогите проекту жить. + Spotube создают люди, а не бизнес на подписках. Если он стал частью вашей жизни, даже небольшая поддержка поможет нам уделять ему то внимание, которого он заслуживает.\n\nСпасибо, что находите место для независимого софта. + Начните с плагина каталога, чтобы находить треки и плейлисты. Выберите аудиоплагин для их воспроизведения. Тексты песен и история прослушиваний — по желанию. Найдите плагин ниже, проверьте его разрешения, установите и выберите здесь. + Обзор плагинов + Ваши источники + Повторить + Другие плагины + Новых плагинов не найдено. Плагин также можно установить по ссылке или из файла .smplug выше. + Каталог плагинов сейчас недоступен. Повторите попытку или установите плагин по ссылке или из файла. + Добро пожаловать + Поддержка + Плагины + Руководство + + Как пользоваться Spotube + Четыре простых шага — и музыка уже играет. + Вход + Подключите аккаунт Spotify или выберите свой плагин каталога. + Ищите и слушайте + Найдите любой трек, альбом или исполнителя и нажмите «Воспроизвести». + Слушайте офлайн + Скачивайте треки, чтобы слушать их без интернета. + Следите за текстом + Открывайте синхронизированный текст в плеере во время воспроизведения. + + + Ваша музыка. Ваши источники. Ваши правила. + Бесплатный музыкальный плеер с открытым исходным кодом от сообщества, которое просто любит музыку. + Открытый код + Без рекламы + Без телеметрии + На плагинах + Поехали + + + Нас поддерживают слушатели + Создан с любовью. Живёт благодаря вам. + Spotube на 100% бесплатный, свободный и открытый — без рекламы, подписок и слежки. Каждая функция существует благодаря таким людям, как вы, которые помогают проекту развиваться. + Patreon + Ежемесячная поддержка + Поддержите постоянную разработку и инфраструктуру + Buy Me a Coffee + Разовый кофе + Взбодрите ночную сессию программирования небольшим донатом + Open Collective + Фонд сообщества + Прозрачное финансирование под управлением сообщества + От всего сердца спасибо, что верите в открытую музыку. + Поддержка полностью добровольная — Spotube в любом случае останется бесплатным. + Поддержать + Открыть %1$s + + + Поддержать Spotube + Поддержите Spotube + Spotube бесплатный, свободный и открытый — без рекламы, подписок и слежки. Если он приносит вам радость, поддержите проект. + Эти напоминания можно отключить в разделе «Настройки → Поддержка». + Может, позже + Напоминания о поддержке + Раз в 7 дней ненавязчиво напоминать о поддержке проекта. + + + Модульная архитектура + Выберите, откуда брать музыку. + В Spotube каталог, стриминг аудио и синхронизированные тексты работают отдельно. Настройте движки ниже или меняйте их в любой момент. + Движок метаданных + Отвечает за поиск музыки, списки треков и плейлисты. + Аудиодвижок + Получает и декодирует аудиопотоки для воспроизведения. + Движок текстов + Показывает синхронизированные караоке-тексты в реальном времени. + Движок скробблинга + Записывает историю прослушиваний в Last.fm или ListenBrainz. + Активен + Встроенный + Готов + Необязательно + Выберите движок + Установленные движки + Сделать основным + NewPipe YouTube и LRCLib Lyrics встроены и работают сразу после установки. + Управлять плагинами сообщества и добавлять новые можно в любой момент в разделе «Настройки → Плагины». + + + Ретромодерн-архитектура + Чистый Hi-Fi без посредников + Вот как Spotube организует децентрализованное воспроизведение прямо на вашем устройстве. + НАПРЯМУЮ, P2P • 256 KBPS OPUS + Синий час + Spotube · демонстрация плеера + Немного музыки для мгновений между делами. + 1. Децентрализованный поиск + Запросы идут напрямую в выбранный плагин каталога. Spotube никогда не анализирует ваши вкусы. + 2. Прямое получение потока + Аудио поступает напрямую с серверов контента на ваше устройство. Никаких прокси и удалённого сбора данных. + 3. Локальная обработка и воспроизведение + Декодирование и кеширование на устройстве, эквалайзер и загрузка для офлайн-прослушивания по желанию. + 4. Синхронные тексты и джем + Караоке-тексты следуют за воспроизведением, а ещё есть удалённое управление по LAN и групповой джем. + Никаких серверов-посредников. Воспроизведение происходит прямо на вашем устройстве — приватность по умолчанию. + + + Выберите устройство + %1$s: «%2$s» + Воспроизвести + Добавить в очередь + Воспроизвести следующим + Это устройство + На этом устройстве + Удалённое устройство + Удалённое устройство (%1$s) + На подключённом устройстве + + + Не удалось подключиться + Соединение потеряно + Не удалось отправить + Ошибка поиска: %1$s + Не удалось подключиться: %1$s + + + Устройства + Обновить + Подключено к %1$s:%2$d + Отключиться + Подключение... + Поиск устройств в сети... + Устройства не найдены + Убедитесь, что на другом устройстве в настройках включено «Разрешить удалённое управление». + Неизвестное устройство + Определение адреса... + ID: %1$s... + + + Удалённое управление + Отключено + Ошибка подключения: %1$s + Обложка альбома + Неизвестный трек + Неизвестный исполнитель + Перемотка + Перемешать + Предыдущий + Следующий + Режим повтора + Громкость + Удалить из очереди + Другие действия + + + Где воспроизвести? + У вас активный джем. Выберите, где воспроизвести эту подборку. + Воспроизвести здесь + Предложить в джем + + + Брокер для джема не настроен + Не указан хост брокера + Не удалось подключиться + Соединение потеряно + + + Брокер + anonymous + + + Добавлено в очередь джема + + %1$d трек добавлен в очередь джема + %1$d трека добавлено в очередь джема + %1$d треков добавлено в очередь джема + %1$d трека добавлено в очередь джема + + + + Запрос на удалённое управление + «%1$s» хочет управлять воспроизведением на этом устройстве. + Отклонить + Разрешить + Разрешать всегда + + + Результаты поиска + Все + Треки + Пользователи + Нет доступных типов поиска + Начните вводить запрос + Начните вводить, чтобы найти треки + Ничего не найдено + Показать все + Поиск + Загрузка треков... + Загрузка альбомов... + Загрузка исполнителей... + Загрузка плейлистов... + Загрузка пользователей... + Альбомы не найдены + Исполнители не найдены + Плейлисты не найдены + Пользователи не найдены + Роемся в ящиках с пластинками... + Ищем по всей вселенной... + Прогреваем колонки... + Сдуваем пыль с винила... + Советуемся с богами музыки... + Настраиваем антенны... + Перебираем пластинки... + Не удалось загрузить возможности поиска + Не удалось выполнить поиск + Не удалось найти треки + Не удалось найти альбомы + Не удалось найти исполнителей + Не удалось найти плейлисты + Не удалось найти пользователей + + + Обзор + + + Синхронный + Простой + Синхронизированный текст недоступен + Текст недоступен + + + %1$s в %2$s + %1$d kbps + %1$s kHz • %2$d кан. + Плагин метаданных не выбран + + + Назад + Повторить + Закрыть окно + Загрузка... + Неизвестно + Неизвестная ошибка + + + Пользователь + Плейлист + + + Нам очень жаль + Что-то пошло не так. Нам важно ваше впечатление, и мы делаем всё, чтобы это исправить. Попробуйте ещё раз или посмотрите подробности ошибки ниже. + Подробности ошибки + Копировать в буфер обмена + Показать полный журнал + Полный журнал ошибки + + + Перемешать + Перемешать и слушать + Подписаться + Отписаться + Изменить плейлист + Автор: %1$s + + + Действия с треком + Добавить в джем + Запустить радио + Воспроизвести следующим + Сделать следующим + Удалить из очереди + Удалить из избранного + Добавить в избранное + Добавить в плейлист + Скачать + Поделиться + + + Исходный порядок + Название + Исполнитель + Альбом + Длительность + Сортировка + Треки не найдены + Фильтр + Действия с выбранными + Скачать все + Скачать (%1$d) + Добавить все в очередь + Добавить в очередь (%1$d) + Воспроизвести все следующими + Воспроизвести следующими (%1$d) + Добавить все в плейлист + Добавить в плейлист (%1$d) + Добавить все в джем + Добавить в джем (%1$d) + и ещё %1$d + + + Нет URL для загрузки + Назад по истории браузера + Вперёд по истории браузера + + + Фильтр чёрного списка... + Чёрный список пуст + В чёрном списке нет совпадений с «%1$s» + Треки + Трек + Добавить в чёрный список + Удалить из чёрного списка + + + Нет метаданных трека + Не удалось получить URL потока + Не удалось получить URL потока. Сначала попробуйте воспроизвести трек. + + + Сохранённые треки + Вы + + + Вы подписаны + Популярные треки + Слушать популярные треки + Добавить популярные треки в очередь + Похожие исполнители + Рекомендуемые плейлисты + Не удалось загрузить страницу исполнителя + Число подписчиков недоступно + Подписчиков: %1$s тыс. + Подписчиков: %1$s млн + Подписчиков: %1$s млрд + + %1$d подписчик + %1$d подписчика + %1$d подписчиков + %1$d подписчика + + + + Нравится + Больше не нравится + Название трека-заглушки + Исполнители-заглушки + Неизвестный альбом + Перемотка + Громкость + Включить перемешивание + Выключить перемешивание + Предыдущий + Следующий + Воспроизвести или приостановить + Повтор выключен + Повтор трека + Повтор всех + Выключить звук + Включить звук + Другие действия + Параметры плеера + Свернуть плеер + Развернуть текст + Сейчас играет + Текст + Альтернативный источник + Альтернативный источник + Скачать + Скачано + Загрузка... + Уже скачано + Повторная загрузка... + Загрузка: %1$s + Воспроизведение остановится через %1$s. + Поставить воспроизведение на паузу через выбранное число минут. + Минуты + От 1 до 600 минут + Отменить таймер + Запустить + URL воспроизведения был получен с другими настройками стриминга. Выделенный поток соответствует текущим предпочтениям; активный URL обновится при следующем запросе потока. + Подобранный источник аудио + Загрузка подобранного источника… + Удалённый источник не нужен, пока воспроизводится локальный кешированный файл. + Подходящий источник недоступен. + Для этого трека нет кешированного подобранного источника аудио. + Сведения о потоках ещё не закешированы. Сначала воспроизведите трек, чтобы загрузить их. + Не удалось загрузить сведения о треке. + Доступные потоки + Этот трек воспроизводится из локального кеша. Варианты потоков сейчас не используются. + Загрузка потоков… + Для этого источника нет доступных потоков. + Выбранный поток + Соответствует вашим настройкам формата и качества стриминга + Активный URL воспроизведения + Из локального кеша + Трек из метаданных + Название + ID + Альбом + Точность совпадения + Внешний URI + ID альбома + Тип альбома + Дата выхода альбома + Треков в альбоме + Жанры альбома + Длительность + Номер трека + Номер диска + Ненормативный контент + Популярность + ISRC + URL миниатюр + + + Показать или скрыть боковую панель + + + Альтернативные источники + Сейчас ничего не играет + Поиск альтернативных источников... + Альтернативные источники не найдены + Совпадение: %1$d% + Текущий источник + + + Исключить + Удалить участника из сессии + Исключить и запретить возвращаться + Удалить предложения + Удалить все треки, которые участник добавил в очередь + Изменить порядок + Удалить из очереди + %1$s • Добавил(а) %2$s + + + Неизвестно + Загрузка плейлиста... + Добавить треки + Добавить треки в плейлист + Поиск треков... + Поиск + Треки не найдены + Найдите треки, чтобы добавить их + Готово + Добавить + Добавлено + + + Загрузка альбома... + + + Добавить в ваш плейлист + Поиск + Поиск по вашим плейлистам... + Загрузка ваших плейлистов... + У вас пока нет своих плейлистов.\nСоздайте плейлист, чтобы добавлять в него треки. + Новый плейлист + Изменить плейлист + Создать + Название + Название плейлиста + Описание + Описание (необязательно) + Открытый + Этот плейлист виден всем + Совместный + Другие могут добавлять треки в этот плейлист + Обложка + Обложка плейлиста + Нет изображения + Сменить изображение + Выбрать изображение + + + Установить + Заменить + В zip-архиве не найден plugin.json + В zip-архиве не найден manifest.zipline.json + Неверный формат plugin.json: %1$s + Не удалось прочитать плагин: %1$s + Не удалось установить плагин: %1$s + Доступна новая версия %1$s. Подтвердите обновление перед установкой. + %1$s уже установлен. Подтвердите замену перед установкой. + Встроенные плагины уже включены, их нельзя добавить повторно. + Встроенные плагины нельзя удалить. + %1$s указывает недопустимую apiVersion (%2$s). Ожидается семантическая версия, совместимая с %3$s. + Недопустимая версия API плагинов Spotube: %1$s. + %1$s рассчитан на API плагинов %2$s, а Spotube поддерживает несовместимую версию API %3$s. Установите плагин, собранный для той же основной версии API. + + + Несовместимый API плагина + Установить плагин? + %1$s будет добавлен к установленным плагинам. + Обновить плагин? + Установлена более старая версия (%1$s). Обновить до %2$s? + Заменить установленный плагин? + %1$s использует недопустимую семантическую версию (%2$s), поэтому её нельзя сравнить как обновление. Всё равно заменить установленный плагин? + Установленную версию (%1$s) нельзя сравнить по правилам семантического версионирования. Заменить её на %2$s? + Версия %1$s уже установлена. Заменить существующий плагин новой копией? + Установленная версия %1$s новее предложенной версии %2$s. Всё равно заменить? + Заменить установленный плагин новой копией? + + + %1$s: + Версия + Версия API + Автор + Лицензия + Разрешения + Функции + Репозиторий + Контакты + Сообщить об ошибке + Поддержать %1$s + + + Официальный + Проверенный + Звёзды на GitHub + URL репозитория на GitHub + + + Релизы не найдены + Выберите релиз + Выберите релиз + Предварительный релиз + Примечания к выпуску + + + Главная + Поиск + На устройстве + Подключения + Устройства + + + Скрыть окно + Показать окно + Следующий трек + Предыдущий трек + Перемешать + Повтор + Повтор: ВЫКЛ + Повтор: ТРЕК + Повтор: ВСЕ + Громкость: %1$d% + Громкость + + Громкость - + Выключить звук + Включить звук + Нравится + Больше не нравится + Выход + + + Свернуть + Развернуть + Восстановить + Закрыть + + + Служба воспроизведения + Воспроизведение + Управление воспроизведением + + + Обзор + Сейчас играет + Рекомендуемое + Сохранённые треки + Популярные треки + diff --git a/composeApp/src/commonMain/composeResources/values-th/strings.xml b/composeApp/src/commonMain/composeResources/values-th/strings.xml new file mode 100644 index 00000000..dffc10f4 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-th/strings.xml @@ -0,0 +1,932 @@ + + + + คิว + ล้างคิว + กรองคิว... + ไม่มีรายการในคิว + + ค้นหาเพลง ศิลปิน อัลบั้ม... + ล้างประวัติทั้งหมด + ค้นหา... + + + คลังเพลงของคุณ + คลังเพลง + เพลย์ลิสต์ + อัลบั้ม + ศิลปิน + เพลงในเครื่อง + ดาวน์โหลด + ค้นหาเพลย์ลิสต์ที่บันทึกไว้… + ค้นหาอัลบั้มที่บันทึกไว้… + ค้นหาศิลปินที่บันทึกไว้… + ค้นหาโฟลเดอร์หรือเพลง… + กรองคลังเพลง + ปิดตัวกรอง + สร้างเพลย์ลิสต์ + รีเฟรชเพลงในเครื่อง + เปลี่ยนเป็นมุมมองตาราง + เปลี่ยนเป็นมุมมองรายการ + ไม่มีเพลย์ลิสต์ที่บันทึกไว้ + ไม่มีเพลย์ลิสต์ที่ตรงกับ “%1$s” + ไม่มีอัลบั้มที่บันทึกไว้ + ไม่มีอัลบั้มที่ตรงกับ “%1$s” + ไม่มีศิลปินที่บันทึกไว้ + ไม่มีศิลปินที่ตรงกับ “%1$s” + + + โฟลเดอร์ + โฟลเดอร์ + %1$s • %2$s + + %1$d เพลง + + + %1$d โฟลเดอร์ + + กลับไปที่โฟลเดอร์ + เล่นโฟลเดอร์ + ต้องได้รับสิทธิ์เข้าถึงไฟล์เสียง + อนุญาตการเข้าถึงไฟล์เสียงเพื่อเรียกดูเพลงในอุปกรณ์ของคุณ + อนุญาตการเข้าถึง + ไม่พบโฟลเดอร์ในเครื่อง + ไม่มีเพลงในโฟลเดอร์นี้ + ไม่มีโฟลเดอร์หรือเพลงที่ตรงกัน + รีเฟรชสื่อในเครื่องไม่สำเร็จ + + + กำลังดำเนินการ %1$d • เสร็จสิ้น %2$d + ล้างรายการที่เสร็จแล้ว + ยังไม่มีการดาวน์โหลด + กำลังดาวน์โหลด %1$d% + อยู่ในคิว + เสร็จสิ้น + ยกเลิกแล้ว + ล้มเหลว + ล้มเหลว: %1$s + ยกเลิกการดาวน์โหลด + ลองดาวน์โหลดอีกครั้ง + ลบการดาวน์โหลด + + + ศิลปิน + อยู่ในบัญชีดำ + เพลงที่ถูกใจ + เพลงที่คุณบันทึกไว้ + ไม่ทราบศิลปิน + ชื่อเพลง + ชื่อศิลปิน + + เลือกแล้ว + + การตั้งค่า + + ปลั๊กอิน + ภาษาและภูมิภาค + รูปลักษณ์ + การเล่น + การดาวน์โหลด + แคช + เดสก์ท็อป + อัปเดต + สนับสนุน + เกี่ยวกับ + + ปิด + บันทึก + ยกเลิก + + ภาษา + ภาษาของอินเทอร์เฟซแอป ปัจจุบัน: %1$s + ค่าเริ่มต้นของระบบ + ประเทศ + ประเทศสำหรับเนื้อหาและความพร้อมใช้งาน ปัจจุบัน: %1$s + %1$s (%2$s) + + ธีม + เลือกว่าจะให้ Spotube แสดงผลตามระบบอย่างไร ปัจจุบัน: %1$s + สว่าง + มืด + ตามระบบ + + พื้นผิว + วัสดุพื้นหลังของแอป ปัจจุบัน: %1$s + สายน้ำ + น้ำแข็ง + โครม + อะคริลิก + กระจก + + สีเน้น + ชุดสีปัจจุบัน: %1$s + สีเน้น + เลือกชุดสีเน้นและดูตัวอย่างทั้งในธีมสว่างและธีมมืด + สว่าง + มืด + ตัวอย่าง + ปุ่ม + กรีนก็อบลิน + ม่วงไฟฟ้า + ฟ้าครามมหาสมุทร + ส้มอัสดง + สวนกุหลาบ + น้ำเงินเที่ยงคืน + เทาเมทัลลิก + + รูปลักษณ์และความรู้สึก + วัสดุพื้นผิวและสีเน้น ปัจจุบัน: %1$s · %2$s + รูปลักษณ์และความรู้สึก + เลือกวัสดุพื้นผิว แล้วเลือกสีเน้นที่เข้ากัน ทุกตัวเลือกเป็นชุดที่คัดสรรมาแล้ว + พื้นผิว + สีเน้น + รอง + + ชุดสี + ชุดสีเน้น Millennium ปัจจุบัน: %1$s + ชุดสี + เลือกชุดสีเน้น Millennium และดูตัวอย่างทั้งในโหมดสว่างและมืด + ชุดสี + ชุดสีและสีเน้น Millennium ปัจจุบัน: %1$s + น้ำเงิน + ส้ม + เขียวมะนาว + + กำหนดโฟลเดอร์ดาวน์โหลดเอง + โฟลเดอร์ปัจจุบัน: %1$s + โฟลเดอร์ปัจจุบัน: โฟลเดอร์เพลงเริ่มต้น + เว้นว่างไว้เพื่อใช้โฟลเดอร์เพลงเริ่มต้น + C:\Music\Spotube + โฟลเดอร์สื่อในเครื่อง + โฟลเดอร์ที่กำหนดเองเพื่อสแกนบนเดสก์ท็อป เพิ่มแล้ว: %1$d + ยังไม่ได้เพิ่มโฟลเดอร์ที่กำหนดเอง + โฟลเดอร์ที่กำหนดเองสำหรับคลังเพลงในเครื่องของคุณ + จัดการ + เพิ่มโฟลเดอร์ + ลบโฟลเดอร์ + ยังไม่ได้เพิ่มโฟลเดอร์ที่กำหนดเอง + ลบ %1$s ออกจากรายการสแกน + โฟลเดอร์ดาวน์โหลดและโฟลเดอร์แคชเพลงจะถูกรวมโดยอัตโนมัติ + โฟลเดอร์ดาวน์โหลดของคุณจะถูกรวมโดยอัตโนมัติ + โฟลเดอร์แคชเพลงจะถูกรวมโดยอัตโนมัติ + กำลังบันทึก… + บันทึกโฟลเดอร์ไม่ได้ การเปลี่ยนแปลงของคุณยังอยู่ โปรดลองอีกครั้ง + + %1$d โฟลเดอร์ + + รูปแบบเพลงที่ดาวน์โหลด + โคเดกและคอนเทนเนอร์ที่ต้องการสำหรับการดาวน์โหลด ปัจจุบัน: %1$s + คุณภาพเพลงที่ดาวน์โหลด + + รูปแบบการสตรีมเพลง + โคเดกและคอนเทนเนอร์ที่ต้องการสำหรับการสตรีมเพลง ปัจจุบัน: %1$s + คุณภาพการสตรีมเพลง + ปัจจุบัน: %1$s + เปิดใช้แคชเพลง + จัดเก็บเสียงที่สตรีมไว้ในเครื่องเพื่อให้เล่นซ้ำได้เร็วขึ้น + โฟลเดอร์แคช + ปัจจุบัน: %1$s + ปัจจุบัน: แคชเริ่มต้นของแอป + ขีดจำกัดขนาดแคช + ไม่จำกัด + ขีดจำกัดปัจจุบัน: %1$d MB + กำหนดขนาดแคชสูงสุดเป็น MB ใส่ 0 หรือเว้นว่างไว้เพื่อไม่จำกัด + 500 + ขนาดต้องเป็น 0 หรือจำนวนบวก + เปิดใช้การเล่นต่อเนื่องไม่สิ้นสุด + เล่นต่อด้วยเพลงแนะนำเมื่อคิวของคุณหมด + เปิดใช้ Connect + เปิดให้ควบคุมการเล่นจากระยะไกลผ่านฟีเจอร์ Spotube Connect + พอร์ตเซิร์ฟเวอร์พร็อกซีการเล่น + อนุญาตการควบคุมระยะไกล + ให้อุปกรณ์อื่นในเครือข่ายเดียวกันควบคุมการเล่นได้เมื่อเชื่อมต่อ + ชื่ออุปกรณ์สำหรับควบคุมระยะไกล + ชื่อที่อุปกรณ์อื่นจะเห็นเมื่อค้นหาอุปกรณ์นี้ ปัจจุบัน: %1$s + เลือกชื่อที่จำง่ายเพื่อแสดงให้อุปกรณ์อื่นในเครือข่ายของคุณเห็นเมื่อค้นพบ Spotube เครื่องนี้ + เช่น Spotube ห้องนั่งเล่น + ชื่อโฮสต์ของอุปกรณ์ + พอร์ตที่เซิร์ฟเวอร์พร็อกซีการเล่นใช้ ปัจจุบัน: %1$d + เลือกพอร์ตระหว่าง 1 ถึง 65535 + 14769 + ใส่จำนวนเต็มที่ถูกต้อง + พอร์ตต้องอยู่ระหว่าง 1 ถึง 65535 + + บัญชีดำ + จัดการเพลงและศิลปินในบัญชีดำ + + ย่อไปที่ถาดระบบ + ให้ Spotube ทำงานต่อในถาดระบบเมื่อย่อหน้าต่าง + Discord Rich Presence + แสดงสถานะการเล่นปัจจุบันของคุณใน Discord บนเดสก์ท็อป + + ตรวจหาอัปเดตโดยอัตโนมัติ + ค้นหา Spotube เวอร์ชันใหม่ในเบื้องหลัง + + จัดการปลั๊กอิน + จัดการปลั๊กอินของคุณ + ปลั๊กอิน%1$sเริ่มต้น + เมทาดาทา + เสียง + เนื้อเพลง + Scrobble + ยังไม่ได้เลือกปลั๊กอิน + เปลี่ยน + เลือก + เลือกแล้ว + ล้างการเลือก + ไม่มีปลั๊กอินให้ใช้ + ปลั๊กอิน %1$s + ล้าง + + กำหนดค่า + ตัวจัดการปลั๊กอิน + โปรดใส่ URL + URL ต้องขึ้นต้นด้วย http:// หรือ https:// + ดาวน์โหลดปลั๊กอินไม่สำเร็จ + ยังไม่ได้ติดตั้งปลั๊กอิน + วาง URL สำหรับดาวน์โหลดหรือเลือกไฟล์ด้านบน + + ติดตั้งปลั๊กอินแล้ว %1$d รายการ + + ติดตั้งปลั๊กอิน + https://example.com/plugin.smplug + ดาวน์โหลด + ติดตั้งจากไฟล์ (.smplug) + ใช้งานอยู่ + v%1$s + ในตัว + ลบปลั๊กอิน + เข้าสู่ระบบ + ออกจากระบบ + ดาวน์โหลดจาก URL + ติดตั้งจากไฟล์ + ติดตั้ง + + • %1$s + สิทธิ์ที่ร้องขอ + ปลั๊กอินนี้ไม่ต้องใช้สิทธิ์พิเศษ + ที่ติดตั้งอยู่เทียบกับที่ให้มา + ติดตั้งอยู่ + ที่ให้มา + API %1$s → %2$s + พื้นที่จัดเก็บถาวร + อ่านและเขียนข้อมูลลงดิสก์ + คำขอเครือข่าย + ส่งและรับข้อมูลผ่านอินเทอร์เน็ต + WebView + แสดงเนื้อหาเว็บภายในแอป + แจมกลุ่ม + MQTT Broker + โฮสต์ของ Broker + %1$s:%2$d + พอร์ตของ Broker + ใช้ TLS + ชื่อผู้ใช้ (ไม่บังคับ) + รหัสผ่าน (ไม่บังคับ) + คำนำหน้า Client ID + ใช้เพื่อระบุ Spotube เครื่องนี้ + เข้ารหัสการเชื่อมต่อกับ Broker + ตรวจสอบการตั้งค่า Broker ปัจจุบันก่อนเริ่มห้อง + ทดสอบการเชื่อมต่อ + กำลังทดสอบ… + เชื่อมต่อแล้วใน %1$d ms + ล้มเหลว: %1$s + Broker ตัวอย่าง — กำหนดค่าของคุณเองเพื่อโฮสต์เอง + การเชื่อมต่อ + ข้อมูลเข้าสู่ระบบ + ตัวตน + การวินิจฉัย + + + แจมกลุ่ม + สร้าง + เข้าร่วม + สร้างห้อง แล้วแชร์รหัสห้องให้เพื่อนของคุณ + สร้างห้อง + เข้าร่วมห้อง + กำลังเชื่อมต่อ… + รหัสห้อง + ABC123 + Broker: %1$s + ตั้งค่า Broker ในการตั้งค่าเพื่อสร้างหรือเข้าร่วมห้อง + การตั้งค่า Broker + เชื่อมต่อแล้ว + กำลังเชื่อมต่อใหม่… + คัดลอกรหัส + คัดลอกแล้ว! + แชร์ + ผู้เข้าร่วม (%1$d) + โฮสต์ + ผู้ฟัง + กำลังรอผู้ฟัง… + นำออก + แบน + จัดการ + ออกจากห้อง + เชื่อมต่อไม่ได้ + ปิด + สร้างเซสชันแจมไม่สำเร็จ + เข้าร่วมเซสชันแจมไม่สำเร็จ + ใส่รหัสห้อง 6 ตัวอักษร ที่ประกอบด้วยตัวอักษรและตัวเลข + มาร่วมแจมกับฉันบน Spotube ด้วยรหัส: %1$s + + ไปที่อัลบั้ม + ตั้งเวลาปิด + รายละเอียดเพลง + เล่น + หยุดชั่วคราว + ไม่มีเพลงที่กำลังเล่น + เพิ่มลงในคิว + + ละเว้น + อัปเดต + Spotube %1$s พร้อมให้ใช้งานแล้ว + ดูว่ามีอะไรใหม่ในเวอร์ชันนี้ได้ที่หน้าดาวน์โหลดของ Spotube + %1$s %2$s พร้อมให้ใช้งานแล้ว + ติดตั้งอยู่ในปัจจุบัน: %1$s + ดูว่ามีอะไรใหม่ในอัปเดตนี้ได้จากหน้าของปลั๊กอิน + + ข้าม + ถัดไป + ย้อนกลับ + เริ่มต้นใช้งาน + Spotube + ดนตรีที่ไร้พันธนาการ + เครื่องเล่นโอเพนซอร์สสำหรับโลกแห่งดนตรี\nให้คุณสำรวจ ให้คุณสร้างในแบบของคุณเอง + ช่วยให้โปรเจกต์นี้อยู่ต่อไป + Spotube สร้างขึ้นโดยผู้คน ไม่ใช่ธุรกิจแบบสมัครสมาชิก หากแอปนี้กลายเป็นส่วนหนึ่งในชีวิตประจำวันของคุณ การสนับสนุนเพียงเล็กน้อยจะช่วยให้เราดูแลแอปได้อย่างที่ควรเป็น\n\nขอบคุณที่เปิดพื้นที่ให้ซอฟต์แวร์อิสระ + เริ่มต้นด้วยปลั๊กอินแคตตาล็อกเพื่อค้นหาเพลงและเพลย์ลิสต์ เลือกปลั๊กอินเสียงเพื่อเล่นเพลง ส่วนเนื้อเพลงและประวัติการฟังเป็นตัวเลือกเสริม ค้นพบปลั๊กอินด้านล่าง ตรวจสอบสิทธิ์ ติดตั้ง แล้วเลือกใช้ที่นี่ + ค้นพบปลั๊กอิน + แหล่งที่มาของคุณ + ลองอีกครั้ง + ปลั๊กอินเพิ่มเติม + ไม่พบปลั๊กอินใหม่ คุณยังติดตั้งจากลิงก์หรือไฟล์ .smplug ด้านบนได้ + ไม่สามารถค้นหาปลั๊กอินได้ในขณะนี้ ลองอีกครั้ง หรือติดตั้งจากลิงก์หรือไฟล์ + ยินดีต้อนรับ + สนับสนุน + ปลั๊กอิน + คู่มือ + + วิธีใช้ Spotube + แค่ 4 ขั้นตอนง่าย ๆ ก็เริ่มฟังได้เลย + เข้าสู่ระบบ + เชื่อมต่อบัญชี Spotify ของคุณ หรือเลือกปลั๊กอินแคตตาล็อกของคุณเอง + ค้นหาและเล่น + ค้นหาเพลง อัลบั้ม หรือศิลปิน แล้วกดเล่น + ฟังแบบออฟไลน์ + ดาวน์โหลดเพลงเพื่อฟังต่อได้แม้ไม่มีการเชื่อมต่อ + ร้องตามเนื้อเพลง + เปิดเนื้อเพลงแบบซิงค์จากเครื่องเล่นขณะที่เพลงกำลังเล่น + + + เพลงของคุณ แหล่งที่มาของคุณ กฎของคุณ + เครื่องเล่นเพลงฟรีและโอเพนซอร์ส สร้างโดยชุมชนที่รักในเสียงเพลง + โอเพนซอร์ส + ไม่มีโฆษณา + ไม่มีการเก็บข้อมูล + ขับเคลื่อนด้วยปลั๊กอิน + ไปกันเลย + + + ผู้สนับสนุนคือแรงผลักดันของเรา + สร้างด้วยใจ อยู่ได้ด้วยคุณ + Spotube ฟรี เสรี และโอเพนซอร์ส 100% — ไม่มีโฆษณา ไม่มีการสมัครสมาชิก ไม่มีการติดตาม ทุกฟีเจอร์มีอยู่ได้เพราะคนอย่างคุณเลือกที่จะช่วยให้การพัฒนาเดินหน้าต่อไป + Patreon + ผู้สนับสนุนรายเดือน + สนับสนุนการพัฒนาและโครงสร้างพื้นฐานอย่างต่อเนื่อง + Buy Me a Coffee + กาแฟครั้งเดียว + เติมพลังให้การเขียนโค้ดยามดึกด้วยทิปเล็ก ๆ + Open Collective + กองทุนชุมชน + การสนับสนุนทางการเงินที่โปร่งใสและบริหารโดยชุมชน + ขอบคุณจากใจที่เชื่อมั่นในดนตรีแบบเปิด + การสนับสนุนเป็นทางเลือกโดยสมบูรณ์ — Spotube ยังคงฟรีไม่ว่าอย่างไร + สนับสนุน + เปิด %1$s + + + สนับสนุน Spotube + สนับสนุน Spotube + Spotube ฟรี เสรี และโอเพนซอร์ส — ไม่มีโฆษณา ไม่มีการสมัครสมาชิก ไม่มีการติดตาม หากแอปนี้ทำให้คุณมีความสุข ลองพิจารณาสนับสนุนโปรเจกต์นี้ + คุณปิดการแจ้งเตือนเหล่านี้ได้ที่ การตั้งค่า → สนับสนุน + ไว้ทีหลัง + การแจ้งเตือนให้สนับสนุน + แสดงการแจ้งเตือนเบา ๆ ให้สนับสนุนโปรเจกต์ทุก 7 วัน + + + สถาปัตยกรรมแบบโมดูล + เลือกได้ว่าเพลงของคุณมาจากไหน + Spotube แยกการค้นหาแคตตาล็อก การสตรีมเสียง และเนื้อเพลงแบบซิงค์ออกจากกัน กำหนดค่าเอนจินของคุณด้านล่างหรือเปลี่ยนได้ทุกเมื่อ + เอนจินเมทาดาทา + ขับเคลื่อนการค้นพบเพลง การค้นหา รายชื่อเพลง และเพลย์ลิสต์ + เอนจินเสียง + ดึงและถอดรหัสสตรีมเสียงที่จะเล่น + เอนจินเนื้อเพลง + สตรีมเนื้อเพลงคาราโอเกะที่ซิงค์ตามเวลาแบบเรียลไทม์ + เอนจิน Scrobble + บันทึกประวัติการฟังไปยัง Last.fm หรือ ListenBrainz + ใช้งานอยู่ + ในตัว + พร้อม + ไม่บังคับ + เลือกเอนจิน + เอนจินที่ติดตั้งไว้ + ตั้งเป็นค่าเริ่มต้น + NewPipe YouTube และ LRCLib Lyrics มีมาในตัวและพร้อมสตรีมได้ทันที + จัดการหรือเพิ่มปลั๊กอินจากชุมชนได้ทุกเมื่อที่ การตั้งค่า → ปลั๊กอิน + + + สถาปัตยกรรมย้อนยุคผสมสมัยใหม่ + Hi-Fi บริสุทธิ์ ไม่มีคนกลาง + นี่คือวิธีที่ Spotube จัดการการเล่นแบบกระจายศูนย์บนเครื่องของคุณโดยตรง + เพียร์ทูเพียร์โดยตรง • 256 KBPS OPUS + Blue hour + Spotube · ตัวอย่างเครื่องเล่น + ดนตรีเล็ก ๆ สำหรับช่วงเวลาระหว่างทาง + 1. การค้นพบแบบกระจายศูนย์ + คำค้นหาส่งตรงไปยังปลั๊กอินแคตตาล็อกที่คุณเลือก Spotube ไม่เคยเก็บข้อมูลรสนิยมของคุณ + 2. ระบุสตรีมโดยตรง + เสียงสตรีมตรงจากเซิร์ฟเวอร์เนื้อหามายังเครื่องของคุณ ไม่ผ่านพร็อกซีหรือการเก็บข้อมูลจากระยะไกล + 3. DSP และการเล่นในเครื่อง + ถอดรหัสและแคชในเครื่อง พร้อมอีควอไลเซอร์และการดาวน์โหลดแบบออฟไลน์ (ไม่บังคับ) + 4. เนื้อเพลงแบบซิงค์และแจม + เนื้อเพลงคาราโอเกะเลื่อนตามเวลาที่เล่น พร้อมการควบคุมระยะไกลผ่าน LAN และแจมกลุ่ม + ไม่มีเซิร์ฟเวอร์คนกลาง การเล่นเกิดขึ้นบนอุปกรณ์ของคุณ — เป็นส่วนตัวตั้งแต่การออกแบบ + + + เลือกอุปกรณ์ + %1$s: “%2$s” + เล่น + เพิ่มลงในคิว + เล่นถัดไป + อุปกรณ์นี้ + บนอุปกรณ์นี้ + อุปกรณ์ระยะไกล + อุปกรณ์ระยะไกล (%1$s) + บนอุปกรณ์ที่เชื่อมต่ออยู่ + + + การเชื่อมต่อล้มเหลว + การเชื่อมต่อขาดหาย + ส่งไม่สำเร็จ + การค้นหาล้มเหลว: %1$s + เชื่อมต่อไม่สำเร็จ: %1$s + + + อุปกรณ์ + รีเฟรช + เชื่อมต่อกับ %1$s:%2$d แล้ว + ยกเลิกการเชื่อมต่อ + กำลังเชื่อมต่อ... + กำลังค้นหาอุปกรณ์ในเครือข่าย... + ไม่พบอุปกรณ์ + ตรวจสอบว่าอุปกรณ์อีกเครื่องเปิด “อนุญาตการควบคุมระยะไกล” ไว้ในการตั้งค่า + อุปกรณ์ที่ไม่รู้จัก + กำลังระบุ... + ID: %1$s... + + + รีโมตคอนโทรล + ยกเลิกการเชื่อมต่อแล้ว + ข้อผิดพลาดในการเชื่อมต่อ: %1$s + ปกอัลบั้ม + ไม่ทราบชื่อเพลง + ไม่ทราบศิลปิน + เลื่อนตำแหน่ง + สุ่มเพลง + ก่อนหน้า + ถัดไป + โหมดเล่นซ้ำ + ระดับเสียง + นำออกจากคิว + ตัวเลือกเพิ่มเติม + + + เล่นที่ไหน? + คุณมีเซสชันแจมที่ใช้งานอยู่ เลือกว่าจะเล่นคอลเลกชันนี้ที่ไหน + เล่นที่นี่ + เสนอให้แจม + + + ยังไม่ได้กำหนดค่า Broker สำหรับแจม + โฮสต์ของ Broker ว่างอยู่ + การเชื่อมต่อล้มเหลว + การเชื่อมต่อขาดหาย + + + Broker + anonymous + + + เพิ่มลงในคิวแจมแล้ว + + เพิ่ม %1$d เพลงลงในคิวแจมแล้ว + + + + คำขอควบคุมระยะไกล + “%1$s” ต้องการควบคุมการเล่นบนอุปกรณ์นี้ + ปฏิเสธ + อนุญาต + อนุญาตเสมอ + + + ผลการค้นหา + ทั้งหมด + เพลง + ผู้ใช้ + ไม่มีประเภทการค้นหาให้ใช้ + เริ่มพิมพ์เพื่อค้นหา + เริ่มพิมพ์เพื่อค้นหาเพลง + ไม่พบผลลัพธ์ + ดูทั้งหมด + กำลังค้นหา + กำลังโหลดเพลง... + กำลังโหลดอัลบั้ม... + กำลังโหลดศิลปิน... + กำลังโหลดเพลย์ลิสต์... + กำลังโหลดผู้ใช้... + ไม่พบอัลบั้ม + ไม่พบศิลปิน + ไม่พบเพลย์ลิสต์ + ไม่พบผู้ใช้ + กำลังคุ้ยกล่องแผ่นเสียง... + กำลังค้นหาทั่วจักรวาล... + กำลังวอร์มลำโพง... + กำลังปัดฝุ่นแผ่นไวนิล... + กำลังปรึกษาเทพแห่งดนตรี... + กำลังจูนเสาอากาศ... + กำลังพลิกหาแผ่นเสียง... + โหลดความสามารถในการค้นหาไม่สำเร็จ + ค้นหาไม่สำเร็จ + ค้นหาเพลงไม่สำเร็จ + ค้นหาอัลบั้มไม่สำเร็จ + ค้นหาศิลปินไม่สำเร็จ + ค้นหาเพลย์ลิสต์ไม่สำเร็จ + ค้นหาผู้ใช้ไม่สำเร็จ + + + เรียกดู + + + ซิงค์ + ธรรมดา + ไม่มีเนื้อเพลงแบบซิงค์ + ไม่มีเนื้อเพลงแบบธรรมดา + + + %1$s ใน %2$s + %1$d kbps + %1$s kHz • %2$d ช่อง + ยังไม่ได้เลือกปลั๊กอินเมทาดาทา + + + ย้อนกลับ + ลองอีกครั้ง + ปิดกล่องโต้ตอบ + กำลังโหลด... + ไม่ทราบ + ข้อผิดพลาดที่ไม่ทราบสาเหตุ + + + ผู้ใช้ + เพลย์ลิสต์ + + + ขออภัยเป็นอย่างยิ่ง + มีบางอย่างทำงานไม่ถูกต้องในขณะนี้ ประสบการณ์ของคุณสำคัญสำหรับเรา และเรากำลังพยายามแก้ไขอย่างเต็มที่ โปรดลองอีกครั้งหรือตรวจสอบรายละเอียดข้อผิดพลาดด้านล่าง + รายละเอียดข้อผิดพลาด + คัดลอกไปยังคลิปบอร์ด + ดูบันทึกทั้งหมด + บันทึกข้อผิดพลาดทั้งหมด + + + สุ่มเพลง + เล่นแบบสุ่ม + ติดตาม + เลิกติดตาม + แก้ไขเพลย์ลิสต์ + โดย %1$s + + + ตัวเลือกเพลง + เพิ่มลงในแจม + เริ่มวิทยุ + เล่นถัดไป + ย้ายไปเป็นเพลงถัดไป + นำออกจากคิว + นำออกจากรายการโปรด + บันทึกเป็นรายการโปรด + เพิ่มลงในเพลย์ลิสต์ + ดาวน์โหลด + แชร์ + + + ลำดับเดิม + ชื่อเพลง + ศิลปิน + อัลบั้ม + ความยาว + เรียงลำดับ + ไม่พบเพลง + กรอง + การดำเนินการแบบกลุ่ม + ดาวน์โหลดทั้งหมด + ดาวน์โหลด %1$d เพลง + เพิ่มทั้งหมดลงในคิว + เพิ่ม %1$d เพลงลงในคิว + เล่นทั้งหมดถัดไป + เล่น %1$d เพลงถัดไป + เพิ่มทั้งหมดลงในเพลย์ลิสต์ + เพิ่ม %1$d เพลงลงในเพลย์ลิสต์ + เพิ่มทั้งหมดลงในแจม + เพิ่ม %1$d เพลงลงในแจม + +อีก %1$d + + + ไม่มี URL ให้โหลด + ย้อนกลับในประวัติเบราว์เซอร์ + ไปข้างหน้าในประวัติเบราว์เซอร์ + + + กรองบัญชีดำ... + ไม่มีรายการในบัญชีดำ + ไม่มีรายการในบัญชีดำที่ตรงกับ “%1$s” + เพลง + เพลง + เพิ่มลงในบัญชีดำ + นำออกจากบัญชีดำ + + + ไม่มีเมทาดาทาของเพลง + ไม่สามารถระบุ URL ของสตรีมได้ + ไม่สามารถระบุ URL ของสตรีมได้ ลองเล่นเพลงนี้ก่อน + + + เพลงที่บันทึกไว้ + คุณ + + + กำลังติดตาม + เพลงยอดนิยม + เล่นเพลงยอดนิยม + เพิ่มเพลงยอดนิยมลงในคิว + ศิลปินที่เกี่ยวข้อง + เพลย์ลิสต์แนะนำ + โหลดภาพรวมศิลปินไม่สำเร็จ + ไม่มีข้อมูลผู้ติดตาม + ผู้ติดตาม %1$sK คน + ผู้ติดตาม %1$sM คน + ผู้ติดตาม %1$sB คน + + ผู้ติดตาม %1$d คน + + + + ถูกใจ + เลิกถูกใจ + ชื่อเพลงตัวอย่าง + ศิลปินตัวอย่าง + ไม่ทราบอัลบั้ม + เลื่อนตำแหน่ง + ระดับเสียง + เปิดการสุ่มเพลง + ปิดการสุ่มเพลง + ก่อนหน้า + ถัดไป + เล่นหรือหยุดชั่วคราว + ปิดการเล่นซ้ำ + เล่นซ้ำเพลงเดียว + เล่นซ้ำทั้งหมด + ปิดเสียง + เปิดเสียง + ตัวเลือกเพิ่มเติม + ตัวเลือกเครื่องเล่น + ยุบเครื่องเล่น + ขยายเนื้อเพลง + กำลังเล่น + เนื้อเพลง + แหล่งที่มาสำรอง + แหล่งที่มาสำรอง + ดาวน์โหลด + ดาวน์โหลดแล้ว + กำลังดาวน์โหลด... + ดาวน์โหลดไว้แล้ว + กำลังลองดาวน์โหลดอีกครั้ง... + กำลังดาวน์โหลด %1$s + การเล่นจะหยุดชั่วคราวในอีก %1$s + หยุดการเล่นชั่วคราวหลังจากจำนวนนาทีที่เลือก + นาที + เลือกระหว่าง 1 ถึง 600 นาที + ยกเลิกตัวตั้งเวลา + เริ่ม + URL สำหรับเล่นถูกระบุด้วยการตั้งค่าการสตรีมที่ต่างออกไป สตรีมที่ไฮไลต์ตรงกับค่าที่คุณต้องการในปัจจุบัน URL ที่ใช้งานอยู่จะรีเฟรชเมื่อมีการขอสตรีมครั้งถัดไป + แหล่งเสียงที่จับคู่ได้ + กำลังโหลดแหล่งที่จับคู่ได้… + ไม่จำเป็นต้องใช้แหล่งระยะไกลที่จับคู่ได้ขณะเล่นไฟล์ที่แคชไว้ในเครื่อง + ไม่มีแหล่งที่จับคู่ได้ + ไม่มีแหล่งเสียงที่จับคู่ได้ถูกแคชไว้สำหรับเพลงนี้ + ยังไม่ได้แคชรายละเอียดสตรีม เล่นเพลงนี้ก่อนเพื่อโหลดข้อมูล + โหลดรายละเอียดเพลงไม่สำเร็จ + สตรีมที่มีให้ใช้ + เพลงนี้กำลังเล่นจากแคชในเครื่อง จึงไม่ได้ใช้สตรีมรูปแบบต่าง ๆ สำหรับการเล่นนี้ + กำลังโหลดสตรีม… + ไม่มีสตรีมสำหรับแหล่งนี้ + สตรีมที่เลือกอยู่ในปัจจุบัน + ตรงกับรูปแบบและคุณภาพการสตรีมที่คุณต้องการ + URL ที่ใช้เล่นอยู่ + เล่นจากแคชในเครื่อง + เพลงจากเมทาดาทา + ชื่อเพลง + ID + อัลบั้ม + ความแม่นยำ + URI ภายนอก + ID อัลบั้ม + ประเภทอัลบั้ม + วันที่วางจำหน่ายอัลบั้ม + จำนวนเพลงในอัลบั้ม + แนวเพลงของอัลบั้ม + ความยาว + หมายเลขเพลง + หมายเลขแผ่น + เนื้อหาไม่เหมาะสม + ความนิยม + ISRC + URL ภาพขนาดย่อ + + + สลับแถบด้านข้าง + + + แหล่งที่มาสำรอง + ไม่มีเพลงที่กำลังเล่นอยู่ + กำลังค้นหาแหล่งที่มาสำรอง... + ไม่พบแหล่งที่มาสำรอง + ความแม่นยำ: %1$d% + แหล่งที่ใช้งานอยู่ + + + เตะออก + นำผู้ใช้นี้ออกจากเซสชัน + เตะออกและไม่ให้กลับเข้ามาอีก + ลบเพลงที่เสนอ + ลบทุกเพลงที่ผู้ใช้นี้เพิ่มลงในคิว + จัดลำดับใหม่ + นำออกจากคิว + %1$s • เพิ่มโดย %2$s + + + ไม่ทราบ + กำลังโหลดเพลย์ลิสต์... + เพิ่มเพลง + เพิ่มเพลงลงในเพลย์ลิสต์ + ค้นหาเพลง... + ค้นหา + ไม่พบเพลง + ค้นหาเพลงที่จะเพิ่ม + เสร็จสิ้น + เพิ่ม + เพิ่มแล้ว + + + กำลังโหลดอัลบั้ม... + + + เพิ่มลงในเพลย์ลิสต์ของคุณ + ค้นหา + ค้นหาเพลย์ลิสต์ของคุณ... + กำลังโหลดเพลย์ลิสต์ของคุณ... + คุณยังไม่มีเพลย์ลิสต์ของตัวเอง\nสร้างเพลย์ลิสต์เพื่อเพิ่มเพลง + สร้างเพลย์ลิสต์ + แก้ไขเพลย์ลิสต์ + สร้าง + ชื่อ + ชื่อเพลย์ลิสต์ + คำอธิบาย + คำอธิบาย (ไม่บังคับ) + สาธารณะ + ทุกคนสามารถเห็นเพลย์ลิสต์นี้ + ทำร่วมกัน + ผู้อื่นสามารถเพิ่มเพลงลงในเพลย์ลิสต์นี้ได้ + ภาพปก + ปกเพลย์ลิสต์ + ไม่มีภาพ + เปลี่ยนภาพ + เลือกภาพ + + + ติดตั้ง + แทนที่ + ไม่พบ plugin.json ในไฟล์ zip + ไม่พบ manifest.zipline.json ในไฟล์ zip + รูปแบบ plugin.json ไม่ถูกต้อง: %1$s + อ่านปลั๊กอินไม่สำเร็จ: %1$s + ติดตั้งปลั๊กอินไม่สำเร็จ: %1$s + มี %1$s เวอร์ชันใหม่กว่า โปรดยืนยันการอัปเดตก่อนติดตั้ง + %1$s ติดตั้งอยู่แล้ว โปรดยืนยันการแทนที่ก่อนติดตั้ง + ปลั๊กอินในตัวรวมอยู่แล้วและไม่สามารถเพิ่มซ้ำได้ + ไม่สามารถลบปลั๊กอินในตัวได้ + %1$s ระบุ apiVersion ไม่ถูกต้อง (%2$s) ต้องเป็น semantic versioning ที่เข้ากันได้กับ %3$s + เวอร์ชัน API ปลั๊กอินของ Spotube %1$s ไม่ถูกต้อง + %1$s ใช้ API ปลั๊กอิน %2$s แต่ Spotube รองรับ API ปลั๊กอินแบบ breaking %3$s โปรดติดตั้งปลั๊กอินที่สร้างสำหรับ API เวอร์ชัน breaking เดียวกัน + + + API ปลั๊กอินเข้ากันไม่ได้ + ติดตั้งปลั๊กอินหรือไม่? + %1$s จะถูกเพิ่มลงในปลั๊กอินที่ติดตั้งไว้ของคุณ + อัปเดตปลั๊กอินหรือไม่? + มีการติดตั้งเวอร์ชันเก่ากว่าอยู่ (%1$s) อัปเดตเป็น %2$s หรือไม่? + แทนที่ปลั๊กอินที่ติดตั้งไว้หรือไม่? + %1$s ใช้ semantic version ที่ไม่ถูกต้อง (%2$s) จึงไม่สามารถเปรียบเทียบเป็นการอัปเดตได้ ต้องการแทนที่ปลั๊กอินที่ติดตั้งไว้ต่อไปหรือไม่? + ไม่สามารถเปรียบเทียบเวอร์ชันที่ติดตั้งไว้ (%1$s) ด้วย semantic versioning ได้ แทนที่ด้วย %2$s หรือไม่? + เวอร์ชัน %1$s ติดตั้งอยู่แล้ว แทนที่ปลั๊กอินเดิมด้วยสำเนาที่ให้มาหรือไม่? + เวอร์ชันที่ติดตั้งไว้ %1$s ใหม่กว่าเวอร์ชันที่ให้มา %2$s ต้องการแทนที่ต่อไปหรือไม่? + แทนที่ปลั๊กอินที่ติดตั้งไว้ด้วยสำเนาที่ให้มาหรือไม่? + + + %1$s: + เวอร์ชัน + เวอร์ชัน API + ผู้สร้าง + สัญญาอนุญาต + สิทธิ์การเข้าถึง + ความสามารถ + รีโพซิทอรี + ติดต่อ + รายงานข้อบกพร่อง + สนับสนุน %1$s + + + ทางการ + ยืนยันแล้ว + ดาวบน GitHub + URL รีโพซิทอรี GitHub + + + ไม่พบรีลีส + เลือกรีลีส + เลือกรีลีส + รุ่นทดสอบก่อนเปิดตัว + บันทึกประจำรุ่น + + + หน้าแรก + ค้นหา + ในอุปกรณ์ + เชื่อมต่อ + อุปกรณ์ + + + ซ่อนหน้าต่าง + แสดงหน้าต่าง + เพลงถัดไป + เพลงก่อนหน้า + สุ่มเพลง + เล่นซ้ำ + เล่นซ้ำ: ปิด + เล่นซ้ำ: เพลงเดียว + เล่นซ้ำ: ทั้งหมด + ระดับเสียง: %1$d% + เพิ่มเสียง + + ลดเสียง - + ปิดเสียง + เปิดเสียง + ถูกใจเพลง + เลิกถูกใจเพลง + ออก + + + ย่อขนาด + ขยายใหญ่สุด + คืนขนาด + ปิด + + + บริการเล่นเพลง + การเล่น + ตัวควบคุมการเล่นสื่อ + + + เรียกดู + กำลังเล่น + แนะนำ + เพลงที่บันทึกไว้ + เพลงยอดนิยม + diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml new file mode 100644 index 00000000..6f7bea1a --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml @@ -0,0 +1,938 @@ + + + + Sıra + Sırayı temizle + Sırada filtrele... + Sıra boş + + Şarkı, sanatçı, albüm ara... + Tüm geçmişi temizle + Ara... + + + Kitaplığın + Kitaplık + Çalma listeleri + Albümler + Sanatçılar + Yerel parçalar + İndirilenler + Kayıtlı çalma listelerinde ara… + Kayıtlı albümlerde ara… + Kayıtlı sanatçılarda ara… + Klasör veya parça ara… + Kitaplığı filtrele + Filtreyi kapat + Çalma listesi oluştur + Yerel parçaları yenile + Izgara görünümüne geç + Liste görünümüne geç + Kayıtlı çalma listesi yok + “%1$s” ile eşleşen çalma listesi yok + Kayıtlı albüm yok + “%1$s” ile eşleşen albüm yok + Kayıtlı sanatçı yok + “%1$s” ile eşleşen sanatçı yok + + + Klasörler + Klasör + %1$s • %2$s + + %1$d parça + %1$d parça + + + %1$d klasör + %1$d klasör + + Klasörlere dön + Klasörü çal + Ses erişimi gerekli + Cihazındaki müziklere göz atmak için ses erişimine izin ver. + Erişim izni ver + Yerel klasör bulunamadı + Bu klasörde parça yok + Eşleşen klasör veya parça yok + Yerel medya yenilenemedi + + + %1$d etkin • %2$d tamamlandı + Tamamlananları temizle + Henüz indirme yok + İndiriliyor: %1$d% + Sırada + Tamamlandı + İptal edildi + Başarısız + Başarısız: %1$s + İndirmeyi iptal et + İndirmeyi yeniden dene + İndirmeyi kaldır + + + Sanatçı + Kara listede + Beğenilen Şarkılar + Kaydettiğin şarkılar + Bilinmeyen sanatçı + Parça adı + Sanatçı adı + + Seçili + + Ayarlar + + Eklentiler + Dil ve Bölge + Görünüm + Çalma + İndirmeler + Önbellek + Masaüstü + Güncellemeler + Destek + Hakkında + + Kapat + Kaydet + İptal + + Dil + Uygulama arayüzünün dili. Geçerli: %1$s + Sistem varsayılanı + Ülke + İçerik ve kullanılabilirlik için bölge ülkesi. Geçerli: %1$s + %1$s (%2$s) + + Tema + Spotube\'un sistem görünümüne nasıl uyacağını seç. Geçerli: %1$s + Açık + Koyu + Sistem + + Yüzey + Uygulamanın arka plan malzemesi. Geçerli: %1$s + Su + Buz + Krom + Akrilik + Cam + + Vurgu rengi + Geçerli palet: %1$s + Vurgu rengi + Bir vurgu paleti seç ve hem açık hem koyu temada önizle. + Açık + Koyu + Önizleme + Düğme + Yeşil Goblin + Elektrik Moru + Okyanus Camgöbeği + Gün Batımı Turuncusu + Gül Bahçesi + Gece Mavisi + Metalik Arduvaz + + Görünüm ve tarz + Yüzey malzemesi ve vurgu. Geçerli: %1$s · %2$s + Görünüm ve tarz + Önce bir yüzey malzemesi, ardından ona uyan bir vurgu seç. Her seçenek özenle hazırlanmış bir kombinasyon. + Yüzey + Vurgu + İkincil + + Palet + Millennium vurgu paleti. Geçerli: %1$s + Palet + Bir Millennium vurgu paleti seç ve açık ile koyu temada önizle. + Palet + Millennium paleti ve vurgu. Geçerli: %1$s + Mavi + Turuncu + Limon yeşili + + Özel indirme klasörü + Geçerli klasör: %1$s + Geçerli klasör: varsayılan müzik klasörü + Varsayılan müzik klasörünü kullanmak için boş bırak. + C:\Music\Spotube + Yerel medya klasörleri + Masaüstünde taranacak özel klasörler. Eklenen: %1$d + Henüz özel klasör eklenmedi + Yerel müzik kitaplığın için özel klasörler. + Yönet + Klasör ekle + Klasörü kaldır + Özel klasör eklenmedi. + %1$s klasörünü tarama listesinden kaldır + İndirme ve müzik önbelleği klasörleri otomatik olarak dahil edilir. + İndirme klasörün otomatik olarak dahil edilir. + Müzik önbelleği klasörü otomatik olarak dahil edilir. + Kaydediliyor… + Klasörler kaydedilemedi. Değişikliklerin duruyor; tekrar dene. + + %1$d klasör + %1$d klasör + + İndirme müzik biçimi + İndirmeler için tercih edilen kodek ve kapsayıcı. Geçerli: %1$s + İndirme müzik kalitesi + + Akış müzik biçimi + Müzik akışı için tercih edilen kodek ve kapsayıcı. Geçerli: %1$s + Akış müzik kalitesi + Geçerli: %1$s + Müzik önbelleğini etkinleştir + Tekrar çalmayı hızlandırmak için akışla gelen sesi cihazda sakla. + Önbellek klasörü + Geçerli: %1$s + Geçerli: varsayılan uygulama önbelleği + Önbellek boyutu sınırı + Sınır yok + Geçerli sınır: %1$d MB + Maksimum önbellek boyutunu MB cinsinden belirle. Sınır olmaması için 0 gir veya boş bırak. + 500 + Boyut 0 veya pozitif bir sayı olmalı. + Sonsuz çalmayı etkinleştir + Sıran bittiğinde önerilen parçalarla çalmaya devam et. + Connect\'i etkinleştir + Uzaktan çalma kontrollerini Spotube Connect özelliğiyle sun. + Çalma proxy sunucusu portu + Uzaktan kontrole izin ver + Aynı ağdaki diğer cihazlar bağlandığında çalmayı kontrol edebilsin. + Uzaktan kontrol cihaz adı + Diğer cihazların bu cihazı bulurken gördüğü ad. Geçerli: %1$s + Ağındaki diğer cihazlar bu Spotube\'u bulduğunda görünecek kolay bir ad seç. + örn. Salon Spotube + cihaz ana bilgisayar adı + Çalma proxy sunucusunun kullandığı port. Geçerli: %1$d + 1 ile 65535 arasında bir port seç. + 14769 + Geçerli bir tam sayı gir. + Port 1 ile 65535 arasında olmalı. + + Kara liste + Kara listedeki parçaları ve sanatçıları yönet + + Sistem tepsisine küçült + Pencere küçültüldüğünde Spotube sistem tepsisinde çalışmaya devam etsin. + Discord Rich Presence + Masaüstünde şu an çalanı Discord\'da göster. + + Güncellemeleri otomatik denetle + Yeni Spotube sürümlerini arka planda ara. + + Eklentileri Yönet + Eklentilerini yönet + Varsayılan %1$s eklentisi + Meta veri + Ses + Şarkı sözleri + Scrobble + Eklenti seçilmedi + Değiştir + Seç + Seçili + Seçimi temizle + Kullanılabilir eklenti yok + %1$s eklentisi + Temizle + + Yapılandır + Eklenti Yöneticisi + Lütfen bir URL gir + URL, http:// veya https:// ile başlamalı + Eklenti indirilemedi + Henüz yüklü eklenti yok + Bir indirme URL\'si yapıştır veya yukarıdan bir dosya seç. + + %1$d eklenti yüklü + %1$d eklenti yüklü + + Eklenti Yükle + https://example.com/plugin.smplug + İndir + Dosyadan yükle (.smplug) + Etkin + v%1$s + Yerleşik + Eklentiyi kaldır + Giriş yap + Çıkış yap + URL\'den indir + Dosyadan yükle + Yükle + + • %1$s + İstenen izinler + Bu eklenti özel bir izin gerektirmiyor. + Yüklü ve sağlanan + Yüklü + Sağlanan + API %1$s → %2$s + Kalıcı Depolama + Diske veri okuma ve yazma + Ağ İstekleri + İnternet üzerinden veri gönderme ve alma + WebView + Uygulama içinde web içeriği görüntüleme + Grup Jam + MQTT Aracısı + Aracı ana bilgisayarı + %1$s:%2$d + Aracı portu + TLS kullan + Kullanıcı adı (isteğe bağlı) + Şifre (isteğe bağlı) + İstemci kimliği ön eki + Bu Spotube örneğini tanımlamak için kullanılır + Aracı bağlantısını şifrele + Oda başlatmadan önce geçerli aracı ayarlarını kontrol et. + Bağlantıyı test et + Test ediliyor… + %1$d ms\'de bağlandı + Başarısız: %1$s + Geçici aracı — kendin barındırmak için kendi aracını yapılandır + Bağlantı + Kimlik bilgileri + Kimlik + Tanılama + + + Grup Jam + Oluştur + Katıl + Bir oda oluştur, sonra kodunu arkadaşlarınla paylaş. + Oda oluştur + Odaya katıl + Bağlanıyor… + Oda kodu + ABC123 + Aracı: %1$s + Oda açmak veya bir odaya katılmak için Ayarlar\'da bir aracı ayarla. + Aracı ayarları + Bağlandı + Yeniden bağlanıyor… + Kodu kopyala + Kopyalandı! + Paylaş + Katılımcılar (%1$d) + Sahip + Dinleyici + Dinleyiciler bekleniyor… + Çıkar + Engelle + Yönet + Odadan ayrıl + Bağlanılamadı + Kapat + Jam oturumu oluşturulamadı + Jam oturumuna katılınamadı + Harf ve rakamlardan oluşan altı karakterli bir oda kodu gir. + Spotube Jam\'ime şu kodla katıl: %1$s + + Albüme git + Uyku zamanlayıcısı + Parça ayrıntıları + Çal + Duraklat + Hiçbir şey çalmıyor + Sıraya ekle + + Yok say + Güncelle + Spotube %1$s kullanıma sunuldu + Bu sürümdeki yenilikleri Spotube indirme sayfasında gör. + %1$s %2$s kullanıma sunuldu + Şu an yüklü: %1$s + Bu güncellemedeki yenilikleri eklentinin sayfasında gör. + + Atla + İleri + Geri + Başla + Spotube + Müzik, hiçbir kısıtlama olmadan. + Koca bir müzik dünyası için açık kaynaklı bir oynatıcı.\nKeşfetmesi sana kalmış. Kendine göre şekillendirmesi de. + Bu projeyi yaşat. + Spotube\'u bir abonelik şirketi değil, insanlar geliştiriyor. Günlük hayatının bir parçası olduysa, küçük bir destek bile ona hak ettiği özeni göstermemize yardımcı olur.\n\nBağımsız yazılıma yer açtığın için teşekkürler. + Şarkı ve çalma listesi bulmak için bir katalog eklentisiyle başla. Onları çalmak için bir ses eklentisi seç. Şarkı sözleri ve dinleme geçmişi isteğe bağlı. Aşağıdan bir eklenti keşfet, izinlerini incele, yükle ve burada seç. + Eklentileri keşfet + Kaynakların + Tekrar dene + Daha fazla eklenti + Yeni eklenti bulunamadı. Yukarıdan bir bağlantı veya .smplug dosyasıyla da yükleyebilirsin. + Eklenti keşfi şu anda kullanılamıyor. Tekrar dene ya da bir bağlantı veya dosyadan yükle. + Hoş geldin + Destek + Eklentiler + Rehber + + Spotube nasıl kullanılır + Dört hızlı adım ve dinlemeye başlıyorsun. + Giriş yap + Spotify hesabını bağla veya kendi katalog eklentini seç. + Ara ve çal + Herhangi bir şarkıyı, albümü veya sanatçıyı bul, sonra çal\'a bas. + Çevrimdışı dinle + Bağlantı olmadan dinlemeye devam etmek için parçaları indir. + Sözleri takip et + Bir parça çalarken oynatıcıdan senkronize sözleri aç. + + + Senin müziğin. Senin kaynakların. Senin kuralların. + Müziği sadece seven bir topluluğun geliştirdiği ücretsiz, açık kaynaklı bir müzik oynatıcı. + Açık kaynak + Reklamsız + Telemetri yok + Eklenti destekli + Hadi Başlayalım + + + Destekçilerimiz bizi ayakta tutuyor + Tutkuyla yapıldı. Seninle yaşıyor. + Spotube %100 ücretsiz, özgür ve açık kaynak — reklam yok, abonelik yok, takip yok. Her özellik, kodun akmaya devam etmesine yardım etmeyi seçen senin gibi insanlar sayesinde var. + Patreon + Aylık Sponsor + Sürekli geliştirmeyi ve altyapıyı destekle + Buy Me a Coffee + Tek Seferlik Kahve + Küçük bir bahşişle gece yarısı kodlama seansına güç ver + Open Collective + Topluluk Fonu + Şeffaf, topluluk tarafından yönetilen finansal destek + Açık müziğe inandığın için tüm kalbimizle teşekkür ederiz. + Destek tamamen isteğe bağlı — Spotube her durumda ücretsiz kalır. + Sponsor ol + %1$s sayfasını aç + + + Spotube\'u destekle + Spotube\'u destekle + Spotube ücretsiz, özgür ve açık kaynak — reklam yok, abonelik yok, takip yok. Sana keyif veriyorsa projeyi desteklemeyi düşünebilirsin. + Bu hatırlatıcıları Ayarlar → Destek bölümünden kapatabilirsin. + Belki sonra + Destek hatırlatıcıları + Projeyi desteklemek için her 7 günde bir nazik bir hatırlatma göster. + + + Modüler Mimari + Müziğinin nereden geleceğini seç. + Spotube katalog keşfini, ses akışını ve senkronize sözleri birbirinden ayırır. Motorlarını aşağıdan yapılandır veya istediğin zaman değiştir. + Meta Veri Motoru + Müzik keşfini, aramayı, parça listelerini ve çalma listelerini sağlar. + Ses Motoru + Çalınan ses akışlarını getirir ve çözer. + Şarkı Sözü Motoru + Zamanla senkronize karaoke sözlerini gerçek zamanlı olarak sunar. + Scrobble Motoru + Dinleme geçmişini Last.fm veya ListenBrainz\'e kaydeder. + Etkin + Yerleşik + Hazır + İsteğe bağlı + Bir motor seç + Yüklü Motorlar + Varsayılan yap + NewPipe YouTube ve LRCLib Lyrics yerleşiktir ve kutudan çıktığı gibi akışa hazırdır. + Topluluk eklentilerini istediğin zaman Ayarlar → Eklentiler bölümünden yönetebilir veya yenilerini ekleyebilirsin. + + + Retro-Modern Mimari + Saf Hi-Fi, sıfır aracı + Spotube\'un merkeziyetsiz çalmayı doğrudan cihazında nasıl yönettiğini gör. + DOĞRUDAN EŞLER ARASI • 256 KBPS OPUS + Mavi saat + Spotube · oynatıcı tanıtımı + Aradaki anlar için biraz müzik. + 1. Merkeziyetsiz Keşif + Sorgular doğrudan seçtiğin katalog eklentisine gider. Spotube zevkinin profilini asla çıkarmaz. + 2. Doğrudan Akış Çözümleme + Ses, içerik sunucularından doğrudan cihazına akar. Proxy aktarması veya uzaktan veri toplama yok. + 3. Yerel DSP ve Çalma + Ekolayzer kontrolleri ve isteğe bağlı çevrimdışı indirme ile cihazında çözülür ve önbelleğe alınır. + 4. Senkronize Sözler ve Jam + Karaoke sözleri çalma zamanını takip eder; LAN üzerinden uzaktan kontrol ve Grup Jam de var. + Aracı sunucu yok. Çalma doğrudan cihazında gerçekleşir — tasarımı gereği gizli. + + + Bir cihaz seç + %1$s: “%2$s” + Çal + Sıraya ekle + Sonra çal + Bu Cihaz + Bu cihazda + Uzak Cihaz + Uzak Cihaz (%1$s) + Bağlı cihazda + + + Bağlantı başarısız + Bağlantı kesildi + Gönderilemedi + Keşif başarısız: %1$s + Bağlanılamadı: %1$s + + + Cihazlar + Yenile + %1$s:%2$d adresine bağlı + Bağlantıyı kes + Bağlanıyor... + Ağda cihaz aranıyor... + Cihaz bulunamadı + Diğer cihazın ayarlarında \"Uzaktan kontrole izin ver\" seçeneğinin açık olduğundan emin ol. + Bilinmeyen Cihaz + Çözümleniyor... + Kimlik: %1$s... + + + Uzaktan Kontrol + Bağlantı kesildi + Bağlantı hatası: %1$s + Albüm kapağı + Bilinmeyen Parça + Bilinmeyen Sanatçı + Konum + Karıştır + Önceki + Sonraki + Tekrar modu + Ses düzeyi + Sıradan kaldır + Diğer seçenekler + + + Nerede Çalınsın? + Etkin bir Jam oturumun var. Bu koleksiyonun nerede çalınacağını seç. + Burada çal + Jam\'e öner + + + Jam aracısı yapılandırılmadı + Aracı ana bilgisayarı boş + Bağlantı başarısız + Bağlantı kesildi + + + Aracı + anonymous + + + Jam sırasına eklendi + + %1$d parça Jam sırasına eklendi + %1$d parça Jam sırasına eklendi + + + + Uzaktan Kontrol İsteği + \"%1$s\" bu cihazda çalmayı kontrol etmek istiyor. + Reddet + İzin ver + Her Zaman İzin Ver + + + Arama sonuçları + Tümü + Parçalar + Kullanıcılar + Kullanılabilir arama türü yok + Aramak için yazmaya başla + Parça aramak için yazmaya başla + Sonuç bulunamadı + Tümünü gör + Aranıyor + Parçalar yükleniyor... + Albümler yükleniyor... + Sanatçılar yükleniyor... + Çalma listeleri yükleniyor... + Kullanıcılar yükleniyor... + Albüm bulunamadı + Sanatçı bulunamadı + Çalma listesi bulunamadı + Kullanıcı bulunamadı + Plak kasaları karıştırılıyor... + Evren taranıyor... + Hoparlörler ısınıyor... + Plakların tozu alınıyor... + Müzik tanrılarına danışılıyor... + Antenler ayarlanıyor... + Plaklar karıştırılıyor... + Arama özellikleri yüklenemedi + Arama yapılamadı + Parçalar aranamadı + Albümler aranamadı + Sanatçılar aranamadı + Çalma listeleri aranamadı + Kullanıcılar aranamadı + + + Göz at + + + Senkronize + Düz + Senkronize şarkı sözü yok + Düz şarkı sözü yok + + + %2$s içinde %1$s + %1$d kbps + %1$s kHz • %2$d kanal + Meta veri eklentisi seçilmedi + + + Geri + Tekrar dene + İletişim kutusunu kapat + Yükleniyor... + Bilinmiyor + Bilinmeyen hata + + + Kullanıcı + Çalma listesi + + + Gerçekten Üzgünüz + Şu anda bir şeyler düzgün çalışmıyor. Deneyimin bizim için önemli ve bunu düzeltmek için elimizden geleni yapıyoruz. Lütfen tekrar dene veya aşağıdaki hata ayrıntılarına göz at. + Hata Ayrıntıları + Panoya kopyala + Tam günlüğü görüntüle + Tam Hata Günlüğü + + + Karıştır + Karışık çal + Takip et + Takibi bırak + Çalma listesini düzenle + %1$s tarafından + + + Parça seçenekleri + Jam\'e ekle + Radyo başlat + Sonra çal + Sıradakine taşı + Sıradan kaldır + Favorilerden kaldır + Favorilere kaydet + Çalma listesine ekle + İndir + Paylaş + + + Orijinal + Başlık + Sanatçı + Albüm + Süre + Sırala + Parça bulunamadı + Filtrele + Toplu işlemler + Tümünü İndir + %1$d Parçayı İndir + Tümünü Sıraya Ekle + %1$d Parçayı Sıraya Ekle + Tümünü Sonra Çal + %1$d Parçayı Sonra Çal + Tümünü Çalma Listesine Ekle + %1$d Parçayı Çalma Listesine Ekle + Tümünü Jam\'e Ekle + %1$d Parçayı Jam\'e Ekle + +%1$d kişi daha + + + Yüklenecek URL yok + Tarayıcı geçmişinde geri git + Tarayıcı geçmişinde ileri git + + + Kara listeyi filtrele... + Kara listede öğe yok + Kara listede \'%1$s\' ile eşleşen öğe yok + Parçalar + Parça + Kara listeye ekle + Kara listeden kaldır + + + Parça meta verisi yok + Akış URL\'si çözümlenemedi + Akış URL\'si çözümlenemedi. Önce parçayı çalmayı dene. + + + Kayıtlı Parçalar + Sen + + + Takip ediliyor + Popüler Parçalar + Popüler parçaları çal + Popüler parçaları sıraya ekle + Benzer Sanatçılar + Öne Çıkan Çalma Listeleri + Sanatçı sayfası yüklenemedi + Takipçi sayısı kullanılamıyor + %1$s B takipçi + %1$s Mn takipçi + %1$s Mr takipçi + + %1$d takipçi + %1$d takipçi + + + + Beğen + Beğenmekten vazgeç + Yer tutucu parça adı + Yer tutucu sanatçılar + Bilinmeyen Albüm + Konum + Ses düzeyi + Karıştırmayı aç + Karıştırmayı kapat + Önceki + Sonraki + Çal veya duraklat + Tekrar kapalı + Bir parçayı tekrarla + Tümünü tekrarla + Sesi kapat + Sesi aç + Diğer seçenekler + Oynatıcı seçenekleri + Oynatıcıyı daralt + Sözleri genişlet + Şu An Çalıyor + Şarkı Sözleri + Alternatif Kaynak + Alternatif kaynak + İndir + İndirildi + İndiriliyor... + Zaten indirildi + İndirme yeniden deneniyor... + %1$s indiriliyor + Çalma %1$s sonra duraklatılacak. + Seçilen dakika sayısından sonra çalmayı duraklat. + Dakika + 1 ile 600 dakika arasında seç + Zamanlayıcıyı iptal et + Başlat + Çalma URL\'si farklı akış ayarlarıyla çözümlendi. Vurgulanan akış geçerli tercihinle eşleşiyor; etkin URL bir sonraki akış isteğinde yenilenir. + Eşleşen ses kaynağı + Eşleşen kaynak yükleniyor… + Önbellekteki yerel dosya çalınırken eşleşen bir uzak kaynağa gerek yok. + Eşleşen kaynak yok. + Bu parça için önbellekte eşleşen ses kaynağı yok. + Akış ayrıntıları henüz önbelleğe alınmadı. Yüklemek için önce parçayı çal. + Parça ayrıntıları yüklenemedi. + Kullanılabilir akışlar + Bu parça yerel önbellekten çalınıyor. Bu çalma için akış varyantları kullanılmıyor. + Akışlar yükleniyor… + Bu kaynak için kullanılabilir akış yok. + Şu an seçili akış + Akış biçimi ve kalite tercihinle eşleşiyor + Etkin çalma URL\'si + Yerel önbellekten + Meta veri parçası + Başlık + Kimlik + Albüm + Eşleşme güveni + Harici URI + Albüm kimliği + Albüm türü + Albüm çıkış tarihi + Albümdeki parça sayısı + Albüm türleri + Süre + Parça numarası + Disk numarası + Müstehcen + Popülerlik + ISRC + Küçük resim URL\'leri + + + Kenar çubuğunu aç/kapat + + + Alternatif Kaynaklar + Şu anda çalan parça yok + Alternatif kaynaklar aranıyor... + Alternatif kaynak bulunamadı + Eşleşme: %1$d% + Etkin kaynak + + + At + Oturumdan çıkar + At ve yeniden katılmasını engelle + Önerileri kaldır + Sıraya eklediği tüm parçaları kaldır + Yeniden sırala + Sıradan kaldır + %1$s • Ekleyen: %2$s + + + Bilinmiyor + Çalma listesi yükleniyor... + Parça Ekle + Çalma Listesine Parça Ekle + Parça ara... + Ara + Parça bulunamadı + Eklemek için parça ara + Bitti + Ekle + Eklendi + + + Albüm yükleniyor... + + + Çalma Listene Ekle + Ara + Çalma listelerinde ara... + Çalma listelerin yükleniyor... + Henüz sana ait bir çalma listesi yok.\nParça eklemek için bir tane oluştur. + Çalma Listesi Oluştur + Çalma Listesini Düzenle + Oluştur + Ad + Çalma listesi adı + Açıklama + Açıklama (isteğe bağlı) + Herkese açık + Bu çalma listesini herkes görebilir + Ortak + Başkaları bu çalma listesine parça ekleyebilir + Kapak Görseli + Çalma listesi kapağı + Görsel yok + Görseli Değiştir + Görsel Seç + + + Yükle + Değiştir + Zip dosyasında plugin.json bulunamadı + Zip dosyasında manifest.zipline.json bulunamadı + Geçersiz plugin.json biçimi: %1$s + Eklenti okunamadı: %1$s + Eklenti yüklenemedi: %1$s + %1$s için daha yeni bir sürüm var. Yüklemeden önce güncellemeyi onayla. + %1$s zaten yüklü. Yüklemeden önce değiştirmeyi onayla. + Yerleşik eklentiler zaten dahil ve tekrar eklenemez. + Yerleşik eklentiler kaldırılamaz. + %1$s geçersiz bir apiVersion (%2$s) bildiriyor. %3$s ile uyumlu anlamsal sürümleme bekleniyor. + Spotube eklenti API sürümü %1$s geçersiz. + %1$s, eklenti API\'si %2$s hedefliyor ancak Spotube uyumsuz eklenti API\'si %3$s destekliyor. Aynı ana API sürümü için derlenmiş bir eklenti yükle. + + + Eklenti API\'si uyumlu değil + Eklenti yüklensin mi? + %1$s yüklü eklentilerine eklenecek. + Eklenti güncellensin mi? + Daha eski bir sürüm yüklü (%1$s). %2$s sürümüne güncellensin mi? + Yüklü eklenti değiştirilsin mi? + %1$s geçersiz bir anlamsal sürüm (%2$s) kullanıyor ve güncelleme olarak karşılaştırılamıyor. Yüklü eklenti yine de değiştirilsin mi? + Yüklü sürüm (%1$s) anlamsal sürümlemeyle karşılaştırılamıyor. %2$s ile değiştirilsin mi? + %1$s sürümü zaten yüklü. Mevcut eklenti sağlanan kopyayla değiştirilsin mi? + Yüklü %1$s sürümü, sağlanan %2$s sürümünden daha yeni. Yine de değiştirilsin mi? + Yüklü eklenti sağlanan kopyayla değiştirilsin mi? + + + %1$s: + Sürüm + API Sürümü + Geliştirici + Lisans + Yetenekler + İşlevler + Depo + İletişim + Hata Bildir + %1$s eklentisini destekle + + + Resmi + Doğrulanmış + GitHub yıldızları + GitHub depo URL\'si + + + Sürüm bulunamadı + Sürüm Seç + Bir sürüm seç + Ön sürüm + Sürüm Notları + + + Ana sayfa + Ara + Cihazda + Bağlan + Cihazlar + + + Pencereyi Gizle + Pencereyi Göster + Sonraki Parça + Önceki Parça + Karıştır + Tekrarla + Tekrar: KAPALI + Tekrar: BİR + Tekrar: TÜMÜ + Ses düzeyi: %1$d% + Ses + + Ses - + Sesi Kapat + Sesi Aç + Parçayı Beğen + Beğenmekten Vazgeç + Çıkış + + + Simge durumuna küçült + Ekranı kapla + Önceki boyuta getir + Kapat + + + Çalma hizmeti + Çalma + Medya çalma kontrolleri + + + Göz at + Şu An Çalıyor + Öne Çıkanlar + Kayıtlı Parçalar + Popüler Parçalar + diff --git a/composeApp/src/commonMain/composeResources/values-uk/strings.xml b/composeApp/src/commonMain/composeResources/values-uk/strings.xml new file mode 100644 index 00000000..37273b97 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-uk/strings.xml @@ -0,0 +1,950 @@ + + + + Черга + Очистити чергу + Фільтр черги... + Черга порожня + + Треки, виконавці, альбоми... + Очистити історію + Пошук... + + + Моя бібліотека + Бібліотека + Плейлисти + Альбоми + Виконавці + Локальні треки + Завантаження + Пошук у збережених плейлистах… + Пошук у збережених альбомах… + Пошук серед збережених виконавців… + Пошук папок або треків… + Фільтр бібліотеки + Закрити фільтр + Створити плейлист + Оновити локальні треки + Показати сіткою + Показати списком + Немає збережених плейлистів + Немає плейлистів за запитом «%1$s» + Немає збережених альбомів + Немає альбомів за запитом «%1$s» + Немає збережених виконавців + Немає виконавців за запитом «%1$s» + + + Папки + Папка + %1$s • %2$s + + %1$d трек + %1$d треки + %1$d треків + %1$d трека + + + %1$d папка + %1$d папки + %1$d папок + %1$d папки + + Назад до папок + Слухати папку + Потрібен доступ до аудіо + Дозвольте доступ до аудіо, щоб переглядати музику на пристрої. + Надати доступ + Локальних папок не знайдено + У цій папці немає треків + Немає відповідних папок або треків + Не вдалося оновити локальні файли + + + Активних: %1$d • Завершених: %2$d + Очистити завершені + Завантажень поки немає + Завантаження %1$d% + У черзі + Завершено + Скасовано + Помилка + Помилка: %1$s + Скасувати завантаження + Повторити завантаження + Видалити завантаження + + + Виконавець + У чорному списку + Улюблені треки + Ваші збережені треки + Невідомий виконавець + Назва треку + Ім’я виконавця + + Вибрано + + Налаштування + + Плагіни + Мова та регіон + Оформлення + Відтворення + Завантаження + Кешування + Комп’ютер + Оновлення + Підтримка + Про застосунок + + Закрити + Зберегти + Скасувати + + Мова + Мова інтерфейсу застосунку. Зараз: %1$s + Як у системі + Країна + Країна для контенту та доступності. Зараз: %1$s + %1$s (%2$s) + + Тема + Як Spotube підлаштовується під тему системи. Зараз: %1$s + Світла + Темна + Системна + + Поверхня + Фоновий матеріал застосунку. Зараз: %1$s + Вода + Лід + Хром + Акрил + Скло + + Акцентний колір + Поточна палітра: %1$s + Акцентний колір + Виберіть акцентну палітру й подивіться, як вона виглядає у світлій і темній темах. + Світла + Темна + Попередній перегляд + Кнопка + Зелений гоблін + Електрик-фіолетовий + Океанський ціан + Помаранчевий захід + Трояндовий сад + Опівнічний синій + Металік-графіт + + Зовнішній вигляд + Матеріал поверхні й акцент. Зараз: %1$s · %2$s + Зовнішній вигляд + Виберіть матеріал поверхні, а потім відповідний акцент. Кожен варіант — продумане поєднання. + Поверхня + Акцент + Другорядна + + Палітра + Акцентна палітра Millennium. Зараз: %1$s + Палітра + Виберіть акцентну палітру Millennium і перегляньте її у світлій і темній темах. + Палітра + Палітра Millennium і акцент. Зараз: %1$s + Синя + Помаранчева + Лаймова + + Власна папка для завантажень + Поточна папка: %1$s + Поточна папка: стандартна папка «Музика» + Залиште порожнім, щоб використовувати стандартну папку «Музика». + C:\Music\Spotube + Папки з локальною музикою + Додаткові папки для сканування на комп’ютері. Додано: %1$d + Додаткових папок ще не додано + Додаткові папки для вашої локальної бібліотеки. + Керувати + Додати папку + Видалити папку + Додаткових папок не додано. + Прибрати %1$s зі списку сканування + Папки завантажень і музичного кешу додаються автоматично. + Папка завантажень додається автоматично. + Папка музичного кешу додається автоматично. + Збереження… + Не вдалося зберегти папки. Зміни не втрачено — спробуйте ще раз. + + %1$d папка + %1$d папки + %1$d папок + %1$d папки + + Формат завантаження музики + Бажаний кодек і контейнер для завантажень. Зараз: %1$s + Якість завантаження музики + + Формат стримінгу музики + Бажаний кодек і контейнер для стримінгу. Зараз: %1$s + Якість стримінгу музики + Зараз: %1$s + Кешувати музику + Зберігати прослухане аудіо на пристрої для швидшого повторного відтворення. + Папка кешу + Зараз: %1$s + Зараз: стандартний кеш застосунку + Ліміт розміру кешу + Без обмежень + Поточний ліміт: %1$d MB + Укажіть максимальний розмір кешу в MB. Введіть 0 або залиште порожнім, щоб зняти обмеження. + 500 + Розмір має бути 0 або додатним числом. + Безкінечне відтворення + Продовжувати відтворення рекомендованими треками, коли черга закінчиться. + Увімкнути Connect + Надавати віддалене керування відтворенням через Spotube Connect. + Порт проксі-сервера відтворення + Дозволити віддалене керування + Дозволити іншим пристроям у тій самій мережі керувати відтворенням після підключення. + Назва пристрою для віддаленого керування + Назва, яку бачать інші пристрої під час пошуку. Зараз: %1$s + Задайте зрозумілу назву, яку бачитимуть інші пристрої у вашій мережі, коли знайдуть цей Spotube. + наприклад, Spotube у вітальні + ім’я хоста пристрою + Порт проксі-сервера відтворення. Зараз: %1$d + Укажіть порт від 1 до 65535. + 14769 + Введіть ціле число. + Порт має бути від 1 до 65535. + + Чорний список + Керування заблокованими треками й виконавцями + + Згортати в трей + Залишати Spotube в системному треї, коли вікно згорнуто. + Статус у Discord + Показувати в Discord, що зараз грає (на комп’ютері). + + Автоматично перевіряти оновлення + Шукати нові версії Spotube у фоновому режимі. + + Керування плагінами + Керуйте своїми плагінами + Плагін за замовчуванням: %1$s + Метадані + Аудіо + Тексти пісень + Скроблінг + Плагін не вибрано + Змінити + Вибрати + Вибрано + Скинути вибір + Немає доступних плагінів + Плагін %1$s + Скинути + + Налаштувати + Менеджер плагінів + Введіть URL + URL має починатися з http:// або https:// + Не вдалося завантажити плагін + Плагіни ще не встановлено + Вставте посилання для завантаження або виберіть файл вище. + + %1$d плагін встановлено + %1$d плагіни встановлено + %1$d плагінів встановлено + %1$d плагіна встановлено + + Встановлення плагіна + https://example.com/plugin.smplug + Завантажити + Встановити з файлу (.smplug) + Активний + v%1$s + Вбудований + Видалити плагін + Увійти + Вийти + Завантажити за URL + Встановити з файлу + Встановити + + • %1$s + Запитувані дозволи + Цьому плагіну не потрібні спеціальні дозволи. + Встановлений і новий + Встановлений + Новий + API %1$s → %2$s + Постійне сховище + Читання й запис даних на диск + Мережеві запити + Надсилання й отримання даних через інтернет + WebView + Показ вебвмісту всередині застосунку + Груповий джем + MQTT-брокер + Хост брокера + %1$s:%2$d + Порт брокера + Використовувати TLS + Ім’я користувача (необов’язково) + Пароль (необов’язково) + Префікс ID клієнта + Використовується для ідентифікації цього екземпляра Spotube + Шифрувати підключення до брокера + Перевірте поточні налаштування брокера перед створенням кімнати. + Перевірити підключення + Перевірка… + Підключено за %1$d мс + Помилка: %1$s + Тимчасовий брокер — налаштуйте власний, щоб розміщувати джеми самостійно + Підключення + Облікові дані + Ідентифікація + Діагностика + + + Груповий джем + Створити + Приєднатися + Створіть кімнату й поділіться її кодом із друзями. + Створити кімнату + Увійти в кімнату + Підключення… + Код кімнати + ABC123 + Брокер: %1$s + Налаштуйте брокер у налаштуваннях, щоб створити кімнату або приєднатися до неї. + Налаштування брокера + Підключено + Повторне підключення… + Копіювати код + Скопійовано! + Поділитися + Учасники (%1$d) + Організатор + Слухач + Чекаємо на слухачів… + Видалити + Заблокувати + Керувати + Вийти з кімнати + Не вдалося підключитися + Закрити + Не вдалося створити джем + Не вдалося приєднатися до джему + Введіть код кімнати з шести літер і цифр. + Приєднуйся до мого джему в Spotube за кодом: %1$s + + Перейти до альбому + Таймер сну + Відомості про трек + Відтворити + Пауза + Зараз нічого не грає + Додати до черги + + Пропустити + Оновити + Доступний Spotube %1$s + Дізнайтеся, що нового в цій версії, на сторінці завантажень Spotube. + Доступний %1$s %2$s + Встановлена версія: %1$s + Дізнайтеся, що нового в цьому оновленні, на сторінці плагіна. + + Пропустити + Далі + Назад + Почати + Spotube + Музика без зайвих умов. + Плеєр з відкритим кодом для цілого світу музики.\nДосліджуйте його. Зробіть його своїм. + Допоможіть проєкту жити. + Spotube створюють люди, а не бізнес на підписках. Якщо він став частиною вашого життя, навіть невелика підтримка допоможе нам приділяти йому ту увагу, на яку він заслуговує.\n\nДякуємо, що знаходите місце для незалежного софту. + Почніть із плагіна каталогу, щоб знаходити треки й плейлисти. Виберіть аудіоплагін для їх відтворення. Тексти пісень та історія прослуховувань — за бажанням. Знайдіть плагін нижче, перегляньте його дозволи, встановіть і виберіть тут. + Огляд плагінів + Ваші джерела + Повторити + Інші плагіни + Нових плагінів не знайдено. Плагін також можна встановити за посиланням або з файлу .smplug вище. + Каталог плагінів зараз недоступний. Повторіть спробу або встановіть плагін за посиланням чи з файлу. + Вітаємо + Підтримка + Плагіни + Посібник + + Як користуватися Spotube + Чотири прості кроки — і музика вже грає. + Вхід + Підключіть акаунт Spotify або виберіть власний плагін каталогу. + Шукайте й слухайте + Знайдіть будь-який трек, альбом чи виконавця й натисніть «Відтворити». + Слухайте офлайн + Завантажуйте треки, щоб слухати їх без інтернету. + Стежте за текстом + Відкривайте синхронізований текст у плеєрі під час відтворення. + + + Ваша музика. Ваші джерела. Ваші правила. + Безкоштовний музичний плеєр з відкритим кодом від спільноти, яка просто любить музику. + Відкритий код + Без реклами + Без телеметрії + На плагінах + Поїхали + + + Нас підтримують слухачі + Створено з любов’ю. Живе завдяки вам. + Spotube на 100% безкоштовний, вільний і відкритий — без реклами, підписок і стеження. Кожна функція існує завдяки таким людям, як ви, які допомагають проєкту розвиватися. + Patreon + Щомісячна підтримка + Підтримайте постійну розробку та інфраструктуру + Buy Me a Coffee + Разова кава + Підбадьорте нічну сесію програмування невеликим донатом + Open Collective + Фонд спільноти + Прозоре фінансування під керуванням спільноти + Щиро дякуємо, що вірите у відкриту музику. + Підтримка повністю добровільна — Spotube у будь-якому разі залишиться безкоштовним. + Підтримати + Відкрити %1$s + + + Підтримати Spotube + Підтримайте Spotube + Spotube безкоштовний, вільний і відкритий — без реклами, підписок і стеження. Якщо він приносить вам радість, підтримайте проєкт. + Ці нагадування можна вимкнути в розділі «Налаштування → Підтримка». + Можливо, пізніше + Нагадування про підтримку + Раз на 7 днів ненав’язливо нагадувати про підтримку проєкту. + + + Модульна архітектура + Виберіть, звідки брати музику. + У Spotube каталог, стримінг аудіо й синхронізовані тексти працюють окремо. Налаштуйте рушії нижче або змінюйте їх будь-коли. + Рушій метаданих + Відповідає за пошук музики, списки треків і плейлисти. + Аудіорушій + Отримує й декодує аудіопотоки для відтворення. + Рушій текстів + Показує синхронізовані караоке-тексти в реальному часі. + Рушій скроблінгу + Записує історію прослуховувань у Last.fm або ListenBrainz. + Активний + Вбудований + Готовий + Необов’язково + Виберіть рушій + Встановлені рушії + Зробити основним + NewPipe YouTube і LRCLib Lyrics вбудовані й працюють одразу після встановлення. + Керувати плагінами спільноти й додавати нові можна будь-коли в розділі «Налаштування → Плагіни». + + + Ретромодерн-архітектура + Чистий Hi-Fi без посередників + Ось як Spotube організовує децентралізоване відтворення просто на вашому пристрої. + НАПРЯМУ, P2P • 256 KBPS OPUS + Синя година + Spotube · демонстрація плеєра + Трохи музики для миттєвостей поміж справ. + 1. Децентралізований пошук + Запити надходять безпосередньо до вибраного плагіна каталогу. Spotube ніколи не аналізує ваші смаки. + 2. Пряме отримання потоку + Аудіо надходить безпосередньо із серверів контенту на ваш пристрій. Жодних проксі та віддаленого збору даних. + 3. Локальна обробка й відтворення + Декодування й кешування на пристрої, еквалайзер і завантаження для офлайн-прослуховування за бажанням. + 4. Синхронні тексти й джем + Караоке-тексти стежать за відтворенням, а ще є віддалене керування через LAN і груповий джем. + Жодних серверів-посередників. Відтворення відбувається просто на вашому пристрої — приватність за замовчуванням. + + + Виберіть пристрій + %1$s: «%2$s» + Відтворити + Додати до черги + Відтворити наступним + Цей пристрій + На цьому пристрої + Віддалений пристрій + Віддалений пристрій (%1$s) + На підключеному пристрої + + + Не вдалося підключитися + З’єднання втрачено + Не вдалося надіслати + Помилка пошуку: %1$s + Не вдалося підключитися: %1$s + + + Пристрої + Оновити + Підключено до %1$s:%2$d + Від’єднатися + Підключення... + Пошук пристроїв у мережі... + Пристроїв не знайдено + Переконайтеся, що на іншому пристрої в налаштуваннях увімкнено «Дозволити віддалене керування». + Невідомий пристрій + Визначення адреси... + ID: %1$s... + + + Віддалене керування + Від’єднано + Помилка підключення: %1$s + Обкладинка альбому + Невідомий трек + Невідомий виконавець + Перемотування + Перемішати + Попередній + Наступний + Режим повтору + Гучність + Видалити з черги + Інші дії + + + Де відтворити? + У вас активний джем. Виберіть, де відтворити цю добірку. + Відтворити тут + Запропонувати в джем + + + Брокер для джему не налаштовано + Не вказано хост брокера + Не вдалося підключитися + З’єднання втрачено + + + Брокер + anonymous + + + Додано до черги джему + + %1$d трек додано до черги джему + %1$d треки додано до черги джему + %1$d треків додано до черги джему + %1$d трека додано до черги джему + + + + Запит на віддалене керування + «%1$s» хоче керувати відтворенням на цьому пристрої. + Відхилити + Дозволити + Дозволяти завжди + + + Результати пошуку + Усі + Треки + Користувачі + Немає доступних типів пошуку + Почніть вводити запит + Почніть вводити, щоб знайти треки + Нічого не знайдено + Показати всі + Пошук + Завантаження треків... + Завантаження альбомів... + Завантаження виконавців... + Завантаження плейлистів... + Завантаження користувачів... + Альбомів не знайдено + Виконавців не знайдено + Плейлистів не знайдено + Користувачів не знайдено + Риємося в ящиках із платівками... + Шукаємо по всьому всесвіту... + Розігріваємо колонки... + Здуваємо пил із вінілу... + Радимося з богами музики... + Налаштовуємо антени... + Перебираємо платівки... + Не вдалося завантажити можливості пошуку + Не вдалося виконати пошук + Не вдалося знайти треки + Не вдалося знайти альбоми + Не вдалося знайти виконавців + Не вдалося знайти плейлисти + Не вдалося знайти користувачів + + + Огляд + + + Синхронний + Простий + Синхронізований текст недоступний + Текст недоступний + + + %1$s у %2$s + %1$d kbps + %1$s kHz • %2$d кан. + Плагін метаданих не вибрано + + + Назад + Повторити + Закрити вікно + Завантаження... + Невідомо + Невідома помилка + + + Користувач + Плейлист + + + Нам дуже прикро + Щось пішло не так. Нам важливі ваші враження, і ми робимо все, щоб це виправити. Спробуйте ще раз або перегляньте подробиці помилки нижче. + Подробиці помилки + Копіювати в буфер обміну + Показати повний журнал + Повний журнал помилки + + + Перемішати + Перемішати й слухати + Підписатися + Відписатися + Редагувати плейлист + Автор: %1$s + + + Дії з треком + Додати в джем + Запустити радіо + Відтворити наступним + Зробити наступним + Видалити з черги + Видалити з улюблених + Додати в улюблені + Додати в плейлист + Завантажити + Поділитися + + + Початковий порядок + Назва + Виконавець + Альбом + Тривалість + Сортування + Треків не знайдено + Фільтр + Дії з вибраними + Завантажити всі + Завантажити (%1$d) + Додати всі до черги + Додати до черги (%1$d) + Відтворити всі наступними + Відтворити наступними (%1$d) + Додати всі в плейлист + Додати в плейлист (%1$d) + Додати всі в джем + Додати в джем (%1$d) + і ще %1$d + + + Немає URL для завантаження + Назад в історії браузера + Уперед в історії браузера + + + Фільтр чорного списку... + Чорний список порожній + У чорному списку немає збігів із «%1$s» + Треки + Трек + Додати до чорного списку + Видалити з чорного списку + + + Немає метаданих треку + Не вдалося отримати URL потоку + Не вдалося отримати URL потоку. Спершу спробуйте відтворити трек. + + + Збережені треки + Ви + + + Ви підписані + Популярні треки + Слухати популярні треки + Додати популярні треки до черги + Схожі виконавці + Рекомендовані плейлисти + Не вдалося завантажити сторінку виконавця + Кількість підписників недоступна + Підписників: %1$s тис. + Підписників: %1$s млн + Підписників: %1$s млрд + + %1$d підписник + %1$d підписники + %1$d підписників + %1$d підписника + + + + Подобається + Більше не подобається + Назва треку-заглушки + Виконавці-заглушки + Невідомий альбом + Перемотування + Гучність + Увімкнути перемішування + Вимкнути перемішування + Попередній + Наступний + Відтворити або призупинити + Повтор вимкнено + Повтор треку + Повтор усіх + Вимкнути звук + Увімкнути звук + Інші дії + Параметри плеєра + Згорнути плеєр + Розгорнути текст + Зараз грає + Текст + Альтернативне джерело + Альтернативне джерело + Завантажити + Завантажено + Завантаження... + Уже завантажено + Повторне завантаження... + Завантаження: %1$s + Відтворення призупиниться через %1$s. + Призупинити відтворення через вибрану кількість хвилин. + Хвилини + Від 1 до 600 хвилин + Скасувати таймер + Запустити + URL відтворення отримано з іншими налаштуваннями стримінгу. Виділений потік відповідає вашим поточним налаштуванням; активний URL оновиться під час наступного запиту потоку. + Підібране джерело аудіо + Завантаження підібраного джерела… + Віддалене джерело не потрібне, поки відтворюється локальний кешований файл. + Відповідне джерело недоступне. + Для цього треку немає кешованого підібраного джерела аудіо. + Відомості про потоки ще не кешовано. Спершу відтворіть трек, щоб завантажити їх. + Не вдалося завантажити відомості про трек. + Доступні потоки + Цей трек відтворюється з локального кешу. Варіанти потоків зараз не використовуються. + Завантаження потоків… + Для цього джерела немає доступних потоків. + Вибраний потік + Відповідає вашим налаштуванням формату та якості стримінгу + Активний URL відтворення + З локального кешу + Трек із метаданих + Назва + ID + Альбом + Точність збігу + Зовнішній URI + ID альбому + Тип альбому + Дата виходу альбому + Треків в альбомі + Жанри альбому + Тривалість + Номер треку + Номер диска + Відвертий контент + Популярність + ISRC + URL мініатюр + + + Показати або сховати бічну панель + + + Альтернативні джерела + Зараз нічого не грає + Пошук альтернативних джерел... + Альтернативних джерел не знайдено + Збіг: %1$d% + Поточне джерело + + + Вигнати + Видалити учасника із сесії + Вигнати й заборонити повертатися + Видалити пропозиції + Видалити всі треки, які учасник додав до черги + Змінити порядок + Видалити з черги + %1$s • Додав(-ла) %2$s + + + Невідомо + Завантаження плейлиста... + Додати треки + Додати треки в плейлист + Пошук треків... + Пошук + Треків не знайдено + Знайдіть треки, щоб додати їх + Готово + Додати + Додано + + + Завантаження альбому... + + + Додати у ваш плейлист + Пошук + Пошук у ваших плейлистах... + Завантаження ваших плейлистів... + У вас поки немає власних плейлистів.\nСтворіть плейлист, щоб додавати до нього треки. + Новий плейлист + Редагувати плейлист + Створити + Назва + Назва плейлиста + Опис + Опис (необов’язково) + Публічний + Цей плейлист бачать усі + Спільний + Інші можуть додавати треки до цього плейлиста + Обкладинка + Обкладинка плейлиста + Немає зображення + Змінити зображення + Вибрати зображення + + + Встановити + Замінити + У zip-архіві не знайдено plugin.json + У zip-архіві не знайдено manifest.zipline.json + Неправильний формат plugin.json: %1$s + Не вдалося прочитати плагін: %1$s + Не вдалося встановити плагін: %1$s + Доступна новіша версія %1$s. Підтвердьте оновлення перед встановленням. + %1$s уже встановлено. Підтвердьте заміну перед встановленням. + Вбудовані плагіни вже включено, їх не можна додати повторно. + Вбудовані плагіни не можна видалити. + %1$s вказує недійсну apiVersion (%2$s). Очікується семантична версія, сумісна з %3$s. + Недійсна версія API плагінів Spotube: %1$s. + %1$s розрахований на API плагінів %2$s, а Spotube підтримує несумісну версію API %3$s. Встановіть плагін, зібраний для тієї самої основної версії API. + + + Несумісний API плагіна + Встановити плагін? + %1$s буде додано до встановлених плагінів. + Оновити плагін? + Встановлено старішу версію (%1$s). Оновити до %2$s? + Замінити встановлений плагін? + %1$s використовує недійсну семантичну версію (%2$s), тому її не можна порівняти як оновлення. Усе одно замінити встановлений плагін? + Встановлену версію (%1$s) не можна порівняти за правилами семантичного версіонування. Замінити її на %2$s? + Версію %1$s уже встановлено. Замінити наявний плагін новою копією? + Встановлена версія %1$s новіша за запропоновану версію %2$s. Усе одно замінити? + Замінити встановлений плагін новою копією? + + + %1$s: + Версія + Версія API + Автор + Ліцензія + Дозволи + Функції + Репозиторій + Контакти + Повідомити про помилку + Підтримати %1$s + + + Офіційний + Перевірений + Зірки на GitHub + URL репозиторію на GitHub + + + Релізів не знайдено + Виберіть реліз + Виберіть реліз + Попередній реліз + Примітки до випуску + + + Головна + Пошук + На пристрої + Підключення + Пристрої + + + Сховати вікно + Показати вікно + Наступний трек + Попередній трек + Перемішати + Повтор + Повтор: ВИМК + Повтор: ТРЕК + Повтор: УСІ + Гучність: %1$d% + Гучність + + Гучність - + Вимкнути звук + Увімкнути звук + Подобається + Більше не подобається + Вийти + + + Згорнути + Розгорнути + Відновити + Закрити + + + Служба відтворення + Відтворення + Керування відтворенням + + + Огляд + Зараз грає + Рекомендоване + Збережені треки + Популярні треки + diff --git a/composeApp/src/commonMain/composeResources/values-vi/strings.xml b/composeApp/src/commonMain/composeResources/values-vi/strings.xml new file mode 100644 index 00000000..4b81d8d0 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-vi/strings.xml @@ -0,0 +1,932 @@ + + + + Danh sách chờ + Xóa danh sách chờ + Lọc danh sách chờ... + Danh sách chờ trống + + Tìm bài hát, nghệ sĩ, album... + Xóa toàn bộ lịch sử + Tìm kiếm... + + + Thư viện của bạn + Thư viện + Danh sách phát + Album + Nghệ sĩ + Bài hát trên máy + Đã tải xuống + Tìm danh sách phát đã lưu… + Tìm album đã lưu… + Tìm nghệ sĩ đã lưu… + Tìm thư mục hoặc bài hát… + Lọc thư viện + Đóng bộ lọc + Tạo danh sách phát + Làm mới bài hát trên máy + Chuyển sang dạng lưới + Chuyển sang dạng danh sách + Chưa có danh sách phát nào được lưu + Không có danh sách phát nào khớp với “%1$s” + Chưa có album nào được lưu + Không có album nào khớp với “%1$s” + Chưa có nghệ sĩ nào được lưu + Không có nghệ sĩ nào khớp với “%1$s” + + + Thư mục + Thư mục + %1$s • %2$s + + %1$d bài hát + + + %1$d thư mục + + Quay lại thư mục + Phát thư mục + Cần quyền truy cập âm thanh + Cho phép truy cập âm thanh để duyệt nhạc trên thiết bị của bạn. + Cấp quyền + Không tìm thấy thư mục nào trên máy + Thư mục này không có bài hát nào + Không có thư mục hoặc bài hát nào khớp + Không thể làm mới nội dung trên máy + + + %1$d đang tải • %2$d đã xong + Xóa mục đã xong + Chưa có nội dung tải xuống + Đang tải xuống %1$d% + Đang chờ + Đã xong + Đã hủy + Thất bại + Thất bại: %1$s + Hủy tải xuống + Thử tải lại + Xóa mục tải xuống + + + Nghệ sĩ + Đã chặn + Bài hát đã thích + Bài hát bạn đã lưu + Nghệ sĩ không xác định + Tên bài hát + Tên nghệ sĩ + + Đã chọn + + Cài đặt + + Plugin + Ngôn ngữ và khu vực + Giao diện + Phát lại + Tải xuống + Bộ nhớ đệm + Máy tính + Cập nhật + Ủng hộ + Giới thiệu + + Đóng + Lưu + Hủy + + Ngôn ngữ + Ngôn ngữ giao diện ứng dụng. Hiện tại: %1$s + Mặc định của hệ thống + Quốc gia + Quốc gia dùng cho nội dung và tính khả dụng. Hiện tại: %1$s + %1$s (%2$s) + + Chủ đề + Chọn cách Spotube khớp với giao diện hệ thống. Hiện tại: %1$s + Sáng + Tối + Theo hệ thống + + Bề mặt + Chất liệu nền của ứng dụng. Hiện tại: %1$s + Nước + Băng + Chrome + Acrylic + Kính + + Màu nhấn + Bảng màu hiện tại: %1$s + Màu nhấn + Chọn bảng màu nhấn và xem trước ở cả chủ đề sáng và tối. + Sáng + Tối + Xem trước + Nút + Xanh yêu tinh + Tím điện + Lam đại dương + Cam hoàng hôn + Vườn hồng + Xanh nửa đêm + Xám kim loại + + Giao diện & cảm nhận + Chất liệu bề mặt và màu nhấn. Hiện tại: %1$s · %2$s + Giao diện & cảm nhận + Chọn chất liệu bề mặt, sau đó chọn màu nhấn phù hợp. Mỗi lựa chọn đều là một sự kết hợp được tuyển chọn. + Bề mặt + Màu nhấn + Phụ + + Bảng màu + Bảng màu nhấn Millennium. Hiện tại: %1$s + Bảng màu + Chọn bảng màu nhấn Millennium và xem trước ở chế độ sáng và tối. + Bảng màu + Bảng màu Millennium và màu nhấn. Hiện tại: %1$s + Xanh lam + Cam + Xanh chanh + + Thư mục tải xuống tùy chỉnh + Thư mục hiện tại: %1$s + Thư mục hiện tại: thư mục nhạc mặc định + Để trống để dùng thư mục nhạc mặc định. + C:\Music\Spotube + Thư mục nhạc trên máy + Thư mục tùy chỉnh để quét trên máy tính. Đã thêm: %1$d + Chưa thêm thư mục tùy chỉnh nào + Thư mục tùy chỉnh cho thư viện nhạc trên máy của bạn. + Quản lý + Thêm thư mục + Xóa thư mục + Chưa thêm thư mục tùy chỉnh nào. + Xóa %1$s khỏi danh sách quét + Thư mục tải xuống và thư mục bộ nhớ đệm nhạc được tự động thêm vào. + Thư mục tải xuống của bạn được tự động thêm vào. + Thư mục bộ nhớ đệm nhạc được tự động thêm vào. + Đang lưu… + Không thể lưu thư mục. Các thay đổi của bạn vẫn còn đây; hãy thử lại. + + %1$d thư mục + + Định dạng nhạc tải xuống + Codec và container ưu tiên khi tải xuống. Hiện tại: %1$s + Chất lượng nhạc tải xuống + + Định dạng nhạc phát trực tuyến + Codec và container ưu tiên khi phát trực tuyến. Hiện tại: %1$s + Chất lượng nhạc phát trực tuyến + Hiện tại: %1$s + Bật bộ nhớ đệm nhạc + Lưu âm thanh đã phát trực tuyến trên máy để phát lại nhanh hơn. + Thư mục bộ nhớ đệm + Hiện tại: %1$s + Hiện tại: bộ nhớ đệm mặc định của ứng dụng + Giới hạn dung lượng bộ nhớ đệm + Không giới hạn + Giới hạn hiện tại: %1$d MB + Đặt dung lượng bộ nhớ đệm tối đa tính bằng MB. Nhập 0 hoặc để trống nếu không muốn giới hạn. + 500 + Dung lượng phải là 0 hoặc một số dương. + Bật phát liên tục + Tiếp tục phát với các bài hát được đề xuất khi danh sách chờ kết thúc. + Bật Connect + Cung cấp điều khiển phát từ xa qua tính năng Spotube Connect. + Cổng máy chủ proxy phát lại + Cho phép điều khiển từ xa + Cho phép các thiết bị khác trong cùng mạng điều khiển phát lại khi kết nối. + Tên thiết bị điều khiển từ xa + Tên mà các thiết bị khác thấy khi tìm thấy thiết bị này. Hiện tại: %1$s + Chọn một tên dễ nhận biết để hiển thị cho các thiết bị khác trong mạng khi chúng tìm thấy Spotube này. + VD: Spotube phòng khách + tên máy chủ của thiết bị + Cổng được máy chủ proxy phát lại sử dụng. Hiện tại: %1$d + Chọn một cổng từ 1 đến 65535. + 14769 + Nhập một số nguyên hợp lệ. + Cổng phải nằm trong khoảng từ 1 đến 65535. + + Danh sách chặn + Quản lý bài hát và nghệ sĩ bị chặn + + Thu nhỏ xuống khay hệ thống + Giữ Spotube chạy trong khay hệ thống khi thu nhỏ cửa sổ. + Discord Rich Presence + Hiển thị trạng thái đang phát trên Discord trên máy tính. + + Tự động kiểm tra cập nhật + Tìm các phiên bản Spotube mới trong nền. + + Quản lý plugin + Quản lý các plugin của bạn + Plugin %1$s mặc định + Siêu dữ liệu + Âm thanh + Lời bài hát + Scrobble + Chưa chọn plugin + Thay đổi + Chọn + Đã chọn + Bỏ chọn + Không có plugin nào + Plugin %1$s + Xóa + + Cấu hình + Trình quản lý plugin + Vui lòng nhập URL + URL phải bắt đầu bằng http:// hoặc https:// + Không thể tải plugin xuống + Chưa cài đặt plugin nào + Dán URL tải xuống hoặc chọn tệp ở trên. + + Đã cài đặt %1$d plugin + + Cài đặt plugin + https://example.com/plugin.smplug + Tải xuống + Cài đặt từ tệp (.smplug) + Đang dùng + v%1$s + Tích hợp sẵn + Gỡ plugin + Đăng nhập + Đăng xuất + Tải xuống từ URL + Cài đặt từ tệp + Cài đặt + + • %1$s + Quyền được yêu cầu + Plugin này không yêu cầu quyền đặc biệt nào. + Đã cài đặt và bản cung cấp + Đã cài đặt + Bản cung cấp + API %1$s → %2$s + Lưu trữ lâu dài + Đọc và ghi dữ liệu vào ổ đĩa + Yêu cầu mạng + Gửi và nhận dữ liệu qua internet + WebView + Hiển thị nội dung web trong ứng dụng + Jam nhóm + MQTT Broker + Máy chủ broker + %1$s:%2$d + Cổng broker + Dùng TLS + Tên người dùng (không bắt buộc) + Mật khẩu (không bắt buộc) + Tiền tố Client ID + Dùng để nhận diện Spotube này + Mã hóa kết nối tới broker + Kiểm tra cài đặt broker hiện tại trước khi tạo phòng. + Kiểm tra kết nối + Đang kiểm tra… + Đã kết nối trong %1$d ms + Thất bại: %1$s + Broker tạm thời — hãy cấu hình broker riêng để tự lưu trữ + Kết nối + Thông tin đăng nhập + Danh tính + Chẩn đoán + + + Jam nhóm + Tạo + Tham gia + Tạo phòng, sau đó chia sẻ mã phòng với bạn bè. + Tạo phòng + Vào phòng + Đang kết nối… + Mã phòng + ABC123 + Broker: %1$s + Hãy thiết lập broker trong Cài đặt để tạo hoặc tham gia phòng. + Cài đặt broker + Đã kết nối + Đang kết nối lại… + Sao chép mã + Đã sao chép! + Chia sẻ + Người tham gia (%1$d) + Chủ phòng + Người nghe + Đang chờ người nghe… + Xóa + Cấm + Quản lý + Rời phòng + Không thể kết nối + Bỏ qua + Không thể tạo phiên Jam + Không thể tham gia phiên Jam + Nhập mã phòng gồm sáu ký tự chữ và số. + Tham gia Jam Spotube của mình với mã: %1$s + + Đi tới album + Hẹn giờ tắt + Chi tiết bài hát + Phát + Tạm dừng + Không có gì đang phát + Thêm vào danh sách chờ + + Bỏ qua + Cập nhật + Đã có Spotube %1$s + Xem có gì mới trong bản phát hành này trên trang tải xuống Spotube. + Đã có %1$s %2$s + Đang cài đặt: %1$s + Xem có gì mới trong bản cập nhật này trên trang của plugin. + + Bỏ qua + Tiếp + Quay lại + Bắt đầu + Spotube + Âm nhạc, không ràng buộc. + Trình phát mã nguồn mở cho cả thế giới âm nhạc.\nTùy bạn khám phá. Tùy bạn biến nó thành của riêng mình. + Giúp dự án này tiếp tục sống. + Spotube được tạo ra bởi con người, không phải một doanh nghiệp thu phí thuê bao. Nếu Spotube trở thành một phần cuộc sống hằng ngày của bạn, một chút ủng hộ sẽ giúp chúng tôi chăm chút nó xứng đáng hơn.\n\nCảm ơn bạn đã dành chỗ cho phần mềm độc lập. + Bắt đầu với một plugin danh mục để tìm bài hát và danh sách phát. Chọn một plugin âm thanh để phát chúng. Lời bài hát và lịch sử nghe là tùy chọn. Hãy khám phá plugin bên dưới, xem lại quyền của nó, cài đặt rồi chọn nó tại đây. + Khám phá plugin + Nguồn của bạn + Thử lại + Thêm plugin + Không tìm thấy plugin mới. Bạn cũng có thể cài đặt từ liên kết hoặc tệp .smplug ở trên. + Hiện không thể khám phá plugin. Hãy thử lại hoặc cài đặt từ liên kết hay tệp. + Chào mừng + Ủng hộ + Plugin + Hướng dẫn + + Cách dùng Spotube + Bốn bước nhanh là bạn đã có thể nghe nhạc. + Đăng nhập + Kết nối tài khoản Spotify hoặc chọn plugin danh mục của riêng bạn. + Tìm kiếm và phát + Tìm bất kỳ bài hát, album hay nghệ sĩ nào rồi nhấn phát. + Nghe ngoại tuyến + Tải bài hát xuống để tiếp tục nghe khi không có kết nối. + Theo dõi lời bài hát + Mở lời bài hát đồng bộ từ trình phát khi bài hát đang phát. + + + Âm nhạc của bạn. Nguồn của bạn. Luật của bạn. + Trình phát nhạc miễn phí, mã nguồn mở, được xây dựng bởi một cộng đồng yêu âm nhạc. + Mã nguồn mở + Không quảng cáo + Không thu thập dữ liệu + Vận hành bằng plugin + Bắt đầu thôi + + + Người ủng hộ giúp chúng tôi tiếp bước + Tạo nên bằng đam mê. Duy trì nhờ bạn. + Spotube miễn phí 100%, tự do và mã nguồn mở — không quảng cáo, không thuê bao, không theo dõi. Mọi tính năng tồn tại là nhờ những người như bạn đã chọn giúp mã nguồn tiếp tục phát triển. + Patreon + Tài trợ hằng tháng + Hỗ trợ phát triển và hạ tầng lâu dài + Buy Me a Coffee + Một ly cà phê + Tiếp sức cho một buổi code đêm khuya bằng một khoản tip nhỏ + Open Collective + Quỹ cộng đồng + Hỗ trợ tài chính minh bạch, do cộng đồng quản lý + Từ tận đáy lòng, cảm ơn bạn đã tin vào âm nhạc mở. + Ủng hộ hoàn toàn là tùy chọn — Spotube vẫn luôn miễn phí. + Tài trợ + Mở %1$s + + + Ủng hộ Spotube + Ủng hộ Spotube + Spotube miễn phí, tự do và mã nguồn mở — không quảng cáo, không thuê bao, không theo dõi. Nếu Spotube mang lại niềm vui cho bạn, hãy cân nhắc ủng hộ dự án. + Bạn có thể tắt lời nhắc này trong Cài đặt → Ủng hộ. + Để sau + Lời nhắc ủng hộ + Hiển thị lời nhắc nhẹ nhàng về việc ủng hộ dự án mỗi 7 ngày. + + + Kiến trúc mô-đun + Chọn nơi lấy nhạc của bạn. + Spotube tách riêng việc khám phá danh mục, phát âm thanh trực tuyến và lời bài hát đồng bộ. Cấu hình các engine bên dưới hoặc chuyển đổi bất cứ lúc nào. + Engine siêu dữ liệu + Cung cấp khám phá nhạc, tìm kiếm, danh sách bài hát và danh sách phát. + Engine âm thanh + Lấy và giải mã luồng âm thanh để phát. + Engine lời bài hát + Truyền lời bài hát karaoke đồng bộ theo thời gian thực. + Engine scrobble + Ghi lại lịch sử nghe lên Last.fm hoặc ListenBrainz. + Đang dùng + Tích hợp sẵn + Sẵn sàng + Tùy chọn + Chọn một engine + Engine đã cài đặt + Đặt làm mặc định + NewPipe YouTube và LRCLib Lyrics được tích hợp sẵn và có thể phát ngay. + Quản lý hoặc thêm plugin cộng đồng bất cứ lúc nào trong Cài đặt → Plugin. + + + Kiến trúc cổ điển hiện đại + Hi-Fi thuần túy, không trung gian + Đây là cách Spotube điều phối việc phát nhạc phi tập trung ngay trên máy của bạn. + NGANG HÀNG TRỰC TIẾP • 256 KBPS OPUS + Giờ xanh + Spotube · minh họa trình phát + Một chút âm nhạc cho những khoảnh khắc xen giữa. + 1. Khám phá phi tập trung + Truy vấn được gửi thẳng tới plugin danh mục bạn chọn. Spotube không bao giờ lập hồ sơ sở thích của bạn. + 2. Phân giải luồng trực tiếp + Âm thanh được truyền trực tiếp từ máy chủ nội dung tới máy của bạn. Không qua proxy, không thu thập dữ liệu từ xa. + 3. DSP & phát lại cục bộ + Giải mã và lưu đệm cục bộ với bộ chỉnh âm và tùy chọn tải xuống ngoại tuyến. + 4. Lời bài hát đồng bộ & Jam + Lời karaoke bám theo thời điểm phát, cùng với điều khiển từ xa qua LAN và Jam nhóm. + Không có máy chủ trung gian. Việc phát nhạc diễn ra ngay trên thiết bị của bạn — riêng tư từ thiết kế. + + + Chọn thiết bị + %1$s: “%2$s” + Phát + Thêm vào danh sách chờ + Phát tiếp theo + Thiết bị này + Trên thiết bị này + Thiết bị từ xa + Thiết bị từ xa (%1$s) + Trên thiết bị đã kết nối + + + Kết nối thất bại + Mất kết nối + Gửi thất bại + Tìm thiết bị thất bại: %1$s + Không thể kết nối: %1$s + + + Thiết bị + Làm mới + Đã kết nối với %1$s:%2$d + Ngắt kết nối + Đang kết nối... + Đang tìm thiết bị trong mạng... + Không tìm thấy thiết bị nào + Hãy đảm bảo thiết bị kia đã bật \"Cho phép điều khiển từ xa\" trong cài đặt. + Thiết bị không xác định + Đang phân giải... + ID: %1$s... + + + Điều khiển từ xa + Đã ngắt kết nối + Lỗi kết nối: %1$s + Ảnh bìa album + Bài hát không xác định + Nghệ sĩ không xác định + Tua + Phát ngẫu nhiên + Trước + Tiếp + Chế độ lặp + Âm lượng + Xóa khỏi danh sách chờ + Tùy chọn khác + + + Phát ở đâu? + Bạn đang có một phiên Jam. Chọn nơi phát bộ sưu tập này. + Phát tại đây + Đề xuất cho Jam + + + Chưa cấu hình broker cho Jam + Máy chủ broker đang trống + Kết nối thất bại + Mất kết nối + + + Broker + ẩn danh + + + Đã thêm vào danh sách chờ của Jam + + Đã thêm %1$d bài hát vào danh sách chờ của Jam + + + + Yêu cầu điều khiển từ xa + \"%1$s\" muốn điều khiển việc phát trên thiết bị này. + Từ chối + Cho phép + Luôn cho phép + + + Kết quả tìm kiếm + Tất cả + Bài hát + Người dùng + Không có loại tìm kiếm nào + Nhập để bắt đầu tìm kiếm + Nhập để bắt đầu tìm bài hát + Không tìm thấy kết quả + Xem tất cả + Đang tìm kiếm + Đang tải bài hát... + Đang tải album... + Đang tải nghệ sĩ... + Đang tải danh sách phát... + Đang tải người dùng... + Không tìm thấy album nào + Không tìm thấy nghệ sĩ nào + Không tìm thấy danh sách phát nào + Không tìm thấy người dùng nào + Đang lục tìm kho đĩa... + Đang tìm khắp vũ trụ... + Đang làm nóng loa... + Đang phủi bụi đĩa than... + Đang hỏi ý các vị thần âm nhạc... + Đang dò ăng-ten... + Đang lật giở các đĩa nhạc... + Không thể tải khả năng tìm kiếm + Tìm kiếm thất bại + Không thể tìm bài hát + Không thể tìm album + Không thể tìm nghệ sĩ + Không thể tìm danh sách phát + Không thể tìm người dùng + + + Duyệt + + + Đồng bộ + Thường + Không có lời bài hát đồng bộ + Không có lời bài hát + + + %1$s trong %2$s + %1$d kbps + %1$s kHz • %2$d kênh + Chưa chọn plugin siêu dữ liệu + + + Quay lại + Thử lại + Đóng hộp thoại + Đang tải... + Không xác định + Lỗi không xác định + + + Người dùng + Danh sách phát + + + Chúng tôi thật sự xin lỗi + Đã có sự cố xảy ra. Trải nghiệm của bạn rất quan trọng với chúng tôi và chúng tôi đang cố gắng hết sức để khắc phục. Vui lòng thử lại hoặc xem chi tiết lỗi bên dưới. + Chi tiết lỗi + Sao chép vào bộ nhớ tạm + Xem nhật ký đầy đủ + Nhật ký lỗi đầy đủ + + + Phát ngẫu nhiên + Phát ngẫu nhiên + Theo dõi + Bỏ theo dõi + Sửa danh sách phát + Của %1$s + + + Tùy chọn bài hát + Thêm vào Jam + Phát radio + Phát tiếp theo + Chuyển lên phát tiếp theo + Xóa khỏi danh sách chờ + Xóa khỏi mục yêu thích + Lưu vào mục yêu thích + Thêm vào danh sách phát + Tải xuống + Chia sẻ + + + Mặc định + Tên + Nghệ sĩ + Album + Thời lượng + Sắp xếp + Không tìm thấy bài hát nào + Lọc + Thao tác hàng loạt + Tải xuống tất cả + Tải xuống %1$d + Thêm tất cả vào danh sách chờ + Thêm %1$d vào danh sách chờ + Phát tất cả tiếp theo + Phát %1$d bài tiếp theo + Thêm tất cả vào danh sách phát + Thêm %1$d vào danh sách phát + Thêm tất cả vào Jam + Thêm %1$d vào Jam + +%1$d người khác + + + Không có URL để tải + Quay lại trang trước + Đi tới trang sau + + + Lọc danh sách chặn... + Không có mục nào bị chặn + Không có mục bị chặn nào khớp với \'%1$s\' + Bài hát + Bài hát + Thêm vào danh sách chặn + Xóa khỏi danh sách chặn + + + Không có siêu dữ liệu bài hát + Không thể phân giải URL luồng + Không thể phân giải URL luồng. Hãy thử phát bài hát trước. + + + Bài hát đã lưu + Bạn + + + Đang theo dõi + Bài hát hàng đầu + Phát bài hát hàng đầu + Thêm bài hát hàng đầu vào danh sách chờ + Nghệ sĩ liên quan + Danh sách phát nổi bật + Không thể tải thông tin nghệ sĩ + Không có thông tin người theo dõi + %1$s N người theo dõi + %1$s Tr người theo dõi + %1$s T người theo dõi + + %1$d người theo dõi + + + + Thích + Bỏ thích + Tên bài hát trống + Nghệ sĩ trống + Album không xác định + Tua + Âm lượng + Bật phát ngẫu nhiên + Tắt phát ngẫu nhiên + Trước + Tiếp + Phát hoặc tạm dừng + Tắt lặp lại + Lặp lại một bài + Lặp lại tất cả + Tắt tiếng + Bật tiếng + Tùy chọn khác + Tùy chọn trình phát + Thu gọn trình phát + Mở rộng lời bài hát + Đang phát + Lời bài hát + Nguồn thay thế + Nguồn thay thế + Tải xuống + Đã tải xuống + Đang tải xuống... + Đã tải xuống trước đó + Đang thử tải lại... + Đang tải xuống %1$s + Nhạc sẽ tạm dừng sau %1$s. + Tạm dừng phát sau số phút đã chọn. + Phút + Chọn từ 1 đến 600 phút + Hủy hẹn giờ + Bắt đầu + URL phát được phân giải với cài đặt phát trực tuyến khác. Luồng được đánh dấu khớp với lựa chọn hiện tại của bạn; URL đang dùng sẽ được làm mới ở lần yêu cầu luồng tiếp theo. + Nguồn âm thanh khớp + Đang tải nguồn khớp… + Không cần nguồn từ xa khớp khi đang phát tệp đệm trên máy. + Không có nguồn khớp nào. + Chưa có nguồn âm thanh khớp nào được lưu đệm cho bài hát này. + Chi tiết luồng chưa được lưu đệm. Hãy phát bài hát trước để tải chúng. + Không thể tải chi tiết bài hát. + Các luồng khả dụng + Bài hát này đang được phát từ bộ nhớ đệm trên máy. Các biến thể luồng không được dùng cho lần phát này. + Đang tải luồng… + Không có luồng nào cho nguồn này. + Luồng đang được chọn + Khớp với định dạng & chất lượng phát trực tuyến bạn chọn + URL phát đang dùng + Phát từ bộ nhớ đệm trên máy + Bài hát theo siêu dữ liệu + Tên + ID + Album + Độ tin cậy + URI bên ngoài + ID album + Loại album + Ngày phát hành album + Số bài hát trong album + Thể loại album + Thời lượng + Số thứ tự bài hát + Số đĩa + Nội dung tục tĩu + Độ phổ biến + ISRC + URL ảnh thu nhỏ + + + Bật/tắt thanh bên + + + Nguồn thay thế + Hiện không có bài hát nào đang phát + Đang tìm nguồn thay thế... + Không tìm thấy nguồn thay thế nào + Độ tin cậy: %1$d% + Nguồn đang dùng + + + Mời ra + Xóa người này khỏi phiên + Mời ra và không cho tham gia lại + Xóa đề xuất + Xóa mọi bài hát họ đã thêm vào danh sách chờ + Sắp xếp lại + Xóa khỏi danh sách chờ + %1$s • Được thêm bởi %2$s + + + Không xác định + Đang tải danh sách phát... + Thêm bài hát + Thêm bài hát vào danh sách phát + Tìm bài hát... + Tìm kiếm + Không tìm thấy bài hát nào + Tìm bài hát để thêm + Xong + Thêm + Đã thêm + + + Đang tải album... + + + Thêm vào danh sách phát của bạn + Tìm kiếm + Tìm danh sách phát của bạn... + Đang tải danh sách phát của bạn... + Bạn chưa có danh sách phát nào.\nHãy tạo một danh sách để thêm bài hát. + Tạo danh sách phát + Sửa danh sách phát + Tạo + Tên + Tên danh sách phát + Mô tả + Mô tả (không bắt buộc) + Công khai + Ai cũng có thể xem danh sách phát này + Cộng tác + Người khác có thể thêm bài hát vào danh sách phát này + Ảnh bìa + Ảnh bìa danh sách phát + Không có ảnh + Đổi ảnh + Chọn ảnh + + + Cài đặt + Thay thế + Không tìm thấy plugin.json trong tệp zip + Không tìm thấy manifest.zipline.json trong tệp zip + Định dạng plugin.json không hợp lệ: %1$s + Không thể đọc plugin: %1$s + Không thể cài đặt plugin: %1$s + Đã có phiên bản mới hơn của %1$s. Hãy xác nhận cập nhật trước khi cài đặt. + %1$s đã được cài đặt. Hãy xác nhận thay thế trước khi cài đặt. + Plugin tích hợp sẵn đã có trong ứng dụng và không thể thêm lại. + Không thể gỡ plugin tích hợp sẵn. + %1$s khai báo apiVersion không hợp lệ (%2$s). Cần phiên bản theo chuẩn semantic versioning tương thích với %3$s. + Phiên bản API plugin Spotube %1$s không hợp lệ. + %1$s nhắm tới API plugin %2$s, nhưng Spotube hỗ trợ API plugin %3$s có thay đổi không tương thích. Hãy cài đặt plugin được xây dựng cho cùng phiên bản API chính. + + + API plugin không tương thích + Cài đặt plugin? + %1$s sẽ được thêm vào danh sách plugin đã cài đặt. + Cập nhật plugin? + Đang cài đặt phiên bản cũ hơn (%1$s). Cập nhật lên %2$s? + Thay thế plugin đã cài đặt? + %1$s dùng phiên bản semantic không hợp lệ (%2$s) nên không thể so sánh như một bản cập nhật. Vẫn thay thế plugin đã cài đặt? + Không thể so sánh phiên bản đã cài đặt (%1$s) theo semantic versioning. Thay thế bằng %2$s? + Phiên bản %1$s đã được cài đặt. Thay thế plugin hiện có bằng bản được cung cấp? + Phiên bản đã cài đặt %1$s mới hơn phiên bản được cung cấp %2$s. Vẫn thay thế? + Thay thế plugin đã cài đặt bằng bản được cung cấp? + + + %1$s: + Phiên bản + Phiên bản API + Tác giả + Giấy phép + Khả năng + Chức năng + Kho mã + Liên hệ + Báo lỗi + Ủng hộ %1$s + + + Chính thức + Đã xác minh + Sao GitHub + URL kho mã GitHub + + + Không tìm thấy bản phát hành nào + Chọn bản phát hành + Chọn một bản phát hành + Bản thử nghiệm + Ghi chú phát hành + + + Trang chủ + Tìm kiếm + Trên thiết bị + Kết nối + Thiết bị + + + Ẩn cửa sổ + Hiện cửa sổ + Bài tiếp theo + Bài trước + Phát ngẫu nhiên + Lặp lại + Lặp lại: TẮT + Lặp lại: MỘT BÀI + Lặp lại: TẤT CẢ + Âm lượng: %1$d% + Âm lượng + + Âm lượng - + Tắt tiếng + Bật tiếng + Thích bài hát + Bỏ thích bài hát + Thoát + + + Thu nhỏ + Phóng to + Khôi phục + Đóng + + + Dịch vụ phát nhạc + Phát nhạc + Điều khiển phát nội dung + + + Duyệt + Đang phát + Nổi bật + Bài hát đã lưu + Bài hát hàng đầu + diff --git a/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml new file mode 100644 index 00000000..6ab50884 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -0,0 +1,932 @@ + + + + 待播清單 + 清除待播清單 + 篩選待播清單... + 待播清單中沒有項目 + + 搜尋歌曲、藝人、專輯... + 清除所有紀錄 + 搜尋... + + + 你的音樂庫 + 音樂庫 + 播放清單 + 專輯 + 藝人 + 本機曲目 + 下載 + 搜尋已儲存的播放清單… + 搜尋已儲存的專輯… + 搜尋已儲存的藝人… + 搜尋資料夾或曲目… + 篩選音樂庫 + 關閉篩選 + 建立播放清單 + 重新整理本機曲目 + 切換為格狀檢視 + 切換為清單檢視 + 沒有已儲存的播放清單 + 沒有符合「%1$s」的播放清單 + 沒有已儲存的專輯 + 沒有符合「%1$s」的專輯 + 沒有已儲存的藝人 + 沒有符合「%1$s」的藝人 + + + 資料夾 + 資料夾 + %1$s • %2$s + + %1$d 首曲目 + + + %1$d 個資料夾 + + 返回資料夾 + 播放資料夾 + 需要音訊存取權限 + 允許存取音訊,即可瀏覽裝置上的音樂。 + 授予權限 + 找不到本機資料夾 + 此資料夾中沒有曲目 + 沒有符合的資料夾或曲目 + 無法重新整理本機媒體 + + + %1$d 個進行中 • %2$d 個已完成 + 清除已完成項目 + 尚無下載項目 + 下載中 %1$d% + 等待中 + 已完成 + 已取消 + 失敗 + 失敗:%1$s + 取消下載 + 重試下載 + 移除下載項目 + + + 藝人 + 已封鎖 + 喜歡的曲目 + 你已儲存的歌曲 + 未知藝人 + 曲目名稱 + 藝人名稱 + + 已選取 + + 設定 + + 外掛程式 + 語言與地區 + 外觀 + 播放 + 下載 + 快取 + 桌面版 + 更新 + 支持 + 關於 + + 關閉 + 儲存 + 取消 + + 語言 + 應用程式介面的語言。目前:%1$s + 跟隨系統 + 國家/地區 + 用於內容與可用性的國家/地區。目前:%1$s + %1$s(%2$s) + + 主題 + 選擇 Spotube 如何配合系統外觀。目前:%1$s + 淺色 + 深色 + 跟隨系統 + + 表面材質 + 應用程式的背景材質。目前:%1$s + 水波 + 冰晶 + 金屬鉻 + 壓克力 + 玻璃 + + 強調色 + 目前色盤:%1$s + 強調色 + 選擇強調色盤,並在淺色與深色主題中預覽效果。 + 淺色 + 深色 + 預覽 + 按鈕 + 妖精綠 + 電光紫 + 海洋青 + 夕陽橘 + 玫瑰園 + 午夜藍 + 金屬石板灰 + + 外觀與風格 + 表面材質與強調色。目前:%1$s · %2$s + 外觀與風格 + 先選擇表面材質,再挑選與之搭配的強調色。每個選項都是精心調配的組合。 + 表面材質 + 強調色 + 次要 + + 色盤 + Millennium 強調色盤。目前:%1$s + 色盤 + 選擇 Millennium 強調色盤,並在淺色與深色模式中預覽。 + 色盤 + Millennium 色盤與強調色。目前:%1$s + 藍色 + 橘色 + 萊姆綠 + + 自訂下載資料夾 + 目前資料夾:%1$s + 目前資料夾:預設音樂資料夾 + 留空即使用預設音樂資料夾。 + C:\Music\Spotube + 本機媒體資料夾 + 桌面版要掃描的自訂資料夾。已新增:%1$d + 尚未新增自訂資料夾 + 本機音樂庫的自訂資料夾。 + 管理 + 新增資料夾 + 移除資料夾 + 尚未新增自訂資料夾。 + 從掃描清單中移除 %1$s + 下載資料夾與音樂快取資料夾會自動納入。 + 你的下載資料夾會自動納入。 + 音樂快取資料夾會自動納入。 + 儲存中… + 無法儲存資料夾。你的變更仍保留在此,請再試一次。 + + %1$d 個資料夾 + + 下載音樂格式 + 下載時偏好的編碼與封裝格式。目前:%1$s + 下載音樂品質 + + 串流音樂格式 + 串流音樂時偏好的編碼與封裝格式。目前:%1$s + 串流音樂品質 + 目前:%1$s + 啟用音樂快取 + 將串流音訊儲存在本機,加快重複播放的速度。 + 快取資料夾 + 目前:%1$s + 目前:預設應用程式快取 + 快取大小上限 + 無上限 + 目前上限:%1$d MB + 設定快取大小上限(MB)。輸入 0 或留空表示無上限。 + 500 + 大小必須為 0 或正數。 + 啟用無限播放 + 待播清單播完後,以推薦曲目繼續播放。 + 啟用 Connect + 透過 Spotube Connect 功能提供遠端播放控制。 + 播放代理伺服器連接埠 + 允許遠端控制 + 讓同一網路中的其他裝置在連線後控制播放。 + 遠端控制裝置名稱 + 其他裝置搜尋到此裝置時所顯示的名稱。目前:%1$s + 選擇一個好記的名稱,讓網路上的其他裝置找到這個 Spotube 時顯示。 + 例如:客廳的 Spotube + 裝置主機名稱 + 播放代理伺服器使用的連接埠。目前:%1$d + 請選擇 1 到 65535 之間的連接埠。 + 14769 + 請輸入有效的整數。 + 連接埠必須介於 1 到 65535 之間。 + + 封鎖清單 + 管理已封鎖的曲目與藝人 + + 最小化至系統匣 + 視窗最小化時,讓 Spotube 在系統匣中繼續執行。 + Discord 動態狀態 + 在桌面版的 Discord 中顯示目前播放狀態。 + + 自動檢查更新 + 在背景檢查 Spotube 的新版本。 + + 管理外掛程式 + 管理你的外掛程式 + 預設%1$s外掛程式 + 中繼資料 + 音訊 + 歌詞 + 收聽紀錄 + 未選取外掛程式 + 變更 + 選取 + 已選取 + 清除選取 + 沒有可用的外掛程式 + %1$s 外掛程式 + 清除 + + 設定 + 外掛程式管理員 + 請輸入 URL + URL 必須以 http:// 或 https:// 開頭 + 無法下載外掛程式 + 尚未安裝任何外掛程式 + 請貼上下載 URL,或從上方選擇檔案。 + + 已安裝 %1$d 個外掛程式 + + 安裝外掛程式 + https://example.com/plugin.smplug + 下載 + 從檔案安裝(.smplug) + 使用中 + v%1$s + 內建 + 移除外掛程式 + 登入 + 登出 + 從 URL 下載 + 從檔案安裝 + 安裝 + + • %1$s + 要求的權限 + 此外掛程式不需要任何特殊權限。 + 已安裝與提供版本比較 + 已安裝 + 提供的版本 + API %1$s → %2$s + 永久儲存空間 + 讀取及寫入磁碟資料 + 網路要求 + 透過網際網路傳送及接收資料 + WebView + 在應用程式內顯示網頁內容 + 群組 Jam + MQTT 代理伺服器 + 代理伺服器主機 + %1$s:%2$d + 代理伺服器連接埠 + 使用 TLS + 使用者名稱(選填) + 密碼(選填) + 用戶端 ID 前綴 + 用於識別這個 Spotube + 加密與代理伺服器的連線 + 在建立房間前檢查目前的代理伺服器設定。 + 測試連線 + 測試中… + 已連線,耗時 %1$d 毫秒 + 失敗:%1$s + 預留的代理伺服器 — 請設定你自己的伺服器以自行架設 + 連線 + 憑證 + 身分 + 診斷 + + + 群組 Jam + 建立 + 加入 + 建立房間,然後將代碼分享給朋友。 + 建立房間 + 加入房間 + 連線中… + 房間代碼 + ABC123 + 代理伺服器:%1$s + 請先在「設定」中設定代理伺服器,才能建立或加入房間。 + 代理伺服器設定 + 已連線 + 重新連線中… + 複製代碼 + 已複製! + 分享 + 參與者(%1$d) + 主持人 + 聽眾 + 正在等待聽眾加入… + 移除 + 封鎖 + 管理 + 離開房間 + 無法連線 + 關閉 + 無法建立 Jam + 無法加入 Jam + 請輸入由英文字母和數字組成的六位房間代碼。 + 用代碼 %1$s 加入我的 Spotube Jam + + 前往專輯 + 睡眠定時器 + 曲目詳細資料 + 播放 + 暫停 + 目前沒有播放內容 + 加入待播清單 + + 略過 + 更新 + Spotube %1$s 已推出 + 前往 Spotube 下載頁面查看此版本的新功能。 + %1$s %2$s 已推出 + 目前安裝版本:%1$s + 前往外掛程式頁面查看此更新的新功能。 + + 略過 + 下一步 + 上一步 + 開始使用 + Spotube + 音樂,無拘無束。 + 一款開放原始碼的播放器,帶你走進音樂的世界。\n任你探索,由你打造。 + 讓這個專案延續下去。 + Spotube 是由一群人打造的,而不是靠訂閱賺錢的企業。如果它已成為你日常生活的一部分,一點點支持就能幫助我們給予它應有的照顧。\n\n感謝你為獨立軟體留出一席之地。 + 先從目錄外掛程式開始,用來尋找歌曲與播放清單。接著選擇音訊外掛程式來播放。歌詞與收聽紀錄為選用項目。在下方探索外掛程式,檢視其權限並安裝,然後在這裡選取。 + 探索外掛程式 + 你的來源 + 再試一次 + 更多外掛程式 + 找不到新的外掛程式。你也可以從上方透過連結或 .smplug 檔案安裝。 + 目前無法探索外掛程式。請再試一次,或透過連結或檔案安裝。 + 歡迎 + 支持 + 外掛程式 + 指南 + + 如何使用 Spotube + 只要四個簡單步驟,就能開始聆聽。 + 登入 + 連結你的 Spotify 帳戶,或選擇你自己的目錄外掛程式。 + 搜尋並播放 + 找到任何歌曲、專輯或藝人,然後按下播放。 + 離線聆聽 + 下載曲目,沒有網路也能繼續播放。 + 跟著歌詞唱 + 播放曲目時,從播放器開啟同步歌詞。 + + + 你的音樂,你的來源,你的規則。 + 一款免費、開放原始碼的音樂播放器,由熱愛音樂的社群打造。 + 開放原始碼 + 無廣告 + 無遙測 + 外掛程式驅動 + 出發吧 + + + 支持者讓我們走下去 + 以熱情打造,因你而延續。 + Spotube 百分之百免費、自由且開放原始碼 — 沒有廣告、沒有訂閱、沒有追蹤。每一項功能的存在,都是因為像你這樣的人選擇支持程式碼持續前進。 + Patreon + 每月贊助 + 支持持續的開發與基礎設施 + Buy Me a Coffee + 請喝一杯咖啡 + 小小打賞,為深夜寫程式加點油 + Open Collective + 社群基金 + 透明、由社群治理的資金支持 + 衷心感謝你相信開放的音樂。 + 支持完全是自願的 — 無論如何,Spotube 都會保持免費。 + 贊助 + 開啟 %1$s + + + 支持 Spotube + 支持 Spotube + Spotube 免費、自由且開放原始碼 — 沒有廣告、沒有訂閱、沒有追蹤。如果它為你帶來快樂,歡迎考慮支持這個專案。 + 你可以在「設定 → 支持」中關閉這些提醒。 + 稍後再說 + 支持提醒 + 每 7 天顯示一次溫和的提醒,邀請你支持這個專案。 + + + 模組化架構 + 選擇你的音樂來源。 + Spotube 將目錄探索、音訊串流與同步歌詞分開處理。在下方設定你的引擎,或隨時切換。 + 中繼資料引擎 + 負責音樂探索、搜尋、曲目清單與播放清單。 + 音訊引擎 + 取得並解碼要播放的音訊串流。 + 歌詞引擎 + 即時串流與時間同步的卡拉 OK 歌詞。 + 收聽紀錄引擎 + 將收聽紀錄傳送至 Last.fm 或 ListenBrainz。 + 使用中 + 內建 + 就緒 + 選用 + 選擇引擎 + 已安裝的引擎 + 設為預設 + NewPipe YouTube 與 LRCLib Lyrics 皆為內建,安裝後即可直接串流。 + 隨時可在「設定 → 外掛程式」中管理或新增社群外掛程式。 + + + 復古摩登架構 + 純粹 Hi-Fi,沒有中間人 + 以下說明 Spotube 如何直接在你的裝置上協調去中心化的播放。 + 直接點對點 • 256 KBPS OPUS + 藍調時刻 + Spotube · 播放器示範 + 為片刻之間的時光,送上一點音樂。 + 1. 去中心化探索 + 查詢會直接送到你選擇的目錄外掛程式。Spotube 絕不分析你的喜好。 + 2. 直接解析串流 + 音訊直接從內容伺服器串流到你的裝置。沒有代理轉送,也沒有遠端資料蒐集。 + 3. 本機 DSP 與播放 + 在本機解碼並快取,提供等化器控制及選用的離線下載。 + 4. 同步歌詞與 Jam + 卡拉 OK 歌詞跟隨播放時間軸,並支援 LAN 遠端控制與群組 Jam。 + 沒有中間伺服器。播放直接在你的裝置上進行 — 隱私從設計開始。 + + + 選擇裝置 + %1$s:「%2$s」 + 播放 + 加入待播清單 + 下一首播放 + 這部裝置 + 在這部裝置上 + 遠端裝置 + 遠端裝置(%1$s) + 在已連線的裝置上 + + + 連線失敗 + 連線中斷 + 傳送失敗 + 搜尋失敗:%1$s + 無法連線:%1$s + + + 裝置 + 重新整理 + 已連線至 %1$s:%2$d + 中斷連線 + 連線中... + 正在搜尋網路上的裝置... + 找不到裝置 + 請確認另一部裝置已在設定中啟用「允許遠端控制」。 + 未知裝置 + 解析中... + ID:%1$s... + + + 遠端控制 + 已中斷連線 + 連線錯誤:%1$s + 專輯封面 + 未知曲目 + 未知藝人 + 播放進度 + 隨機播放 + 上一首 + 下一首 + 重複播放模式 + 音量 + 從待播清單移除 + 更多選項 + + + 要在哪裡播放? + 你有一個進行中的 Jam。請選擇要在哪裡播放這個合輯。 + 在這裡播放 + 推薦到 Jam + + + 尚未設定 Jam 代理伺服器 + 代理伺服器主機為空白 + 連線失敗 + 連線中斷 + + + 代理伺服器 + anonymous + + + 已加入 Jam 待播清單 + + 已將 %1$d 首加入 Jam 待播清單 + + + + 遠端控制要求 + 「%1$s」想要控制這部裝置上的播放。 + 拒絕 + 允許 + 一律允許 + + + 搜尋結果 + 全部 + 曲目 + 使用者 + 沒有可用的搜尋類型 + 輸入文字開始搜尋 + 輸入文字開始搜尋曲目 + 找不到結果 + 查看全部 + 搜尋中 + 正在載入曲目... + 正在載入專輯... + 正在載入藝人... + 正在載入播放清單... + 正在載入使用者... + 找不到專輯 + 找不到藝人 + 找不到播放清單 + 找不到使用者 + 正在翻找唱片箱... + 正在搜尋宇宙... + 正在暖機喇叭... + 正在擦拭黑膠唱片... + 正在請教音樂之神... + 正在調整天線... + 正在翻閱唱片... + 無法載入搜尋功能 + 搜尋失敗 + 無法搜尋曲目 + 無法搜尋專輯 + 無法搜尋藝人 + 無法搜尋播放清單 + 無法搜尋使用者 + + + 瀏覽 + + + 同步 + 純文字 + 沒有可用的同步歌詞 + 沒有可用的純文字歌詞 + + + %1$s(%2$s) + %1$d kbps + %1$s kHz • %2$d 聲道 + 未選取中繼資料外掛程式 + + + 返回 + 重試 + 關閉對話框 + 載入中... + 未知 + 未知錯誤 + + + 使用者 + 播放清單 + + + 我們深感抱歉 + 目前有些功能無法正常運作。你的使用體驗對我們很重要,我們正盡全力修正。請再試一次,或查看下方的錯誤詳細資料。 + 錯誤詳細資料 + 複製到剪貼簿 + 檢視完整記錄 + 完整錯誤記錄 + + + 隨機播放 + 隨機播放 + 追蹤 + 取消追蹤 + 編輯播放清單 + 由 %1$s 建立 + + + 曲目選項 + 加入 Jam + 開始播放電台 + 下一首播放 + 移至下一首 + 從待播清單移除 + 從最愛中移除 + 加入最愛 + 加入播放清單 + 下載 + 分享 + + + 原始順序 + 標題 + 藝人 + 專輯 + 長度 + 排序 + 找不到曲目 + 篩選 + 批次操作 + 全部下載 + 下載 %1$d 首 + 全部加入待播清單 + 將 %1$d 首加入待播清單 + 全部設為下一首播放 + 將 %1$d 首設為下一首播放 + 全部加入播放清單 + 將 %1$d 首加入播放清單 + 全部加入 Jam + 將 %1$d 首加入 Jam + +%1$d 位 + + + 沒有可載入的 URL + 返回上一頁 + 前往下一頁 + + + 篩選封鎖清單... + 沒有已封鎖的項目 + 沒有符合「%1$s」的已封鎖項目 + 曲目 + 曲目 + 加入封鎖清單 + 從封鎖清單移除 + + + 沒有曲目中繼資料 + 無法解析串流 URL + 無法解析串流 URL。請先嘗試播放此曲目。 + + + 已儲存的曲目 + 你 + + + 追蹤中 + 熱門曲目 + 播放熱門曲目 + 將熱門曲目加入待播清單 + 相關藝人 + 精選播放清單 + 無法載入藝人總覽 + 無法取得追蹤者人數 + %1$sK 位追蹤者 + %1$sM 位追蹤者 + %1$sB 位追蹤者 + + %1$d 位追蹤者 + + + + 喜歡 + 取消喜歡 + 無曲目標題 + 無藝人 + 未知專輯 + 播放進度 + 音量 + 開啟隨機播放 + 關閉隨機播放 + 上一首 + 下一首 + 播放或暫停 + 重複播放:關閉 + 重複播放:單曲 + 重複播放:全部 + 靜音 + 取消靜音 + 更多選項 + 播放器選項 + 收合播放器 + 展開歌詞 + 現正播放 + 歌詞 + 替代來源 + 替代來源 + 下載 + 已下載 + 下載中... + 已經下載過了 + 正在重試下載... + 正在下載 %1$s + 播放將在 %1$s 後暫停。 + 在所選的分鐘數後暫停播放。 + 分鐘 + 請選擇 1 到 600 分鐘 + 取消定時器 + 開始 + 播放 URL 是以不同的串流設定解析的。醒目標示的串流符合你目前的偏好;使用中的 URL 會在下次要求串流時更新。 + 配對的音訊來源 + 正在載入配對的來源… + 播放本機快取檔案時,不需要配對的遠端來源。 + 沒有可用的配對來源。 + 此曲目沒有已快取的配對音訊來源。 + 串流詳細資料尚未快取。請先播放此曲目以載入資料。 + 無法載入曲目詳細資料。 + 可用的串流 + 此曲目正從本機快取播放,本次播放不會使用串流變體。 + 正在載入串流… + 此來源沒有可用的串流。 + 目前選取的串流 + 符合你的串流格式與品質偏好 + 使用中的播放 URL + 從本機快取播放 + 中繼資料曲目 + 標題 + ID + 專輯 + 信賴度 + 外部 URI + 專輯 ID + 專輯類型 + 專輯發行日期 + 專輯曲目數 + 專輯類型風格 + 長度 + 曲目編號 + 光碟編號 + 兒少不宜 + 熱門度 + ISRC + 縮圖 URL + + + 切換側邊欄 + + + 替代來源 + 目前沒有播放中的曲目 + 正在搜尋替代來源... + 找不到替代來源 + 信賴度:%1$d% + 使用中的來源 + + + 踢出 + 將對方移出這個 Jam + 踢出並禁止對方再次加入 + 移除推薦曲目 + 移除對方加入待播清單的所有曲目 + 重新排序 + 從待播清單移除 + %1$s • 由 %2$s 加入 + + + 未知 + 正在載入播放清單... + 新增曲目 + 將曲目加入播放清單 + 搜尋曲目... + 搜尋 + 找不到曲目 + 搜尋要加入的曲目 + 完成 + 新增 + 已新增 + + + 正在載入專輯... + + + 加入你的播放清單 + 搜尋 + 搜尋你的播放清單... + 正在載入你的播放清單... + 你還沒有自己的播放清單。\n建立一個來加入曲目吧。 + 建立播放清單 + 編輯播放清單 + 建立 + 名稱 + 播放清單名稱 + 說明 + 說明(選填) + 公開 + 所有人都能看到這個播放清單 + 協作 + 其他人可以將曲目加入這個播放清單 + 封面圖片 + 播放清單封面 + 沒有圖片 + 更換圖片 + 選擇圖片 + + + 安裝 + 取代 + 在 zip 檔案中找不到 plugin.json + 在 zip 檔案中找不到 manifest.zipline.json + plugin.json 格式無效:%1$s + 無法讀取外掛程式:%1$s + 無法安裝外掛程式:%1$s + %1$s 有較新的版本。安裝前請先確認更新。 + %1$s 已經安裝。安裝前請先確認取代。 + 內建外掛程式已包含在內,無法再次新增。 + 無法移除內建外掛程式。 + %1$s 宣告的 apiVersion 無效(%2$s)。應為與 %3$s 相容的語意化版本。 + Spotube 外掛程式 API 版本 %1$s 無效。 + %1$s 的目標為外掛程式 API %2$s,但 Spotube 支援的是不相容變更後的外掛程式 API %3$s。請安裝針對相同主要 API 版本建置的外掛程式。 + + + 外掛程式 API 不相容 + 要安裝外掛程式嗎? + %1$s 將加入你已安裝的外掛程式。 + 要更新外掛程式嗎? + 已安裝舊版(%1$s)。要更新至 %2$s 嗎? + 要取代已安裝的外掛程式嗎? + %1$s 使用了無效的語意化版本(%2$s),無法作為更新進行比較。仍要取代已安裝的外掛程式嗎? + 已安裝的版本(%1$s)無法以語意化版本進行比較。要以 %2$s 取代嗎? + 版本 %1$s 已經安裝。要以提供的版本取代現有的外掛程式嗎? + 已安裝的版本 %1$s 比提供的版本 %2$s 新。仍要取代嗎? + 要以提供的版本取代已安裝的外掛程式嗎? + + + %1$s: + 版本 + API 版本 + 作者 + 授權條款 + 權限 + 功能 + 儲存庫 + 聯絡方式 + 回報錯誤 + 支持 %1$s + + + 官方 + 已驗證 + GitHub 星星數 + GitHub 儲存庫 URL + + + 找不到發行版本 + 選擇發行版本 + 選擇一個發行版本 + 預先發行版 + 版本資訊 + + + 首頁 + 搜尋 + 本機 + 連線 + 裝置 + + + 隱藏視窗 + 顯示視窗 + 下一首 + 上一首 + 隨機播放 + 重複播放 + 重複播放:關閉 + 重複播放:單曲 + 重複播放:全部 + 音量:%1$d% + 音量 + + 音量 - + 靜音 + 取消靜音 + 喜歡此曲目 + 取消喜歡此曲目 + 結束 + + + 最小化 + 最大化 + 還原 + 關閉 + + + 播放服務 + 播放 + 媒體播放控制項 + + + 瀏覽 + 現正播放 + 精選 + 已儲存的曲目 + 熱門曲目 + diff --git a/composeApp/src/commonMain/composeResources/values-zh/strings.xml b/composeApp/src/commonMain/composeResources/values-zh/strings.xml new file mode 100644 index 00000000..e95e0b45 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-zh/strings.xml @@ -0,0 +1,932 @@ + + + + 播放队列 + 清空队列 + 筛选队列... + 队列为空 + + 搜索歌曲、艺人、专辑... + 清除全部历史记录 + 搜索... + + + 我的音乐库 + 音乐库 + 歌单 + 专辑 + 艺人 + 本地歌曲 + 下载 + 搜索已收藏的歌单… + 搜索已收藏的专辑… + 搜索已关注的艺人… + 搜索文件夹或歌曲… + 筛选音乐库 + 关闭筛选 + 创建歌单 + 刷新本地歌曲 + 切换到网格视图 + 切换到列表视图 + 暂无收藏的歌单 + 没有与“%1$s”匹配的歌单 + 暂无收藏的专辑 + 没有与“%1$s”匹配的专辑 + 暂无关注的艺人 + 没有与“%1$s”匹配的艺人 + + + 文件夹 + 文件夹 + %1$s • %2$s + + %1$d 首歌曲 + + + %1$d 个文件夹 + + 返回文件夹 + 播放文件夹 + 需要音频访问权限 + 允许访问音频,以浏览设备上的音乐。 + 授予权限 + 未找到本地文件夹 + 此文件夹中没有歌曲 + 没有匹配的文件夹或歌曲 + 刷新本地媒体失败 + + + %1$d 个进行中 • %2$d 个已完成 + 清除已完成 + 暂无下载 + 正在下载 %1$d% + 排队中 + 已完成 + 已取消 + 失败 + 失败:%1$s + 取消下载 + 重试下载 + 移除下载 + + + 艺人 + 已拉黑 + 喜欢的歌曲 + 你收藏的歌曲 + 未知艺人 + 歌曲标题 + 艺人名称 + + 已选择 + + 设置 + + 插件 + 语言和地区 + 外观 + 播放 + 下载 + 缓存 + 桌面 + 更新 + 支持 + 关于 + + 关闭 + 保存 + 取消 + + 语言 + 应用界面语言。当前:%1$s + 跟随系统 + 国家/地区 + 用于内容和可用性的国家/地区。当前:%1$s + %1$s (%2$s) + + 主题 + 选择 Spotube 如何匹配系统外观。当前:%1$s + 浅色 + 深色 + 跟随系统 + + 材质 + 应用的背景材质。当前:%1$s + 水润 + 冰晶 + 铬金属 + 亚克力 + 玻璃 + + 强调色 + 当前配色:%1$s + 强调色 + 选择一种强调配色,并在浅色和深色主题下预览。 + 浅色 + 深色 + 预览 + 按钮 + 妖精绿 + 电光紫 + 海洋青 + 日落橙 + 玫瑰园 + 午夜蓝 + 金属灰 + + 外观与风格 + 材质和强调色。当前:%1$s · %2$s + 外观与风格 + 先选择一种材质,再选择与之搭配的强调色。每个选项都是精心挑选的组合。 + 材质 + 强调色 + 次要 + + 配色 + Millennium 强调配色。当前:%1$s + 配色 + 选择一种 Millennium 强调配色,并在浅色和深色下预览。 + 配色 + Millennium 配色和强调色。当前:%1$s + 蓝色 + 橙色 + 青柠 + + 自定义下载文件夹 + 当前文件夹:%1$s + 当前文件夹:默认音乐文件夹 + 留空则使用默认音乐文件夹。 + C:\Music\Spotube + 本地媒体文件夹 + 桌面端要扫描的自定义文件夹。已添加:%1$d + 尚未添加自定义文件夹 + 用于本地音乐库的自定义文件夹。 + 管理 + 添加文件夹 + 移除文件夹 + 未添加自定义文件夹。 + 从扫描列表中移除 %1$s + 下载文件夹和音乐缓存文件夹会自动包含在内。 + 你的下载文件夹会自动包含在内。 + 音乐缓存文件夹会自动包含在内。 + 正在保存… + 无法保存文件夹。你的更改仍然保留,请重试。 + + %1$d 个文件夹 + + 下载音乐格式 + 下载时首选的编解码器和封装格式。当前:%1$s + 下载音质 + + 流媒体音乐格式 + 在线播放时首选的编解码器和封装格式。当前:%1$s + 在线播放音质 + 当前:%1$s + 启用音乐缓存 + 将在线播放的音频保存到本地,加快重复播放速度。 + 缓存文件夹 + 当前:%1$s + 当前:默认应用缓存 + 缓存大小上限 + 无限制 + 当前上限:%1$d MB + 设置缓存的最大大小(MB)。输入 0 或留空表示无限制。 + 500 + 大小必须为 0 或正数。 + 启用无尽播放 + 队列播放完毕后,继续播放推荐的歌曲。 + 启用 Connect + 通过 Spotube Connect 功能开放远程播放控制。 + 播放代理服务器端口 + 允许远程控制 + 允许同一网络中的其他设备在连接后控制播放。 + 远程控制设备名称 + 其他设备发现本设备时看到的名称。当前:%1$s + 设置一个易于识别的名称,网络中的其他设备发现此 Spotube 实例时会显示该名称。 + 例如:客厅的 Spotube + 设备主机名 + 播放代理服务器使用的端口。当前:%1$d + 请选择 1 到 65535 之间的端口。 + 14769 + 请输入有效的整数。 + 端口必须在 1 到 65535 之间。 + + 黑名单 + 管理已拉黑的歌曲和艺人 + + 最小化到托盘 + 窗口最小化时,让 Spotube 在系统托盘中继续运行。 + Discord 动态状态 + 在桌面端的 Discord 中显示当前播放状态。 + + 自动检查更新 + 在后台检查 Spotube 新版本。 + + 管理插件 + 管理你的插件 + 默认%1$s插件 + 元数据 + 音频 + 歌词 + 听歌记录 + 未选择插件 + 更改 + 选择 + 已选择 + 清除选择 + 没有可用的插件 + %1$s 插件 + 清除 + + 配置 + 插件管理器 + 请输入 URL + URL 必须以 http:// 或 https:// 开头 + 插件下载失败 + 尚未安装任何插件 + 在上方粘贴下载 URL 或选择文件。 + + 已安装 %1$d 个插件 + + 安装插件 + https://example.com/plugin.smplug + 下载 + 从文件安装 (.smplug) + 已启用 + v%1$s + 内置 + 移除插件 + 登录 + 退出登录 + 从 URL 下载 + 从文件安装 + 安装 + + • %1$s + 请求的权限 + 此插件不需要任何特殊权限。 + 已安装与待安装对比 + 已安装 + 待安装 + API %1$s → %2$s + 持久存储 + 在磁盘上读写数据 + 网络请求 + 通过互联网收发数据 + WebView + 在应用内显示网页内容 + 一起听 + MQTT 代理服务器 + 代理服务器主机 + %1$s:%2$d + 代理服务器端口 + 使用 TLS + 用户名(可选) + 密码(可选) + 客户端 ID 前缀 + 用于识别此 Spotube 实例 + 加密与代理服务器的连接 + 创建房间前,检查当前的代理服务器设置。 + 测试连接 + 正在测试… + 已连接,耗时 %1$d 毫秒 + 失败:%1$s + 占位代理服务器 — 如需自托管,请配置你自己的服务器 + 连接 + 凭据 + 身份 + 诊断 + + + 一起听 + 创建 + 加入 + 创建一个房间,然后把房间码分享给朋友。 + 创建房间 + 加入房间 + 正在连接… + 房间码 + ABC123 + 代理服务器:%1$s + 请先在设置中配置代理服务器,才能创建或加入房间。 + 代理服务器设置 + 已连接 + 正在重新连接… + 复制房间码 + 已复制! + 分享 + 参与者(%1$d) + 房主 + 听众 + 正在等待听众加入… + 移除 + 封禁 + 管理 + 离开房间 + 无法连接 + 关闭 + 创建一起听会话失败 + 加入一起听会话失败 + 请输入由字母和数字组成的六位房间码。 + 用房间码加入我的 Spotube 一起听:%1$s + + 转到专辑 + 睡眠定时器 + 歌曲详情 + 播放 + 暂停 + 当前没有播放内容 + 添加到队列 + + 忽略 + 更新 + Spotube %1$s 现已推出 + 前往 Spotube 下载页面查看此版本的新功能。 + %1$s %2$s 现已推出 + 当前已安装:%1$s + 前往插件页面查看此更新的新功能。 + + 跳过 + 下一步 + 返回 + 开始使用 + Spotube + 音乐,无拘无束。 + 一款开源播放器,带你探索音乐世界。\n由你探索,由你定义。 + 让这个项目延续下去。 + Spotube 由一群热爱它的人打造,而不是一门订阅生意。如果它已成为你日常生活的一部分,一点点支持就能帮助我们给予它应有的用心呵护。\n\n感谢你为独立软件留出一席之地。 + 先安装一个曲库插件来查找歌曲和歌单,再选择一个音频插件来播放它们。歌词和听歌记录是可选的。在下方发现插件,查看其权限并安装,然后在此处选择它。 + 发现插件 + 你的音源 + 重试 + 更多插件 + 未发现新插件。你也可以通过上方的链接或 .smplug 文件安装插件。 + 插件发现功能暂不可用。请重试,或通过链接或文件安装。 + 欢迎 + 支持 + 插件 + 指南 + + 如何使用 Spotube + 只需四个简单步骤,即可开始聆听。 + 登录 + 连接你的 Spotify 帐号,或选择你自己的曲库插件。 + 搜索并播放 + 查找任意歌曲、专辑或艺人,然后点击播放。 + 离线收听 + 下载歌曲,无网络时也能继续播放。 + 跟随歌词 + 播放歌曲时,从播放器中打开同步歌词。 + + + 你的音乐。你的音源。你做主。 + 一款免费、开源的音乐播放器,由一群热爱音乐的社区成员打造。 + 开源 + 无广告 + 无遥测 + 插件驱动 + 出发吧 + + + 支持者让我们走得更远 + 用热情打造,因你而延续。 + Spotube 100% 免费、自由且开源 — 无广告、无订阅、无追踪。每一项功能的存在,都是因为像你这样的人选择帮助代码持续前行。 + Patreon + 每月赞助 + 支持持续的开发和基础设施 + Buy Me a Coffee + 请杯咖啡 + 用一笔小额打赏,为深夜编码加油 + Open Collective + 社区基金 + 透明、由社区管理的资金支持 + 衷心感谢你对开放音乐的信任。 + 支持完全自愿 — 无论如何 Spotube 都将保持免费。 + 赞助 + 打开 %1$s + + + 支持 Spotube + 支持 Spotube + Spotube 免费、自由且开源 — 无广告、无订阅、无追踪。如果它为你带来了快乐,请考虑支持这个项目。 + 你可以在“设置 → 支持”中关闭这些提醒。 + 以后再说 + 支持提醒 + 每 7 天温馨提醒一次支持本项目。 + + + 模块化架构 + 选择你的音乐来源。 + Spotube 将曲库发现、音频流和同步歌词分开处理。在下方配置各个引擎,或随时切换。 + 元数据引擎 + 为音乐发现、搜索、歌曲列表和歌单提供支持。 + 音频引擎 + 获取并解码要播放的音频流。 + 歌词引擎 + 实时传输逐句同步的卡拉 OK 歌词。 + 听歌记录引擎 + 将听歌记录同步到 Last.fm 或 ListenBrainz。 + 已启用 + 内置 + 就绪 + 可选 + 选择一个引擎 + 已安装的引擎 + 设为默认 + NewPipe YouTube 和 LRCLib 歌词已内置,开箱即可使用。 + 随时可以在“设置 → 插件”中管理或添加新的社区插件。 + + + 复古与现代融合的架构 + 纯粹高保真,零中间商 + 看看 Spotube 如何直接在你的设备上协调去中心化播放。 + 直接点对点 • 256 KBPS OPUS + 蓝调时刻 + Spotube · 播放器演示 + 一点音乐,献给那些间隙时光。 + 1. 去中心化发现 + 查询直接发送到你选择的曲库插件。Spotube 绝不分析你的喜好。 + 2. 直接解析音频流 + 音频直接从内容服务器传输到你的设备。没有代理中转,也没有远程数据收集。 + 3. 本地 DSP 与播放 + 在本地解码和缓存,提供均衡器控制和可选的离线下载。 + 4. 同步歌词与一起听 + 卡拉 OK 歌词跟随播放进度,并支持 LAN 远程控制和一起听。 + 没有中间服务器。播放直接在你的设备上进行 — 隐私从设计开始。 + + + 选择设备 + %1$s:“%2$s” + 播放 + 添加到队列 + 下一首播放 + 本设备 + 在本设备上 + 远程设备 + 远程设备(%1$s) + 在已连接的设备上 + + + 连接失败 + 连接已断开 + 发送失败 + 发现设备失败:%1$s + 连接失败:%1$s + + + 设备 + 刷新 + 已连接到 %1$s:%2$d + 断开连接 + 正在连接... + 正在搜索网络中的设备... + 未找到设备 + 请确保另一台设备已在设置中启用“允许远程控制”。 + 未知设备 + 正在解析... + ID:%1$s... + + + 远程控制 + 已断开连接 + 连接错误:%1$s + 专辑封面 + 未知歌曲 + 未知艺人 + 进度 + 随机播放 + 上一首 + 下一首 + 循环模式 + 音量 + 从队列中移除 + 更多选项 + + + 在哪里播放? + 你正在进行一起听会话。请选择在哪里播放此合集。 + 在此播放 + 推荐到一起听 + + + 未配置一起听代理服务器 + 代理服务器主机为空 + 连接失败 + 连接已断开 + + + 代理服务器 + anonymous + + + 已添加到一起听队列 + + 已将 %1$d 首歌曲添加到一起听队列 + + + + 远程控制请求 + “%1$s”请求控制此设备上的播放。 + 拒绝 + 允许 + 始终允许 + + + 搜索结果 + 全部 + 歌曲 + 用户 + 没有可用的搜索类型 + 输入内容开始搜索 + 输入内容开始搜索歌曲 + 未找到结果 + 查看全部 + 正在搜索 + 正在加载歌曲... + 正在加载专辑... + 正在加载艺人... + 正在加载歌单... + 正在加载用户... + 未找到专辑 + 未找到艺人 + 未找到歌单 + 未找到用户 + 正在翻箱倒柜找唱片... + 正在探索音乐宇宙... + 正在预热音箱... + 正在擦拭黑胶唱片... + 正在请教音乐之神... + 正在调试天线... + 正在翻阅唱片... + 加载搜索功能失败 + 搜索失败 + 搜索歌曲失败 + 搜索专辑失败 + 搜索艺人失败 + 搜索歌单失败 + 搜索用户失败 + + + 浏览 + + + 同步 + 纯文本 + 暂无同步歌词 + 暂无纯文本歌词 + + + %1$s(%2$s 封装) + %1$d kbps + %1$s kHz • %2$d 声道 + 未选择元数据插件 + + + 返回 + 重试 + 关闭对话框 + 正在加载... + 未知 + 未知错误 + + + 用户 + 歌单 + + + 非常抱歉 + 出了点问题。你的体验对我们很重要,我们正在全力修复。请重试,或查看下方的错误详情。 + 错误详情 + 复制到剪贴板 + 查看完整日志 + 完整错误日志 + + + 随机播放 + 随机播放 + 关注 + 取消关注 + 编辑歌单 + 创建者:%1$s + + + 歌曲选项 + 添加到一起听 + 开启电台 + 下一首播放 + 移到下一首 + 从队列中移除 + 从收藏中移除 + 收藏 + 添加到歌单 + 下载 + 分享 + + + 默认 + 标题 + 艺人 + 专辑 + 时长 + 排序 + 未找到歌曲 + 筛选 + 批量操作 + 全部下载 + 下载 %1$d 首 + 全部添加到队列 + 将 %1$d 首添加到队列 + 全部下一首播放 + 下一首播放 %1$d 首 + 全部添加到歌单 + 将 %1$d 首添加到歌单 + 全部添加到一起听 + 将 %1$d 首添加到一起听 + 另外 %1$d 位 + + + 没有要加载的 URL + 在浏览历史中后退 + 在浏览历史中前进 + + + 筛选黑名单... + 黑名单为空 + 没有与“%1$s”匹配的黑名单项目 + 歌曲 + 歌曲 + 加入黑名单 + 从黑名单中移除 + + + 没有歌曲元数据 + 无法解析音频流 URL + 无法解析音频流 URL。请先尝试播放这首歌曲。 + + + 收藏的歌曲 + 你 + + + 已关注 + 热门歌曲 + 播放热门歌曲 + 将热门歌曲添加到队列 + 相关艺人 + 精选歌单 + 加载艺人概览失败 + 粉丝数不可用 + %1$sK 位粉丝 + %1$sM 位粉丝 + %1$sB 位粉丝 + + %1$d 位粉丝 + + + + 喜欢 + 取消喜欢 + 无歌曲标题 + 无艺人 + 未知专辑 + 进度 + 音量 + 开启随机播放 + 关闭随机播放 + 上一首 + 下一首 + 播放或暂停 + 循环模式:关闭 + 循环模式:单曲循环 + 循环模式:全部循环 + 静音 + 取消静音 + 更多选项 + 播放器选项 + 收起播放器 + 展开歌词 + 正在播放 + 歌词 + 备用音源 + 备用音源 + 下载 + 已下载 + 正在下载... + 已下载过 + 正在重试下载... + 正在下载 %1$s + 将在 %1$s后暂停播放。 + 在选定的分钟数后暂停播放。 + 分钟 + 请选择 1 到 600 分钟 + 取消定时 + 开始 + 播放 URL 是使用不同的流媒体设置解析的。高亮显示的音频流符合你当前的偏好;当前使用的 URL 将在下次请求音频流时刷新。 + 匹配的音源 + 正在加载匹配的音源… + 播放本地缓存文件时,无需匹配远程音源。 + 没有可用的匹配音源。 + 此歌曲没有缓存的匹配音源。 + 音频流详情尚未缓存。请先播放这首歌曲以加载详情。 + 加载歌曲详情失败。 + 可用的音频流 + 此歌曲正从本地缓存播放,本次播放不使用音频流变体。 + 正在加载音频流… + 此音源没有可用的音频流。 + 当前选择的音频流 + 符合你的流媒体格式和音质偏好 + 当前播放 URL + 从本地缓存播放 + 元数据歌曲 + 标题 + ID + 专辑 + 匹配度 + 外部 URI + 专辑 ID + 专辑类型 + 专辑发行日期 + 专辑歌曲数 + 专辑流派 + 时长 + 曲目编号 + 碟片编号 + 含露骨内容 + 热度 + ISRC + 缩略图 URL + + + 切换侧边栏 + + + 备用音源 + 当前没有正在播放的歌曲 + 正在搜索备用音源... + 未找到备用音源 + 匹配度:%1$d% + 当前音源 + + + 踢出 + 将其移出会话 + 踢出并禁止其重新加入 + 移除推荐 + 移除其添加到队列中的所有歌曲 + 调整顺序 + 从队列中移除 + %1$s • 由 %2$s 添加 + + + 未知 + 正在加载歌单... + 添加歌曲 + 添加歌曲到歌单 + 搜索歌曲... + 搜索 + 未找到歌曲 + 搜索要添加的歌曲 + 完成 + 添加 + 已添加 + + + 正在加载专辑... + + + 添加到你的歌单 + 搜索 + 搜索你的歌单... + 正在加载你的歌单... + 你还没有创建任何歌单。\n创建一个歌单来添加歌曲吧。 + 创建歌单 + 编辑歌单 + 创建 + 名称 + 歌单名称 + 简介 + 简介(可选) + 公开 + 任何人都可以看到此歌单 + 协作 + 其他人可以向此歌单添加歌曲 + 封面图片 + 歌单封面 + 无图片 + 更换图片 + 选择图片 + + + 安装 + 替换 + 在 zip 文件中未找到 plugin.json + 在 zip 文件中未找到 manifest.zipline.json + plugin.json 格式无效:%1$s + 读取插件失败:%1$s + 安装插件失败:%1$s + %1$s 有新版本可用。请在安装前确认更新。 + %1$s 已安装。请在安装前确认替换。 + 内置插件已包含在应用中,无法再次添加。 + 内置插件无法移除。 + %1$s 声明了无效的 apiVersion(%2$s)。应为与 %3$s 兼容的语义化版本。 + Spotube 插件 API 版本 %1$s 无效。 + %1$s 面向插件 API %2$s,但 Spotube 支持的是存在破坏性变更的插件 API %3$s。请安装针对相同主版本 API 构建的插件。 + + + 插件 API 不兼容 + 安装插件? + %1$s 将被添加到你已安装的插件中。 + 更新插件? + 已安装旧版本(%1$s)。要更新到 %2$s 吗? + 替换已安装的插件? + %1$s 使用了无效的语义化版本(%2$s),无法作为更新进行比较。仍要替换已安装的插件吗? + 已安装的版本(%1$s)无法按语义化版本进行比较。要将其替换为 %2$s 吗? + 版本 %1$s 已安装。要用提供的副本替换现有插件吗? + 已安装的版本 %1$s 比提供的版本 %2$s 更新。仍要替换吗? + 要用提供的副本替换已安装的插件吗? + + + %1$s: + 版本 + API 版本 + 作者 + 许可证 + 权限 + 功能 + 代码仓库 + 联系方式 + 报告问题 + 支持 %1$s + + + 官方 + 已验证 + GitHub 星标 + GitHub 仓库 URL + + + 未找到发布版本 + 选择版本 + 选择一个发布版本 + 预发布版 + 更新说明 + + + 首页 + 搜索 + 本机 + 连接 + 设备 + + + 隐藏窗口 + 显示窗口 + 下一首 + 上一首 + 随机播放 + 循环 + 循环:关闭 + 循环:单曲 + 循环:全部 + 音量:%1$d% + 音量 + + 音量 - + 静音 + 取消静音 + 喜欢这首歌 + 取消喜欢这首歌 + 退出 + + + 最小化 + 最大化 + 还原 + 关闭 + + + 播放服务 + 播放 + 媒体播放控制 + + + 浏览 + 正在播放 + 精选 + 收藏的歌曲 + 热门歌曲 + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 6de1648b..62a749a7 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -103,19 +103,20 @@ Language and Region Appearance Playback - Downloadsf + Downloads Caching Desktop Updates Support About - Close - Save - Cancel + Close + Save + Cancel Language Language for the app interface. Current: %1$s + System default Country Regional country for content and availability. Current: %1$s %1$s (%2$s) @@ -262,9 +263,10 @@ Failed to download plugin No plugins installed yet Paste a download URL or pick a file above. - %1$d %2$s installed - plugin - plugins + + %1$d plugin installed + %1$d plugins installed + Install a Plugin https://example.com/plugin.smplug Download @@ -470,4 +472,467 @@ 4. Synced Lyrics & Jam Karaoke lyrics track the playback timestamp, with LAN remote control and Group Jam. No middleman servers. Playback happens right on your device — private by design. + + + Choose a device + %1$s: \"%2$s\" + Play + Add to queue + Play next + This Device + On this device + Remote Device + Remote Device (%1$s) + On the connected device + + + Connection failed + Connection lost + Send failed + Discovery failed: %1$s + Failed to connect: %1$s + + + Devices + Refresh + Connected to %1$s:%2$d + Disconnect + Connecting... + Searching for devices on the network... + No devices found + Make sure the other device has \"Allow remote control\" enabled in settings. + Unknown Device + Resolving... + ID: %1$s... + + + Remote Control + Disconnected + Connection error: %1$s + Album cover + Unknown Track + Unknown Artist + Seek + Shuffle + Previous + Next + Loop mode + Volume + Remove from queue + More options + + + Play Where? + You have an active jam session. Choose where to play this collection. + Play here + Suggest to Jam + + + No jam broker configured + Broker host is empty + Connection failed + Connection lost + + + Broker + anonymous + + + Added to the jam queue + + Added %1$d to the jam queue + Added %1$d to the jam queue + + + + Remote Control Request + \"%1$s\" wants to control playback on this device. + Deny + Allow + Allow Always + + + Search results + All + Tracks + Users + No search types available + Start typing to search + Start typing to search tracks + No results found + See all + Searching + Loading tracks... + Loading albums... + Loading artists... + Loading playlists... + Loading users... + No albums found + No artists found + No playlists found + No users found + Digging through the crates... + Searching the cosmos... + Warming up the speakers... + Dusting off the vinyl... + Consulting the music gods... + Tuning the antennas... + Flipping through the records... + Failed to load search capabilities + Failed to search + Failed to search tracks + Failed to search albums + Failed to search artists + Failed to search playlists + Failed to search users + + + Browse + + + Synced + Plain + No synced lyrics available + No plain lyrics available + + + %1$s in %2$s + %1$d kbps + %1$s kHz • %2$d ch + No metadata plugin selected + + + Back + Retry + Close dialog + Loading... + Unknown + Unknown error + + + User + Playlist + + + We\'re Truly Sorry + Something isn\'t working right now. Your experience matters to us, and we\'re doing our best to fix this. Please try again or check the error details below. + Error Details + Copy to clipboard + View full log + Full Error Log + + + Shuffle + Shuffle play + Follow + Unfollow + Edit playlist + By %1$s + + + Track options + Add to Jam + Start radio + Play next + Move to next + Remove from queue + Remove from favorites + Save as favorite + Add to playlist + Download + Share + + + Original + Title + Artist + Album + Duration + Sort + No tracks found + Filter + Bulk actions + Download All + Download %1$d + Add All to Queue + Add %1$d to Queue + Play All Next + Play %1$d Next + Add All to Playlist + Add %1$d to Playlist + Add All to Jam + Add %1$d to Jam + +%1$d more + + + No URL to load + Go back to browser history + Go forward to browser history + + + Filter blacklist... + No blacklisted items + No blacklisted items match \'%1$s\' + Tracks + Track + Add to blacklist + Remove from blacklist + + + No track metadata + Could not resolve stream URL + Could not resolve stream URL. Try playing the track first. + + + Saved Tracks + You + + + Following + Top Tracks + Play top tracks + Add top tracks to queue + Related Artists + Featured Playlists + Failed to load artist overview + Followers unavailable + %1$sK followers + %1$sM followers + %1$sB followers + + %1$d follower + %1$d followers + + + + Like + Unlike + No-op track title + No-op artists + Unknown Album + Seek + Volume + Enable shuffle + Disable shuffle + Previous + Next + Play or pause + Loop mode off + Loop mode one + Loop mode all + Mute + Unmute + More options + Player options + Collapse player + Expand lyrics + Now Playing + Lyrics + Alternative Source + Alternative source + Download + Downloaded + Downloading... + Already downloaded + Retrying download... + Downloading %1$s + Playback will pause in %1$s. + Pause playback after the selected number of minutes. + Minutes + Choose between 1 and 600 minutes + Cancel timer + Start + The playback URL was resolved with different streaming settings. The highlighted stream matches your current preference; the active URL refreshes on the next stream request. + Matched audio source + Loading matched source… + A matched remote source is not needed while playing the local cached file. + No matched source is available. + No matched audio source is cached for this track. + Stream details are not cached yet. Play the track first to load them. + Failed to load track details. + Available streams + This track is being served from the local cache. Stream variants are not used for this playback. + Loading streams… + No streams are available for this source. + Currently selected stream + Matches your streaming format & quality preference + Active playback URL + Served from local cache + Metadata track + Title + ID + Album + Confidence + External URI + Album ID + Album type + Album release date + Album track count + Album genres + Duration + Track number + Disc number + Explicit + Popularity + ISRC + Thumbnail URLs + + + Toggle Sidebar + + + Alternative Sources + No track currently playing + Searching for alternative sources... + No alternative sources found + Confidence: %1$d% + Active source + + + Kick + Remove them from the session + Kick and prevent them from rejoining + Remove suggestions + Remove every track they added to the queue + Reorder + Remove from queue + %1$s • Added by %2$s + + + Unknown + Loading playlist... + Add Tracks + Add Tracks to Playlist + Search tracks... + Search + No tracks found + Search for tracks to add + Done + Add + Added + + + Loading album... + + + Add to Your Playlist + Search + Search your playlists... + Loading your playlists... + You don\'t own any playlists yet.\nCreate one to add tracks. + Create Playlist + Edit Playlist + Create + Name + Playlist name + Description + Description (optional) + Public + Anyone can see this playlist + Collaborative + Others can add tracks to this playlist + Cover Image + Playlist cover + No image + Change Image + Pick Image + + + Install + Replace + plugin.json not found in the zip file + manifest.zipline.json not found in the zip file + Invalid plugin.json format: %1$s + Failed to read plugin: %1$s + Failed to install plugin: %1$s + A newer version of %1$s is available. Confirm the update before installing. + %1$s is already installed. Confirm replacing it before installing. + Built-in plugins are already included and can\'t be added again. + Built-in plugins can\'t be removed. + %1$s declares an invalid apiVersion (%2$s). Expected semantic versioning compatible with %3$s. + Spotube plugin API version %1$s is invalid. + %1$s targets plugin API %2$s, but Spotube supports breaking plugin API %3$s. Install a plugin built for the same breaking API version. + + + Plugin API not compatible + Install plugin? + %1$s will be added to your installed plugins. + Update plugin? + An older version is installed (%1$s). Update to %2$s? + Replace installed plugin? + %1$s uses an invalid semantic version (%2$s) and can\'t be compared as an update. Replace the installed plugin anyway? + The installed version (%1$s) can\'t be compared using semantic versioning. Replace it with %2$s? + Version %1$s is already installed. Replace the existing plugin with the supplied copy? + Installed version %1$s is newer than the supplied version %2$s. Replace it anyway? + Replace the installed plugin with the supplied copy? + + + %1$s: + Version + API Version + Author + License + Capabilities + Abilities + Repository + Contact + Report Bugs + Support %1$s + + + Official + Verified + GitHub stars + GitHub repository URL + + + No releases found + Select Release + Select a release + Pre-release + Release Notes + + + Home + Search + On Device + Connect + Devices + + + Hide Window + Show Window + Next Track + Previous Track + Shuffle + Loop + Loop: OFF + Loop: ONE + Loop: ALL + Volume: %1$d% + Volume + + Volume - + Mute + Unmute + Like Track + Unlike Track + Exit + + + Minimize + Maximize + Restore + Close + + + Playback service + Playback + Media playback controls + + + Browse + Now Playing + Featured + Saved Tracks + Top Tracks diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt index fb59f5ec..bcb8ba2e 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.ui.NavDisplay import dev.krtirtho.spotube.core.DebugConfig +import dev.krtirtho.spotube.core.i18n.AppLocaleProvider import dev.krtirtho.spotube.core.navigation.Navigator import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES @@ -73,48 +74,63 @@ import kotlin.time.Clock import org.koin.compose.koinInject import org.koin.compose.navigation3.koinEntryProvider import org.koin.core.annotation.KoinExperimentalAPI +import org.jetbrains.compose.resources.StringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.jam_screen_title +import spotube.composeapp.generated.resources.library_tab_albums +import spotube.composeapp.generated.resources.library_tab_artists +import spotube.composeapp.generated.resources.library_tab_downloads +import spotube.composeapp.generated.resources.library_tab_local_tracks +import spotube.composeapp.generated.resources.library_tab_playlists +import spotube.composeapp.generated.resources.library_toolbar_compact_title +import spotube.composeapp.generated.resources.nav_devices +import spotube.composeapp.generated.resources.nav_group_connect +import spotube.composeapp.generated.resources.nav_group_on_device +import spotube.composeapp.generated.resources.nav_home +import spotube.composeapp.generated.resources.nav_search +import spotube.composeapp.generated.resources.settings_screen_title interface NavigationItem { data class Tab( - val title: String, val icon: ImageVector, val route: Routes, + val title: StringResource, val icon: ImageVector, val route: Routes, val data: T? = null ) : NavigationItem data class Group( - val title: String, val items: List> + val title: StringResource, val items: List> ) : NavigationItem } private val tabs = listOf( - NavigationItem.Tab("Home", Iconsax.IconsaxHome, Routes.Home), - NavigationItem.Tab("Search", Iconsax.IconsaxSearch, Routes.Search) + NavigationItem.Tab(Res.string.nav_home, Iconsax.IconsaxHome, Routes.Home), + NavigationItem.Tab(Res.string.nav_search, Iconsax.IconsaxSearch, Routes.Search) ) val sidebarTabs = tabs + listOf( NavigationItem.Group( - "Library", + Res.string.library_toolbar_compact_title, items = listOf( NavigationItem.Tab( - "Playlists", + Res.string.library_tab_playlists, Iconsax.IconsaxMusicDashboard, Routes.Library, LibraryTab.Playlists ), - NavigationItem.Tab("Albums", Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums), - NavigationItem.Tab("Artists", Iconsax.User, Routes.Library, LibraryTab.Artists) + NavigationItem.Tab(Res.string.library_tab_albums, Iconsax.IconsaxCd, Routes.Library, LibraryTab.Albums), + NavigationItem.Tab(Res.string.library_tab_artists, Iconsax.User, Routes.Library, LibraryTab.Artists) ) ), NavigationItem.Group( - "On Device", + Res.string.nav_group_on_device, items = listOf( NavigationItem.Tab( - "Local Tracks", + Res.string.library_tab_local_tracks, Iconsax.IconsaxMusicLibrary, Routes.Library, LibraryTab.LocalTracks ), NavigationItem.Tab( - "Downloads", + Res.string.library_tab_downloads, Iconsax.IconsaxDirectboxReceive, Routes.Library, LibraryTab.Downloads @@ -122,16 +138,16 @@ val sidebarTabs = tabs + listOf( ) ), NavigationItem.Group( - "Connect", + Res.string.nav_group_connect, items = listOf( - NavigationItem.Tab("Devices", Iconsax.IconsaxMirrorScreenRegular, Routes.Devices), - NavigationItem.Tab("Group Jam", Iconsax.IconsaxSoundTwotone, Routes.Jam) + NavigationItem.Tab(Res.string.nav_devices, Iconsax.IconsaxMirrorScreenRegular, Routes.Devices), + NavigationItem.Tab(Res.string.jam_screen_title, Iconsax.IconsaxSoundTwotone, Routes.Jam) ) ) ) val bottombarTabs = tabs + listOf( - NavigationItem.Tab("Library", Iconsax.IconsaxMusicLibrary, Routes.Library), - NavigationItem.Tab("Settings", Iconsax.IconsaxSetting2, Routes.Settings) + NavigationItem.Tab(Res.string.library_toolbar_compact_title, Iconsax.IconsaxMusicLibrary, Routes.Library), + NavigationItem.Tab(Res.string.settings_screen_title, Iconsax.IconsaxSetting2, Routes.Settings) ) @OptIn(KoinExperimentalAPI::class) @@ -194,65 +210,68 @@ fun App( Navigator(navigationState) } - SpotubeTheme(settings = userSettings) { - CompositionLocalProvider(LocalSupportPrompt provides { - supportDialogIsReminder = false - showSupportDialog = true - }) { - Box(modifier = Modifier.fillMaxSize()) { - val currentRoute = - navigationState.backStacks[navigationState.topLevelRoute]?.last() + // Below the navigation state, so switching languages keeps the current screen. + AppLocaleProvider(userSettings.language) { + SpotubeTheme(settings = userSettings) { + CompositionLocalProvider(LocalSupportPrompt provides { + supportDialogIsReminder = false + showSupportDialog = true + }) { + Box(modifier = Modifier.fillMaxSize()) { + val currentRoute = + navigationState.backStacks[navigationState.topLevelRoute]?.last() - Box(Modifier.fillMaxSize()) { - AppShell(navigator, navigationState) { - SharedTransitionLayout { - CompositionLocalProvider( - LocalSharedTransitionScope provides this@SharedTransitionLayout - ) { - Column { - NavDisplay( - modifier = Modifier.fillMaxSize(), - onBack = navigator::pop, - entries = navigationState.toEntries(koinEntryProvider()), - transitionSpec = navPushTransitionSpec, - popTransitionSpec = navPopTransitionSpec, - predictivePopTransitionSpec = navPredictivePopTransitionSpec - ) + Box(Modifier.fillMaxSize()) { + AppShell(navigator, navigationState) { + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this@SharedTransitionLayout + ) { + Column { + NavDisplay( + modifier = Modifier.fillMaxSize(), + onBack = navigator::pop, + entries = navigationState.toEntries(koinEntryProvider()), + transitionSpec = navPushTransitionSpec, + popTransitionSpec = navPopTransitionSpec, + predictivePopTransitionSpec = navPredictivePopTransitionSpec + ) + } } } } - } - when (currentRoute) { - Routes.WebView -> WebViewScreen(koinInject()) - else -> {} + when (currentRoute) { + Routes.WebView -> WebViewScreen(koinInject()) + else -> {} + } } } + content() + availableAppUpdate?.let { update -> + AppUpdateDialog( + update = update, + onDismiss = appUpdateService::dismissUpdate, + onIgnore = appUpdateService::ignoreUpdate, + onUpdate = { + runCatching { openUrlInBrowser("https://spotube.cc/downloads/") } + appUpdateService.dismissUpdate() + }) + } + availablePluginUpdate?.let { update -> + PluginUpdateDialog( + update = update, + onDismiss = pluginUpdateService::dismissUpdate, + onIgnore = pluginUpdateService::ignoreUpdate, + onUpdate = { + runCatching { openUrlInBrowser(update.info.directDownloadUrl) } + pluginUpdateService.dismissUpdate() + }) + } + SupportDialog( + visible = showSupportDialog, + onDismissRequest = { showSupportDialog = false }, + isReminder = supportDialogIsReminder) } - content() - availableAppUpdate?.let { update -> - AppUpdateDialog( - update = update, - onDismiss = appUpdateService::dismissUpdate, - onIgnore = appUpdateService::ignoreUpdate, - onUpdate = { - runCatching { openUrlInBrowser("https://spotube.cc/downloads/") } - appUpdateService.dismissUpdate() - }) - } - availablePluginUpdate?.let { update -> - PluginUpdateDialog( - update = update, - onDismiss = pluginUpdateService::dismissUpdate, - onIgnore = pluginUpdateService::ignoreUpdate, - onUpdate = { - runCatching { openUrlInBrowser(update.info.directDownloadUrl) } - pluginUpdateService.dismissUpdate() - }) - } - SupportDialog( - visible = showSupportDialog, - onDismissRequest = { showSupportDialog = false }, - isReminder = supportDialogIsReminder) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.kt new file mode 100644 index 00000000..60fbd612 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.i18n + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.key +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import dev.krtirtho.spotube.modules.settings.SupportedLanguages + +/** + * Points the platform locale (which Compose resources resolve strings from) at [languageTag], + * or back to the system language when it is null, and returns the platform-specific + * composition local that carries it. + */ +@Composable +internal expect fun provideAppLocale(languageTag: String?): ProvidedValue<*> + +/** + * Renders [content] in the app language picked in settings, overriding the system language + * unless [language] is [SupportedLanguages.SYSTEM]. + * + * Compose resources only re-resolve strings when the calling composable recomposes, so the + * content is keyed on the language: switching languages recreates the subtree. Keep state that + * must survive a language switch (e.g. navigation) above this provider. + */ +@Composable +fun AppLocaleProvider(language: SupportedLanguages, content: @Composable () -> Unit) { + val languageTag = language.languageTag + val layoutDirection = when { + language == SupportedLanguages.SYSTEM -> null + language.isRtl -> LayoutDirection.Rtl + else -> LayoutDirection.Ltr + } + val locales = listOfNotNull( + provideAppLocale(languageTag), + layoutDirection?.let { LocalLayoutDirection provides it }) + CompositionLocalProvider(*locales.toTypedArray()) { + key(languageTag) { + content() + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomClient.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomClient.kt index c09b4e20..e36b974b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomClient.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomClient.kt @@ -44,6 +44,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.serialization.json.Json +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.jam_error_broker_host_empty +import spotube.composeapp.generated.resources.jam_error_connection_failed +import spotube.composeapp.generated.resources.jam_error_connection_lost +import spotube.composeapp.generated.resources.jam_error_no_broker +import spotube.composeapp.generated.resources.settings_jam_broker_test_ok import org.koin.core.component.KoinComponent import kotlin.time.Duration.Companion.seconds @@ -92,7 +99,7 @@ class JamRoomClient : KoinComponent { /** One-off connection check used by the settings screen. Returns latency description. */ suspend fun testConnection(broker: JamBroker): Result { - if (broker.host.isBlank()) return Result.failure(IllegalArgumentException("Broker host is empty")) + if (broker.host.isBlank()) return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_broker_host_empty))) val started = kotlin.time.TimeSource.Monotonic.markNow() return runCatching { val client = startClient(broker, clientId = "${broker.clientIdPrefix}-test") @@ -100,7 +107,7 @@ class JamRoomClient : KoinComponent { withTimeout(broker.connectionTimeoutSeconds.seconds) { client.awaitConnectivity() } - "Connected in ${started.elapsedNow().inWholeMilliseconds} ms" + getString(Res.string.settings_jam_broker_test_ok, started.elapsedNow().inWholeMilliseconds) } finally { runCatching { client.shutdown(sendDisconnect = true, drain = false) } } @@ -116,7 +123,7 @@ class JamRoomClient : KoinComponent { isHost: Boolean): Result { disconnect() if (broker.host.isBlank()) { - return Result.failure(IllegalArgumentException("No jam broker configured")) + return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_no_broker))) } return runCatching { roomCode = code @@ -140,7 +147,7 @@ class JamRoomClient : KoinComponent { Unit }.onFailure { e -> log.w(e) { "Failed to connect to jam broker" } - _connectionError.value = e.message ?: "Connection failed" + _connectionError.value = e.message ?: getString(Res.string.jam_error_connection_failed) _isConnected.value = false runCatching { client?.shutdown(sendDisconnect = true, drain = false) } client = null @@ -245,7 +252,10 @@ class JamRoomClient : KoinComponent { else -> { _isConnected.value = false - _connectionError.value = "Connection lost" + scope.launch { + val message = getString(Res.string.jam_error_connection_lost) + if (!_isConnected.value) _connectionError.value = message + } } } } @@ -309,4 +319,4 @@ class JamRoomClient : KoinComponent { private fun cmdTopic(code: String) = "spotube/jam/$code/cmd" private fun presenceTopic(code: String, clientId: String) = "spotube/jam/$code/presence/$clientId" private fun roomFilter(code: String) = "spotube/jam/$code/#" -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomService.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomService.kt index b9918040..422b7b44 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomService.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/jam/JamRoomService.kt @@ -40,6 +40,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlin.random.Random +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.jam_error_invalid_code +import spotube.composeapp.generated.resources.jam_error_no_broker import kotlin.time.Clock /** @@ -157,7 +161,7 @@ class JamRoomService( suspend fun createRoom(): Result { val broker = settingsRepository.userSettings.value.jamBroker if (broker.host.isBlank()) { - return Result.failure(IllegalStateException("No jam broker configured")) + return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_broker))) } val code = JamRoomCode.generate() localClientId = newClientId(broker) @@ -188,11 +192,11 @@ class JamRoomService( suspend fun joinRoom(code: String): Result { val broker = settingsRepository.userSettings.value.jamBroker if (broker.host.isBlank()) { - return Result.failure(IllegalStateException("No jam broker configured")) + return Result.failure(IllegalStateException(getString(Res.string.jam_error_no_broker))) } val normalized = JamRoomCode.normalize(code) if (!JamRoomCode.isValid(normalized)) { - return Result.failure(IllegalArgumentException("Invalid room code")) + return Result.failure(IllegalArgumentException(getString(Res.string.jam_error_invalid_code))) } localClientId = newClientId(broker) val name = participantName("Guest") @@ -558,4 +562,4 @@ class JamRoomService( private const val HOST_TAKEOVER_DELAY_MS = 1_500L private const val AUTO_ADVANCE_WINDOW_MS = 2_000L } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt index 995d66e5..68e1d03b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/ConnectionRequestDialog.kt @@ -35,7 +35,14 @@ import androidx.compose.ui.unit.dp import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumDialog +import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.remote_request_allow +import spotube.composeapp.generated.resources.remote_request_allow_always +import spotube.composeapp.generated.resources.remote_request_deny +import spotube.composeapp.generated.resources.remote_request_message +import spotube.composeapp.generated.resources.remote_request_title @Composable fun ConnectionRequestDialogHost() { @@ -55,9 +62,9 @@ fun ConnectionRequestDialogHost() { handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Deny) pendingRequest = null }, - title = "Remote Control Request") { + title = stringResource(Res.string.remote_request_title)) { Text( - text = "\"${request.deviceName}\" wants to control playback on this device.", + text = stringResource(Res.string.remote_request_message, request.deviceName), style = AppTheme.typography.bodyMedium, textAlign = TextAlign.Start) Row( @@ -70,14 +77,14 @@ fun ConnectionRequestDialogHost() { pendingRequest = null }, style = MillenniumButtonStyle.Secondary) { - Text("Deny") + Text(stringResource(Res.string.remote_request_deny)) } MillenniumButton( onClick = { handler.resolveConnectionRequest(request.sessionId, ConnectionRequestResponse.Allow) pendingRequest = null }) { - Text("Allow") + Text(stringResource(Res.string.remote_request_allow)) } MillenniumButton( onClick = { @@ -85,7 +92,7 @@ fun ConnectionRequestDialogHost() { pendingRequest = null }, style = MillenniumButtonStyle.Quiet) { - Text("Allow Always") + Text(stringResource(Res.string.remote_request_allow_always)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt index 5dd7c43b..30bfa3b1 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemoteControlClient.kt @@ -38,6 +38,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.json.Json +import org.jetbrains.compose.resources.StringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.devices_error_connection_failed +import spotube.composeapp.generated.resources.devices_error_connection_lost +import spotube.composeapp.generated.resources.devices_error_send_failed /** * WebSocket client for controlling a remote Spotube instance. @@ -94,7 +99,7 @@ class RemoteControlClient { } } catch (e: Exception) { logger.e(e) { "Failed to connect to $host:$port" } - _connectionState.value = ConnectionState.Error(e.message ?: "Connection failed") + _connectionState.value = ConnectionState.Error(e.message, Res.string.devices_error_connection_failed) disconnect() } } @@ -150,7 +155,7 @@ class RemoteControlClient { logger.d { "Receive loop exited normally" } } catch (e: Exception) { logger.e(e) { "Error in receive loop" } - _connectionState.value = ConnectionState.Error(e.message ?: "Connection lost") + _connectionState.value = ConnectionState.Error(e.message, Res.string.devices_error_connection_lost) } } @@ -170,7 +175,7 @@ class RemoteControlClient { logger.d { "Sent command: $command" } } catch (e: Exception) { logger.e(e) { "Failed to send command" } - _connectionState.value = ConnectionState.Error(e.message ?: "Send failed") + _connectionState.value = ConnectionState.Error(e.message, Res.string.devices_error_send_failed) } } @@ -195,5 +200,9 @@ sealed interface ConnectionState { data object Disconnected : ConnectionState data object Connecting : ConnectionState data class Connected(val host: String, val port: Int) : ConnectionState - data class Error(val message: String) : ConnectionState -} \ No newline at end of file + /** + * [message] is the raw (untranslated) cause when available; [fallback] is the + * localized description to show when it isn't. + */ + data class Error(val message: String?, val fallback: StringResource) : ConnectionState +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt index 3627dff7..936a2855 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/remote/RemotePlaybackController.kt @@ -36,6 +36,11 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getPluralString +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.jam_queue_added_track +import spotube.composeapp.generated.resources.jam_queue_added_tracks enum class PlaybackDestinationAction { Play, @@ -179,7 +184,7 @@ class RemotePlaybackController( JamRole.Guest -> jamRoomService.suggestTrack(track) null -> return@launch } - _events.emit("Added to the jam queue") + _events.emit(getString(Res.string.jam_queue_added_track)) } catch (e: Exception) { logger.e(e) { "Failed to add track to jam session" } } @@ -207,7 +212,7 @@ class RemotePlaybackController( JamRole.Guest -> jamRoomService.suggestPlaylist(tracks) null -> return@launch } - _events.emit("Added ${tracks.size} to the jam queue") + _events.emit(getPluralString(Res.plurals.jam_queue_added_tracks, tracks.size, tracks.size)) } catch (e: Exception) { logger.e(e) { "Failed to add tracks to jam session" } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt index 7d3c25a2..24bda126 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDialogBottomSheet.kt @@ -43,6 +43,8 @@ import org.spotube.millennium.adaptive.WindowBreakpoints import androidx.compose.ui.unit.dp import dev.krtirtho.spotube.resources.iconsax.FluentDismiss import dev.krtirtho.spotube.resources.iconsax.Iconsax +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun AdaptiveDialogBottomSheet( @@ -92,7 +94,7 @@ fun AdaptiveDialogBottomSheet( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( Iconsax.FluentDismiss, - contentDescription = "Close dialog") + contentDescription = stringResource(Res.string.common_close_dialog)) } } Spacer(Modifier.height(12.dp)) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.kt index 2b40616e..97d7aa7b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.kt @@ -28,6 +28,8 @@ import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.resources.iconsax.ArrowLeft3 import dev.krtirtho.spotube.resources.iconsax.Iconsax import org.koin.compose.koinInject +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun ApplicationBackButton() { @@ -40,7 +42,7 @@ fun ApplicationBackButton() { style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.ArrowLeft3, - contentDescription = "Back" + contentDescription = stringResource(Res.string.common_back) ) } } @@ -51,4 +53,4 @@ expect fun ApplicationMainBar( subtitle: @Composable () -> Unit = {}, actions: @Composable (RowScope.() -> Unit) = {}, backButton: Boolean = true, - transparent: Boolean = false) \ No newline at end of file + transparent: Boolean = false) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt index 7356b4b5..07da835f 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt @@ -66,6 +66,8 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle import dev.krtirtho.spotube.resources.iconsax.User import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -107,8 +109,8 @@ fun CollectionDetails( onClick = onAddToJam!!) { MillenniumIcon( imageVector = Iconsax.IconsaxAddSquare, - contentDescription = "Add to Jam") - TextWithShimmer(text = "Add to Jam") + contentDescription = stringResource(Res.string.track_options_add_to_jam)) + TextWithShimmer(text = stringResource(Res.string.track_options_add_to_jam)) } } } @@ -127,16 +129,16 @@ fun CollectionDetails( onClick = onPlay) { MillenniumIcon( imageVector = if (isPlaying) Iconsax.IconsaxPauseCircle else Iconsax.IconsaxPlayCircle2, - contentDescription = if (isPlaying) "Pause" else "Play") - TextWithShimmer(text = if (isPlaying) "Pause" else "Play") + contentDescription = if (isPlaying) stringResource(Res.string.player_pause_content_description) else stringResource(Res.string.player_play_content_description)) + TextWithShimmer(text = if (isPlaying) stringResource(Res.string.player_pause_content_description) else stringResource(Res.string.player_play_content_description)) } MillenniumButton( modifier = modifier, onClick = onShufflePlay, style = MillenniumButtonStyle.Secondary ) { - MillenniumIcon(imageVector = Iconsax.IconsaxShuffle, contentDescription = "Shuffle play") - TextWithShimmer(text = "Shuffle") + MillenniumIcon(imageVector = Iconsax.IconsaxShuffle, contentDescription = stringResource(Res.string.collection_details_shuffle_play)) + TextWithShimmer(text = stringResource(Res.string.collection_details_shuffle)) } } } @@ -149,7 +151,7 @@ fun CollectionDetails( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxAddSquare, - contentDescription = "Add to queue" + contentDescription = stringResource(Res.string.player_add_to_queue_content_description) ) } MillenniumSeparator(modifier = Modifier.height(24.dp), vertical = true) @@ -160,7 +162,7 @@ fun CollectionDetails( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxHeart2, - contentDescription = "Unfollow") + contentDescription = stringResource(Res.string.collection_details_unfollow)) } } else { MillenniumIconButton( @@ -168,7 +170,7 @@ fun CollectionDetails( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxHeart, - contentDescription = "Follow", + contentDescription = stringResource(Res.string.collection_details_follow), tint = AppTheme.colors.onSurfaceVariant) } } @@ -180,7 +182,7 @@ fun CollectionDetails( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxEdit, - contentDescription = "Edit playlist") + contentDescription = stringResource(Res.string.collection_details_edit_playlist)) } } } @@ -213,7 +215,7 @@ fun CollectionDetails( tint = AppTheme.colors.onPrimaryContainer) } TextWithShimmer( - text = "By $ownerName", + text = stringResource(Res.string.collection_details_by_owner, ownerName), style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt index 1b660b4c..5dbbaa16 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt @@ -52,6 +52,8 @@ import org.jetbrains.compose.resources.painterResource import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButtonStyle +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun CollectionView( @@ -124,7 +126,7 @@ fun CollectionView( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = if (isPlaying) Iconsax.IconsaxPauseCircle else Iconsax.IconsaxPlayCircle2, - contentDescription = if (isPlaying) "Pause" else "Play") + contentDescription = if (isPlaying) stringResource(Res.string.player_pause_content_description) else stringResource(Res.string.player_play_content_description)) } } } else { @@ -145,11 +147,11 @@ fun CollectionView( if (isLoading && tracks.isEmpty()) { dev.krtirtho.spotube.core.ui.misc.SkeletonTree(true) { CollectionDetails( - title = title.ifBlank { "Loading..." }, + title = title.ifBlank { stringResource(Res.string.common_loading) }, description = description, imageURL = imageURL, imageResource = imageResource, - ownerName = ownerName.ifBlank { "Unknown" }, + ownerName = ownerName.ifBlank { stringResource(Res.string.common_unknown) }, ownerImageURL = ownerImageURL, onOwnerClick = onOwnerClick, onPlay = onPlay, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt index be7fed6b..398cd494 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt @@ -65,6 +65,8 @@ import compose.icons.feathericons.AlertCircle import compose.icons.feathericons.Copy import compose.icons.feathericons.Maximize2 import compose.icons.feathericons.RefreshCw +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun ErrorDisplay( @@ -87,14 +89,14 @@ fun ErrorDisplay( tint = AppTheme.colors.error.copy(alpha = 0.7f)) Text( - text = "We're Truly Sorry", + text = stringResource(Res.string.error_title), style = AppTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = AppTheme.colors.onSurface, textAlign = TextAlign.Center) Text( - text = "Something isn't working right now. Your experience matters to us, and we're doing our best to fix this. Please try again or check the error details below.", + text = stringResource(Res.string.error_description), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center, @@ -112,7 +114,7 @@ fun ErrorDisplay( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text( - text = "Error Details", + text = stringResource(Res.string.error_details), style = AppTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = AppTheme.colors.onSurfaceVariant) @@ -127,7 +129,7 @@ fun ErrorDisplay( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = FeatherIcons.Copy, - contentDescription = "Copy to clipboard", + contentDescription = stringResource(Res.string.error_copy_to_clipboard), modifier = Modifier.size(16.dp)) } @@ -137,7 +139,7 @@ fun ErrorDisplay( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = FeatherIcons.Maximize2, - contentDescription = "View full log", + contentDescription = stringResource(Res.string.error_view_full_log), modifier = Modifier.size(16.dp)) } } @@ -170,7 +172,7 @@ fun ErrorDisplay( imageVector = FeatherIcons.RefreshCw, contentDescription = null, modifier = Modifier.padding(end = 8.dp).size(18.dp)) - Text("Retry") + Text(stringResource(Res.string.common_retry)) } } } @@ -198,7 +200,7 @@ private fun ErrorLogDialog( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Text( - text = "Full Error Log", + text = stringResource(Res.string.error_full_log_title), style = AppTheme.typography.titleLarge, fontWeight = FontWeight.Bold) @@ -209,7 +211,7 @@ private fun ErrorLogDialog( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = FeatherIcons.Copy, - contentDescription = "Copy to clipboard") + contentDescription = stringResource(Res.string.error_copy_to_clipboard)) } } Box( @@ -236,7 +238,7 @@ private fun ErrorLogDialog( MillenniumButton( onClick = onDismiss, style = MillenniumButtonStyle.Quiet) { - Text("Close") + Text(stringResource(Res.string.common_close)) } } } @@ -260,4 +262,4 @@ private fun ErrorLogDialogPreview() { errorMessage = "This is an error message", onDismiss = {}) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/FeaturedCarousel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/FeaturedCarousel.kt index 142358fe..4ae55ebc 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/FeaturedCarousel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/FeaturedCarousel.kt @@ -83,6 +83,8 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxPlayCircle import dev.krtirtho.spotube.resources.iconsax.Play import kotlinx.coroutines.launch import org.koin.compose.koinInject +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun FeaturedCarousel( @@ -166,7 +168,7 @@ private fun FeaturedCarouselCard( val subtitle = when (item) { is MetadataBrowseItem.Album -> item.data.artists.joinToString { it.name } is MetadataBrowseItem.Playlist -> item.data.owner?.displayName ?: item.data.owner?.username - ?: "Playlist" + ?: stringResource(Res.string.media_playlist) is MetadataBrowseItem.Track -> item.data.artists.joinToString { it.name } else -> "" diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt index b15a985a..e8cf94de 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt @@ -111,17 +111,20 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxNext import dev.krtirtho.spotube.resources.iconsax.IconsaxPause import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay import dev.krtirtho.spotube.resources.iconsax.IconsaxSort +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* private val CompactTrackListBreakpoint = 600.dp private val LargeTrackListBreakpoint = 840.dp private const val ShimmerRowCount = 6 -enum class TrackSortOption(val label: String) { - None("Original"), - Title("Title"), - Artist("Artist"), - Album("Album"), - Duration("Duration"), +enum class TrackSortOption(val label: StringResource) { + None(Res.string.track_list_sort_original), + Title(Res.string.track_list_sort_title), + Artist(Res.string.track_list_sort_artist), + Album(Res.string.track_list_sort_album), + Duration(Res.string.track_list_sort_duration), } @OptIn(ExperimentalFoundationApi::class) @@ -311,7 +314,7 @@ fun TrackList( } if (showEmptyMessage && !isLoading && visibleTracks.isEmpty() && error == null) { - TrackListFeedbackRow("No tracks found") + TrackListFeedbackRow(stringResource(Res.string.track_list_no_tracks)) } if (isLoadingNextPage) { @@ -365,7 +368,7 @@ fun TrackList( MillenniumTextField( state = filterState, label = "", - placeholder = "Filter", + placeholder = stringResource(Res.string.track_list_filter_placeholder), modifier = Modifier .widthIn(max = 400.dp) .align(Alignment.CenterEnd)) @@ -377,7 +380,7 @@ fun TrackList( AdaptiveDropdownBottomSheet( items = TrackSortOption.entries.map { option -> AdaptiveMenuItem( - label = option.label, + label = stringResource(option.label), onClick = { sortBy = option }, selected = sortBy == option) }, @@ -387,7 +390,7 @@ fun TrackList( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxSort, - contentDescription = "Sort") + contentDescription = stringResource(Res.string.track_list_sort)) } }) } @@ -407,30 +410,30 @@ fun TrackList( add( AdaptiveMenuItem( icon = Iconsax.IconsaxDirectboxReceive, - label = if (isAll) "Download All" else "Download $trackCount", + label = if (isAll) stringResource(Res.string.track_list_download_all) else stringResource(Res.string.track_list_download_count, trackCount), onClick = { onBulkDownload(targetTracks) })) if (!isJamGuest) { add( AdaptiveMenuItem( icon = Iconsax.IconsaxAddSquare, - label = if (isAll) "Add All to Queue" else "Add $trackCount to Queue", + label = if (isAll) stringResource(Res.string.track_list_add_all_to_queue) else stringResource(Res.string.track_list_add_count_to_queue, trackCount), onClick = { onBulkAddToQueue(targetTracks) })) add( AdaptiveMenuItem( icon = Iconsax.IconsaxNext, - label = if (isAll) "Play All Next" else "Play $trackCount Next", + label = if (isAll) stringResource(Res.string.track_list_play_all_next) else stringResource(Res.string.track_list_play_count_next, trackCount), onClick = { onBulkPlayNext(targetTracks) })) } add( AdaptiveMenuItem( icon = Iconsax.IconsaxMusicPlaylist, - label = if (isAll) "Add All to Playlist" else "Add $trackCount to Playlist", + label = if (isAll) stringResource(Res.string.track_list_add_all_to_playlist) else stringResource(Res.string.track_list_add_count_to_playlist, trackCount), onClick = { onBulkAddToPlaylist(targetTracks) })) } + if (isInJam) { listOf( AdaptiveMenuItem( icon = Iconsax.IconsaxAddSquare, - label = if (isAll) "Add All to Jam" else "Add $trackCount to Jam", + label = if (isAll) stringResource(Res.string.track_list_add_all_to_jam) else stringResource(Res.string.track_list_add_count_to_jam, trackCount), onClick = { onBulkAddToJam(targetTracks) })) } else { emptyList() @@ -441,7 +444,7 @@ fun TrackList( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.Iconsax3DotsMore, - contentDescription = "Bulk actions") + contentDescription = stringResource(Res.string.track_list_bulk_actions)) } }) } @@ -644,7 +647,7 @@ private fun TrackListRow( contentAlignment = Alignment.Center) { MillenniumIcon( imageVector = if (isCurrentTrack && isCurrentTrackPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, - contentDescription = if (isCurrentTrack && isCurrentTrackPlaying) "Pause" else "Play", + contentDescription = if (isCurrentTrack && isCurrentTrackPlaying) stringResource(Res.string.player_pause_content_description) else stringResource(Res.string.player_play_content_description), tint = Color.White, modifier = Modifier.size(18.dp).shimmerApply()) } @@ -713,7 +716,7 @@ private fun TrackListRow( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.Iconsax3DotsMore, - contentDescription = "Track options") + contentDescription = stringResource(Res.string.track_options_title)) } } } @@ -762,7 +765,7 @@ private fun ArtistChips( val remaining = artists.size - visibleArtists.size if (remaining > 0) { TextWithShimmer( - text = "+$remaining more", + text = stringResource(Res.string.track_list_more_artists, remaining), style = AppTheme.typography.bodySmall, color = AppTheme.colors.primary, maxLines = 1, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt index 0f4653b8..e17d0d66 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackOptions.kt @@ -58,6 +58,8 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemoveFilled import dev.krtirtho.spotube.resources.iconsax.IconsaxNext import dev.krtirtho.spotube.resources.iconsax.IconsaxShare +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* sealed interface TrackOptionsAction { data object AddToJam : TrackOptionsAction @@ -116,7 +118,7 @@ fun TrackOptions( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.Iconsax3DotsMore, - contentDescription = "Track options", + contentDescription = stringResource(Res.string.track_options_title), modifier = Modifier.shimmerApply() ) } @@ -238,6 +240,7 @@ fun TrackOptionsSheetHeader(track: MetadataTrack) { } } +@Composable private fun buildTrackMenuItems( track: MetadataTrack, state: TrackOptionsState, @@ -249,14 +252,14 @@ private fun buildTrackMenuItems( add( AdaptiveMenuItem( icon = Iconsax.IconsaxAddSquare, - label = "Add to Jam", + label = stringResource(Res.string.track_options_add_to_jam), onClick = { onAction(TrackOptionsAction.AddToJam) })) } add( AdaptiveMenuItem( icon = Iconsax.IconsaxMusicCircle, - label = "Start radio", + label = stringResource(Res.string.track_options_start_radio), onClick = { onAction(TrackOptionsAction.StartRadio) })) // A guest's queue is the shared jam queue — mutating it locally is not @@ -266,13 +269,13 @@ private fun buildTrackMenuItems( add( AdaptiveMenuItem( icon = Iconsax.IconsaxNext, - label = "Play next", + label = stringResource(Res.string.track_options_play_next), onClick = { onAction(TrackOptionsAction.PlayNext) })) } else if (state.isInQueue && !state.isCurrentlyPlaying) { add( AdaptiveMenuItem( icon = Iconsax.IconsaxNext, - label = "Move to next", + label = stringResource(Res.string.track_options_move_to_next), onClick = { onAction(TrackOptionsAction.PlayNext) })) } @@ -280,13 +283,13 @@ private fun buildTrackMenuItems( add( AdaptiveMenuItem( icon = Iconsax.IconsaxAddSquare, - label = "Add to queue", + label = stringResource(Res.string.player_add_to_queue_content_description), onClick = { onAction(TrackOptionsAction.AddToQueue) })) } else { add( AdaptiveMenuItem( icon = Iconsax.IconsaxMusicSquareRemove, - label = "Remove from queue", + label = stringResource(Res.string.track_options_remove_from_queue), onClick = { onAction(TrackOptionsAction.RemoveFromQueue) })) } } @@ -294,32 +297,32 @@ private fun buildTrackMenuItems( add( AdaptiveMenuItem( icon = if (state.isFavorite) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart, - label = if (state.isFavorite) "Remove from favorites" else "Save as favorite", + label = if (state.isFavorite) stringResource(Res.string.track_options_remove_from_favorites) else stringResource(Res.string.track_options_save_as_favorite), onClick = { onAction(TrackOptionsAction.ToggleFavorite) })) add( AdaptiveMenuItem( icon = Iconsax.IconsaxMusicPlaylist, - label = "Add to playlist", + label = stringResource(Res.string.track_options_add_to_playlist), onClick = { onAction(TrackOptionsAction.AddToPlaylist) })) add( AdaptiveMenuItem( icon = Iconsax.IconsaxDirectboxReceive, - label = "Download", + label = stringResource(Res.string.track_options_download), onClick = { onAction(TrackOptionsAction.Download) })) add( AdaptiveMenuItem( icon = Iconsax.IconsaxMusicSquareRemoveFilled, - label = if (state.isBlacklisted) "Remove from blacklist" else "Add to blacklist", + label = if (state.isBlacklisted) stringResource(Res.string.blacklist_remove) else stringResource(Res.string.blacklist_add), onClick = { onAction(TrackOptionsAction.ToggleBlacklist) })) if (!track.externalUri.isNullOrBlank()) { add( AdaptiveMenuItem( icon = Iconsax.IconsaxShare, - label = "Share", + label = stringResource(Res.string.track_options_share), onClick = { onAction(TrackOptionsAction.Share) })) } @@ -327,7 +330,7 @@ private fun buildTrackMenuItems( add( AdaptiveMenuItem( icon = Iconsax.IconsaxCd, - label = "Go to album", + label = stringResource(Res.string.player_go_to_album), onClick = onAlbumClick)) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/UserCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/UserCard.kt index 868ec005..c42a5e34 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/UserCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/UserCard.kt @@ -40,6 +40,8 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUser import dev.krtirtho.spotube.core.ui.component.cards.AvatarCard +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun UserCard( @@ -47,7 +49,7 @@ fun UserCard( modifier: Modifier = Modifier) { AvatarCard( title = user.displayName ?: user.username, - subtitle = "User", + subtitle = stringResource(Res.string.media_user), imageURL = user.thumbnails.firstOrNull()?.url, onClick = { // TODO: Route to user details screen. diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt index 583ba6e2..0b0596af 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt @@ -63,6 +63,8 @@ import dev.nucleusframework.webview.web.WebView import dev.nucleusframework.webview.web.WebViewNavigator import dev.nucleusframework.webview.web.WebViewState import dev.nucleusframework.webview.web.rememberWebViewNavigator +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* class PostMessageHandler( private val onMessageReceived: (String) -> Unit = {} @@ -83,7 +85,7 @@ class PostMessageHandler( fun PlatformWebViewScreen(webViewController: WebViewController) { if (webViewController.getContent() == null) { // This should never happen, but just in case - Text("No URL to load") + Text(stringResource(Res.string.webview_no_url)) return } @@ -173,7 +175,7 @@ fun PlatformWebViewScreen(webViewController: WebViewController) { ) { MillenniumIcon( FeatherIcons.ChevronLeft, - contentDescription = "Go back to browser history" + contentDescription = stringResource(Res.string.webview_go_back) ) } MillenniumIconButton( @@ -185,7 +187,7 @@ fun PlatformWebViewScreen(webViewController: WebViewController) { ) { MillenniumIcon( FeatherIcons.ChevronRight, - contentDescription = "Go forward to browser history" + contentDescription = stringResource(Res.string.webview_go_forward) ) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt index ed4b9a11..3aa949b8 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt @@ -31,6 +31,10 @@ import org.koin.compose.koinInject import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.ui.component.CollectionView import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.album_loading_title +import spotube.composeapp.generated.resources.media_unknown_artist @Composable fun AlbumScreen( @@ -56,7 +60,7 @@ fun AlbumScreen( val dataState = state as? AlbumScreenState.Data val album = dataState?.album val ownerName = - album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" } + album?.artists?.joinToString { it.name }.orEmpty().ifBlank { stringResource(Res.string.media_unknown_artist) } val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url val firstArtistId = album?.artists?.firstOrNull()?.id val artworkUrl = album?.thumbnails?.firstOrNull()?.url.orEmpty() @@ -67,7 +71,7 @@ fun AlbumScreen( val isLoading = state is AlbumScreenState.Loading && (dataState == null || dataState.tracks.isEmpty()) CollectionView( - title = album?.title ?: "Loading album...", + title = album?.title ?: stringResource(Res.string.album_loading_title), description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}", imageURL = artworkUrl, ownerName = ownerName, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt index 2cd84136..f1c8476a 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumViewModel.kt @@ -49,6 +49,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface AlbumScreenState { data object Loading : AlbumScreenState @@ -172,7 +175,7 @@ class AlbumViewModel( nextPagination = tracksResult?.nextPagination) }.onFailure { e -> logger.e(e) { "Failed to load album" } - _state.value = AlbumScreenState.Error(e.message ?: "Unknown error") + _state.value = AlbumScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } suspend fun loadMoreTracks() = runCatching { @@ -190,7 +193,7 @@ class AlbumViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more tracks" } - _state.value = AlbumScreenState.Error(e.message ?: "Unknown error") + _state.value = AlbumScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun loadNextTracksPage() { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt index 51934952..5092a82b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt @@ -93,6 +93,9 @@ import dev.krtirtho.spotube.resources.iconsax.User import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlin.math.roundToInt +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun ArtistScreen( @@ -334,7 +337,7 @@ private fun ArtistErrorContent( style = AppTheme.typography.bodyLarge ) MillenniumButton(onClick = onRetry) { - Text("Retry") + Text(stringResource(Res.string.common_retry)) } } } @@ -509,7 +512,7 @@ private fun ArtistHeaderActions( style = MillenniumButtonStyle.Secondary ) { Text( - "Following", + stringResource(Res.string.artist_following), textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -517,12 +520,12 @@ private fun ArtistHeaderActions( } } else { MillenniumButton(onClick = onFollowClick) { - Text("Follow", modifier = Modifier.width(65.dp), textAlign = TextAlign.Center) + Text(stringResource(Res.string.collection_details_follow), modifier = Modifier.width(65.dp), textAlign = TextAlign.Center) } } MillenniumTooltip( - text = if (isBlacklisted) "Remove from blacklist" else "Add to blacklist" + text = if (isBlacklisted) stringResource(Res.string.blacklist_remove) else stringResource(Res.string.blacklist_add) ) { if (isBlacklisted) { MillenniumIconButton( @@ -531,7 +534,7 @@ private fun ArtistHeaderActions( ) { MillenniumIcon( imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Remove from blacklist", + contentDescription = stringResource(Res.string.blacklist_remove), tint = AppTheme.colors.error ) } @@ -542,7 +545,7 @@ private fun ArtistHeaderActions( ) { MillenniumIcon( imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Add to blacklist" + contentDescription = stringResource(Res.string.blacklist_add) ) } } @@ -563,7 +566,7 @@ private fun TopTracksHeader( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Top Tracks", + text = stringResource(Res.string.artist_top_tracks), style = AppTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold ) @@ -575,7 +578,7 @@ private fun TopTracksHeader( ) { MillenniumIcon( imageVector = Iconsax.IconsaxPlay, - contentDescription = "Play top tracks", + contentDescription = stringResource(Res.string.artist_play_top_tracks), tint = LocalTextStyle.current.color ) } @@ -585,7 +588,7 @@ private fun TopTracksHeader( ) { MillenniumIcon( imageVector = Iconsax.IconsaxAddSquare, - contentDescription = "Add top tracks to queue" + contentDescription = stringResource(Res.string.artist_add_top_tracks_to_queue) ) } } @@ -615,7 +618,7 @@ private fun AlbumsSection( } } - SectionHeader(title = "Albums") + SectionHeader(title = stringResource(Res.string.library_tab_albums)) Spacer(modifier = Modifier.size(8.dp)) LazyRow( state = rowState, @@ -653,7 +656,7 @@ private fun RelatedArtistsSection( } } - SectionHeader(title = "Related Artists") + SectionHeader(title = stringResource(Res.string.artist_related_artists)) Spacer(modifier = Modifier.size(8.dp)) LazyRow( state = rowState, @@ -691,7 +694,7 @@ private fun FeaturedPlaylistsSection( } } - SectionHeader(title = "Featured Playlists") + SectionHeader(title = stringResource(Res.string.artist_featured_playlists)) Spacer(modifier = Modifier.size(8.dp)) LazyRow( state = rowState, @@ -716,13 +719,14 @@ private fun SectionHeader(title: String) { ) } +@Composable private fun formatFollowers(count: Int?): String { - if (count == null) return "Followers unavailable" + if (count == null) return stringResource(Res.string.artist_followers_unavailable) return when { - count >= 1_000_000_000 -> "${formatAbbreviatedCount(count, 1_000_000_000)}B followers" - count >= 1_000_000 -> "${formatAbbreviatedCount(count, 1_000_000)}M followers" - count >= 1_000 -> "${formatAbbreviatedCount(count, 1_000)}K followers" - else -> "$count followers" + count >= 1_000_000_000 -> stringResource(Res.string.artist_followers_billions, formatAbbreviatedCount(count, 1_000_000_000)) + count >= 1_000_000 -> stringResource(Res.string.artist_followers_millions, formatAbbreviatedCount(count, 1_000_000)) + count >= 1_000 -> stringResource(Res.string.artist_followers_thousands, formatAbbreviatedCount(count, 1_000)) + else -> pluralStringResource(Res.plurals.artist_followers_count, count, count) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistViewModel.kt index 9fa5ac86..a508f193 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistViewModel.kt @@ -46,6 +46,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.* sealed interface ArtistScreenState { data object Loading : ArtistScreenState @@ -380,11 +382,11 @@ class ArtistViewModel( featuredPlaylists = overview.featuredPlaylists.items, featuredPlaylistsNextPagination = overview.featuredPlaylists.nextPagination) } else { - _state.value = ArtistScreenState.Error("Failed to load artist overview") + _state.value = ArtistScreenState.Error(getString(Res.string.artist_error_load_failed)) } }.onFailure { e -> logger.e(e) { "Failed to load artist overview" } - _state.value = ArtistScreenState.Error(e.message ?: "Unknown error") + _state.value = ArtistScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt index 6ec6972e..6cf3eaca 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt @@ -63,6 +63,8 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemoveFilled import dev.krtirtho.spotube.resources.iconsax.IconsaxUserRemove import dev.krtirtho.spotube.resources.iconsax.User import org.koin.compose.viewmodel.koinViewModel +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun BlacklistScreen() { @@ -70,7 +72,7 @@ fun BlacklistScreen() { Column(modifier = Modifier.fillMaxSize()) { ApplicationMainBar( backButton = true, - title = { Text("Blacklist") }) + title = { Text(stringResource(Res.string.settings_blacklist_title)) }) BlacklistContent( viewModel = viewModel, modifier = Modifier.weight(1f)) @@ -130,7 +132,7 @@ internal fun BlacklistContent( state = filterState, label = "", modifier = Modifier.fillMaxWidth(), - placeholder = "Filter blacklist...") + placeholder = stringResource(Res.string.blacklist_filter_placeholder)) if (isEmpty) { Box( @@ -140,9 +142,9 @@ internal fun BlacklistContent( contentAlignment = Alignment.Center) { Text( text = if (hasActiveFilter) { - "No blacklisted items match '$filterQuery'" + stringResource(Res.string.blacklist_no_results, filterQuery) } else { - "No blacklisted items" + stringResource(Res.string.blacklist_empty) }, style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) @@ -156,7 +158,7 @@ internal fun BlacklistContent( if (filteredTracks.isNotEmpty()) { item { Text( - text = "Tracks", + text = stringResource(Res.string.blacklist_section_tracks), style = AppTheme.typography.titleSmall, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 8.dp)) @@ -171,7 +173,7 @@ internal fun BlacklistContent( if (filteredArtists.isNotEmpty()) { item { Text( - text = "Artists", + text = stringResource(Res.string.library_tab_artists), style = AppTheme.typography.titleSmall, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 8.dp)) @@ -199,7 +201,7 @@ private fun BlacklistedTrackRow( horizontalArrangement = Arrangement.spacedBy(12.dp)) { MillenniumIcon( imageVector = Iconsax.IconsaxMusicCircle, - contentDescription = "Track", + contentDescription = stringResource(Res.string.blacklist_item_track), modifier = Modifier.size(20.dp), tint = AppTheme.colors.onSurfaceVariant) @@ -231,7 +233,7 @@ private fun BlacklistedTrackRow( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxMusicSquareRemoveFilled, - contentDescription = "Remove from blacklist", + contentDescription = stringResource(Res.string.blacklist_remove), tint = AppTheme.colors.error) } } @@ -249,7 +251,7 @@ private fun BlacklistedArtistRow( horizontalArrangement = Arrangement.spacedBy(12.dp)) { MillenniumIcon( imageVector = Iconsax.User, - contentDescription = "Artist", + contentDescription = stringResource(Res.string.media_artist), modifier = Modifier.size(20.dp), tint = AppTheme.colors.onSurfaceVariant) @@ -284,7 +286,7 @@ private fun BlacklistedArtistRow( maxLines = 1, overflow = TextOverflow.Ellipsis) Text( - text = "Artist", + text = stringResource(Res.string.media_artist), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) } @@ -294,7 +296,7 @@ private fun BlacklistedArtistRow( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Remove from blacklist", + contentDescription = stringResource(Res.string.blacklist_remove), tint = AppTheme.colors.error) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt index 01b115c8..a6b029b7 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt @@ -56,7 +56,21 @@ import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen import dev.krtirtho.spotube.resources.iconsax.IconsaxRefreshRight +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.devices_connected_to +import spotube.composeapp.generated.resources.devices_connecting +import spotube.composeapp.generated.resources.devices_device_id +import spotube.composeapp.generated.resources.devices_disconnect +import spotube.composeapp.generated.resources.devices_empty +import spotube.composeapp.generated.resources.devices_empty_hint +import spotube.composeapp.generated.resources.devices_refresh +import spotube.composeapp.generated.resources.devices_resolving +import spotube.composeapp.generated.resources.devices_searching +import spotube.composeapp.generated.resources.devices_title +import spotube.composeapp.generated.resources.devices_unknown_device +import spotube.composeapp.generated.resources.jam_error_dismiss @Composable fun DevicesScreen( @@ -86,7 +100,7 @@ fun DevicesScreen( .background(MillenniumTheme.colors.background)) { ApplicationMainBar( backButton = true, - title = { Text("Devices") }, + title = { Text(stringResource(Res.string.devices_title)) }, actions = { if (isDiscovering && connectingToDevice == null) { MillenniumIndeterminateProgress( @@ -96,7 +110,7 @@ fun DevicesScreen( } else { MillenniumIcon( imageVector = Iconsax.IconsaxRefreshRight, - contentDescription = "Refresh", + contentDescription = stringResource(Res.string.devices_refresh), modifier = Modifier .size(24.dp) .clickable(enabled = connectingToDevice == null) { @@ -111,7 +125,9 @@ fun DevicesScreen( .fillMaxSize() .weight(1f)) { // Error banner - error?.let { errorMessage -> + error?.let { devicesError -> + val errorMessage = devicesError.rawMessage + ?: stringResource(devicesError.message, *devicesError.args.toTypedArray()) Box( modifier = Modifier .fillMaxWidth() @@ -125,7 +141,7 @@ fun DevicesScreen( onClick = { viewModel.clearError() }, modifier = Modifier.padding(top = 8.dp), style = MillenniumButtonStyle.Secondary) { - Text("Dismiss") + Text(stringResource(Res.string.jam_error_dismiss)) } } } @@ -140,14 +156,14 @@ fun DevicesScreen( .padding(16.dp)) { Column { Text( - text = "Connected to ${state.host}:${state.port}", + text = stringResource(Res.string.devices_connected_to, state.host, state.port), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.primary) MillenniumButton( onClick = { viewModel.disconnect() }, modifier = Modifier.padding(top = 8.dp), style = MillenniumButtonStyle.Secondary) { - Text("Disconnect") + Text(stringResource(Res.string.devices_disconnect)) } } } @@ -164,7 +180,7 @@ fun DevicesScreen( MillenniumIndeterminateProgress( modifier = Modifier.size(24.dp)) Text( - text = "Connecting...", + text = stringResource(Res.string.devices_connecting), style = AppTheme.typography.bodyMedium) } } @@ -182,15 +198,15 @@ fun DevicesScreen( Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = if (isDiscovering) { - "Searching for devices on the network..." + stringResource(Res.string.devices_searching) } else { - "No devices found" + stringResource(Res.string.devices_empty) }, style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) if (!isDiscovering) { Text( - text = "Make sure the other device has \"Allow remote control\" enabled in settings.", + text = stringResource(Res.string.devices_empty_hint), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp, start = 32.dp, end = 32.dp)) @@ -244,7 +260,7 @@ private fun DeviceRow( } Column(modifier = Modifier.weight(1f)) { Text( - text = device.name.ifBlank { "Unknown Device" }, + text = device.name.ifBlank { stringResource(Res.string.devices_unknown_device) }, style = AppTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) @@ -252,7 +268,7 @@ private fun DeviceRow( text = if (device.host.isNotBlank() && device.port > 0) { "${device.host}:${device.port}" } else { - "Resolving..." + stringResource(Res.string.devices_resolving) }, style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant, @@ -260,7 +276,7 @@ private fun DeviceRow( overflow = TextOverflow.Ellipsis) if (device.deviceId.isNotBlank()) { Text( - text = "ID: ${device.deviceId.take(8)}...", + text = stringResource(Res.string.devices_device_id, device.deviceId.take(8)), style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant.copy(alpha = 0.6f), maxLines = 1, @@ -268,4 +284,4 @@ private fun DeviceRow( } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesViewModel.kt index eb2e33b1..8e5089e3 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesViewModel.kt @@ -37,6 +37,20 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import org.jetbrains.compose.resources.StringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.devices_error_connect_failed +import spotube.composeapp.generated.resources.devices_error_discovery_failed + +/** + * A user-facing error for the devices screen. [rawMessage] (an untranslated + * cause reported by the transport) takes precedence when present; otherwise the + * composable resolves [message] with [args]. + */ +data class DevicesError( + val message: StringResource, + val args: List = emptyList(), + val rawMessage: String? = null) class DevicesViewModel( private val navigationCommands: NavigationCommands) : ViewModel(), KoinComponent { @@ -58,8 +72,8 @@ class DevicesViewModel( private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) val connectionState: StateFlow = _connectionState.asStateFlow() - private val _error = MutableStateFlow(null) - val error: StateFlow = _error.asStateFlow() + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() private var discoveryJob: Job? = null @@ -69,7 +83,7 @@ class DevicesViewModel( remoteControlClient.connectionState.collect { state -> _connectionState.value = state if (state is ConnectionState.Error) { - _error.value = state.message + _error.value = DevicesError(message = state.fallback, rawMessage = state.message) _connectingToDevice.value = null } else if (state is ConnectionState.Disconnected) { _connectingToDevice.value = null @@ -125,7 +139,7 @@ class DevicesViewModel( } } catch (e: Exception) { logger.e(e) { "Discovery failed" } - _error.value = "Discovery failed: ${e.message}" + _error.value = DevicesError(Res.string.devices_error_discovery_failed, listOf(e.message.toString())) _isDiscovering.value = false } } @@ -193,7 +207,7 @@ class DevicesViewModel( _connectingToDevice.value = null } catch (e: Exception) { logger.e(e) { "Failed to connect to device" } - _error.value = "Failed to connect: ${e.message}" + _error.value = DevicesError(Res.string.devices_error_connect_failed, listOf(e.message.toString())) _connectingToDevice.value = null } } @@ -208,4 +222,4 @@ class DevicesViewModel( fun clearError() { _error.value = null } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt index 323cebc4..2aec64e1 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/PlayDestinationPicker.kt @@ -42,7 +42,20 @@ import dev.krtirtho.spotube.core.remote.RemotePlaybackController import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxCd import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen +import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.devices_destination_action_add_to_queue +import spotube.composeapp.generated.resources.devices_destination_action_play +import spotube.composeapp.generated.resources.devices_destination_action_play_next +import spotube.composeapp.generated.resources.common_cancel +import spotube.composeapp.generated.resources.devices_destination_prompt +import spotube.composeapp.generated.resources.devices_destination_remote_device +import spotube.composeapp.generated.resources.devices_destination_remote_device_host +import spotube.composeapp.generated.resources.devices_destination_remote_subtitle +import spotube.composeapp.generated.resources.devices_destination_this_device +import spotube.composeapp.generated.resources.devices_destination_this_device_subtitle +import spotube.composeapp.generated.resources.devices_destination_title /** * Globally hosted dialog shown when the user tries to play / add to queue / @@ -59,25 +72,25 @@ fun PlayDestinationPickerHost() { val pendingRequest = request ?: return val remoteDeviceName = when (val state = connectionState) { - is ConnectionState.Connected -> "Remote Device (${state.host})" - else -> "Remote Device" + is ConnectionState.Connected -> stringResource(Res.string.devices_destination_remote_device_host, state.host) + else -> stringResource(Res.string.devices_destination_remote_device) } val actionLabel = when (pendingRequest.action) { - PlaybackDestinationAction.Play -> "Play" - PlaybackDestinationAction.AddToQueue -> "Add to queue" - PlaybackDestinationAction.PlayNext -> "Play next" + PlaybackDestinationAction.Play -> stringResource(Res.string.devices_destination_action_play) + PlaybackDestinationAction.AddToQueue -> stringResource(Res.string.devices_destination_action_add_to_queue) + PlaybackDestinationAction.PlayNext -> stringResource(Res.string.devices_destination_action_play_next) } MillenniumDialog( visible = true, onDismissRequest = controller::dismissPicker, - title = "Where to $actionLabel?") { + title = stringResource(Res.string.devices_destination_title)) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = "$actionLabel \"${pendingRequest.title}\" on:", + text = stringResource(Res.string.devices_destination_prompt, actionLabel, pendingRequest.title), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) @@ -92,12 +105,12 @@ fun PlayDestinationPickerHost() { }, title = { Text( - text = "This Device", + text = stringResource(Res.string.devices_destination_this_device), style = AppTheme.typography.bodyLarge) }, subtitle = { Text( - text = "$actionLabel here", + text = stringResource(Res.string.devices_destination_this_device_subtitle), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) }) @@ -119,7 +132,7 @@ fun PlayDestinationPickerHost() { }, subtitle = { Text( - text = "$actionLabel on the connected device", + text = stringResource(Res.string.devices_destination_remote_subtitle), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) }) @@ -128,8 +141,8 @@ fun PlayDestinationPickerHost() { MillenniumButton( onClick = controller::dismissPicker, style = MillenniumButtonStyle.Quiet) { - Text("Cancel") + Text(stringResource(Res.string.common_cancel)) } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt index 7ce08e4c..136c3408 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt @@ -82,7 +82,29 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxPrevious import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateMusic import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeHigh +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.devices_connecting +import spotube.composeapp.generated.resources.devices_disconnect +import spotube.composeapp.generated.resources.devices_remote_album_cover +import spotube.composeapp.generated.resources.devices_remote_connection_error +import spotube.composeapp.generated.resources.devices_remote_disconnected +import spotube.composeapp.generated.resources.devices_remote_loop_mode +import spotube.composeapp.generated.resources.devices_remote_more_options +import spotube.composeapp.generated.resources.devices_remote_next +import spotube.composeapp.generated.resources.devices_remote_previous +import spotube.composeapp.generated.resources.devices_remote_remove_from_queue +import spotube.composeapp.generated.resources.devices_remote_seek +import spotube.composeapp.generated.resources.devices_remote_shuffle +import spotube.composeapp.generated.resources.devices_remote_title +import spotube.composeapp.generated.resources.devices_remote_unknown_artist +import spotube.composeapp.generated.resources.devices_remote_unknown_track +import spotube.composeapp.generated.resources.devices_remote_volume +import spotube.composeapp.generated.resources.player_pause_content_description +import spotube.composeapp.generated.resources.player_play_content_description +import spotube.composeapp.generated.resources.queue_no_queue_entries +import spotube.composeapp.generated.resources.queue_title import kotlin.time.Duration.Companion.milliseconds @Composable @@ -108,7 +130,7 @@ fun RemoteControlScreen( .background(MillenniumTheme.colors.background) ) { ApplicationMainBar( - title = { Text("Remote Control") }, + title = { Text(stringResource(Res.string.devices_remote_title)) }, backButton = true, actions = { MillenniumIconButton( @@ -117,7 +139,7 @@ fun RemoteControlScreen( ) { MillenniumIcon( imageVector = Iconsax.IconsaxMusicFilter, - contentDescription = "Queue", + contentDescription = stringResource(Res.string.queue_title), tint = if (isQueueVisible) { AppTheme.colors.primary } else { @@ -134,7 +156,7 @@ fun RemoteControlScreen( ) { MillenniumIcon( imageVector = Iconsax.IconsaxCloseSquare, - contentDescription = "Disconnect" + contentDescription = stringResource(Res.string.devices_disconnect) ) } } @@ -162,7 +184,7 @@ fun RemoteControlScreen( .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { - Text("Connecting...") + Text(stringResource(Res.string.devices_connecting)) } } @@ -173,7 +195,7 @@ fun RemoteControlScreen( .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { - Text("Disconnected") + Text(stringResource(Res.string.devices_remote_disconnected)) } } @@ -184,7 +206,11 @@ fun RemoteControlScreen( .padding(bottom = shellBottomInset), contentAlignment = Alignment.Center ) { - Text("Connection error: ${(connectionState as ConnectionState.Error).message}") + val errorState = connectionState as ConnectionState.Error + Text( + stringResource( + Res.string.devices_remote_connection_error, + errorState.message ?: stringResource(errorState.fallback))) } } } @@ -259,7 +285,7 @@ private fun RemoteControlContent( if (playerState.currentTrackCoverUrl != null) { AsyncImage( model = playerState.currentTrackCoverUrl, - contentDescription = "Album cover", + contentDescription = stringResource(Res.string.devices_remote_album_cover), modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) @@ -286,7 +312,7 @@ private fun RemoteControlContent( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = playerState.currentTrackTitle ?: "Unknown Track", + text = playerState.currentTrackTitle ?: stringResource(Res.string.devices_remote_unknown_track), style = AppTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, @@ -297,7 +323,7 @@ private fun RemoteControlContent( Spacer(modifier = Modifier.height(8.dp)) Text( - text = playerState.currentTrackArtists ?: "Unknown Artist", + text = playerState.currentTrackArtists ?: stringResource(Res.string.devices_remote_unknown_artist), style = AppTheme.typography.bodyLarge, color = AppTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center, @@ -327,7 +353,7 @@ private fun RemoteControlContent( MillenniumSlider( value = playerState.positionMs.toFloat(), onValueChange = { onSeek(it.toLong()) }, - label = "Seek", + label = stringResource(Res.string.devices_remote_seek), valueRange = 0f..playerState.durationMs.toFloat().coerceAtLeast(1f), modifier = Modifier.fillMaxWidth() ) @@ -364,7 +390,7 @@ private fun RemoteControlContent( ) { MillenniumIcon( imageVector = Iconsax.IconsaxShuffle, - contentDescription = "Shuffle", + contentDescription = stringResource(Res.string.devices_remote_shuffle), tint = if (playerState.shuffleEnabled) { AppTheme.colors.primary } else { @@ -380,7 +406,7 @@ private fun RemoteControlContent( ) { MillenniumIcon( imageVector = Iconsax.IconsaxPrevious, - contentDescription = "Previous", + contentDescription = stringResource(Res.string.devices_remote_previous), modifier = Modifier.size(32.dp) ) } @@ -396,7 +422,11 @@ private fun RemoteControlContent( } else { Iconsax.IconsaxPlay }, - contentDescription = if (playerState.isPlaying) "Pause" else "Play", + contentDescription = if (playerState.isPlaying) { + stringResource(Res.string.player_pause_content_description) + } else { + stringResource(Res.string.player_play_content_description) + }, modifier = Modifier.size(40.dp), tint = LocalTextStyle.current.color ) @@ -409,7 +439,7 @@ private fun RemoteControlContent( ) { MillenniumIcon( imageVector = Iconsax.IconsaxNext, - contentDescription = "Next", + contentDescription = stringResource(Res.string.devices_remote_next), modifier = Modifier.size(32.dp) ) } @@ -421,7 +451,7 @@ private fun RemoteControlContent( ) { MillenniumIcon( imageVector = Iconsax.IconsaxRepeateMusic, - contentDescription = "Loop mode", + contentDescription = stringResource(Res.string.devices_remote_loop_mode), tint = if (playerState.loopMode != "none") { AppTheme.colors.primary } else { @@ -441,7 +471,7 @@ private fun RemoteControlContent( ) { MillenniumIcon( imageVector = Iconsax.IconsaxVolumeHigh, - contentDescription = "Volume", + contentDescription = stringResource(Res.string.devices_remote_volume), tint = AppTheme.colors.onSurfaceVariant, modifier = Modifier.size(24.dp) ) @@ -449,7 +479,7 @@ private fun RemoteControlContent( MillenniumSlider( value = playerState.volume, onValueChange = onSetVolume, - label = "Volume", + label = stringResource(Res.string.devices_remote_volume), valueRange = 0f..1f, modifier = Modifier.weight(1f) ) @@ -473,13 +503,13 @@ private fun RemoteQueueSection( verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text( - text = "Queue", + text = stringResource(Res.string.queue_title), style = AppTheme.typography.titleLarge ) if (queueState.entries.isEmpty()) { Text( - text = "No queue entries", + text = stringResource(Res.string.queue_no_queue_entries), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant ) @@ -574,7 +604,7 @@ private fun RemoteQueueItemRow( MillenniumDropdownMenu( expanded = showMenu, onExpandedChange = { showMenu = it }, - items = listOf("Remove from queue"), + items = listOf(stringResource(Res.string.devices_remote_remove_from_queue)), onItemClick = { onRemoveClick() showMenu = false @@ -586,7 +616,7 @@ private fun RemoteQueueItemRow( ) { MillenniumIcon( Iconsax.Iconsax3DotsMore, - contentDescription = "More options", + contentDescription = stringResource(Res.string.devices_remote_more_options), modifier = Modifier.size(18.dp) ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt index 8330c0e8..4c7a49e0 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/downloads/DownloadManager.kt @@ -57,6 +57,8 @@ import okio.SYSTEM import okio.buffer import okio.use import kotlin.time.Clock +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.* data class DownloadItem( val id: String, @@ -196,11 +198,12 @@ class DownloadManager( private suspend fun executeDownload(item: DownloadItem) { val track = item.track ?: run { logger.e { "Download item has no track metadata" } + val noMetadataMessage = getString(Res.string.downloads_error_no_metadata) downloadsMap.update { map -> map[item.id]?.let { map + (item.id to it.copy( - status = DownloadStatus.Failed("No track metadata"), - errorMessage = "No track metadata")) + status = DownloadStatus.Failed(noMetadataMessage), + errorMessage = noMetadataMessage)) } ?: map } return @@ -210,11 +213,13 @@ class DownloadManager( val streamInfo = streamingUrlRepository.resolveStreamInfo(track) if (streamInfo == null) { logger.w { "Could not resolve stream URL for ${track.title}" } + val unresolvedStatus = getString(Res.string.downloads_error_stream_unresolved_short) + val unresolvedMessage = getString(Res.string.downloads_error_stream_unresolved) downloadsMap.update { map -> map[item.id]?.let { map + (item.id to it.copy( - status = DownloadStatus.Failed("Could not resolve stream URL"), - errorMessage = "Could not resolve stream URL. Try playing the track first.")) + status = DownloadStatus.Failed(unresolvedStatus), + errorMessage = unresolvedMessage)) } ?: map } return @@ -254,10 +259,11 @@ class DownloadManager( throw e } catch (e: Exception) { logger.e(e) { "Download failed: ${item.title}" } + val failureMessage = e.message ?: getString(Res.string.common_unknown_error) downloadsMap.update { map -> map[item.id]?.let { map + (item.id to it.copy( - status = DownloadStatus.Failed(e.message ?: "Unknown error"), + status = DownloadStatus.Failed(failureMessage), errorMessage = e.message)) } ?: map } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt index 4cd088ed..15506867 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt @@ -86,6 +86,9 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.support_button_label +import spotube.composeapp.generated.resources.devices_title +import spotube.composeapp.generated.resources.home_title_browse +import spotube.composeapp.generated.resources.jam_screen_title import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import org.jetbrains.compose.resources.Font @@ -147,7 +150,7 @@ fun HomeScreen(viewModel: HomeScreenViewModel) { color = AppTheme.colors.onSurface ) } else { - Text("Browse") + Text(stringResource(Res.string.home_title_browse)) } }, @@ -159,7 +162,7 @@ fun HomeScreen(viewModel: HomeScreenViewModel) { ) { MillenniumIcon( imageVector = Iconsax.IconsaxMirroringScreen, - contentDescription = "Devices" + contentDescription = stringResource(Res.string.devices_title) ) } MillenniumIconButton( @@ -168,7 +171,7 @@ fun HomeScreen(viewModel: HomeScreenViewModel) { ) { MillenniumIcon( imageVector = Iconsax.User, - contentDescription = "Group Jam" + contentDescription = stringResource(Res.string.jam_screen_title) ) } val supportLabel = stringResource(Res.string.support_button_label) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreenViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreenViewModel.kt index 667fe23f..297e64e2 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreenViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreenViewModel.kt @@ -35,6 +35,9 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface HomeScreenState { data object Loading : HomeScreenState @@ -111,7 +114,7 @@ class HomeScreenViewModel( paginationStrategies = paginationStrategies) }.onFailure { e -> logger.e(e) { "Failed to load home screen data" } - state.value = HomeScreenState.Error(e.message ?: "Unknown error") + state.value = HomeScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun selectGenre(genreId: String) { @@ -155,7 +158,7 @@ class HomeScreenViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more home screen data" } - state.value = HomeScreenState.Error(e.message ?: "Unknown error") + state.value = HomeScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun refresh() { @@ -164,4 +167,4 @@ class HomeScreenViewModel( loadInitialData() } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt index c9dc4bca..adff1af3 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/PlayDestinationPicker.kt @@ -27,6 +27,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.jam_destination_message +import spotube.composeapp.generated.resources.jam_destination_play_here +import spotube.composeapp.generated.resources.jam_destination_suggest +import spotube.composeapp.generated.resources.jam_destination_title +import spotube.composeapp.generated.resources.common_cancel /** * Dialog shown when the user tries to play a collection while a jam session is active. @@ -43,24 +50,24 @@ fun PlayDestinationPicker( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Play Where?") { + title = stringResource(Res.string.jam_destination_title)) { Text( - text = "You have an active jam session. Choose where to play this collection.", + text = stringResource(Res.string.jam_destination_message), style = AppTheme.typography.bodyMedium) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { MillenniumButton( onClick = onDismiss, style = MillenniumButtonStyle.Quiet) { - Text("Cancel") + Text(stringResource(Res.string.common_cancel)) } MillenniumButton( onClick = onPlayLocally, style = MillenniumButtonStyle.Quiet) { - Text("Play here") + Text(stringResource(Res.string.jam_destination_play_here)) } MillenniumButton(onClick = onSuggestToJam) { - Text("Suggest to Jam") + Text(stringResource(Res.string.jam_destination_suggest)) } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryTab.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryTab.kt index 3c9abd4d..6027cc2c 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryTab.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryTab.kt @@ -27,12 +27,12 @@ import dev.krtirtho.spotube.resources.iconsax.User import org.jetbrains.compose.resources.StringResource import spotube.composeapp.generated.resources.* -enum class LibraryTab(val title: String, val icon: ImageVector) { - Playlists("Playlists", Iconsax.IconsaxMusicDashboard), - Albums("Albums", Iconsax.IconsaxCd), - Artists("Artists", Iconsax.User), - LocalTracks("Local Tracks", Iconsax.IconsaxMusicLibrary), - Downloads("Downloads", Iconsax.IconsaxDirectboxReceive); +enum class LibraryTab(val icon: ImageVector) { + Playlists(Iconsax.IconsaxMusicDashboard), + Albums(Iconsax.IconsaxCd), + Artists(Iconsax.User), + LocalTracks(Iconsax.IconsaxMusicLibrary), + Downloads(Iconsax.IconsaxDirectboxReceive); val titleResource: StringResource get() = when (this) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/album/LibraryAlbumsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/album/LibraryAlbumsViewModel.kt index 2f9de499..e2f9d7ee 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/album/LibraryAlbumsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/album/LibraryAlbumsViewModel.kt @@ -36,6 +36,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface LibraryAlbumsState { data object Loading : LibraryAlbumsState @@ -131,7 +134,7 @@ class LibraryAlbumsViewModel( ) }.onFailure { e -> logger.e(e) { "Failed to load albums" } - _state.value = LibraryAlbumsState.Error(e.message ?: "Unknown error") + _state.value = LibraryAlbumsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } suspend fun loadMoreData() = runCatching { @@ -147,7 +150,7 @@ class LibraryAlbumsViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more albums" } - _state.value = LibraryAlbumsState.Error(e.message ?: "Unknown error") + _state.value = LibraryAlbumsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun onQueryChange(query: String) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/artist/LibraryArtistsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/artist/LibraryArtistsViewModel.kt index 7e8cb1df..694d3595 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/artist/LibraryArtistsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/artist/LibraryArtistsViewModel.kt @@ -36,6 +36,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface LibraryArtistsState { data object Loading : LibraryArtistsState @@ -131,7 +134,7 @@ class LibraryArtistsViewModel( nextPagination = result?.nextPagination) }.onFailure { e -> logger.e(e) { "Failed to load artists" } - _state.value = LibraryArtistsState.Error(e.message ?: "Unknown error") + _state.value = LibraryArtistsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } suspend fun loadMoreData() = runCatching { @@ -155,7 +158,7 @@ class LibraryArtistsViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more artists" } - _state.value = LibraryArtistsState.Error(e.message ?: "Unknown error") + _state.value = LibraryArtistsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun onQueryChange(query: String) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt index d5908f90..6c84fd81 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/AddToPlaylistPicker.kt @@ -67,6 +67,16 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import org.koin.compose.koinInject +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.library_add_to_playlist_empty +import spotube.composeapp.generated.resources.library_add_to_playlist_loading +import spotube.composeapp.generated.resources.library_add_to_playlist_search_content_description +import spotube.composeapp.generated.resources.library_add_to_playlist_search_placeholder +import spotube.composeapp.generated.resources.library_add_to_playlist_title +import spotube.composeapp.generated.resources.library_local_tracks_count +import spotube.composeapp.generated.resources.common_cancel @Composable fun AddToPlaylistPicker( @@ -126,12 +136,12 @@ fun AddToPlaylistPicker( horizontalArrangement = Arrangement.spacedBy(8.dp)) { MillenniumIcon( imageVector = Iconsax.IconsaxFilterSearch, - contentDescription = "Search") + contentDescription = stringResource(Res.string.library_add_to_playlist_search_content_description)) MillenniumTextField( state = searchState, modifier = Modifier.weight(1f), - placeholder = "Search your playlists...") + placeholder = stringResource(Res.string.library_add_to_playlist_search_placeholder)) } PlaylistPickerContent( @@ -148,7 +158,7 @@ fun AddToPlaylistPicker( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Add to Your Playlist", + title = stringResource(Res.string.library_add_to_playlist_title), modifier = modifier) { Box(modifier = Modifier.fillMaxWidth().height(400.dp)) { content() @@ -156,7 +166,7 @@ fun AddToPlaylistPicker( MillenniumButton( onClick = onDismiss, style = MillenniumButtonStyle.Secondary) { - Text("Cancel") + Text(stringResource(Res.string.common_cancel)) } } } else { @@ -167,7 +177,7 @@ fun AddToPlaylistPicker( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp)) { Text( - text = "Add to Your Playlist", + text = stringResource(Res.string.library_add_to_playlist_title), style = AppTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp)) content() @@ -183,11 +193,11 @@ private fun PlaylistPickerContent( onPlaylistSelected: (String) -> Unit) { if (isLoading) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Loading your playlists...") + Text(stringResource(Res.string.library_add_to_playlist_loading)) } } else if (playlists.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("You don't own any playlists yet.\nCreate one to add tracks.") + Text(stringResource(Res.string.library_add_to_playlist_empty)) } } else { LazyColumn( @@ -242,7 +252,7 @@ private fun PlaylistPickerRow( maxLines = 1, overflow = TextOverflow.Ellipsis) Text( - text = "${playlist.trackCount} tracks", + text = pluralStringResource(Res.plurals.library_local_tracks_count, playlist.trackCount, playlist.trackCount), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/LibraryPlaylistsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/LibraryPlaylistsViewModel.kt index 4c6662aa..882b9d2b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/LibraryPlaylistsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/LibraryPlaylistsViewModel.kt @@ -36,6 +36,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface LibraryPlaylistsState { data object Loading : LibraryPlaylistsState @@ -127,7 +130,7 @@ class LibraryPlaylistsViewModel( ) }.onFailure { e -> logger.e(e) { "Failed to load playlists" } - _state.value = LibraryPlaylistsState.Error(e.message ?: "Unknown error") + _state.value = LibraryPlaylistsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } suspend fun loadMoreData() = runCatching { @@ -143,7 +146,7 @@ class LibraryPlaylistsViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more playlists" } - _state.value = LibraryPlaylistsState.Error(e.message ?: "Unknown error") + _state.value = LibraryPlaylistsState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun onQueryChange(query: String) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt index ffcf1dfb..7551a157 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/playlist/PlaylistFormSheet.kt @@ -67,6 +67,26 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.library_playlist_form_change_image +import spotube.composeapp.generated.resources.library_playlist_form_collaborative +import spotube.composeapp.generated.resources.library_playlist_form_collaborative_description +import spotube.composeapp.generated.resources.library_playlist_form_cover_content_description +import spotube.composeapp.generated.resources.library_playlist_form_cover_image +import spotube.composeapp.generated.resources.library_playlist_form_create +import spotube.composeapp.generated.resources.library_playlist_form_create_title +import spotube.composeapp.generated.resources.library_playlist_form_description_label +import spotube.composeapp.generated.resources.library_playlist_form_description_placeholder +import spotube.composeapp.generated.resources.library_playlist_form_edit_title +import spotube.composeapp.generated.resources.library_playlist_form_name_label +import spotube.composeapp.generated.resources.library_playlist_form_name_placeholder +import spotube.composeapp.generated.resources.library_playlist_form_no_image +import spotube.composeapp.generated.resources.library_playlist_form_pick_image +import spotube.composeapp.generated.resources.library_playlist_form_public +import spotube.composeapp.generated.resources.library_playlist_form_public_description +import spotube.composeapp.generated.resources.common_cancel +import spotube.composeapp.generated.resources.common_save data class PlaylistFormData( val name: String = "", @@ -146,7 +166,7 @@ fun PlaylistFormSheet( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = if (isEditing) "Edit Playlist" else "Create Playlist", + title = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title), modifier = modifier ) { PlaylistFormFields( @@ -168,13 +188,13 @@ fun PlaylistFormSheet( }, enabled = name.isNotBlank() ) { - Text(if (isEditing) "Save" else "Create") + Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create)) } MillenniumButton( onClick = onDismiss, style = MillenniumButtonStyle.Secondary ) { - Text("Cancel") + Text(stringResource(Res.string.common_cancel)) } } } @@ -187,7 +207,7 @@ fun PlaylistFormSheet( .padding(horizontal = 16.dp, vertical = 8.dp) ) { Text( - text = if (isEditing) "Edit Playlist" else "Create Playlist", + text = stringResource(if (isEditing) Res.string.library_playlist_form_edit_title else Res.string.library_playlist_form_create_title), style = AppTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp) ) @@ -211,7 +231,7 @@ fun PlaylistFormSheet( onClick = onDismiss, style = MillenniumButtonStyle.Secondary ) { - Text("Cancel") + Text(stringResource(Res.string.common_cancel)) } MillenniumButton( onClick = { @@ -220,7 +240,7 @@ fun PlaylistFormSheet( }, enabled = name.isNotBlank() ) { - Text(if (isEditing) "Save" else "Create") + Text(stringResource(if (isEditing) Res.string.common_save else Res.string.library_playlist_form_create)) } } Spacer(modifier = Modifier.height(8.dp)) @@ -249,16 +269,16 @@ private fun PlaylistFormFields( ) { MillenniumTextField( state = nameState, - label = "Name", + label = stringResource(Res.string.library_playlist_form_name_label), modifier = Modifier.fillMaxWidth(), - placeholder = "Playlist name" + placeholder = stringResource(Res.string.library_playlist_form_name_placeholder) ) MillenniumTextField( state = descriptionState, - label = "Description", + label = stringResource(Res.string.library_playlist_form_description_label), modifier = Modifier.fillMaxWidth(), - placeholder = "Description (optional)" + placeholder = stringResource(Res.string.library_playlist_form_description_placeholder) ) Row( @@ -270,9 +290,9 @@ private fun PlaylistFormFields( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Text("Public", style = AppTheme.typography.bodyLarge) + Text(stringResource(Res.string.library_playlist_form_public), style = AppTheme.typography.bodyLarge) Text( - "Anyone can see this playlist", + stringResource(Res.string.library_playlist_form_public_description), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant ) @@ -289,9 +309,9 @@ private fun PlaylistFormFields( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { - Text("Collaborative", style = AppTheme.typography.bodyLarge) + Text(stringResource(Res.string.library_playlist_form_collaborative), style = AppTheme.typography.bodyLarge) Text( - "Others can add tracks to this playlist", + stringResource(Res.string.library_playlist_form_collaborative_description), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant ) @@ -303,7 +323,7 @@ private fun PlaylistFormFields( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Cover Image", style = AppTheme.typography.bodyLarge) + Text(stringResource(Res.string.library_playlist_form_cover_image), style = AppTheme.typography.bodyLarge) Row( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically @@ -317,7 +337,7 @@ private fun PlaylistFormFields( when { imagePreviewUrl != null -> AsyncImage( model = imagePreviewUrl, - contentDescription = "Playlist cover", + contentDescription = stringResource(Res.string.library_playlist_form_cover_content_description), modifier = Modifier.size(64.dp), contentScale = ContentScale.Crop ) @@ -332,7 +352,7 @@ private fun PlaylistFormFields( else -> { Text( - "No image", + stringResource(Res.string.library_playlist_form_no_image), style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant ) @@ -340,7 +360,7 @@ private fun PlaylistFormFields( } } MillenniumButton(onClick = onPickImage) { - Text(if (imageBase64.isNotBlank() || imagePreviewUrl != null) "Change Image" else "Pick Image") + Text(stringResource(if (imageBase64.isNotBlank() || imagePreviewUrl != null) Res.string.library_playlist_form_change_image else Res.string.library_playlist_form_pick_image)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt index 5d98d25b..5bc23b20 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt @@ -64,7 +64,15 @@ import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error +import spotube.composeapp.generated.resources.lyrics_mode_plain +import spotube.composeapp.generated.resources.lyrics_mode_synced +import spotube.composeapp.generated.resources.lyrics_no_plain +import spotube.composeapp.generated.resources.lyrics_no_synced +import spotube.composeapp.generated.resources.common_close @Composable fun LyricsScreen( @@ -88,7 +96,7 @@ fun LyricsScreen( ) { MillenniumIcon( imageVector = Iconsax.IconsaxArrowDown4, - contentDescription = "Close" + contentDescription = stringResource(Res.string.common_close) ) } } else { @@ -103,12 +111,12 @@ fun LyricsScreen( horizontalArrangement = Arrangement.spacedBy(3.dp) ) { LyricsModeTab( - label = "Synced", + label = stringResource(Res.string.lyrics_mode_synced), isSelected = uiState.mode == LyricType.SYNCED, onClick = { viewModel.setMode(LyricType.SYNCED) } ) LyricsModeTab( - label = "Plain", + label = stringResource(Res.string.lyrics_mode_plain), isSelected = uiState.mode == LyricType.STATIC, onClick = { viewModel.setMode(LyricType.STATIC) } ) @@ -140,7 +148,7 @@ fun LyricsScreen( contentAlignment = Alignment.Center ) { Text( - text = uiState.error ?: "Unknown error", + text = uiState.error ?: stringResource(Res.string.common_unknown_error), style = AppTheme.typography.bodyLarge, color = AppTheme.colors.error, textAlign = TextAlign.Center @@ -210,7 +218,7 @@ private fun SyncedLyricsContent( contentAlignment = Alignment.Center ) { Text( - text = "No synced lyrics available", + text = stringResource(Res.string.lyrics_no_synced), style = AppTheme.typography.bodyLarge, color = AppTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center @@ -296,7 +304,7 @@ private fun PlainLyricsContent( contentAlignment = Alignment.Center ) { Text( - text = "No plain lyrics available", + text = stringResource(Res.string.lyrics_no_plain), style = AppTheme.typography.bodyLarge, color = AppTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center @@ -318,4 +326,4 @@ private fun PlainLyricsContent( ) } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt index ef9d8a74..dfe87d1d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/AddTracksToPlaylistDialog.kt @@ -72,6 +72,16 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import org.koin.compose.koinInject +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.playlist_add_tracks_add +import spotube.composeapp.generated.resources.playlist_add_tracks_added_content_description +import spotube.composeapp.generated.resources.playlist_add_tracks_done +import spotube.composeapp.generated.resources.playlist_add_tracks_hint +import spotube.composeapp.generated.resources.playlist_add_tracks_no_results +import spotube.composeapp.generated.resources.playlist_add_tracks_search_content_description +import spotube.composeapp.generated.resources.playlist_add_tracks_search_placeholder +import spotube.composeapp.generated.resources.playlist_add_tracks_title @Composable fun AddTracksToPlaylistDialog( @@ -144,10 +154,10 @@ fun AddTracksToPlaylistDialog( MillenniumTextField( state = queryState, modifier = Modifier.weight(1f), - placeholder = "Search tracks...") + placeholder = stringResource(Res.string.playlist_add_tracks_search_placeholder)) MillenniumIconButton( icon = Iconsax.IconsaxSearch, - contentDescription = "Search", + contentDescription = stringResource(Res.string.playlist_add_tracks_search_content_description), onClick = handleSearch, style = MillenniumIconButtonStyle.Secondary) } @@ -168,7 +178,7 @@ fun AddTracksToPlaylistDialog( .fillMaxWidth() .weight(1f), contentAlignment = Alignment.Center) { - Text("No tracks found") + Text(stringResource(Res.string.playlist_add_tracks_no_results)) } } searchResults.isNotEmpty() -> { @@ -192,7 +202,7 @@ fun AddTracksToPlaylistDialog( .fillMaxWidth() .weight(1f), contentAlignment = Alignment.Center) { - Text("Search for tracks to add") + Text(stringResource(Res.string.playlist_add_tracks_hint)) } } } @@ -203,13 +213,13 @@ fun AddTracksToPlaylistDialog( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Add Tracks to Playlist", + title = stringResource(Res.string.playlist_add_tracks_title), modifier = modifier) { Box(modifier = Modifier.fillMaxWidth().height(500.dp)) { content() } MillenniumButton(onClick = onDismiss) { - Text("Done") + Text(stringResource(Res.string.playlist_add_tracks_done)) } } } else { @@ -221,7 +231,7 @@ fun AddTracksToPlaylistDialog( .padding(horizontal = 16.dp, vertical = 8.dp) .height(600.dp)) { Text( - text = "Add Tracks to Playlist", + text = stringResource(Res.string.playlist_add_tracks_title), style = AppTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp)) content() @@ -282,7 +292,7 @@ private fun TrackSearchResultRow( isAdded -> { MillenniumIcon( imageVector = Iconsax.IconsaxCheckCircle, - contentDescription = "Added", + contentDescription = stringResource(Res.string.playlist_add_tracks_added_content_description), tint = AppTheme.colors.primary, modifier = Modifier.size(24.dp)) } @@ -294,7 +304,7 @@ private fun TrackSearchResultRow( MillenniumButton( onClick = onAdd, style = MillenniumButtonStyle.Secondary) { - Text("Add") + Text(stringResource(Res.string.playlist_add_tracks_add)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt index 09ae39bb..a80c867c 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt @@ -51,6 +51,11 @@ import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.playlist_add_tracks_button +import spotube.composeapp.generated.resources.playlist_loading_title +import spotube.composeapp.generated.resources.playlist_owner_unknown @Composable fun PlaylistScreen( @@ -78,7 +83,7 @@ fun PlaylistScreen( val dataState = state as? PlaylistScreenState.Data val playlist = dataState?.playlist val owner = playlist?.owner - val ownerName = owner?.displayName ?: owner?.username ?: "Unknown" + val ownerName = owner?.displayName ?: owner?.username ?: stringResource(Res.string.playlist_owner_unknown) val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty() @@ -104,7 +109,7 @@ fun PlaylistScreen( MillenniumIcon( imageVector = Iconsax.IconsaxAddSquare, contentDescription = null) - Text("Add Tracks") + Text(stringResource(Res.string.playlist_add_tracks_button)) } } } @@ -112,7 +117,7 @@ fun PlaylistScreen( } else null CollectionView( - title = playlist?.title ?: "Loading playlist...", + title = playlist?.title ?: stringResource(Res.string.playlist_loading_title), description = playlist?.description.orEmpty(), imageURL = artworkUrl, ownerName = ownerName, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt index ec441718..589e13cf 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt @@ -50,6 +50,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_unknown_error sealed interface PlaylistScreenState { data object Loading : PlaylistScreenState @@ -174,7 +177,7 @@ class PlaylistViewModel( nextPagination = tracksResult?.nextPagination) }.onFailure { e -> logger.e(e) { "Failed to load playlist" } - _state.value = PlaylistScreenState.Error(e.message ?: "Unknown error") + _state.value = PlaylistScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } suspend fun loadMoreTracks() = runCatching { @@ -193,7 +196,7 @@ class PlaylistViewModel( } }.onFailure { e -> logger.e(e) { "Failed to load more tracks" } - _state.value = PlaylistScreenState.Error(e.message ?: "Unknown error") + _state.value = PlaylistScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } fun loadNextTracksPage() { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt index 5d13734d..557c0710 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginManager.kt @@ -61,7 +61,35 @@ import okio.Path.Companion.toPath import okio.SYSTEM import okio.buffer import okio.use +import org.jetbrains.compose.resources.getString import org.koin.core.component.KoinComponent +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.plugin_action_install +import spotube.composeapp.generated.resources.plugin_action_replace +import spotube.composeapp.generated.resources.plugin_error_api_incompatible +import spotube.composeapp.generated.resources.plugin_error_api_version_invalid +import spotube.composeapp.generated.resources.plugin_error_app_api_version_invalid +import spotube.composeapp.generated.resources.plugin_error_builtin_add +import spotube.composeapp.generated.resources.plugin_error_builtin_remove +import spotube.composeapp.generated.resources.plugin_error_install_failed +import spotube.composeapp.generated.resources.plugin_error_invalid_plugin_json +import spotube.composeapp.generated.resources.plugin_error_plugin_json_missing +import spotube.composeapp.generated.resources.plugin_error_read_failed +import spotube.composeapp.generated.resources.plugin_error_replace_needs_confirmation +import spotube.composeapp.generated.resources.plugin_error_update_needs_confirmation +import spotube.composeapp.generated.resources.plugin_error_zipline_manifest_missing +import spotube.composeapp.generated.resources.plugin_prompt_incompatible_title +import spotube.composeapp.generated.resources.plugin_prompt_install_message +import spotube.composeapp.generated.resources.plugin_prompt_install_title +import spotube.composeapp.generated.resources.plugin_prompt_replace_downgrade +import spotube.composeapp.generated.resources.plugin_prompt_replace_generic +import spotube.composeapp.generated.resources.plugin_prompt_replace_invalid_incoming +import spotube.composeapp.generated.resources.plugin_prompt_replace_invalid_installed +import spotube.composeapp.generated.resources.plugin_prompt_replace_same_version +import spotube.composeapp.generated.resources.plugin_prompt_replace_title +import spotube.composeapp.generated.resources.plugin_prompt_update_message +import spotube.composeapp.generated.resources.plugin_prompt_update_title +import spotube.composeapp.generated.resources.update_action_update const val PLUGIN_API_VERSION = "0.0.1" @@ -328,7 +356,7 @@ class PluginManager( val pluginJsonPath = tempDir / "plugin.json".toPath() if (!FileSystem.SYSTEM.exists(pluginJsonPath)) { - throw IllegalArgumentException("plugin.json not found in the zip file") + throw IllegalArgumentException(getString(Res.string.plugin_error_plugin_json_missing)) } val pluginJson = @@ -336,7 +364,7 @@ class PluginManager( val pluginEntry = try { Json.decodeFromString(pluginJson) } catch (e: Exception) { - throw IllegalArgumentException("Invalid plugin.json format: ${e.message}") + throw IllegalArgumentException(getString(Res.string.plugin_error_invalid_plugin_json, e.message.orEmpty())) } // Preserve logo.png before deleting temp dir so the permission dialog can show it @@ -350,7 +378,7 @@ class PluginManager( pendingPlugin.value = buildPendingPlugin(pluginEntry, bytes) } catch (e: Exception) { - throw Exception("Failed to read plugin: ${e.message}", e) + throw Exception(getString(Res.string.plugin_error_read_failed, e.message.orEmpty()), e) } finally { if (FileSystem.SYSTEM.exists(tempDir)) { try { @@ -436,11 +464,11 @@ class PluginManager( val pluginJsonPath = tempDir / "plugin.json".toPath() if (!FileSystem.SYSTEM.exists(pluginJsonPath)) { - throw IllegalArgumentException("plugin.json not found in the zip file") + throw IllegalArgumentException(getString(Res.string.plugin_error_plugin_json_missing)) } val pluginManifestPath = tempDir / "manifest.zipline.json".toPath() if (!FileSystem.SYSTEM.exists(pluginManifestPath)) { - throw IllegalArgumentException("manifest.zipline.json not found in the zip file") + throw IllegalArgumentException(getString(Res.string.plugin_error_zipline_manifest_missing)) } val pluginJson = FileSystem.SYSTEM.source(pluginJsonPath).buffer().use { source -> @@ -450,7 +478,7 @@ class PluginManager( val pluginEntry = try { Json.decodeFromString(pluginJson) } catch (e: Exception) { - throw IllegalArgumentException("Invalid plugin.json format: ${e.message}") + throw IllegalArgumentException(getString(Res.string.plugin_error_invalid_plugin_json, e.message.orEmpty())) } ensurePluginApiCompatible(pluginEntry) @@ -459,8 +487,8 @@ class PluginManager( if (installedPlugin != null && !allowReplacingInstalled) { throw IllegalArgumentException( when (compareVersions(installedPlugin, pluginEntry)) { - VersionRelation.Update -> "A newer version of ${pluginEntry.name} is available. Confirm the update before installing." - VersionRelation.Replace -> "${pluginEntry.name} is already installed. Confirm replacing it before installing." + VersionRelation.Update -> getString(Res.string.plugin_error_update_needs_confirmation, pluginEntry.name) + VersionRelation.Replace -> getString(Res.string.plugin_error_replace_needs_confirmation, pluginEntry.name) } ) } @@ -490,7 +518,7 @@ class PluginManager( addPlugin(pluginEntry) } catch (e: Exception) { - throw Exception("Failed to install plugin: ${e.message}", e) + throw Exception(getString(Res.string.plugin_error_install_failed, e.message.orEmpty()), e) } finally { if (FileSystem.SYSTEM.exists(tempDir)) { try { @@ -521,7 +549,7 @@ class PluginManager( private suspend fun addPlugin(plugin: PluginEntry) { if (plugin in BUILT_IN_PLUGINS) { - throw IllegalArgumentException("Built-in plugins are already included and can't be added again.") + throw IllegalArgumentException(getString(Res.string.plugin_error_builtin_add)) } val currentState = state.value @@ -543,7 +571,7 @@ class PluginManager( suspend fun removePlugin(plugin: PluginEntry) { if (plugin in BUILT_IN_PLUGINS) { - throw IllegalArgumentException("Built-in plugins can't be removed.") + throw IllegalArgumentException(getString(Res.string.plugin_error_builtin_remove)) } val currentState = state.value @@ -589,46 +617,49 @@ class PluginManager( Replace, } - private fun buildPendingPlugin(pluginEntry: PluginEntry, bytes: ByteArray): PendingPlugin { + private suspend fun buildPendingPlugin(pluginEntry: PluginEntry, bytes: ByteArray): PendingPlugin { val apiError = getPluginApiCompatibilityError(pluginEntry) if (apiError != null) { return PendingPlugin( entry = pluginEntry, kind = InstallPromptKind.INFO, - title = "Plugin API not compatible", + title = getString(Res.string.plugin_prompt_incompatible_title), message = apiError) } val installedPlugin = findInstalledPlugin(pluginEntry.id) ?: return PendingPlugin( entry = pluginEntry, kind = InstallPromptKind.INSTALL, - title = "Install plugin?", - message = "${pluginEntry.name} will be added to your installed plugins.", + title = getString(Res.string.plugin_prompt_install_title), + message = getString(Res.string.plugin_prompt_install_message, pluginEntry.name), bytes = bytes, - confirmLabel = "Install") + confirmLabel = getString(Res.string.plugin_action_install)) return when (compareVersions(installedPlugin, pluginEntry)) { VersionRelation.Update -> PendingPlugin( entry = pluginEntry, kind = InstallPromptKind.UPDATE, - title = "Update plugin?", - message = "An older version is installed (${installedPlugin.version}). Update to ${pluginEntry.version}?", + title = getString(Res.string.plugin_prompt_update_title), + message = getString( + Res.string.plugin_prompt_update_message, + installedPlugin.version, + pluginEntry.version), bytes = bytes, existingEntry = installedPlugin, - confirmLabel = "Update") + confirmLabel = getString(Res.string.update_action_update)) VersionRelation.Replace -> PendingPlugin( entry = pluginEntry, kind = InstallPromptKind.REPLACE, - title = "Replace installed plugin?", + title = getString(Res.string.plugin_prompt_replace_title), message = buildReplaceMessage(installedPlugin, pluginEntry), bytes = bytes, existingEntry = installedPlugin, - confirmLabel = "Replace") + confirmLabel = getString(Res.string.plugin_action_replace)) } } - private fun buildReplaceMessage( + private suspend fun buildReplaceMessage( installedPlugin: PluginEntry, incomingPlugin: PluginEntry ): String { @@ -636,11 +667,22 @@ class PluginManager( val installedVersion = parseSemVerOrNull(installedPlugin.version) return when { - incomingVersion == null -> "${incomingPlugin.name} uses an invalid semantic version (${incomingPlugin.version}) and can't be compared as an update. Replace the installed plugin anyway?" - installedVersion == null -> "The installed version (${installedPlugin.version}) can't be compared using semantic versioning. Replace it with ${incomingPlugin.version}?" - incomingVersion == installedVersion -> "Version ${incomingPlugin.version} is already installed. Replace the existing plugin with the supplied copy?" - incomingVersion < installedVersion -> "Installed version ${installedPlugin.version} is newer than the supplied version ${incomingPlugin.version}. Replace it anyway?" - else -> "Replace the installed plugin with the supplied copy?" + incomingVersion == null -> getString( + Res.string.plugin_prompt_replace_invalid_incoming, + incomingPlugin.name, + incomingPlugin.version) + installedVersion == null -> getString( + Res.string.plugin_prompt_replace_invalid_installed, + installedPlugin.version, + incomingPlugin.version) + incomingVersion == installedVersion -> getString( + Res.string.plugin_prompt_replace_same_version, + incomingPlugin.version) + incomingVersion < installedVersion -> getString( + Res.string.plugin_prompt_replace_downgrade, + installedPlugin.version, + incomingPlugin.version) + else -> getString(Res.string.plugin_prompt_replace_generic) } } @@ -671,17 +713,21 @@ class PluginManager( } } - private fun ensurePluginApiCompatible(pluginEntry: PluginEntry) { + private suspend fun ensurePluginApiCompatible(pluginEntry: PluginEntry) { getPluginApiCompatibilityError(pluginEntry)?.let { error -> throw IllegalArgumentException(error) } } - private fun getPluginApiCompatibilityError(pluginEntry: PluginEntry): String? { + private suspend fun getPluginApiCompatibilityError(pluginEntry: PluginEntry): String? { val pluginApiVersion = parseSemVerOrNull(pluginEntry.apiVersion) - ?: return "${pluginEntry.name} declares an invalid apiVersion (${pluginEntry.apiVersion}). Expected semantic versioning compatible with $PLUGIN_API_VERSION." + ?: return getString( + Res.string.plugin_error_api_version_invalid, + pluginEntry.name, + pluginEntry.apiVersion, + PLUGIN_API_VERSION) val appApiVersion = parseSemVerOrNull(PLUGIN_API_VERSION) - ?: return "Spotube plugin API version $PLUGIN_API_VERSION is invalid." + ?: return getString(Res.string.plugin_error_app_api_version_invalid, PLUGIN_API_VERSION) val compatible = if (pluginApiVersion.major == 0 || appApiVersion.major == 0) { pluginApiVersion.major == appApiVersion.major && pluginApiVersion.minor == appApiVersion.minor @@ -692,7 +738,11 @@ class PluginManager( return if (compatible) { null } else { - "${pluginEntry.name} targets plugin API ${pluginEntry.apiVersion}, but Spotube supports breaking plugin API $PLUGIN_API_VERSION. Install a plugin built for the same breaking API version." + getString( + Res.string.plugin_error_api_incompatible, + pluginEntry.name, + pluginEntry.apiVersion, + PLUGIN_API_VERSION) } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt index 2942c39a..5ddffeb0 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt @@ -109,9 +109,26 @@ import io.github.vinceglb.filekit.dialogs.FileKitType import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher import okio.Path import kotlinx.coroutines.flow.collect +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.plugin_discover_badge_official +import spotube.composeapp.generated.resources.plugin_discover_badge_verified +import spotube.composeapp.generated.resources.plugin_discover_repository_content_description +import spotube.composeapp.generated.resources.plugin_discover_stars_content_description +import spotube.composeapp.generated.resources.plugin_info_abilities +import spotube.composeapp.generated.resources.plugin_info_api_version +import spotube.composeapp.generated.resources.plugin_info_author +import spotube.composeapp.generated.resources.plugin_info_capabilities +import spotube.composeapp.generated.resources.plugin_info_contact +import spotube.composeapp.generated.resources.plugin_info_label_format +import spotube.composeapp.generated.resources.plugin_info_license +import spotube.composeapp.generated.resources.plugin_info_report_bugs +import spotube.composeapp.generated.resources.plugin_info_repository +import spotube.composeapp.generated.resources.plugin_info_version +import spotube.composeapp.generated.resources.plugin_support_dialog_title +import spotube.composeapp.generated.resources.common_close import spotube.composeapp.generated.resources.welcome_plugins_title import spotube.composeapp.generated.resources.welcome_plugins_setup_guide import spotube.composeapp.generated.resources.welcome_plugins_discover @@ -129,9 +146,7 @@ import spotube.composeapp.generated.resources.plugin_error_download_failed import spotube.composeapp.generated.resources.plugin_error_enter_url import spotube.composeapp.generated.resources.plugin_error_url_scheme import spotube.composeapp.generated.resources.plugin_install_section_title -import spotube.composeapp.generated.resources.plugin_installed_count -import spotube.composeapp.generated.resources.plugin_installed_plural -import spotube.composeapp.generated.resources.plugin_installed_singular +import spotube.composeapp.generated.resources.plugin_installed_plugins_count import spotube.composeapp.generated.resources.plugin_screen_title import spotube.composeapp.generated.resources.plugin_section_file_title import spotube.composeapp.generated.resources.plugin_section_install @@ -385,16 +400,11 @@ internal fun PluginScreenContent( } } else { item { - val noun = if (data.plugins.size == 1) { - stringResource(Res.string.plugin_installed_singular) - } else { - stringResource(Res.string.plugin_installed_plural) - } Text( - stringResource( - Res.string.plugin_installed_count, + pluralStringResource( + Res.plurals.plugin_installed_plugins_count, data.plugins.size, - noun + data.plugins.size ), style = AppTheme.typography.labelMedium, color = AppTheme.colors.onSurfaceVariant, @@ -720,47 +730,44 @@ private fun PluginInfoDialog( ) } - DetailRow("Version", plugin.version) - DetailRow("API Version", plugin.apiVersion) - DetailRow("Author", plugin.author) + DetailRow(stringResource(Res.string.plugin_info_version), plugin.version) + DetailRow(stringResource(Res.string.plugin_info_api_version), plugin.apiVersion) + DetailRow(stringResource(Res.string.plugin_info_author), plugin.author) if (plugin.license.isNotBlank()) { - DetailRow("License", plugin.license) + DetailRow(stringResource(Res.string.plugin_info_license), plugin.license) } if (plugin.capabilities.isNotEmpty()) { - DetailChipsRow("Capabilities", plugin.capabilities.map { it.name }) + DetailChipsRow( + stringResource(Res.string.plugin_info_capabilities), + plugin.capabilities.map { it.name }) } if (plugin.abilities.isNotEmpty()) { - DetailChipsRow("Abilities", plugin.abilities.map { ability -> - when (ability) { - PluginAbility.METADATA -> "Metadata" - PluginAbility.AUDIO -> "Audio" - PluginAbility.LYRICS -> "Lyrics" - PluginAbility.SCROBBLE -> "Scrobble" - } - }) + DetailChipsRow( + stringResource(Res.string.plugin_info_abilities), + plugin.abilities.map { ability -> ability.displayLabel() }) } if (plugin.repository.isNotBlank()) { - ClickableDetailRow(label = "Repository", value = plugin.repository) { + ClickableDetailRow(label = stringResource(Res.string.plugin_info_repository), value = plugin.repository) { openUrlInBrowser(plugin.repository) } } if (plugin.contact.isNotBlank()) { - DetailRow("Contact", plugin.contact) + DetailRow(stringResource(Res.string.plugin_info_contact), plugin.contact) } if (plugin.bugs.isNotBlank()) { - ClickableDetailRow(label = "Report Bugs", value = plugin.bugs) { + ClickableDetailRow(label = stringResource(Res.string.plugin_info_report_bugs), value = plugin.bugs) { openUrlInBrowser(plugin.bugs) } } } Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { MillenniumButton(onClick = onDismiss) { - Text("Close") + Text(stringResource(Res.string.common_close)) } } } @@ -775,7 +782,7 @@ private fun SupportDialog( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Support ${plugin.name}") { + title = stringResource(Res.string.plugin_support_dialog_title, plugin.name)) { if (isLoading) { Box( modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), @@ -792,7 +799,7 @@ private fun SupportDialog( } Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { MillenniumButton(onClick = onDismiss) { - Text("Close") + Text(stringResource(Res.string.common_close)) } } } @@ -861,7 +868,7 @@ private fun DiscoverRepoCard( .background(AppTheme.colors.primary.copy(alpha = 0.15f)) ) { Text( - "Official", + stringResource(Res.string.plugin_discover_badge_official), style = AppTheme.typography.labelSmall, color = AppTheme.colors.primary, modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp) @@ -885,7 +892,7 @@ private fun DiscoverRepoCard( tint = Color(0xFF4CAF50) ) Text( - "Verified", + stringResource(Res.string.plugin_discover_badge_verified), style = AppTheme.typography.labelSmall, color = Color(0xFF4CAF50) ) @@ -917,7 +924,7 @@ private fun DiscoverRepoCard( ) { MillenniumIcon( Iconsax.IconsaxHeart, - contentDescription = "Github Stars", + contentDescription = stringResource(Res.string.plugin_discover_stars_content_description), modifier = Modifier.size(11.dp), tint = AppTheme.colors.onSurfaceVariant ) @@ -942,7 +949,7 @@ private fun DiscoverRepoCard( ) { MillenniumIcon( Iconsax.CarbonGithubLogo, - contentDescription = "Github Repository URL", + contentDescription = stringResource(Res.string.plugin_discover_repository_content_description), modifier = Modifier.size(10.dp), tint = AppTheme.colors.onSurfaceVariant ) @@ -985,7 +992,7 @@ private fun DetailRow(label: String, value: String) { horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - "$label:", + stringResource(Res.string.plugin_info_label_format, label), style = AppTheme.typography.labelMedium, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.widthIn(min = 90.dp) @@ -1005,7 +1012,7 @@ private fun ClickableDetailRow(label: String, value: String, onClick: () -> Unit horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - "$label:", + stringResource(Res.string.plugin_info_label_format, label), style = AppTheme.typography.labelMedium, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.widthIn(min = 90.dp) @@ -1027,7 +1034,7 @@ private fun DetailChipsRow(label: String, chips: List) { horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - "$label:", + stringResource(Res.string.plugin_info_label_format, label), style = AppTheme.typography.labelMedium, color = AppTheme.colors.onSurfaceVariant, modifier = Modifier.widthIn(min = 90.dp) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/DefaultAbilityPluginGrid.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/DefaultAbilityPluginGrid.kt index 36ff939a..d0e1e0e7 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/DefaultAbilityPluginGrid.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/DefaultAbilityPluginGrid.kt @@ -66,6 +66,7 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxTextalignLeft import okio.Path import org.jetbrains.compose.resources.stringResource import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.plugin_version_label import spotube.composeapp.generated.resources.settings_plugins_ability_audio import spotube.composeapp.generated.resources.settings_plugins_ability_lyrics import spotube.composeapp.generated.resources.settings_plugins_ability_metadata @@ -110,8 +111,9 @@ internal fun PluginAbility.displayLabel(): String = when (this) { } /** A short "v1.2.3 · Author" summary shown under a plugin in the picker. */ +@Composable private fun PluginEntry.metadataLabel(): String = buildString { - append("v").append(version) + append(stringResource(Res.string.plugin_version_label, version)) if (author.isNotBlank()) { append(" · ").append(author) } @@ -326,7 +328,7 @@ private fun AbilityPluginTile( text = buildString { append(ability.displayLabel()) if (!version.isNullOrBlank()) { - append(" · v").append(version) + append(" · ").append(stringResource(Res.string.plugin_version_label, version)) } }, style = AppTheme.typography.labelMedium, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt index 84b675ca..f598b5c6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginInstallDialog.kt @@ -57,12 +57,20 @@ import dev.krtirtho.spotube.modules.plugin.GitHubRelease import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4 import dev.krtirtho.spotube.resources.iconsax.IconsaxBox +import org.jetbrains.compose.resources.stringResource import org.spotube.millennium.composables.MillenniumButton import org.spotube.millennium.composables.MillenniumButtonStyle import org.spotube.millennium.composables.MillenniumDialog import org.spotube.millennium.composables.MillenniumDropdownMenu import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIndeterminateProgress +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.plugin_action_install +import spotube.composeapp.generated.resources.plugin_install_no_releases +import spotube.composeapp.generated.resources.plugin_install_prerelease +import spotube.composeapp.generated.resources.plugin_install_release_notes +import spotube.composeapp.generated.resources.plugin_install_select_release_label +import spotube.composeapp.generated.resources.plugin_install_select_release_placeholder @Composable fun PluginInstallDialog( @@ -135,13 +143,13 @@ fun PluginInstallDialog( } } else if (releases.isEmpty()) { Text( - "No releases found", + stringResource(Res.string.plugin_install_no_releases), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant ) } else { Text( - "Select Release", + stringResource(Res.string.plugin_install_select_release_label), style = AppTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = AppTheme.colors.onSurface @@ -168,7 +176,8 @@ fun PluginInstallDialog( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - selectedRelease?.tagName ?: "Select a release", + selectedRelease?.tagName + ?: stringResource(Res.string.plugin_install_select_release_placeholder), style = AppTheme.typography.bodyMedium ) MillenniumIcon( @@ -184,14 +193,14 @@ fun PluginInstallDialog( selectedRelease?.let { release -> if (release.prerelease) { Text( - "Pre-release", + stringResource(Res.string.plugin_install_prerelease), style = AppTheme.typography.labelSmall, color = AppTheme.colors.error ) } if (!release.body.isNullOrBlank()) { Text( - "Release Notes", + stringResource(Res.string.plugin_install_release_notes), style = AppTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = AppTheme.colors.onSurface @@ -225,7 +234,7 @@ fun PluginInstallDialog( onClick = { selectedRelease?.let { onInstall(it) } }, enabled = selectedRelease != null && !isLoadingReleases ) { - Text("Install") + Text(stringResource(Res.string.plugin_action_install)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt index 40be3978..5c385c41 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/components/PluginPermissionsDialog.kt @@ -70,8 +70,8 @@ import spotube.composeapp.generated.resources.plugin_permissions_none import spotube.composeapp.generated.resources.plugin_permissions_requested_title import spotube.composeapp.generated.resources.plugin_permissions_supplied_label import spotube.composeapp.generated.resources.plugin_version_label -import spotube.composeapp.generated.resources.settings_action_cancel -import spotube.composeapp.generated.resources.settings_action_close +import spotube.composeapp.generated.resources.common_cancel +import spotube.composeapp.generated.resources.common_close @Composable fun PluginPermissionDialog( @@ -197,9 +197,9 @@ fun PluginPermissionDialog( style = MillenniumButtonStyle.Secondary) { Text( if (confirmLabel == null) { - stringResource(Res.string.settings_action_close) + stringResource(Res.string.common_close) } else { - stringResource(Res.string.settings_action_cancel) + stringResource(Res.string.common_cancel) } ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt index 324c464f..2eb73d78 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt @@ -34,6 +34,9 @@ import dev.krtirtho.spotube.core.ui.component.CollectionView import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.liked_tracks +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.* @Composable fun SavedTracksScreen( @@ -62,11 +65,11 @@ fun SavedTracksScreen( val isLoading = state is SavedTracksScreenState.Loading && (dataState == null || dataState.tracks.isEmpty()) CollectionView( - title = "Saved Tracks", - description = dataState?.let { "${it.totalCount} tracks" } ?: "", + title = stringResource(Res.string.saved_tracks_title), + description = dataState?.let { pluralStringResource(Res.plurals.library_local_tracks_count, it.totalCount, it.totalCount) } ?: "", imageURL = "", imageResource = Res.drawable.liked_tracks, - ownerName = "You", + ownerName = stringResource(Res.string.saved_tracks_owner_you), ownerImageURL = null, onOwnerClick = {}, onPlay = viewModel::playSavedTracks, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt index 44ecc52e..c8bdb3dd 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksViewModel.kt @@ -57,6 +57,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.compose.koinInject import org.koin.core.component.KoinComponent +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.* const val SAVED_TRACKS_COLLECTION_ID = "saved_tracks" @@ -171,7 +173,7 @@ class SavedTracksViewModel( totalCount = totalCount) }.onFailure { e -> logger.e(e) { "Failed to load saved tracks" } - _state.value = SavedTracksScreenState.Error(e.message ?: "Unknown error") + _state.value = SavedTracksScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } } @@ -188,7 +190,7 @@ class SavedTracksViewModel( totalCount = currentState.totalCount) }.onFailure { e -> logger.e(e) { "Failed to load more tracks" } - _state.value = SavedTracksScreenState.Error(e.message ?: "Unknown error") + _state.value = SavedTracksScreenState.Error(e.message ?: getString(Res.string.common_unknown_error)) } } } @@ -367,7 +369,7 @@ fun rememberIsSavedTracks( val result = repository.isSavedTracks(trackIds) state = SavedState.Success(result) }.onFailure { e -> - state = SavedState.Error(e.message ?: "Unknown error") + state = SavedState.Error(e.message ?: getString(Res.string.common_unknown_error)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchRepository.kt index 5ff73aeb..65af0b10 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchRepository.kt @@ -34,6 +34,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.search_error_no_metadata_plugin class SearchRepository( private val pluginManager: PluginManager @@ -70,7 +73,7 @@ class SearchRepository( suspend fun loadSupportedSearchTypes(): List { return supportedTypesCache.get(Unit) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.supportedSearchTypes } } @@ -80,7 +83,7 @@ class SearchRepository( suspend fun searchAll(query: String): List { return allSearchCache.get(query) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.search(query) } } @@ -94,7 +97,7 @@ class SearchRepository( val key = query to pagination return trackSearchCache.get(key) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.searchTracks(query, pagination) } } @@ -108,7 +111,7 @@ class SearchRepository( val key = query to pagination return albumSearchCache.get(key) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.searchAlbums(query, pagination) } } @@ -122,7 +125,7 @@ class SearchRepository( val key = query to pagination return artistSearchCache.get(key) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.searchArtists(query, pagination) } } @@ -136,7 +139,7 @@ class SearchRepository( val key = query to pagination return playlistSearchCache.get(key) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.searchPlaylists(query, pagination) } } @@ -150,7 +153,7 @@ class SearchRepository( val key = query to pagination return userSearchCache.get(key) { val plugin = pluginManager.selectedMetadataPlugin.value - ?: throw IllegalStateException("No metadata plugin selected") + ?: throw IllegalStateException(getString(Res.string.search_error_no_metadata_plugin)) pluginManager.withScope { plugin.use { metadataSearchAPI.searchUsers(query, pagination) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt index 8db81996..24d466c6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt @@ -123,6 +123,36 @@ import org.jetbrains.compose.resources.stringResource import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.search_clear_history import spotube.composeapp.generated.resources.search_placeholder +import spotube.composeapp.generated.resources.library_tab_albums +import spotube.composeapp.generated.resources.library_tab_artists +import spotube.composeapp.generated.resources.library_tab_playlists +import spotube.composeapp.generated.resources.search_results_title +import spotube.composeapp.generated.resources.search_no_search_types +import spotube.composeapp.generated.resources.search_start_typing +import spotube.composeapp.generated.resources.search_no_results +import spotube.composeapp.generated.resources.search_section_tracks +import spotube.composeapp.generated.resources.search_section_users +import spotube.composeapp.generated.resources.search_start_typing_tracks +import spotube.composeapp.generated.resources.search_loading_tracks +import spotube.composeapp.generated.resources.search_no_albums +import spotube.composeapp.generated.resources.search_loading_albums +import spotube.composeapp.generated.resources.search_no_artists +import spotube.composeapp.generated.resources.search_loading_artists +import spotube.composeapp.generated.resources.search_no_playlists +import spotube.composeapp.generated.resources.search_loading_playlists +import spotube.composeapp.generated.resources.search_no_users +import spotube.composeapp.generated.resources.search_loading_users +import spotube.composeapp.generated.resources.search_see_all +import spotube.composeapp.generated.resources.search_searching +import spotube.composeapp.generated.resources.search_warm_crates +import spotube.composeapp.generated.resources.search_warm_cosmos +import spotube.composeapp.generated.resources.search_warm_speakers +import spotube.composeapp.generated.resources.search_warm_vinyl +import spotube.composeapp.generated.resources.search_warm_music_gods +import spotube.composeapp.generated.resources.search_warm_antennas +import spotube.composeapp.generated.resources.search_warm_records +import spotube.composeapp.generated.resources.search_tab_all +import org.jetbrains.compose.resources.StringResource import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -137,6 +167,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) { val audioPlayerQueue: AudioPlayerQueue = koinInject() val shareService: ShareService = koinInject() val remotePlaybackController: RemotePlaybackController = koinInject() + val searchResultsTitle = stringResource(Res.string.search_results_title) val downloadsViewModel: DownloadsViewModel = koinViewModel() val libraryRepository: LibraryRepository = koinInject() val blacklistRepository: BlacklistRepository = koinInject() @@ -283,7 +314,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) { } fun bulkAddToQueue(tracks: List) { - remotePlaybackController.requestTracksAddToQueue(tracks, "Search results") + remotePlaybackController.requestTracksAddToQueue(tracks, searchResultsTitle) } fun bulkAddToJam(tracks: List) { @@ -291,7 +322,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) { } fun bulkPlayNext(tracks: List) { - remotePlaybackController.requestTracksPlayNext(tracks, "Search results") + remotePlaybackController.requestTracksPlayNext(tracks, searchResultsTitle) } Column( @@ -360,7 +391,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) { selectedType == null -> { SearchMessage( - message = "No search types available", + message = stringResource(Res.string.search_no_search_types), modifier = Modifier .fillMaxSize() ) @@ -616,7 +647,7 @@ private fun SearchAllTab( modifier: Modifier = Modifier ) { if (query.isBlank()) { - SearchMessage(message = "Start typing to search", modifier = modifier) + SearchMessage(message = stringResource(Res.string.search_start_typing), modifier = modifier) return } @@ -644,7 +675,7 @@ private fun SearchAllTab( } if (!hasAnyContent && !isLoadingAll) { - SearchMessage(message = "No results found", modifier = modifier) + SearchMessage(message = stringResource(Res.string.search_no_results), modifier = modifier) return } @@ -677,7 +708,7 @@ private fun SearchAllTab( headerContent = { if (showTracks) { Text( - text = "Tracks", + text = stringResource(Res.string.search_section_tracks), style = AppTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 12.dp) @@ -699,7 +730,7 @@ private fun SearchAllTab( ) { if (MetadataSupportedSearchType.PLAYLIST in supportedSearchTypes && playlists.items.isNotEmpty()) { SearchHorizontalSection( - title = "Playlists", + title = stringResource(Res.string.library_tab_playlists), onSeeAll = { onSeeAll(MetadataSupportedSearchType.PLAYLIST) }) { playlists.items.forEach { playlist -> item(key = playlist.id) { PlaylistCard(playlist = playlist) } @@ -709,7 +740,7 @@ private fun SearchAllTab( if (MetadataSupportedSearchType.ALBUM in supportedSearchTypes && albums.items.isNotEmpty()) { SearchHorizontalSection( - title = "Albums", + title = stringResource(Res.string.library_tab_albums), onSeeAll = { onSeeAll(MetadataSupportedSearchType.ALBUM) }) { albums.items.forEach { album -> item(key = album.id) { AlbumCard(album = album) } @@ -719,7 +750,7 @@ private fun SearchAllTab( if (MetadataSupportedSearchType.ARTIST in supportedSearchTypes && artists.items.isNotEmpty()) { SearchHorizontalSection( - title = "Artists", + title = stringResource(Res.string.library_tab_artists), onSeeAll = { onSeeAll(MetadataSupportedSearchType.ARTIST) }) { artists.items.forEach { artist -> item(key = artist.id) { ArtistCard(artist = artist) } @@ -729,7 +760,7 @@ private fun SearchAllTab( if (MetadataSupportedSearchType.USER in supportedSearchTypes && users.items.isNotEmpty()) { SearchHorizontalSection( - title = "Users", + title = stringResource(Res.string.search_section_users), onSeeAll = { onSeeAll(MetadataSupportedSearchType.USER) }) { users.items.forEach { user -> item(key = user.id) { UserCard(user = user) } @@ -762,12 +793,12 @@ private fun SearchTracksTab( modifier: Modifier = Modifier ) { if (query.isBlank()) { - SearchMessage(message = "Start typing to search tracks", modifier = modifier) + SearchMessage(message = stringResource(Res.string.search_start_typing_tracks), modifier = modifier) return } if (tracks.isLoading && tracks.items.isEmpty()) { - SearchLoadingIndicator(message = "Loading tracks...", modifier = modifier) + SearchLoadingIndicator(message = stringResource(Res.string.search_loading_tracks), modifier = modifier) return } @@ -810,8 +841,8 @@ private fun SearchAlbumsTab( error = albums.error, hasNextPage = albums.hasNextPage, onLoadNextPage = onLoadNextPage, - emptyMessage = "No albums found", - loadingMessage = "Loading albums...", + emptyMessage = stringResource(Res.string.search_no_albums), + loadingMessage = stringResource(Res.string.search_loading_albums), modifier = modifier ) { album -> AlbumCard(album = album, modifier = Modifier.fillMaxWidth()) @@ -832,8 +863,8 @@ private fun SearchArtistsTab( error = artists.error, hasNextPage = artists.hasNextPage, onLoadNextPage = onLoadNextPage, - emptyMessage = "No artists found", - loadingMessage = "Loading artists...", + emptyMessage = stringResource(Res.string.search_no_artists), + loadingMessage = stringResource(Res.string.search_loading_artists), modifier = modifier ) { artist -> ArtistCard(artist = artist, modifier = Modifier.fillMaxWidth()) @@ -854,8 +885,8 @@ private fun SearchPlaylistsTab( error = playlists.error, hasNextPage = playlists.hasNextPage, onLoadNextPage = onLoadNextPage, - emptyMessage = "No playlists found", - loadingMessage = "Loading playlists...", + emptyMessage = stringResource(Res.string.search_no_playlists), + loadingMessage = stringResource(Res.string.search_loading_playlists), modifier = modifier ) { playlist -> PlaylistCard(playlist = playlist, modifier = Modifier.fillMaxWidth()) @@ -876,8 +907,8 @@ private fun SearchUsersTab( error = users.error, hasNextPage = users.hasNextPage, onLoadNextPage = onLoadNextPage, - emptyMessage = "No users found", - loadingMessage = "Loading users...", + emptyMessage = stringResource(Res.string.search_no_users), + loadingMessage = stringResource(Res.string.search_loading_users), modifier = modifier ) { user -> UserCard(user = user, modifier = Modifier.fillMaxWidth()) @@ -899,7 +930,7 @@ private fun SearchGridTab( itemContent: @Composable (T) -> Unit ) { if (query.isBlank()) { - SearchMessage(message = "Start typing to search", modifier = modifier) + SearchMessage(message = stringResource(Res.string.search_start_typing), modifier = modifier) return } @@ -993,7 +1024,7 @@ private fun SearchHorizontalSection( ) Text( - text = "See all", + text = stringResource(Res.string.search_see_all), style = AppTheme.typography.labelLarge, color = AppTheme.colors.primary, modifier = Modifier.clickable(onClick = onSeeAll) @@ -1055,14 +1086,14 @@ private fun SearchTrackRow( } } -private val WarmSearchMessages = listOf( - "Digging through the crates...", - "Searching the cosmos...", - "Warming up the speakers...", - "Dusting off the vinyl...", - "Consulting the music gods...", - "Tuning the antennas...", - "Flipping through the records..." +private val WarmSearchMessages: List = listOf( + Res.string.search_warm_crates, + Res.string.search_warm_cosmos, + Res.string.search_warm_speakers, + Res.string.search_warm_vinyl, + Res.string.search_warm_music_gods, + Res.string.search_warm_antennas, + Res.string.search_warm_records ) @Composable @@ -1102,7 +1133,7 @@ private fun SearchLoadingIndicator( label = "message-cycle" ) val displayMessage = - message ?: WarmSearchMessages[messageIndex.toInt() % WarmSearchMessages.size] + message ?: stringResource(WarmSearchMessages[messageIndex.toInt() % WarmSearchMessages.size]) Column( modifier = modifier.fillMaxWidth(), @@ -1115,7 +1146,7 @@ private fun SearchLoadingIndicator( ) { MillenniumIcon( imageVector = Iconsax.IconsaxSearchBroken, - contentDescription = "Searching", + contentDescription = stringResource(Res.string.search_searching), tint = AppTheme.colors.primary, modifier = Modifier .size(48.dp) @@ -1183,10 +1214,10 @@ private fun SearchMessageInline( @Composable private fun MetadataSupportedSearchType.tabTitle(): String = when (this) { - MetadataSupportedSearchType.ALL -> "All" - MetadataSupportedSearchType.TRACK -> "Tracks" - MetadataSupportedSearchType.PLAYLIST -> "Playlists" - MetadataSupportedSearchType.ALBUM -> "Albums" - MetadataSupportedSearchType.ARTIST -> "Artists" - MetadataSupportedSearchType.USER -> "Users" + MetadataSupportedSearchType.ALL -> stringResource(Res.string.search_tab_all) + MetadataSupportedSearchType.TRACK -> stringResource(Res.string.search_section_tracks) + MetadataSupportedSearchType.PLAYLIST -> stringResource(Res.string.library_tab_playlists) + MetadataSupportedSearchType.ALBUM -> stringResource(Res.string.library_tab_albums) + MetadataSupportedSearchType.ARTIST -> stringResource(Res.string.library_tab_artists) + MetadataSupportedSearchType.USER -> stringResource(Res.string.search_section_users) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt index 44b0b425..93ce9c70 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt @@ -41,6 +41,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.serialization.json.Json +import org.jetbrains.compose.resources.getString +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.search_error_capabilities +import spotube.composeapp.generated.resources.search_error_search +import spotube.composeapp.generated.resources.search_error_tracks +import spotube.composeapp.generated.resources.search_error_albums +import spotube.composeapp.generated.resources.search_error_artists +import spotube.composeapp.generated.resources.search_error_playlists +import spotube.composeapp.generated.resources.search_error_users data class SearchPagedState( val items: List = emptyList(), @@ -243,7 +252,7 @@ class SearchScreenViewModel( isLoadingSearchTypes = false, supportedSearchTypes = emptyList(), selectedSearchType = null, - tracks = SearchPagedState(error = it.message ?: "Failed to load search capabilities")) + tracks = SearchPagedState(error = it.message ?: getString(Res.string.search_error_capabilities))) } } } @@ -324,12 +333,13 @@ class SearchScreenViewModel( hasNextPage = false, isLoading = false)) }.onFailure { + val message = it.message ?: getString(Res.string.search_error_search) _state.value = _state.value.copy( - tracks = SearchPagedState(isLoading = false, error = it.message ?: "Failed to search"), - playlists = SearchPagedState(isLoading = false, error = it.message ?: "Failed to search"), - albums = SearchPagedState(isLoading = false, error = it.message ?: "Failed to search"), - artists = SearchPagedState(isLoading = false, error = it.message ?: "Failed to search"), - users = SearchPagedState(isLoading = false, error = it.message ?: "Failed to search")) + tracks = SearchPagedState(isLoading = false, error = message), + playlists = SearchPagedState(isLoading = false, error = message), + albums = SearchPagedState(isLoading = false, error = message), + artists = SearchPagedState(isLoading = false, error = message), + users = SearchPagedState(isLoading = false, error = message)) } return@coroutineScope } @@ -386,7 +396,7 @@ class SearchScreenViewModel( tracks = _state.value.tracks.copy( isLoading = false, hasNextPage = false, - error = throwable.message ?: "Failed to search tracks") + error = throwable.message ?: getString(Res.string.search_error_tracks)) ) } } @@ -426,7 +436,7 @@ class SearchScreenViewModel( albums = _state.value.albums.copy( isLoading = false, hasNextPage = false, - error = throwable.message ?: "Failed to search albums") + error = throwable.message ?: getString(Res.string.search_error_albums)) ) } } @@ -466,7 +476,7 @@ class SearchScreenViewModel( artists = _state.value.artists.copy( isLoading = false, hasNextPage = false, - error = throwable.message ?: "Failed to search artists") + error = throwable.message ?: getString(Res.string.search_error_artists)) ) } } @@ -506,7 +516,7 @@ class SearchScreenViewModel( playlists = _state.value.playlists.copy( isLoading = false, hasNextPage = false, - error = throwable.message ?: "Failed to search playlists") + error = throwable.message ?: getString(Res.string.search_error_playlists)) ) } } @@ -546,7 +556,7 @@ class SearchScreenViewModel( users = _state.value.users.copy( isLoading = false, hasNextPage = false, - error = throwable.message ?: "Failed to search users") + error = throwable.message ?: getString(Res.string.search_error_users)) ) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt index 411935f7..eeaaf81e 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt @@ -54,6 +54,10 @@ import spotube.composeapp.generated.resources.settings_jam_section_diagnostics import spotube.composeapp.generated.resources.settings_jam_section_identity import spotube.composeapp.generated.resources.settings_jam_broker_username import spotube.composeapp.generated.resources.settings_section_jam +import spotube.composeapp.generated.resources.settings_jam_broker_content_description +import spotube.composeapp.generated.resources.settings_jam_broker_username_placeholder +import spotube.composeapp.generated.resources.settings_jam_broker_test_fail +import spotube.composeapp.generated.resources.jam_error_connection_failed @Composable fun JamSettingsScreen( @@ -98,7 +102,7 @@ internal fun JamSettingsContent( title = stringResource(Res.string.settings_jam_broker_host), value = broker.host, placeholder = "broker.example.com", - icon = { SettingsItemIcon(Iconsax.CustomServer, "Broker") }, + icon = { SettingsItemIcon(Iconsax.CustomServer, stringResource(Res.string.settings_jam_broker_content_description)) }, onValueSaved = { host -> viewModel.update { copy( @@ -160,7 +164,7 @@ internal fun JamSettingsContent( TextInputSettingCard( title = stringResource(Res.string.settings_jam_broker_username), value = broker.username.orEmpty(), - placeholder = "anonymous", + placeholder = stringResource(Res.string.settings_jam_broker_username_placeholder), onValueSaved = { username -> viewModel.update { copy(jamBroker = jamBroker.copy(username = username.ifBlank { null })) @@ -218,7 +222,11 @@ internal fun JamSettingsContent( val canTest = broker.host.isNotBlank() && !uiState.isTesting val resultMessage = uiState.testResult?.fold( onSuccess = { it }, - onFailure = { "Failed: ${it.message ?: "Connection failed"}" }) + onFailure = { + stringResource( + Res.string.settings_jam_broker_test_fail, + it.message ?: stringResource(Res.string.jam_error_connection_failed)) + }) SettingCardItem( enabled = canTest, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsConstants.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsConstants.kt index 2c1d3d3b..9db74b04 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsConstants.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsConstants.kt @@ -279,20 +279,50 @@ enum class CountryCode(val code: String, val displayName: String) { } } +/** + * App UI languages. Each one (except [SYSTEM]) must have a matching + * `composeResources/values-/strings.xml`. [displayName] is the language's own name + * (endonym), so it is intentionally not localized. + */ @Serializable enum class SupportedLanguages( val locale: String, val country: String, - val displayName: String + val displayName: String, + val isRtl: Boolean = false ) { - EN("en", "US", "English"), - BN("bn", "BD", "Bengali"); + /** Follow the device/OS language. */ + SYSTEM("", "", ""), + EN("en", "", "English"), + AR("ar", "", "العربية", isRtl = true), + BN("bn", "", "বাংলা"), + DE("de", "", "Deutsch"), + ES("es", "", "Español"), + FA("fa", "", "فارسی", isRtl = true), + FR("fr", "", "Français"), + HI("hi", "", "हिन्दी"), + ID("id", "", "Bahasa Indonesia"), + IT("it", "", "Italiano"), + JA("ja", "", "日本語"), + KO("ko", "", "한국어"), + NL("nl", "", "Nederlands"), + PL("pl", "", "Polski"), + PT_BR("pt", "BR", "Português (Brasil)"), + RU("ru", "", "Русский"), + TH("th", "", "ไทย"), + TR("tr", "", "Türkçe"), + UK("uk", "", "Українська"), + VI("vi", "", "Tiếng Việt"), + ZH_CN("zh", "CN", "简体中文"), + ZH_TW("zh", "TW", "繁體中文"); - companion object { - fun fromLocale(locale: String): SupportedLanguages? { - return entries.find { it.locale == locale } + /** BCP 47 tag (e.g. "zh-TW"), or null for [SYSTEM]. */ + val languageTag: String? + get() = when { + this == SYSTEM -> null + country.isEmpty() -> locale + else -> "$locale-$country" } - } } @Serializable diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt index 8d01591d..a43443f8 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt @@ -19,6 +19,7 @@ package dev.krtirtho.spotube.modules.settings import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioFormat import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import org.spotube.millennium.theme.MillenniumPalette @@ -30,7 +31,10 @@ enum class Theme { @Serializable data class UserSettings( // Language and Region - val language: SupportedLanguages = SupportedLanguages.EN, + // Stored under a new name: the old "language" field was persisted as EN for everyone while + // it had no effect, so reading it would force English on existing users. + @SerialName("appLanguage") + val language: SupportedLanguages = SupportedLanguages.SYSTEM, val country: CountryCode = CountryCode.BD, // Appearance diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt index ec1c5649..40095a64 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsSidebarLayout.kt @@ -93,6 +93,7 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_close import spotube.composeapp.generated.resources.settings_blacklist_title import spotube.composeapp.generated.resources.settings_screen_title import spotube.composeapp.generated.resources.settings_section_about @@ -211,7 +212,7 @@ private fun SettingsPane( style = MillenniumIconButtonStyle.Quiet) { MillenniumIcon( imageVector = Iconsax.IconsaxCloseSquare, - contentDescription = "Close") + contentDescription = stringResource(Res.string.common_close)) } } MillenniumSeparator() diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt index a5b9d8ac..abdecb8c 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/InteractiveSettingCards.kt @@ -161,7 +161,7 @@ internal fun SelectionSettingCard( onClick = { isDialogOpen = false }, style = MillenniumButtonStyle.Quiet ) { - Text(stringResource(Res.string.settings_action_close)) + Text(stringResource(Res.string.common_close)) } } } @@ -267,13 +267,13 @@ internal fun TextInputSettingCard( }, enabled = errorMessage == null ) { - Text(stringResource(Res.string.settings_action_save)) + Text(stringResource(Res.string.common_save)) } MillenniumButton( onClick = { isDialogOpen = false }, style = MillenniumButtonStyle.Secondary ) { - Text(stringResource(Res.string.settings_action_cancel)) + Text(stringResource(Res.string.common_cancel)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/LocalMediaFoldersEditor.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/LocalMediaFoldersEditor.kt index 581a2dcf..ea4b2b5c 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/LocalMediaFoldersEditor.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/components/LocalMediaFoldersEditor.kt @@ -180,10 +180,10 @@ internal fun LocalMediaFoldersEditorContent( enabled = !state.isSaving, style = MillenniumButtonStyle.Secondary ) { - Text(stringResource(Res.string.settings_action_cancel)) + Text(stringResource(Res.string.common_cancel)) } MillenniumButton(onClick = onSave, enabled = state.canSave) { - Text(stringResource(if (state.isSaving) Res.string.settings_local_media_folders_saving else Res.string.settings_action_save)) + Text(stringResource(if (state.isSaving) Res.string.settings_local_media_folders_saving else Res.string.common_save)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt index 19db935a..7337ad09 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt @@ -182,12 +182,12 @@ private fun AppearanceSettingCard( onSaved(draftPalette) isDialogOpen = false }) { - MillenniumText(stringResource(Res.string.settings_action_save)) + MillenniumText(stringResource(Res.string.common_save)) } MillenniumButton( onClick = { isDialogOpen = false }, style = MillenniumButtonStyle.Secondary) { - MillenniumText(stringResource(Res.string.settings_action_cancel)) + MillenniumText(stringResource(Res.string.common_cancel)) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/LanguageRegionSection.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/LanguageRegionSection.kt index a4795ff1..7d1a1eb1 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/LanguageRegionSection.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/LanguageRegionSection.kt @@ -18,6 +18,7 @@ package dev.krtirtho.spotube.modules.settings.sections import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable import compose.icons.FeatherIcons import compose.icons.feathericons.Globe import compose.icons.feathericons.MapPin @@ -46,7 +47,7 @@ internal fun LazyListScope.languageRegionSection( title = stringResource(Res.string.settings_language_title), subtitle = stringResource( Res.string.settings_language_subtitle_current, - settings.language.displayName + languageLabel(settings.language) ), icon = { SettingsItemIcon( @@ -56,9 +57,7 @@ internal fun LazyListScope.languageRegionSection( }, selectedOption = settings.language, options = SupportedLanguages.entries, - optionLabel = { - stringResource(Res.string.settings_option_name_and_code, it.displayName, it.locale) - }, + optionLabel = { languageLabel(it) }, onOptionSelected = { value -> settingsViewModel.updateSettings { copy(language = value) @@ -94,3 +93,11 @@ internal fun LazyListScope.languageRegionSection( ) } +@Composable +private fun languageLabel(language: SupportedLanguages): String = + if (language == SupportedLanguages.SYSTEM) { + stringResource(Res.string.settings_language_system_default) + } else { + stringResource(Res.string.settings_option_name_and_code, language.displayName, language.languageTag!!) + } + diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/utils.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/utils.kt index affcb156..dbbe2b26 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/utils.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/utils.kt @@ -45,6 +45,10 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioFormat import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.settings_audio_format_label +import spotube.composeapp.generated.resources.settings_audio_quality_lossless +import spotube.composeapp.generated.resources.settings_audio_quality_lossy private val standardLossyQualities = listOf( AudioQuality.Lossy(bitrate = 44_000), @@ -156,8 +160,9 @@ internal fun availableAudioQualities(format: AudioFormat, current: AudioQuality) return options } +@Composable internal fun AudioFormat.displayLabel(): String { - return "${codec.uppercase()} in ${container.uppercase()}" + return stringResource(Res.string.settings_audio_format_label, codec.uppercase(), container.uppercase()) } internal fun AudioFormat.preferredQuality(): AudioQuality { @@ -168,16 +173,17 @@ internal fun AudioFormat.resolveQuality(preferred: AudioQuality): AudioQuality { return qualities.firstOrNull { it == preferred } ?: preferredQuality() } +@Composable internal fun AudioQuality.displayLabel(): String { return when (this) { - is AudioQuality.Lossy -> "${bitrate / 1000} kbps" + is AudioQuality.Lossy -> stringResource(Res.string.settings_audio_quality_lossy, bitrate / 1000) is AudioQuality.Lossless -> { val sampleRateLabel = if (sampleRate % 1000 == 0) { "${sampleRate / 1000}" } else { "${sampleRate / 1000.0}" } - "$sampleRateLabel kHz • $channels ch" + stringResource(Res.string.settings_audio_quality_lossless, sampleRateLabel, channels) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt index 96b7f4c7..113642fb 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppBottombar.kt @@ -25,6 +25,7 @@ import org.spotube.millennium.composables.MillenniumIcon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.krtirtho.spotube.NavigationItem +import org.jetbrains.compose.resources.stringResource import dev.krtirtho.spotube.bottombarTabs import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.Navigator @@ -42,10 +43,11 @@ fun AppBottombar( bottombarTabs.filterIsInstance>() .forEach { tab -> val selected = navigationState.topLevelRoute == tab.route + val title = stringResource(tab.title) MillenniumBottomNavigationItem( selected = selected, onClick = { navigator.navigate(tab.route) }, - contentDescription = tab.title, + contentDescription = title, icon = { MillenniumIcon( imageVector = tab.icon, @@ -53,7 +55,7 @@ fun AppBottombar( tint = LocalTextStyle.current.color ) }, - label = { Text(tab.title) }, + label = { Text(title) }, badge = if (tab.route == Routes.Library) { libraryBadge } else { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt index 75de7d32..f869f920 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt @@ -127,6 +127,38 @@ import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf import kotlin.time.Duration.Companion.milliseconds +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.downloads_status_cancelled +import spotube.composeapp.generated.resources.downloads_status_failed +import spotube.composeapp.generated.resources.downloads_status_queued +import spotube.composeapp.generated.resources.player_alternative_source +import spotube.composeapp.generated.resources.player_collapse_content_description +import spotube.composeapp.generated.resources.player_disable_shuffle_content_description +import spotube.composeapp.generated.resources.player_download +import spotube.composeapp.generated.resources.player_download_already_downloaded +import spotube.composeapp.generated.resources.player_download_retrying +import spotube.composeapp.generated.resources.player_download_started +import spotube.composeapp.generated.resources.player_download_status_downloaded +import spotube.composeapp.generated.resources.player_download_status_downloading +import spotube.composeapp.generated.resources.player_enable_shuffle_content_description +import spotube.composeapp.generated.resources.player_expand_lyrics_content_description +import spotube.composeapp.generated.resources.player_go_to_album +import spotube.composeapp.generated.resources.player_loop_mode_all +import spotube.composeapp.generated.resources.player_loop_mode_none +import spotube.composeapp.generated.resources.player_loop_mode_one +import spotube.composeapp.generated.resources.player_lyrics +import spotube.composeapp.generated.resources.player_next_content_description +import spotube.composeapp.generated.resources.player_now_playing +import spotube.composeapp.generated.resources.player_options_content_description +import spotube.composeapp.generated.resources.player_pause_content_description +import spotube.composeapp.generated.resources.player_play_content_description +import spotube.composeapp.generated.resources.player_previous_content_description +import spotube.composeapp.generated.resources.player_seek_label +import spotube.composeapp.generated.resources.player_sleep_timer +import spotube.composeapp.generated.resources.player_track_details +import spotube.composeapp.generated.resources.queue_title // The expanded player on small screens // on the top left it has a angle/chevron down that can be used to collapse the player back to the @@ -270,7 +302,7 @@ fun AppExpandedPlayer( item { OptionTile( icon = Iconsax.SwapHorizontal2, - label = "Alternative Source", + label = stringResource(Res.string.player_alternative_source), onClick = { playerOptionsViewModel.dismissMoreOptionsSheet() onAlternativeSource() @@ -280,12 +312,12 @@ fun AppExpandedPlayer( val download = currentDownload val status = download?.status val statusLabel = when (status) { - is DownloadStatus.Completed -> "Downloaded" - is DownloadStatus.Failed -> "Failed" - is DownloadStatus.Cancelled -> "Cancelled" - is DownloadStatus.Queued -> "Queued" - is DownloadStatus.Downloading -> "Downloading..." - null -> "Download" + is DownloadStatus.Completed -> stringResource(Res.string.player_download_status_downloaded) + is DownloadStatus.Failed -> stringResource(Res.string.downloads_status_failed) + is DownloadStatus.Cancelled -> stringResource(Res.string.downloads_status_cancelled) + is DownloadStatus.Queued -> stringResource(Res.string.downloads_status_queued) + is DownloadStatus.Downloading -> stringResource(Res.string.player_download_status_downloading) + null -> stringResource(Res.string.player_download) } val statusColor = when (status) { is DownloadStatus.Completed -> AppTheme.colors.primary @@ -308,14 +340,14 @@ fun AppExpandedPlayer( when (status) { is DownloadStatus.Completed -> { scope.launch { - snackbarHostState.showSnackbar("Already downloaded") + snackbarHostState.showSnackbar(getString(Res.string.player_download_already_downloaded)) } } is DownloadStatus.Failed, is DownloadStatus.Cancelled -> { download?.let { downloadsViewModel.retry(it.id) } scope.launch { - snackbarHostState.showSnackbar("Retrying download...") + snackbarHostState.showSnackbar(getString(Res.string.player_download_retrying)) } } @@ -329,7 +361,7 @@ fun AppExpandedPlayer( downloadsViewModel.downloadTrack(track) scope.launch { snackbarHostState.showSnackbar( - message = "Downloading ${track.title}" + message = getString(Res.string.player_download_started, track.title) ) } } @@ -348,7 +380,7 @@ fun AppExpandedPlayer( DownloadProgressIcon( track = currentTrack, icon = Iconsax.IconsaxDirectboxReceive, - contentDescription = "Download", + contentDescription = stringResource(Res.string.player_download), modifier = Modifier.size(28.dp), tint = AppTheme.colors.onSurface ) @@ -386,7 +418,7 @@ fun AppExpandedPlayer( item { OptionTile( icon = Iconsax.InconsaxClock, - label = "Sleep Timer", + label = stringResource(Res.string.player_sleep_timer), onClick = { playerOptionsViewModel.dismissMoreOptionsSheet() playerOptionsViewModel.showSleepTimerDialog() @@ -396,7 +428,7 @@ fun AppExpandedPlayer( item { OptionTile( icon = Iconsax.IconsaxCd, - label = "Go to Album", + label = stringResource(Res.string.player_go_to_album), onClick = { playerOptionsViewModel.dismissMoreOptionsSheet() val albumId = currentTrack?.album?.id @@ -409,7 +441,7 @@ fun AppExpandedPlayer( item { OptionTile( icon = Iconsax.Iconsax3DotsMore, - label = "Track Details", + label = stringResource(Res.string.player_track_details), onClick = { playerOptionsViewModel.dismissMoreOptionsSheet() playerOptionsViewModel.showTrackDetails() @@ -439,11 +471,11 @@ fun AppExpandedPlayer( ) { MillenniumIcon( Iconsax.IconsaxArrowDown4, - contentDescription = "Collapse player" + contentDescription = stringResource(Res.string.player_collapse_content_description) ) } Text( - text = "Now Playing", + text = stringResource(Res.string.player_now_playing), style = AppTheme.typography.titleMedium, modifier = Modifier.weight(1f), textAlign = TextAlign.Center, @@ -453,7 +485,7 @@ fun AppExpandedPlayer( onClick = playerOptionsViewModel::showMoreOptionsSheet, style = MillenniumIconButtonStyle.Quiet ) { - MillenniumIcon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options") + MillenniumIcon(Iconsax.Iconsax3DotsMore, contentDescription = stringResource(Res.string.player_options_content_description)) } } @@ -516,7 +548,7 @@ fun AppExpandedPlayer( onClick = onQueue, style = MillenniumIconButtonStyle.Secondary ) { - MillenniumIcon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue") + MillenniumIcon(Iconsax.IconsaxMusicFilter, contentDescription = stringResource(Res.string.queue_title)) } } @@ -532,7 +564,7 @@ fun AppExpandedPlayer( onSeekFinished() }, enabled = playerUiState.seekDuration.inWholeMilliseconds > 0L, - label = "Seek", + label = stringResource(Res.string.player_seek_label), modifier = Modifier.fillMaxWidth() ) Row( @@ -567,7 +599,7 @@ fun AppExpandedPlayer( ) { MillenniumIcon( Iconsax.IconsaxShuffle, - contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle", + contentDescription = stringResource(if (playerUiState.isShuffling) Res.string.player_disable_shuffle_content_description else Res.string.player_enable_shuffle_content_description), tint = if (playerUiState.isShuffling) { AppTheme.colors.primary } else { @@ -580,7 +612,7 @@ fun AppExpandedPlayer( enabled = !isJamGuest, style = MillenniumIconButtonStyle.Quiet ) { - MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = "Previous") + MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = stringResource(Res.string.player_previous_content_description)) } MillenniumIconButton( onClick = ::onPlayPause, @@ -588,7 +620,7 @@ fun AppExpandedPlayer( ) { MillenniumIcon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, - contentDescription = if (playerUiState.isPlaying) "Pause" else "Play", + contentDescription = stringResource(if (playerUiState.isPlaying) Res.string.player_pause_content_description else Res.string.player_play_content_description), modifier = Modifier.size(30.dp), tint = LocalTextStyle.current.color ) @@ -598,7 +630,7 @@ fun AppExpandedPlayer( enabled = !isJamGuest, style = MillenniumIconButtonStyle.Quiet ) { - MillenniumIcon(Iconsax.IconsaxNext, contentDescription = "Next") + MillenniumIcon(Iconsax.IconsaxNext, contentDescription = stringResource(Res.string.player_next_content_description)) } MillenniumIconButton( onClick = ::onLoopToggle, @@ -611,7 +643,7 @@ fun AppExpandedPlayer( LoopState.ONE -> Iconsax.IconsaxRepeateOne LoopState.ALL -> Iconsax.IconsaxRepeatMusic }, - contentDescription = "Loop mode ${playerUiState.loopState.name}", + contentDescription = stringResource(when (playerUiState.loopState) { LoopState.NONE -> Res.string.player_loop_mode_none; LoopState.ONE -> Res.string.player_loop_mode_one; LoopState.ALL -> Res.string.player_loop_mode_all }), tint = if (playerUiState.loopState == LoopState.NONE) { AppTheme.colors.onSurfaceVariant } else { @@ -708,7 +740,7 @@ private fun LyricsPreviewCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Lyrics", + text = stringResource(Res.string.player_lyrics), style = AppTheme.typography.labelMedium, color = AppTheme.colors.onSurfaceVariant ) @@ -718,7 +750,7 @@ private fun LyricsPreviewCard( ) { MillenniumIcon( Iconsax.IconsaxArrowSquareUp, - contentDescription = "Expand lyrics", + contentDescription = stringResource(Res.string.player_expand_lyrics_content_description), tint = AppTheme.colors.onSurfaceVariant ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt index 0b034497..1586083d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt @@ -103,6 +103,24 @@ import spotube.composeapp.generated.resources.player_go_to_album import spotube.composeapp.generated.resources.player_sleep_timer import spotube.composeapp.generated.resources.player_track_details import kotlin.time.Duration.Companion.milliseconds +import spotube.composeapp.generated.resources.player_alternative_source_content_description +import spotube.composeapp.generated.resources.player_disable_shuffle_content_description +import spotube.composeapp.generated.resources.player_download +import spotube.composeapp.generated.resources.player_enable_shuffle_content_description +import spotube.composeapp.generated.resources.player_loop_mode_all +import spotube.composeapp.generated.resources.player_loop_mode_none +import spotube.composeapp.generated.resources.player_loop_mode_one +import spotube.composeapp.generated.resources.player_lyrics +import spotube.composeapp.generated.resources.player_more_options_content_description +import spotube.composeapp.generated.resources.player_mute_content_description +import spotube.composeapp.generated.resources.player_next_content_description +import spotube.composeapp.generated.resources.player_pause_content_description +import spotube.composeapp.generated.resources.player_play_or_pause_content_description +import spotube.composeapp.generated.resources.player_previous_content_description +import spotube.composeapp.generated.resources.player_seek_label +import spotube.composeapp.generated.resources.player_unmute_content_description +import spotube.composeapp.generated.resources.player_volume_label +import spotube.composeapp.generated.resources.queue_title // The main player on large screens shown at the bottom of the screen permanently. It should be // below the sidebar. And should be similarly give a floating vibe with a translucent background @@ -288,7 +306,7 @@ fun AppLargePlayer( onSeekFinished() }, enabled = playerUiState.seekDuration.inWholeMilliseconds > 0L, - label = "Seek", + label = stringResource(Res.string.player_seek_label), modifier = Modifier.fillMaxWidth() ) Row( @@ -319,7 +337,7 @@ fun AppLargePlayer( ) { MillenniumIcon( Iconsax.IconsaxShuffle, - contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle", + contentDescription = stringResource(if (playerUiState.isShuffling) Res.string.player_disable_shuffle_content_description else Res.string.player_enable_shuffle_content_description), tint = if (playerUiState.isShuffling) { AppTheme.colors.primary } else { @@ -332,7 +350,7 @@ fun AppLargePlayer( enabled = !isJamGuest, style = MillenniumIconButtonStyle.Quiet ) { - MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = "Previous") + MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = stringResource(Res.string.player_previous_content_description)) } MillenniumIconButton( onClick = ::onPlayPause, @@ -340,7 +358,7 @@ fun AppLargePlayer( ) { MillenniumIcon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, - contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause", + contentDescription = stringResource(if (playerUiState.isPlaying) Res.string.player_pause_content_description else Res.string.player_play_or_pause_content_description), tint = LocalTextStyle.current.color ) } @@ -349,7 +367,7 @@ fun AppLargePlayer( enabled = !isJamGuest, style = MillenniumIconButtonStyle.Quiet ) { - MillenniumIcon(Iconsax.IconsaxNext, contentDescription = "Next") + MillenniumIcon(Iconsax.IconsaxNext, contentDescription = stringResource(Res.string.player_next_content_description)) } MillenniumIconButton( onClick = ::onLoopToggle, @@ -362,7 +380,7 @@ fun AppLargePlayer( LoopState.ONE -> Iconsax.IconsaxRepeateOne LoopState.ALL -> Iconsax.IconsaxRepeatMusic }, - contentDescription = "Loop mode ${playerUiState.loopState.name}", + contentDescription = stringResource(when (playerUiState.loopState) { LoopState.NONE -> Res.string.player_loop_mode_none; LoopState.ONE -> Res.string.player_loop_mode_one; LoopState.ALL -> Res.string.player_loop_mode_all }), tint = if (playerUiState.loopState == LoopState.NONE) { AppTheme.colors.onSurfaceVariant } else { @@ -381,7 +399,7 @@ fun AppLargePlayer( MillenniumIconButton( style = MillenniumIconButtonStyle.Quiet, onClick = onQueue ) { - MillenniumIcon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue") + MillenniumIcon(Iconsax.IconsaxMusicFilter, contentDescription = stringResource(Res.string.queue_title)) } MillenniumIconButton( style = MillenniumIconButtonStyle.Quiet, @@ -394,7 +412,7 @@ fun AppLargePlayer( DownloadProgressIcon( track = (currentEntry as? QueueEntry.StreamingTrack)?.track, icon = Iconsax.IconsaxDirectboxReceive, - contentDescription = "Download" + contentDescription = stringResource(Res.string.player_download) ) } MillenniumIconButton( @@ -402,13 +420,13 @@ fun AppLargePlayer( ) { MillenniumIcon( Iconsax.SwapHorizontal2, - contentDescription = "Alternative source" + contentDescription = stringResource(Res.string.player_alternative_source_content_description) ) } MillenniumIconButton( style = MillenniumIconButtonStyle.Quiet, onClick = onLyrics ) { - MillenniumIcon(Iconsax.IconsaxMusic, contentDescription = "Lyrics") + MillenniumIcon(Iconsax.IconsaxMusic, contentDescription = stringResource(Res.string.player_lyrics)) } val goToAlbumLabel = stringResource(Res.string.player_go_to_album) val sleepTimerLabel = stringResource(Res.string.player_sleep_timer) @@ -450,7 +468,7 @@ fun AppLargePlayer( ) { MillenniumIcon( Iconsax.Iconsax3DotsMore, - contentDescription = "More options" + contentDescription = stringResource(Res.string.player_more_options_content_description) ) } }) @@ -476,7 +494,7 @@ fun AppLargePlayer( } MillenniumIcon( imageVector = volumeIcon, - contentDescription = if (playerUiState.volume <= 0f) "Unmute" else "Mute" + contentDescription = stringResource(if (playerUiState.volume <= 0f) Res.string.player_unmute_content_description else Res.string.player_mute_content_description) ) } MillenniumSlider( @@ -487,7 +505,7 @@ fun AppLargePlayer( audioPlayer.setVolume(it) } }, - label = "Volume", + label = stringResource(Res.string.player_volume_label), modifier = Modifier.fillMaxWidth(0.62f) ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppPlayerState.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppPlayerState.kt index 06fcf1ef..3141f1dd 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppPlayerState.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppPlayerState.kt @@ -27,6 +27,11 @@ import dev.krtirtho.spotube.core.audioplayer.PlayerState import dev.krtirtho.spotube.core.audioplayer.QueueEntry import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.player_fallback_artists +import spotube.composeapp.generated.resources.player_fallback_title +import spotube.composeapp.generated.resources.player_unknown_album internal data class PlayerArtist( val id: String?, @@ -89,14 +94,17 @@ internal fun rememberPlayerUiState( val isShuffling by audioPlayer.shuffleModeFlow.collectAsState() val volume by audioPlayer.volumeFlow.collectAsState() val metadataDuration = (currentQueueEntry?.durationInMilliseconds() ?: 0L).milliseconds + val fallbackTitle = stringResource(Res.string.player_fallback_title) + val fallbackArtists = stringResource(Res.string.player_fallback_artists) + val unknownAlbum = stringResource(Res.string.player_unknown_album) val state = PlayerUiState( queue = queue, currentQueueEntry = currentQueueEntry, - title = currentQueueEntry?.displayTitle().orEmpty().ifBlank { "No-op track title" }, - artists = currentQueueEntry?.displayArtists().orEmpty().ifBlank { "No-op artists" }, + title = currentQueueEntry?.displayTitle().orEmpty().ifBlank { fallbackTitle }, + artists = currentQueueEntry?.displayArtists().orEmpty().ifBlank { fallbackArtists }, artistList = currentQueueEntry?.displayArtistList().orEmpty(), - album = currentQueueEntry?.displayAlbum().orEmpty(), + album = currentQueueEntry?.displayAlbum(unknownAlbum).orEmpty(), coverUrl = currentQueueEntry?.coverUrl().orEmpty(), position = position, actualDuration = duration, @@ -134,9 +142,9 @@ private fun QueueEntry.displayArtistList(): List { } } -private fun QueueEntry.displayAlbum(): String { +private fun QueueEntry.displayAlbum(unknownAlbum: String): String { return when (this) { - is QueueEntry.StreamingTrack -> track.album?.title ?: "Unknown Album" + is QueueEntry.StreamingTrack -> track.album?.title ?: unknownAlbum is QueueEntry.LocalTrack -> album.orEmpty() } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt index 9adcd54f..e89b40bb 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt @@ -72,6 +72,7 @@ import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.cookie_regular import spotube.composeapp.generated.resources.settings_screen_title import spotube.composeapp.generated.resources.support_button_label +import spotube.composeapp.generated.resources.shell_sidebar_toggle_content_description @Composable fun AppSidebar( @@ -111,7 +112,7 @@ fun AppSidebar( MillenniumSidebarTrigger { MillenniumIconButton( if (it.collapsed) Iconsax.IconsaxSidebarRightBroken else Iconsax.IconsaxSidebarLeftBroken, - contentDescription = "Toggle Sidebar", + contentDescription = stringResource(Res.string.shell_sidebar_toggle_content_description), style = MillenniumIconButtonStyle.Quiet, onClick = { it.toggle() } ) @@ -124,13 +125,13 @@ fun AppSidebar( sidebarTabs.forEach { entry -> when (entry) { is NavigationItem.Group<*> -> { - val isGroupExpanded = groupExpansion[entry.title] ?: true + val isGroupExpanded = groupExpansion[entry.title.key] ?: true MillenniumSidebarGroup( expanded = isGroupExpanded, collapsible = true, groupSpacing = 0.dp, - onExpandedChange = { groupExpansion[entry.title] = it }) { - MillenniumSidebarGroupLabel(entry.title) + onExpandedChange = { groupExpansion[entry.title.key] = it }) { + MillenniumSidebarGroupLabel(stringResource(entry.title)) MillenniumSidebarGroupContent( contentSpacing = 0.dp ) { @@ -190,11 +191,12 @@ private fun SidebarNavItem( selected: Boolean, onClick: () -> Unit ) { + val title = stringResource(item.title) MillenniumSidebarItem( - label = item.title, + label = title, onClick = onClick, selected = selected, - contentDescription = item.title, + contentDescription = title, icon = item.icon ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt index ea66bb29..34397563 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt @@ -37,6 +37,10 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2 import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.player_like_content_description +import spotube.composeapp.generated.resources.player_unlike_content_description @Composable fun PlayerHeartButton( @@ -74,7 +78,7 @@ fun PlayerHeartButton( style = MillenniumIconButtonStyle.Secondary) { MillenniumIcon( imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart, - contentDescription = if (isLiked) "Unlike" else "Like", + contentDescription = stringResource(if (isLiked) Res.string.player_unlike_content_description else Res.string.player_like_content_description), tint = if (isLiked) AppTheme.colors.primary else AppTheme.colors.onSurfaceVariant) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt index 99fd900f..3529f6ab 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionDialogs.kt @@ -54,6 +54,50 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive import dev.krtirtho.spotube.core.server.CacheEntry +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.library_tab_artists +import spotube.composeapp.generated.resources.media_artist +import spotube.composeapp.generated.resources.player_sleep_timer +import spotube.composeapp.generated.resources.player_sleep_timer_cancel +import spotube.composeapp.generated.resources.player_sleep_timer_description +import spotube.composeapp.generated.resources.player_sleep_timer_minutes_label +import spotube.composeapp.generated.resources.player_sleep_timer_minutes_placeholder +import spotube.composeapp.generated.resources.player_sleep_timer_remaining +import spotube.composeapp.generated.resources.player_sleep_timer_start +import spotube.composeapp.generated.resources.player_track_details +import spotube.composeapp.generated.resources.player_track_details_active_url +import spotube.composeapp.generated.resources.player_track_details_album +import spotube.composeapp.generated.resources.player_track_details_album_genres +import spotube.composeapp.generated.resources.player_track_details_album_id +import spotube.composeapp.generated.resources.player_track_details_album_release_date +import spotube.composeapp.generated.resources.player_track_details_album_track_count +import spotube.composeapp.generated.resources.player_track_details_album_type +import spotube.composeapp.generated.resources.player_track_details_available_streams +import spotube.composeapp.generated.resources.player_track_details_confidence +import spotube.composeapp.generated.resources.player_track_details_disc_number +import spotube.composeapp.generated.resources.player_track_details_duration +import spotube.composeapp.generated.resources.player_track_details_explicit +import spotube.composeapp.generated.resources.player_track_details_external_uri +import spotube.composeapp.generated.resources.player_track_details_id +import spotube.composeapp.generated.resources.player_track_details_isrc +import spotube.composeapp.generated.resources.player_track_details_loading_source +import spotube.composeapp.generated.resources.player_track_details_loading_streams +import spotube.composeapp.generated.resources.player_track_details_matched_source +import spotube.composeapp.generated.resources.player_track_details_matches_preference +import spotube.composeapp.generated.resources.player_track_details_metadata_track +import spotube.composeapp.generated.resources.player_track_details_no_source +import spotube.composeapp.generated.resources.player_track_details_no_streams +import spotube.composeapp.generated.resources.player_track_details_popularity +import spotube.composeapp.generated.resources.player_track_details_selected_stream_content_description +import spotube.composeapp.generated.resources.player_track_details_served_from_cache +import spotube.composeapp.generated.resources.player_track_details_served_from_cache_description +import spotube.composeapp.generated.resources.player_track_details_source_not_needed +import spotube.composeapp.generated.resources.player_track_details_stream_mismatch +import spotube.composeapp.generated.resources.player_track_details_thumbnail_urls +import spotube.composeapp.generated.resources.player_track_details_title +import spotube.composeapp.generated.resources.player_track_details_track_number +import spotube.composeapp.generated.resources.common_close @Composable fun PlayerOptionDialogs(viewModel: PlayerOptionsViewModel) { @@ -87,16 +131,16 @@ private fun SleepTimerDialog( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Sleep timer") { + title = stringResource(Res.string.player_sleep_timer)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { if (remainingMs != null) { Text( - text = "Playback will pause in ${formatRemainingTime(remainingMs)}.", + text = stringResource(Res.string.player_sleep_timer_remaining, formatRemainingTime(remainingMs)), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.primary) } else { Text( - text = "Pause playback after the selected number of minutes.", + text = stringResource(Res.string.player_sleep_timer_description), style = AppTheme.typography.bodyMedium) } val minutesState = rememberTextFieldState(minutes) @@ -105,23 +149,23 @@ private fun SleepTimerDialog( } MillenniumTextField( state = minutesState, - label = "Minutes", - placeholder = "Choose between 1 and 600 minutes", + label = stringResource(Res.string.player_sleep_timer_minutes_label), + placeholder = stringResource(Res.string.player_sleep_timer_minutes_placeholder), modifier = Modifier.fillMaxWidth()) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (remainingMs != null) { MillenniumButton( onClick = onCancelTimer, - style = MillenniumButtonStyle.Secondary) { Text("Cancel timer") } + style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.player_sleep_timer_cancel)) } } MillenniumButton( onClick = onDismiss, - style = MillenniumButtonStyle.Secondary) { Text("Close") } + style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) } MillenniumButton( onClick = onStart, enabled = minutes.toLongOrNull()?.let { it in 1L..600L } == true) { - Text("Start") + Text(stringResource(Res.string.player_sleep_timer_start)) } } } @@ -135,7 +179,7 @@ private fun TrackDetailsDialog( MillenniumDialog( visible = true, onDismissRequest = onDismiss, - title = "Track details") { + title = stringResource(Res.string.player_track_details)) { Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Text( details.track.title, @@ -161,7 +205,7 @@ private fun TrackDetailsDialog( background = SolidColor(AppTheme.colors.tertiaryContainer), shape = RoundedCornerShape(12.dp))) { Text( - "The playback URL was resolved with different streaming settings. The highlighted stream matches your current preference; the active URL refreshes on the next stream request.", + stringResource(Res.string.player_track_details_stream_mismatch), modifier = Modifier.padding(12.dp), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onTertiaryContainer) @@ -171,43 +215,43 @@ private fun TrackDetailsDialog( TrackMetadataSection(details.track) Text( - "Matched audio source", + stringResource(Res.string.player_track_details_matched_source), style = AppTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) val source = details.matchedSource if (details.isLoading) { - Text("Loading matched source…", style = AppTheme.typography.bodySmall) + Text(stringResource(Res.string.player_track_details_loading_source), style = AppTheme.typography.bodySmall) } else if (source == null) { Text( - details.error ?: if (details.localCacheEntry != null) { - "A matched remote source is not needed while playing the local cached file." + details.error ?: details.errorRes?.let { stringResource(it) } ?: if (details.localCacheEntry != null) { + stringResource(Res.string.player_track_details_source_not_needed) } else { - "No matched source is available." + stringResource(Res.string.player_track_details_no_source) }, style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) } else { - DetailRow("Title", source.title) - DetailRow("ID", source.id) - DetailRow("Artist", source.artist) - DetailRow("Album", source.album) - DetailRow("Confidence", "${(source.confidence * 100).toInt()}%") - DetailRow("External URI", source.externalUri) + DetailRow(stringResource(Res.string.player_track_details_title), source.title) + DetailRow(stringResource(Res.string.player_track_details_id), source.id) + DetailRow(stringResource(Res.string.media_artist), source.artist) + DetailRow(stringResource(Res.string.player_track_details_album), source.album) + DetailRow(stringResource(Res.string.player_track_details_confidence), "${(source.confidence * 100).toInt()}%") + DetailRow(stringResource(Res.string.player_track_details_external_uri), source.externalUri) } Text( - "Available streams", + stringResource(Res.string.player_track_details_available_streams), style = AppTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) when { details.localCacheEntry != null && details.streams.isEmpty() -> Text( - "This track is being served from the local cache. Stream variants are not used for this playback.", + stringResource(Res.string.player_track_details_served_from_cache_description), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) - details.isLoading -> Text("Loading streams…", style = AppTheme.typography.bodySmall) + details.isLoading -> Text(stringResource(Res.string.player_track_details_loading_streams), style = AppTheme.typography.bodySmall) details.streams.isEmpty() -> Text( - details.error ?: "No streams are available for this source.", + details.error ?: details.errorRes?.let { stringResource(it) } ?: stringResource(Res.string.player_track_details_no_streams), style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant) @@ -244,7 +288,7 @@ private fun TrackDetailsDialog( if (isSelected) { MillenniumIcon( imageVector = Iconsax.IconsaxCheckCircle, - contentDescription = "Currently selected stream", + contentDescription = stringResource(Res.string.player_track_details_selected_stream_content_description), tint = AppTheme.colors.primary) } } @@ -258,7 +302,7 @@ private fun TrackDetailsDialog( color = AppTheme.colors.onSurfaceVariant) if (isSelected) { Text( - "Matches your streaming format & quality preference", + stringResource(Res.string.player_track_details_matches_preference), style = AppTheme.typography.labelSmall, color = AppTheme.colors.primary) } @@ -268,12 +312,12 @@ private fun TrackDetailsDialog( } details.activeStreamUrl?.let { activeUrl -> - DetailRow("Active playback URL", activeUrl) + DetailRow(stringResource(Res.string.player_track_details_active_url), activeUrl) } MillenniumButton( onClick = onDismiss, - style = MillenniumButtonStyle.Secondary) { Text("Close") } + style = MillenniumButtonStyle.Secondary) { Text(stringResource(Res.string.common_close)) } } } } @@ -296,7 +340,7 @@ private fun LocalCacheBanner(entry: CacheEntry) { tint = AppTheme.colors.onSecondaryContainer) Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Text( - "Served from local cache", + stringResource(Res.string.player_track_details_served_from_cache), style = AppTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold, color = AppTheme.colors.onSecondaryContainer) @@ -312,26 +356,26 @@ private fun LocalCacheBanner(entry: CacheEntry) { @Composable private fun TrackMetadataSection(track: MetadataTrack) { Text( - "Metadata track", + stringResource(Res.string.player_track_details_metadata_track), style = AppTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - DetailRow("ID", track.id) - DetailRow("Title", track.title) - DetailRow("Artists", track.artists.joinToString { it.name }) - DetailRow("Album", track.album?.title) - DetailRow("Album ID", track.album?.id) - DetailRow("Album type", track.album?.albumType?.name) - DetailRow("Album release date", track.album?.releaseDate) - DetailRow("Album track count", track.album?.trackCount?.toString()) - DetailRow("Album genres", track.album?.genres?.joinToString()) - DetailRow("Duration", formatRemainingTime(track.durationMs)) - DetailRow("Track number", track.trackNumber?.toString()) - DetailRow("Disc number", track.discNumber?.toString()) - DetailRow("Explicit", track.explicit?.toString()) - DetailRow("Popularity", track.popularity?.toString()) - DetailRow("ISRC", track.isrcCode) - DetailRow("External URI", track.externalUri) - DetailRow("Thumbnail URLs", track.thumbnails?.joinToString { it.url }) + DetailRow(stringResource(Res.string.player_track_details_id), track.id) + DetailRow(stringResource(Res.string.player_track_details_title), track.title) + DetailRow(stringResource(Res.string.library_tab_artists), track.artists.joinToString { it.name }) + DetailRow(stringResource(Res.string.player_track_details_album), track.album?.title) + DetailRow(stringResource(Res.string.player_track_details_album_id), track.album?.id) + DetailRow(stringResource(Res.string.player_track_details_album_type), track.album?.albumType?.name) + DetailRow(stringResource(Res.string.player_track_details_album_release_date), track.album?.releaseDate) + DetailRow(stringResource(Res.string.player_track_details_album_track_count), track.album?.trackCount?.toString()) + DetailRow(stringResource(Res.string.player_track_details_album_genres), track.album?.genres?.joinToString()) + DetailRow(stringResource(Res.string.player_track_details_duration), formatRemainingTime(track.durationMs)) + DetailRow(stringResource(Res.string.player_track_details_track_number), track.trackNumber?.toString()) + DetailRow(stringResource(Res.string.player_track_details_disc_number), track.discNumber?.toString()) + DetailRow(stringResource(Res.string.player_track_details_explicit), track.explicit?.toString()) + DetailRow(stringResource(Res.string.player_track_details_popularity), track.popularity?.toString()) + DetailRow(stringResource(Res.string.player_track_details_isrc), track.isrcCode) + DetailRow(stringResource(Res.string.player_track_details_external_uri), track.externalUri) + DetailRow(stringResource(Res.string.player_track_details_thumbnail_urls), track.thumbnails?.joinToString { it.url }) } @Composable diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionsViewModel.kt index c811248b..012caee7 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerOptionsViewModel.kt @@ -45,6 +45,11 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.time.Duration.Companion.milliseconds +import org.jetbrains.compose.resources.StringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.player_track_details_load_failed +import spotube.composeapp.generated.resources.player_track_details_no_cached_source +import spotube.composeapp.generated.resources.player_track_details_streams_not_cached data class PlayerTrackDetails( val track: MetadataTrack, @@ -58,7 +63,10 @@ data class PlayerTrackDetails( val activeStreamMatchesPreference: Boolean? = null, val localCacheEntry: CacheEntry? = null, val isLoading: Boolean = false, - val error: String? = null) + /** Raw error message (e.g. from an exception); takes precedence over [errorRes]. */ + val error: String? = null, + /** Localized error to show when [error] is null. */ + val errorRes: StringResource? = null) data class PlayerOptionsUiState( val isMoreOptionsMenuOpen: Boolean = false, @@ -187,11 +195,11 @@ class PlayerOptionsViewModel( activeStreamUrl = activeStreamUrl, activeStreamMatchesPreference = activeStreamMatchesPreference, localCacheEntry = localCacheEntry, - error = when { + errorRes = when { source == null && localCacheEntry == null -> - "No matched audio source is cached for this track." + Res.string.player_track_details_no_cached_source streams.isEmpty() && localCacheEntry == null -> - "Stream details are not cached yet. Play the track first to load them." + Res.string.player_track_details_streams_not_cached else -> null }) } @@ -209,7 +217,8 @@ class PlayerOptionsViewModel( trackDetails = PlayerTrackDetails( track = track, isLoading = false, - error = error.message ?: "Failed to load track details.")) + error = error.message, + errorRes = Res.string.player_track_details_load_failed)) } else current } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt index 662ea8ed..129b6ea1 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContent.kt @@ -55,6 +55,14 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioSource import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle import org.koin.compose.viewmodel.koinViewModel +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.alternative_track_active_source +import spotube.composeapp.generated.resources.alternative_track_confidence +import spotube.composeapp.generated.resources.alternative_track_empty +import spotube.composeapp.generated.resources.alternative_track_no_track +import spotube.composeapp.generated.resources.alternative_track_searching +import spotube.composeapp.generated.resources.alternative_track_title @Composable fun AlternativeTrackContent( @@ -73,13 +81,13 @@ fun AlternativeTrackContent( .padding(horizontal = 8.dp, vertical = 4.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( - text = "Alternative Sources", + text = stringResource(Res.string.alternative_track_title), style = AppTheme.typography.titleLarge) val currentTrack = uiState.currentTrack if (currentTrack == null) { Text( - text = "No track currently playing", + text = stringResource(Res.string.alternative_track_no_track), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) return@Column @@ -94,7 +102,7 @@ fun AlternativeTrackContent( modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.width(12.dp)) Text( - text = "Searching for alternative sources...", + text = stringResource(Res.string.alternative_track_searching), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) } @@ -103,7 +111,7 @@ fun AlternativeTrackContent( if (uiState.alternatives.isEmpty()) { Text( - text = "No alternative sources found", + text = stringResource(Res.string.alternative_track_empty), style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant) return@Column @@ -186,7 +194,7 @@ private fun AlternativeSourceRow( overflow = TextOverflow.Ellipsis) } Text( - text = "Confidence: ${(source.confidence * 100).toInt()}%", + text = stringResource(Res.string.alternative_track_confidence, (source.confidence * 100).toInt()), style = AppTheme.typography.labelSmall, color = AppTheme.colors.onSurfaceVariant) } @@ -195,7 +203,7 @@ private fun AlternativeSourceRow( if (isActive) { MillenniumIcon( Iconsax.IconsaxCheckCircle, - contentDescription = "Active source", + contentDescription = stringResource(Res.string.alternative_track_active_source), tint = AppTheme.colors.primary, modifier = Modifier.size(22.dp)) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt index f845a2d1..67fbf043 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt @@ -81,6 +81,17 @@ import spotube.composeapp.generated.resources.queue_clear_queue import spotube.composeapp.generated.resources.queue_filter_queue import spotube.composeapp.generated.resources.queue_no_queue_entries import spotube.composeapp.generated.resources.queue_title +import spotube.composeapp.generated.resources.jam_participants_ban +import spotube.composeapp.generated.resources.jam_participants_host +import spotube.composeapp.generated.resources.player_more_options_content_description +import spotube.composeapp.generated.resources.queue_item_subtitle_added_by +import spotube.composeapp.generated.resources.queue_participant_ban_description +import spotube.composeapp.generated.resources.queue_participant_kick +import spotube.composeapp.generated.resources.queue_participant_kick_description +import spotube.composeapp.generated.resources.queue_participant_remove_suggestions +import spotube.composeapp.generated.resources.queue_participant_remove_suggestions_description +import spotube.composeapp.generated.resources.queue_remove_from_queue +import spotube.composeapp.generated.resources.queue_reorder_content_description @Composable fun PlayerQueueContent( @@ -214,7 +225,7 @@ private fun ParticipantDialog( ) if (participant.isHost) { Text( - text = "Host", + text = stringResource(Res.string.jam_participants_host), style = AppTheme.typography.labelSmall, color = AppTheme.colors.primary ) @@ -237,8 +248,8 @@ private fun ParticipantDialog( tint = AppTheme.colors.error ) }, - title = { Text("Kick") }, - subtitle = { Text("Remove them from the session") }) + title = { Text(stringResource(Res.string.queue_participant_kick)) }, + subtitle = { Text(stringResource(Res.string.queue_participant_kick_description)) }) MillenniumListTile( onClick = { onBan() @@ -251,8 +262,8 @@ private fun ParticipantDialog( tint = AppTheme.colors.error ) }, - title = { Text("Ban") }, - subtitle = { Text("Kick and prevent them from rejoining") }) + title = { Text(stringResource(Res.string.jam_participants_ban)) }, + subtitle = { Text(stringResource(Res.string.queue_participant_ban_description)) }) MillenniumListTile( onClick = { onRemoveSuggestions() @@ -264,8 +275,8 @@ private fun ParticipantDialog( contentDescription = null ) }, - title = { Text("Remove suggestions") }, - subtitle = { Text("Remove every track they added to the queue") }) + title = { Text(stringResource(Res.string.queue_participant_remove_suggestions)) }, + subtitle = { Text(stringResource(Res.string.queue_participant_remove_suggestions_description)) }) } } } @@ -315,7 +326,7 @@ private fun QueueItemRow( ) { MillenniumIcon( Iconsax.IconsaxDragHandle, - contentDescription = if (reorderScope != null) "Reorder" else null, + contentDescription = if (reorderScope != null) stringResource(Res.string.queue_reorder_content_description) else null, modifier = Modifier .size(24.dp) .then( @@ -370,7 +381,7 @@ private fun QueueItemRow( }, subtitle = { Text( - text = item.subtitle, + text = item.addedByParticipant?.let { stringResource(Res.string.queue_item_subtitle_added_by, item.subtitle, it.displayName) } ?: item.subtitle, style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant, maxLines = 1, @@ -408,7 +419,7 @@ private fun QueueItemRow( MillenniumDropdownMenu( expanded = showMenu, onExpandedChange = { showMenu = it }, - items = listOf("Remove from queue"), + items = listOf(stringResource(Res.string.queue_remove_from_queue)), onItemClick = { onRemoveClick() showMenu = false @@ -420,7 +431,7 @@ private fun QueueItemRow( ) { MillenniumIcon( Iconsax.Iconsax3DotsMore, - contentDescription = "More options" + contentDescription = stringResource(Res.string.player_more_options_content_description) ) } }) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContentViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContentViewModel.kt index 253c8d83..f13e6b78 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContentViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContentViewModel.kt @@ -75,7 +75,7 @@ class PlayerQueueContentViewModel( } queue.mapIndexed { index, entry -> val title: String - var subtitle: String + val subtitle: String val durationMs: Long val imageUrl: String? @@ -97,9 +97,6 @@ class PlayerQueueContentViewModel( } val addedByParticipant = participants.firstOrNull { it.id == entry.addedBy } - if (addedByParticipant != null) { - subtitle = "$subtitle • Added by ${addedByParticipant.displayName}" - } QueueItemUi( id = "${entry.url}@$index", @@ -127,7 +124,8 @@ class PlayerQueueContentViewModel( val filtered = if (isFiltered) { items.filter { item -> item.title.lowercase().contains(normalizedFilter) || - item.subtitle.lowercase().contains(normalizedFilter) + item.subtitle.lowercase().contains(normalizedFilter) || + item.addedByParticipant?.displayName?.lowercase()?.contains(normalizedFilter) == true } } else { items diff --git a/composeApp/src/iosMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.ios.kt b/composeApp/src/iosMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.ios.kt new file mode 100644 index 00000000..aaf1c7d2 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.ios.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.i18n + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.staticCompositionLocalOf +import platform.Foundation.NSUserDefaults + +// Compose (and Compose resources) read NSLocale.preferredLanguages, which honours the +// app-level AppleLanguages default. +private const val APPLE_LANGUAGES_KEY = "AppleLanguages" +private const val SYSTEM_LANGUAGE = "system" + +private val LocalAppLocale = staticCompositionLocalOf { SYSTEM_LANGUAGE } + +@Composable +internal actual fun provideAppLocale(languageTag: String?): ProvidedValue<*> { + val defaults = NSUserDefaults.standardUserDefaults + if (languageTag == null) { + defaults.removeObjectForKey(APPLE_LANGUAGES_KEY) + } else { + defaults.setObject(listOf(languageTag), APPLE_LANGUAGES_KEY) + } + return LocalAppLocale provides (languageTag ?: SYSTEM_LANGUAGE) +} diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.jvm.kt new file mode 100644 index 00000000..d4d728ef --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/i18n/AppLocale.jvm.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.i18n + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.staticCompositionLocalOf +import java.util.Locale + +// Captured before the first override so "System default" can restore it. +private val systemLocale: Locale = Locale.getDefault() + +private val LocalAppLocale = staticCompositionLocalOf { systemLocale.toLanguageTag() } + +@Composable +internal actual fun provideAppLocale(languageTag: String?): ProvidedValue<*> { + val locale = languageTag?.let(Locale::forLanguageTag) ?: systemLocale + // Compose resources read Locale.getDefault() both in composition and in getString(). + Locale.setDefault(locale) + return LocalAppLocale provides locale.toLanguageTag() +} diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/systemtray/SystemTray.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/systemtray/SystemTray.kt index 2dd89abe..bbc55527 100644 --- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/systemtray/SystemTray.kt +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/systemtray/SystemTray.kt @@ -25,7 +25,28 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic import dev.nucleusframework.application.NucleusApplicationScope import dev.nucleusframework.composenativetray.tray.api.Tray +import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.player_pause_content_description +import spotube.composeapp.generated.resources.player_play_content_description +import spotube.composeapp.generated.resources.tray_exit +import spotube.composeapp.generated.resources.tray_hide_window +import spotube.composeapp.generated.resources.tray_like_track +import spotube.composeapp.generated.resources.tray_loop +import spotube.composeapp.generated.resources.tray_loop_all +import spotube.composeapp.generated.resources.tray_loop_off +import spotube.composeapp.generated.resources.tray_loop_one +import spotube.composeapp.generated.resources.tray_mute +import spotube.composeapp.generated.resources.tray_next_track +import spotube.composeapp.generated.resources.tray_previous_track +import spotube.composeapp.generated.resources.tray_show_window +import spotube.composeapp.generated.resources.tray_shuffle +import spotube.composeapp.generated.resources.tray_unlike_track +import spotube.composeapp.generated.resources.tray_unmute +import spotube.composeapp.generated.resources.tray_volume +import spotube.composeapp.generated.resources.tray_volume_down +import spotube.composeapp.generated.resources.tray_volume_up /** * Renders the system tray icon + menu via Compose Native Tray (no AWT/Swing). @@ -42,46 +63,67 @@ fun NucleusApplicationScope.SystemTray( val trayService = koinInject() val state by trayService.state.collectAsState() + // Tray menu builders aren't composable, so resolve every label up front. + val hideWindowLabel = stringResource(Res.string.tray_hide_window) + val showWindowLabel = stringResource(Res.string.tray_show_window) + val playLabel = stringResource(Res.string.player_play_content_description) + val pauseLabel = stringResource(Res.string.player_pause_content_description) + val nextTrackLabel = stringResource(Res.string.tray_next_track) + val previousTrackLabel = stringResource(Res.string.tray_previous_track) + val shuffleLabel = stringResource(Res.string.tray_shuffle) + val loopLabel = stringResource(Res.string.tray_loop) + val loopOffLabel = stringResource(Res.string.tray_loop_off) + val loopOneLabel = stringResource(Res.string.tray_loop_one) + val loopAllLabel = stringResource(Res.string.tray_loop_all) + val volumeLabel = stringResource(Res.string.tray_volume, (state.volume * 100).toInt()) + val volumeUpLabel = stringResource(Res.string.tray_volume_up) + val volumeDownLabel = stringResource(Res.string.tray_volume_down) + val muteLabel = stringResource(Res.string.tray_mute) + val unmuteLabel = stringResource(Res.string.tray_unmute) + val likeTrackLabel = stringResource(Res.string.tray_like_track) + val unlikeTrackLabel = stringResource(Res.string.tray_unlike_track) + val exitLabel = stringResource(Res.string.tray_exit) + Tray( icon = Iconsax.IconsaxMusic, tooltip = "Spotube", primaryAction = onToggleWindowVisibility) { - Item(label = if (isWindowVisible) "Hide Window" else "Show Window") { + Item(label = if (isWindowVisible) hideWindowLabel else showWindowLabel) { onToggleWindowVisibility() } Divider() - Item(label = if (state.isPlaying) "Pause" else "Play") { + Item(label = if (state.isPlaying) pauseLabel else playLabel) { trayService.togglePlayPause() } - Item(label = "Next Track") { trayService.skipToNext() } - Item(label = "Previous Track") { trayService.skipToPrevious() } + Item(label = nextTrackLabel) { trayService.skipToNext() } + Item(label = previousTrackLabel) { trayService.skipToPrevious() } Divider() CheckableItem( - label = "Shuffle", + label = shuffleLabel, checked = state.shuffleEnabled, onCheckedChange = trayService::setShuffle) - SubMenu(label = "Loop") { - Item(label = "Loop: OFF") { trayService.setLoop(LoopState.NONE) } - Item(label = "Loop: ONE") { trayService.setLoop(LoopState.ONE) } - Item(label = "Loop: ALL") { trayService.setLoop(LoopState.ALL) } + SubMenu(label = loopLabel) { + Item(label = loopOffLabel) { trayService.setLoop(LoopState.NONE) } + Item(label = loopOneLabel) { trayService.setLoop(LoopState.ONE) } + Item(label = loopAllLabel) { trayService.setLoop(LoopState.ALL) } } Divider() - Item(label = "Volume: ${(state.volume * 100).toInt()}%") - Item(label = "Volume +") { trayService.increaseVolume() } - Item(label = "Volume -") { trayService.decreaseVolume() } - Item(label = if (state.volume <= 0f) "Unmute" else "Mute") { trayService.toggleMute() } + Item(label = volumeLabel) + Item(label = volumeUpLabel) { trayService.increaseVolume() } + Item(label = volumeDownLabel) { trayService.decreaseVolume() } + Item(label = if (state.volume <= 0f) unmuteLabel else muteLabel) { trayService.toggleMute() } Divider() if (state.currentTrackId != null) { - Item(label = if (state.isCurrentTrackLiked) "Unlike Track" else "Like Track") { + Item(label = if (state.isCurrentTrackLiked) unlikeTrackLabel else likeTrackLabel) { trayService.toggleLike() } } Divider() - Item(label = "Exit") { onExit() } + Item(label = exitLabel) { onExit() } } } diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt index 34f67902..a6ed3e85 100644 --- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt @@ -54,7 +54,13 @@ import dev.nucleusframework.window.WindowControls import dev.nucleusframework.window.WindowControlsRenderer import dev.nucleusframework.window.WindowDoubleClickAction import dev.nucleusframework.window.windowDragArea +import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.app_window_close +import spotube.composeapp.generated.resources.app_window_maximize +import spotube.composeapp.generated.resources.app_window_minimize +import spotube.composeapp.generated.resources.app_window_restore private val WindowControlsReservedWidth = 132.dp @@ -109,7 +115,7 @@ private fun WindowButtons(modifier: Modifier = Modifier) { MillenniumIconButton( onClick = { windowState.isMinimized = true }, style = MillenniumIconButtonStyle.Quiet) { - MillenniumIcon(Iconsax.FluentMinus, "Minimize", modifier = Modifier.size(14.dp)) + MillenniumIcon(Iconsax.FluentMinus, stringResource(Res.string.app_window_minimize), modifier = Modifier.size(14.dp)) } MillenniumIconButton( onClick = { @@ -118,9 +124,9 @@ private fun WindowButtons(modifier: Modifier = Modifier) { }, style = MillenniumIconButtonStyle.Quiet) { if (windowState.placement == WindowPlacement.Floating) { - MillenniumIcon(Iconsax.FluentMaximize, "Maximize", modifier = Modifier.size(14.dp)) + MillenniumIcon(Iconsax.FluentMaximize, stringResource(Res.string.app_window_maximize), modifier = Modifier.size(14.dp)) } else { - MillenniumIcon(Iconsax.FluentSquareMultiple, "Restore", modifier = Modifier.size(14.dp)) + MillenniumIcon(Iconsax.FluentSquareMultiple, stringResource(Res.string.app_window_restore), modifier = Modifier.size(14.dp)) } } MillenniumIconButton( @@ -132,7 +138,7 @@ private fun WindowButtons(modifier: Modifier = Modifier) { } }, style = MillenniumIconButtonStyle.Quiet) { - MillenniumIcon(Iconsax.FluentDismiss, "Close", modifier = Modifier.size(14.dp)) + MillenniumIcon(Iconsax.FluentDismiss, stringResource(Res.string.app_window_close), modifier = Modifier.size(14.dp)) } } } diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt index 9cc41de2..df97c006 100644 --- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt +++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt @@ -37,7 +37,9 @@ import dev.krtirtho.spotube.core.systemtray.SystemTrayService import dev.krtirtho.spotube.core.ui.component.LocalApplicationScope import dev.krtirtho.spotube.core.ui.component.LocalWindowScope import dev.krtirtho.spotube.core.ui.component.LocalWindowState +import dev.krtirtho.spotube.core.i18n.AppLocaleProvider import dev.krtirtho.spotube.modules.settings.SettingsProvider +import dev.krtirtho.spotube.modules.settings.SupportedLanguages import dev.nucleusframework.application.DecoratedWindow import dev.nucleusframework.application.NucleusBackend import dev.nucleusframework.application.nucleusApplication @@ -89,10 +91,13 @@ fun main() { trayService.setCallbacks(onToggleWindowVisibility) if (minimizeToTray) { - SystemTray( - isWindowVisible = isWindowVisible, - onToggleWindowVisibility = onToggleWindowVisibility, - onExit = onExit) + // The tray lives outside App(), so it needs its own provider to follow the app language. + AppLocaleProvider(settings?.language ?: SupportedLanguages.SYSTEM) { + SystemTray( + isWindowVisible = isWindowVisible, + onToggleWindowVisibility = onToggleWindowVisibility, + onExit = onExit) + } } DecoratedWindow( diff --git a/composeApp/src/jvmTest/kotlin/dev/krtirtho/spotube/core/i18n/AppLocaleProviderTest.kt b/composeApp/src/jvmTest/kotlin/dev/krtirtho/spotube/core/i18n/AppLocaleProviderTest.kt new file mode 100644 index 00000000..91dac67b --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/dev/krtirtho/spotube/core/i18n/AppLocaleProviderTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.i18n + +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.use +import dev.krtirtho.spotube.modules.settings.SupportedLanguages +import kotlinx.coroutines.runBlocking +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.stringResource +import spotube.composeapp.generated.resources.Res +import spotube.composeapp.generated.resources.common_cancel +import spotube.composeapp.generated.resources.plugin_installed_plugins_count +import java.util.Locale +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalComposeUiApi::class) +class AppLocaleProviderTest { + @Test + fun switchingLanguageUpdatesStringsWithoutRestart() { + val systemLocale = Locale.getDefault() + var language by mutableStateOf(SupportedLanguages.DE) + var cancel = "" + var plugins = "" + var direction = LayoutDirection.Ltr + + try { + ImageComposeScene(width = 100, height = 100) { + AppLocaleProvider(language) { + val cancelText = stringResource(Res.string.common_cancel) + val pluginsText = pluralStringResource(Res.plurals.plugin_installed_plugins_count, 3, 3) + val layoutDirection = LocalLayoutDirection.current + SideEffect { + cancel = cancelText + plugins = pluginsText + direction = layoutDirection + } + } + }.use { scene -> + scene.render() + assertEquals("Abbrechen", cancel) + assertEquals("3 Plugins installiert", plugins) + // Non-composable lookups (ViewModels, services) follow the override too. + assertEquals("Abbrechen", runBlocking { getString(Res.string.common_cancel) }) + + language = SupportedLanguages.ZH_TW + scene.render() + assertEquals("取消", cancel) + assertEquals("已安裝 3 個外掛程式", plugins) + + language = SupportedLanguages.RU + scene.render() + assertEquals("3 плагина установлено", plugins) + + language = SupportedLanguages.AR + scene.render() + assertEquals(LayoutDirection.Rtl, direction) + + language = SupportedLanguages.SYSTEM + scene.render() + assertEquals(systemLocale, Locale.getDefault()) + } + } finally { + Locale.setDefault(systemLocale) + } + } +}