diff --git a/Trdo.Tests/LocalizedTextCultureTests.cs b/Trdo.Tests/LocalizedTextCultureTests.cs new file mode 100644 index 0000000..2d11af7 --- /dev/null +++ b/Trdo.Tests/LocalizedTextCultureTests.cs @@ -0,0 +1,30 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Globalization; +using Trdo.Services; + +namespace Trdo.Tests; + +[TestClass] +public sealed class LocalizedTextCultureTests +{ + [TestMethod] + [DataRow("en-US")] + [DataRow("ru-RU")] + [DataRow("uk-UA")] + [DataRow("de-DE")] + public void EnglishFallback_PreservesDurationTextAcrossSystemCultures(string cultureName) + { + CultureInfo previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + + Assert.AreEqual("2.5 seconds", SongChangeAnnouncementPolicy.DescribeDelay(2.5)); + Assert.AreEqual("2.5 seconds", SongChangeAnnouncementPolicy.DescribeDwell(2.5)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } +} diff --git a/Trdo.Tests/Trdo.Tests.csproj b/Trdo.Tests/Trdo.Tests.csproj index eb8e81a..4cb0455 100644 --- a/Trdo.Tests/Trdo.Tests.csproj +++ b/Trdo.Tests/Trdo.Tests.csproj @@ -22,6 +22,7 @@ ApplicationData, DispatcherQueue or a media player does not belong here. --> + diff --git a/Trdo/App.xaml.cs b/Trdo/App.xaml.cs index 49b24b7..afaf2c7 100644 --- a/Trdo/App.xaml.cs +++ b/Trdo/App.xaml.cs @@ -58,7 +58,7 @@ public partial class App : Application /// therefore has to ask "has this track been shown?", not "is this track new?". /// private string? _lastAnnouncedDisplayText; - private readonly PlayerViewModel _playerVm = PlayerViewModel.Shared; + private readonly PlayerViewModel _playerVm; private readonly UISettings _uiSettings = new(); private Mutex? _singleInstanceMutex; private EventWaitHandle? _trayIconRestoreEvent; @@ -94,6 +94,8 @@ public App() TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; LocalizationService.ApplyLanguage(SettingsService.AppLanguage); + // Build saved grouped views only after the resource resolver is ready. + _playerVm = PlayerViewModel.Shared; SettingsService.MigrateTrayClickActions(); InitializeComponent(); _playerVm.PropertyChanged += PlayerVmOnPropertyChanged; diff --git a/Trdo/Controls/AddLocalMusicWindow.xaml b/Trdo/Controls/AddLocalMusicWindow.xaml index e7a7de3..8e41db2 100644 --- a/Trdo/Controls/AddLocalMusicWindow.xaml +++ b/Trdo/Controls/AddLocalMusicWindow.xaml @@ -64,9 +64,9 @@ - - - - - + @@ -233,7 +233,7 @@ Style="{StaticResource AccentButtonStyle}"> - + diff --git a/Trdo/Controls/MiniPlayerWindow.xaml b/Trdo/Controls/MiniPlayerWindow.xaml index 91d79ff..d052328 100644 --- a/Trdo/Controls/MiniPlayerWindow.xaml +++ b/Trdo/Controls/MiniPlayerWindow.xaml @@ -7,7 +7,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ex="using:WinUIEx" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - Title="Traydio Mini Player" + Width="320" Height="220" IsMaximizable="False" @@ -44,20 +44,20 @@ PointerReleased="ContentCard_PointerReleased"> - - + - + + /> ApplyStationDelay(station, null); @@ -325,12 +325,12 @@ private MenuFlyout BuildDelayMenu() { flyout.Items.Add(new MenuFlyoutItem { - Text = $"Currently {SongChangeAnnouncementPolicy.DescribeDelay(effective)} (no station selected)", + Text = LocalizedText.Format("Ui_SongChangePopupWindow_xaml_a133d8ce0e", "Currently {0} (no station selected)", SongChangeAnnouncementPolicy.DescribeDelay(effective)), IsEnabled = false }); } - MenuFlyoutItem turnOff = new() { Text = "Turn off song popups" }; + MenuFlyoutItem turnOff = new() { Text = LocalizedText.Get("Ui_SongChangePopupWindow_xaml_16a1d60039", "Turn off song popups") }; turnOff.Click += (_, _) => { SettingsService.IsSongChangePopupEnabled = false; diff --git a/Trdo/Models/FavoriteTrack.cs b/Trdo/Models/FavoriteTrack.cs index cbc3404..9e8eed3 100644 --- a/Trdo/Models/FavoriteTrack.cs +++ b/Trdo/Models/FavoriteTrack.cs @@ -1,3 +1,4 @@ +using Trdo.Services; using System; using System.Text.Json.Serialization; @@ -55,7 +56,7 @@ public class FavoriteTrack /// [JsonIgnore] public string ExportedOnDisplay => ExportedAt is DateTime exportedAt - ? $"Exported on {exportedAt:MMM d, yyyy h:mm tt}" + ? LocalizedText.Format("Ui_FavoriteTrack_73edcbce18", "Exported on {0:MMM d, yyyy h:mm tt}", exportedAt) : string.Empty; /// diff --git a/Trdo/Models/StationFilterOption.cs b/Trdo/Models/StationFilterOption.cs index 21c6ec0..f500c5b 100644 --- a/Trdo/Models/StationFilterOption.cs +++ b/Trdo/Models/StationFilterOption.cs @@ -1,3 +1,4 @@ +using Trdo.Services; namespace Trdo.Models; /// @@ -37,13 +38,13 @@ public sealed record StationFilterOption(StationFilterFacet Facet, string Value, /// Short badge text identifying which attribute this narrows. public string FacetLabel => Facet switch { - StationFilterFacet.Country => "Country", - StationFilterFacet.Language => "Language", - _ => "Genre" + StationFilterFacet.Country => LocalizedText.Get("Ui_StationFilterOption_701d021d08", "Country"), + StationFilterFacet.Language => LocalizedText.Get("Ui_StationFilterOption_a4fe65264e", "Language"), + _ => LocalizedText.Get("Ui_StationFilterOption_6da795a866", "Genre") }; /// The station count, formatted for the suggestion row's trailing column. - public string CountLabel => StationCount == 1 ? "1 station" : $"{StationCount:N0} stations"; + public string CountLabel => StationCount == 1 ? LocalizedText.Get("Ui_StationFilterOption_85993e22a3", "1 station") : LocalizedText.Format("Ui_StationFilterOption_d088bdf306", "{0:N0} stations", StationCount); /// Chip text: the value alone would be ambiguous once several facets are in play. public string ChipLabel => $"{FacetLabel}: {Value}"; diff --git a/Trdo/Pages/AddWhiteNoise.xaml b/Trdo/Pages/AddWhiteNoise.xaml index f403c51..2b372a5 100644 --- a/Trdo/Pages/AddWhiteNoise.xaml +++ b/Trdo/Pages/AddWhiteNoise.xaml @@ -54,9 +54,9 @@ - diff --git a/Trdo/Pages/FavoritesPage.xaml.cs b/Trdo/Pages/FavoritesPage.xaml.cs index 086023f..1ec49ce 100644 --- a/Trdo/Pages/FavoritesPage.xaml.cs +++ b/Trdo/Pages/FavoritesPage.xaml.cs @@ -65,7 +65,7 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) { if (ViewModel.Favorites.Count == 0) { - ShowInfo(InfoBarSeverity.Informational, "Nothing to export", "Favorite some songs first."); + ShowInfo(InfoBarSeverity.Informational, LocalizedText.Get("Ui_FavoritesPage_xaml_fd3032d65f", "Nothing to export"), LocalizedText.Get("Ui_FavoritesPage_xaml_d7ce2eb1ae", "Favorite some songs first.")); return; } @@ -74,31 +74,31 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) ToggleSwitch exportAllToggle = new() { - Header = "Export all favorites", - OffContent = $"New only ({unarchived} of {total})", - OnContent = $"All favorites ({total})", + Header = LocalizedText.Get("Ui_FavoritesPage_xaml_bbb8178cea", "Export all favorites"), + OffContent = LocalizedText.Format("Ui_FavoritesPage_xaml_6405b9ceb1", "New only ({0} of {1})", unarchived, total), + OnContent = LocalizedText.Format("Ui_FavoritesPage_xaml_3892c42005", "All favorites ({0})", total), // Nothing new to send means the only useful export is a full one. IsOn = unarchived == 0, }; CheckBox archiveCheckBox = new() { - Content = "Archive exported tracks", + Content = LocalizedText.Get("Ui_FavoritesPage_xaml_ac91f43d6e", "Archive exported tracks"), IsChecked = true, }; StackPanel content = new() { Spacing = 12 }; content.Children.Add(new TextBlock { - Text = "CSV works with playlist transfer services like Soundiiz or TuneMyMusic. " - + "XSPF is a playlist file for players such as VLC. Pick the format when you save.", + Text = LocalizedText.Get("Ui_FavoritesPage_xaml_ed7378da02", "CSV works with playlist transfer services like Soundiiz or TuneMyMusic. ") + + LocalizedText.Get("Ui_FavoritesPage_xaml_e6c5d7bf20", "XSPF is a playlist file for players such as VLC. Pick the format when you save."), TextWrapping = TextWrapping.Wrap, }); content.Children.Add(exportAllToggle); content.Children.Add(archiveCheckBox); content.Children.Add(new TextBlock { - Text = "Archived tracks stay in your favorites but are skipped by later exports.", + Text = LocalizedText.Get("Ui_FavoritesPage_xaml_dd3a540aaa", "Archived tracks stay in your favorites but are skipped by later exports."), FontSize = 12, TextWrapping = TextWrapping.Wrap, Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["TextFillColorTertiaryBrush"], @@ -107,10 +107,10 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) ContentDialog dialog = new() { XamlRoot = XamlRoot, - Title = "Export favorites", + Title = LocalizedText.Get("Ui_FavoritesPage_xaml_45655891f7", "Export favorites"), Content = content, - PrimaryButtonText = "Export", - CloseButtonText = "Cancel", + PrimaryButtonText = LocalizedText.Get("Ui_FavoritesPage_xaml_3664895579", "Export"), + CloseButtonText = LocalizedText.Get("Ui_FavoritesPage_xaml_19766ed6cc", "Cancel"), DefaultButton = ContentDialogButton.Primary, }; @@ -122,8 +122,8 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) if (!exportAll && unarchived == 0) { - ShowInfo(InfoBarSeverity.Informational, "Nothing new to export", - "Every favorite has already been exported. Turn on \"Export all favorites\" or clear the export history."); + ShowInfo(InfoBarSeverity.Informational, LocalizedText.Get("Ui_FavoritesPage_xaml_e3544aef93", "Nothing new to export"), + LocalizedText.Get("Ui_FavoritesPage_xaml_157b5987d2", "Every favorite has already been exported. Turn on \"Export all favorites\" or clear the export history.")); return; } @@ -134,11 +134,11 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) if (!result.Succeeded) return; - string message = $"Exported {result.ExportedCount} track{(result.ExportedCount == 1 ? "" : "s")} to {result.FileName}."; + string message = LocalizedText.Format("Ui_FavoritesPage_xaml_2838dcc9ce", "Exported tracks: {0}. File: {1}.", result.ExportedCount, result.FileName); if (result.ArchivedCount > 0) - message += $" {result.ArchivedCount} archived and will be skipped next time."; + message += LocalizedText.Format("Ui_FavoritesPage_xaml_a924cdf246", " {0} archived and will be skipped next time.", result.ArchivedCount); - ShowInfo(InfoBarSeverity.Success, "Export complete", message); + ShowInfo(InfoBarSeverity.Success, LocalizedText.Get("Ui_FavoritesPage_xaml_81f3f0a234", "Export complete"), message); } catch (Exception ex) { @@ -150,8 +150,8 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) private void ClearExportHistory_Click(object sender, RoutedEventArgs e) { int cleared = ViewModel.ClearAllArchived(); - ShowInfo(InfoBarSeverity.Informational, "Export history cleared", - $"{cleared} track{(cleared == 1 ? "" : "s")} will be included in the next export."); + ShowInfo(InfoBarSeverity.Informational, LocalizedText.Get("Ui_FavoritesPage_xaml_31ea8e04f8", "Export history cleared"), + LocalizedText.Format("Ui_FavoritesPage_xaml_7ce23338b0", "Tracks included in the next export: {0}.", cleared)); } private void ClearArchivedForTrack_Click(object sender, RoutedEventArgs e) diff --git a/Trdo/Pages/NowPlayingPage.xaml b/Trdo/Pages/NowPlayingPage.xaml index a811b03..5ef723b 100644 --- a/Trdo/Pages/NowPlayingPage.xaml +++ b/Trdo/Pages/NowPlayingPage.xaml @@ -339,7 +339,7 @@ - - - diff --git a/Trdo/Pages/SettingsPage.xaml b/Trdo/Pages/SettingsPage.xaml index d2e8820..8db8ede 100644 --- a/Trdo/Pages/SettingsPage.xaml +++ b/Trdo/Pages/SettingsPage.xaml @@ -78,6 +78,7 @@ + 0) { ImportExportInfoBar.Severity = InfoBarSeverity.Success; - ImportExportInfoBar.Message = $"Imported {count} station{(count == 1 ? "" : "s")}."; + ImportExportInfoBar.Message = LocalizedText.Format("Ui_SettingsPage_xaml_a0030fbcf8", "Imported stations: {0}.", count); } else { ImportExportInfoBar.Severity = InfoBarSeverity.Informational; - ImportExportInfoBar.Message = "No stations were imported."; + ImportExportInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_1bc4de1242", "No stations were imported."); } ImportExportInfoBar.IsOpen = true; @@ -149,12 +149,12 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e) if (exported) { ImportExportInfoBar.Severity = InfoBarSeverity.Success; - ImportExportInfoBar.Message = "Stations exported successfully."; + ImportExportInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_e8a4faf205", "Stations exported successfully."); } else { ImportExportInfoBar.Severity = InfoBarSeverity.Informational; - ImportExportInfoBar.Message = "Export cancelled or no stations to export."; + ImportExportInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_56d7566dc6", "Export cancelled or no stations to export."); } ImportExportInfoBar.IsOpen = true; @@ -176,7 +176,7 @@ private async void OpenLogsFolderButton_Click(object sender, RoutedEventArgs e) if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) { DiagnosticsInfoBar.Severity = InfoBarSeverity.Informational; - DiagnosticsInfoBar.Message = "No logs have been written yet."; + DiagnosticsInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_b1140110b6", "No logs have been written yet."); DiagnosticsInfoBar.IsOpen = true; return; } @@ -185,7 +185,7 @@ private async void OpenLogsFolderButton_Click(object sender, RoutedEventArgs e) if (launched) { DiagnosticsInfoBar.Severity = InfoBarSeverity.Success; - DiagnosticsInfoBar.Message = "Opened the logs folder."; + DiagnosticsInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_9fa6ebd966", "Opened the logs folder."); } else { @@ -224,8 +224,8 @@ private void ResetEngineMemoryButton_Click(object sender, RoutedEventArgs e) EngineMemoryInfoBar.Severity = InfoBarSeverity.Success; EngineMemoryInfoBar.Message = removed == 0 - ? "There were no remembered engines to reset." - : $"Forgot the remembered engine for {removed} station{(removed == 1 ? string.Empty : "s")}."; + ? LocalizedText.Get("Ui_SettingsPage_xaml_eb1ba74d5e", "There were no remembered engines to reset.") + : LocalizedText.Format("Ui_SettingsPage_xaml_bc5fb9326a", "Reset the saved playback engine for {0} station(s).", removed); } catch (Exception ex) { @@ -249,7 +249,7 @@ private void CopyDiagnosticsButton_Click(object sender, RoutedEventArgs e) Clipboard.SetContent(package); DiagnosticsInfoBar.Severity = InfoBarSeverity.Success; - DiagnosticsInfoBar.Message = "Copied recent diagnostics to the clipboard."; + DiagnosticsInfoBar.Message = LocalizedText.Get("Ui_SettingsPage_xaml_0a218e3ba5", "Copied recent diagnostics to the clipboard."); DiagnosticsInfoBar.IsOpen = true; } catch (Exception ex) diff --git a/Trdo/Services/LocalizationService.cs b/Trdo/Services/LocalizationService.cs index dcb0046..86e9059 100644 --- a/Trdo/Services/LocalizationService.cs +++ b/Trdo/Services/LocalizationService.cs @@ -24,7 +24,7 @@ public static class LocalizationService /// the language picker on the settings page (excluding the "System" option). /// i.e. also add it (in the same order) in the ComboBox in Pages\SettingsPage.xaml /// - public static string[] SupportedLanguages { get; } = ["en-US", "es-ES", "fr-FR", "zh-CN", "de-DE", "pt-BR", "pt-PT", "ar-SA", "pl-PL"]; + public static string[] SupportedLanguages { get; } = ["en-US", "es-ES", "fr-FR", "zh-CN", "de-DE", "pt-BR", "pt-PT", "ar-SA", "pl-PL", "ru-RU"]; /// /// All selectable options in the settings page language picker, in display order: "System" @@ -65,6 +65,7 @@ public static void ApplyLanguage(string languageTag) : languageTag; ApplicationLanguages.PrimaryLanguageOverride = effectiveTag; + LocalizedText.SetResolver(GetString); LogService.Info("Localization", string.IsNullOrEmpty(effectiveTag) diff --git a/Trdo/Services/LocalizedText.cs b/Trdo/Services/LocalizedText.cs new file mode 100644 index 0000000..d2fddb1 --- /dev/null +++ b/Trdo/Services/LocalizedText.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; + +namespace Trdo.Services; + +/// +/// Platform-independent access to UI text. Plain policy tests use the English fallback; +/// the Windows app supplies its resource lookup before loading the interface. +/// +public static class LocalizedText +{ + private static Func? _resolver; + + /// Registers the resource lookup used by the Windows application. + public static void SetResolver(Func resolver) => _resolver = resolver; + + /// Gets a resource value, or the fallback when no resolver is installed. + public static string Get(string key, string fallback) => _resolver?.Invoke(key, fallback) ?? fallback; + + /// + /// Formats text using the current culture in the application, or invariant culture + /// when no resolver is installed. + /// + public static string Format(string key, string fallback, params object[] values) + { + var resolver = _resolver; + // The English fallback preserves the policies' culture-independent output. + // The Windows app supplies a resolver and keeps the user's number formatting. + return string.Format( + resolver is null ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture, + resolver?.Invoke(key, fallback) ?? fallback, + values); + } +} diff --git a/Trdo/Services/SongChangeAnnouncementPolicy.cs b/Trdo/Services/SongChangeAnnouncementPolicy.cs index 5a7ba49..4f1e27a 100644 --- a/Trdo/Services/SongChangeAnnouncementPolicy.cs +++ b/Trdo/Services/SongChangeAnnouncementPolicy.cs @@ -149,9 +149,7 @@ public static string DescribeDwell(double seconds) { double clamped = ClampDwell(seconds); - return Math.Abs(clamped - Math.Round(clamped)) < 0.05 - ? $"{Math.Round(clamped):0} second{(Math.Round(clamped) == 1 ? "" : "s")}" - : $"{clamped:0.0} seconds"; + return DescribeSeconds(clamped); } /// @@ -180,15 +178,20 @@ public static string DescribeDelay(double seconds) double clamped = ClampDelay(seconds); if (clamped <= 0) - return "No delay"; + return LocalizedText.Get("Ui_SongChangeAnnouncementPolicy_1eef43b1b6", "No delay"); // The top of the range reads better as a minute than as 60 seconds. if (clamped >= MaxDelaySeconds) - return "1 minute"; + return LocalizedText.Get("Ui_SongChangeAnnouncementPolicy_e67b6f61f5", "1 minute"); // Whole seconds read better than "5.0s" for the common preset values. - return Math.Abs(clamped - Math.Round(clamped)) < 0.05 - ? $"{Math.Round(clamped):0} second{(Math.Round(clamped) == 1 ? "" : "s")}" - : $"{clamped:0.0} seconds"; + return DescribeSeconds(clamped); } -} \ No newline at end of file + + private static string DescribeSeconds(double seconds) + { + if (Math.Abs(seconds - Math.Round(seconds)) < 0.05) + return Math.Round(seconds) == 1 ? LocalizedText.Get("Ui_SongChangeAnnouncementPolicy_db3b946b8d", "1 second") : LocalizedText.Format("Ui_SongChangeAnnouncementPolicy_8c48eeb893", "{0:0} seconds", Math.Round(seconds)); + return LocalizedText.Format("Ui_SongChangeAnnouncementPolicy_c432fca10b", "{0:0.0} seconds", seconds); + } +} diff --git a/Trdo/Services/StationGroupingPolicy.cs b/Trdo/Services/StationGroupingPolicy.cs index 7969f9f..aefb075 100644 Binary files a/Trdo/Services/StationGroupingPolicy.cs and b/Trdo/Services/StationGroupingPolicy.cs differ diff --git a/Trdo/Services/StationMetadataBackfillService.cs b/Trdo/Services/StationMetadataBackfillService.cs index e4dcfb0..afe825f 100644 --- a/Trdo/Services/StationMetadataBackfillService.cs +++ b/Trdo/Services/StationMetadataBackfillService.cs @@ -267,10 +267,10 @@ public static string DescribeStation(RadioStation station) if (!string.IsNullOrWhiteSpace(station.Language)) parts.Add(station.Language!); if (station.Bitrate is int bitrate and > 0) - parts.Add(!string.IsNullOrWhiteSpace(station.Codec) ? $"{bitrate} kbps {station.Codec}" : $"{bitrate} kbps"); + parts.Add(!string.IsNullOrWhiteSpace(station.Codec) ? LocalizedText.Format("Ui_StationMetadataBackfillService_8d05b3d678", "{0} kbps {1}", bitrate, station.Codec!) : LocalizedText.Format("Ui_StationMetadataBackfillService_b6b26412c5", "{0} kbps", bitrate)); else if (!string.IsNullOrWhiteSpace(station.Codec)) parts.Add(station.Codec!); - return parts.Count == 0 ? "No extra details were available." : string.Join(" · ", parts); + return parts.Count == 0 ? LocalizedText.Get("Ui_StationMetadataBackfillService_af714df177", "No extra details were available.") : string.Join(" · ", parts); } } diff --git a/Trdo/Services/StationSortPolicy.cs b/Trdo/Services/StationSortPolicy.cs index b25c5ca..46de40f 100644 --- a/Trdo/Services/StationSortPolicy.cs +++ b/Trdo/Services/StationSortPolicy.cs @@ -55,12 +55,12 @@ public static IReadOnlyList Sort(IReadOnlyList stati /// The mode's name, as shown in the sort menu. public static string DisplayName(StationSortMode mode) => mode switch { - StationSortMode.Manual => "Manual", - StationSortMode.Name => "Name", - StationSortMode.Genre => "Genre", - StationSortMode.Country => "Country", - StationSortMode.RecentlyAdded => "Recently added", - _ => "Manual" + StationSortMode.Manual => LocalizedText.Get("Ui_StationSortPolicy_b0b9fe24ff", "Manual"), + StationSortMode.Name => LocalizedText.Get("Ui_StationSortPolicy_dcd1d5223f", "Name"), + StationSortMode.Genre => LocalizedText.Get("Ui_StationSortPolicy_6da795a866", "Genre"), + StationSortMode.Country => LocalizedText.Get("Ui_StationSortPolicy_701d021d08", "Country"), + StationSortMode.RecentlyAdded => LocalizedText.Get("Ui_StationSortPolicy_96ec5d338a", "Recently added"), + _ => LocalizedText.Get("Ui_StationSortPolicy_b0b9fe24ff", "Manual") }; /// @@ -71,7 +71,7 @@ public static IReadOnlyList Sort(IReadOnlyList stati public static string HintText(StationSortMode mode) => mode == StationSortMode.Manual ? string.Empty - : $"Sorted by {DisplayName(mode)} · groups hidden · reordering off"; + : LocalizedText.Format("Ui_StationSortPolicy_724ef7b174", "Sorted by {0} · groups hidden · reordering off", DisplayName(mode)); /// /// Sorts on a key that is often missing, keeping stations without one at the end. A diff --git a/Trdo/Services/StreamWatchdogService.cs b/Trdo/Services/StreamWatchdogService.cs index 1a34e91..20dba61 100644 --- a/Trdo/Services/StreamWatchdogService.cs +++ b/Trdo/Services/StreamWatchdogService.cs @@ -223,14 +223,14 @@ public int BufferDelayMs /// public static string DescribeBufferLevel(double level) => level switch { - 0 => "Default", - 1 => "Medium", - 2 => "Large", - 3 => "Extra Large", - _ when level < 0.5 => "Default", - _ when level < 1.5 => "Medium", - _ when level < 2.5 => "Large", - _ => "Extra Large" + 0 => LocalizedText.Get("Ui_StreamWatchdogService_21b111cbfe", "Default"), + 1 => LocalizedText.Get("Ui_StreamWatchdogService_8e588cd187", "Medium"), + 2 => LocalizedText.Get("Ui_StreamWatchdogService_ab80540d98", "Large"), + 3 => LocalizedText.Get("Ui_StreamWatchdogService_f44f24c71f", "Extra Large"), + _ when level < 0.5 => LocalizedText.Get("Ui_StreamWatchdogService_21b111cbfe", "Default"), + _ when level < 1.5 => LocalizedText.Get("Ui_StreamWatchdogService_8e588cd187", "Medium"), + _ when level < 2.5 => LocalizedText.Get("Ui_StreamWatchdogService_ab80540d98", "Large"), + _ => LocalizedText.Get("Ui_StreamWatchdogService_f44f24c71f", "Extra Large") }; public StreamWatchdogService(RadioPlayerService playerService) diff --git a/Trdo/Strings/ar-SA/Resources.resw b/Trdo/Strings/ar-SA/Resources.resw index 56e272b..32f7553 100644 --- a/Trdo/Strings/ar-SA/Resources.resw +++ b/Trdo/Strings/ar-SA/Resources.resw @@ -960,4 +960,7 @@ مرحبًا بك في Traydio! + + Русский + diff --git a/Trdo/Strings/de-DE/Resources.resw b/Trdo/Strings/de-DE/Resources.resw index 9c05169..95ee964 100644 --- a/Trdo/Strings/de-DE/Resources.resw +++ b/Trdo/Strings/de-DE/Resources.resw @@ -960,4 +960,7 @@ Willkommen bei Traydio! + + Русский + diff --git a/Trdo/Strings/en-US/Resources.resw b/Trdo/Strings/en-US/Resources.resw index c482095..125e265 100644 --- a/Trdo/Strings/en-US/Resources.resw +++ b/Trdo/Strings/en-US/Resources.resw @@ -960,4 +960,541 @@ Welcome to Traydio! + + Русский + + + Volume + + + Station volume + + + Station buffer level + + + Station track info delay in seconds + + + Cancel + + + Save + + + Traydio Mini Player + + + Show Visualizer + + + Always on Top + + + Hide Title Bar + + + Volume + + + More favorites options + + + Clear export history + + + Music search settings + + + Favorite Song + + + Click for details + + + Favorite Song + + + Search Radio Stations + + + Search by name, or use filters... + + + Filters + + + Filters + + + Remove this filter + + + Search Error + + + Country, language or genre... + + + Find a country, language or genre + + + No country, language or genre matches that. + + + Quality and order + + + Codec + + + Minimum bitrate + + + Sort by + + + Hide broken stations + + + Clear all + + + Show results + + + Enter a search term to find stations... + + + Or add station manually + + + Add white noise... + + + Add local music... + + + Preview station + + + Edit station details before adding + + + Add Station + + + Cancel + + + A simple, elegant internet radio player for Windows + + + Community-driven internet radio database with thousands of stations worldwide + + + No station selected + + + Any codec + + + Any bitrate + + + 64 kbps+ + + + 128 kbps+ + + + 192 kbps+ + + + 256 kbps+ + + + 320 kbps+ + + + Most voted + + + Most popular + + + Name (A–Z) + + + Random + + + Hide broken stations + + + Network error: Unable to reach the radio station service. Please check your internet connection. + + + Windows Media Foundation only + + + Windows first, LibVLC fallback + + + LibVLC first, Windows fallback + + + No stations were imported. + + + Stations exported successfully. + + + Export cancelled or no stations to export. + + + No logs have been written yet. + + + Opened the logs folder. + + + There were no remembered engines to reset. + + + Copied recent diagnostics to the clipboard. + + + Add Station + + + Add station functionality coming soon! + + + OK + + + Unknown Station + + + No match found + + + Couldn't reach radio-browser.info + + + Check your connection and try again. + + + Also refresh stations that already have details + + + All your stations already have details. You can look them up again to pick up any changes. + + + One station has no genre or country saved. + + + Look up station details + + + Look up + + + Cancel + + + Nothing to look up + + + Every station has been checked recently. Try again in a few days. + + + Starting… + + + Looking up station details + + + Finishing… + + + Station details + + + Stopped early because radio-browser.info stopped responding. + + + Stopped at your request. What was found so far has been saved. + + + New group + + + Group name + + + Label (optional) + + + The station in this group will be removed as well. This cannot be undone. + + + Delete + + + Nothing to export + + + Favorite some songs first. + + + Export all favorites + + + Archive exported tracks + + + CSV works with playlist transfer services like Soundiiz or TuneMyMusic. + + + XSPF is a playlist file for players such as VLC. Pick the format when you save. + + + Archived tracks stay in your favorites but are skipped by later exports. + + + Export favorites + + + Export + + + Cancel + + + Nothing new to export + + + Every favorite has already been exported. Turn on "Export all favorites" or clear the export history. + + + Export complete + + + Export history cleared + + + Track info delay + + + Turn off song popups + + + Country + + + Language + + + Genre + + + 1 station + + + Manual + + + Name + + + Genre + + + Country + + + Recently added + + + No genre + + + No country + + + Manual + + + Genre + + + Country + + + Default + + + Medium + + + Large + + + Extra Large + + + Buffer: {0} (this station) + + + Buffer: {0} (app setting) + + + Track info delay: {0} (this station) + + + Track info delay: {0} (app setting) + + + Codec: {0} + + + Bitrate: {0} + + + Sort: {0} + + + Server error ({0}): The radio station service is temporarily unavailable. Please try again later. + + + An error occurred while searching: {0} + + + {0:0}s + + + {0}m + + + Imported stations: {0}. + + + Reset the saved playback engine for {0} station(s). + + + {0:N0} stations + + + Exported on {0:MMM d, yyyy h:mm tt} + + + Sorted by {0} · groups hidden · reordering off + + + Grouped by {0} · reordering off + + + Now playing: {0} + + + Track info delay for {0} + + + Use app setting ({0}) + + + Currently {0} (no station selected) + + + New only ({0} of {1}) + + + All favorites ({0}) + + + Exported tracks: {0}. File: {1}. + + + {0} archived and will be skipped next time. + + + Tracks included in the next export: {0}. + + + radio-browser.info has no entry for this station's stream address, so there are no details to add. + + + Updated {0} + + + {0} stations have no genre or country saved. + + + {0} Traydio will ask radio-browser.info about them. Only each station's stream address is sent. + + + {0} of {1} · {2} + + + Updated {0} of {1} stations. + + + {0} had no entry on radio-browser.info. + + + {0} matched more than one entry; the closest was used. + + + {0} were left for next time to keep the request count reasonable. + + + Delete "{0}"? + + + All {0} stations in this group will be removed as well. This cannot be undone. + + + No delay + + + 1 minute + + + 1 second + + + {0:0} seconds + + + {0:0.0} seconds + + + (None) + + + Remove favorite + + + Favorite track + + + New group + + + Unknown Station + + + Unknown + + + No extra details were available. + + + {0} kbps {1} + + + {0} kbps + diff --git a/Trdo/Strings/es-ES/Resources.resw b/Trdo/Strings/es-ES/Resources.resw index c7f2bd3..b32eaa2 100644 --- a/Trdo/Strings/es-ES/Resources.resw +++ b/Trdo/Strings/es-ES/Resources.resw @@ -960,4 +960,7 @@ ¡Bienvenido a Traydio! + + Русский + diff --git a/Trdo/Strings/fr-FR/Resources.resw b/Trdo/Strings/fr-FR/Resources.resw index b90a08c..5de7bc9 100644 --- a/Trdo/Strings/fr-FR/Resources.resw +++ b/Trdo/Strings/fr-FR/Resources.resw @@ -960,4 +960,7 @@ Bienvenue sur Traydio ! + + Русский + diff --git a/Trdo/Strings/pl-PL/Resources.resw b/Trdo/Strings/pl-PL/Resources.resw index 04c85a6..d04e12a 100644 --- a/Trdo/Strings/pl-PL/Resources.resw +++ b/Trdo/Strings/pl-PL/Resources.resw @@ -960,4 +960,7 @@ Witaj w Traydio! + + Русский + diff --git a/Trdo/Strings/pt-BR/Resources.resw b/Trdo/Strings/pt-BR/Resources.resw index eaa1063..4b507a5 100644 --- a/Trdo/Strings/pt-BR/Resources.resw +++ b/Trdo/Strings/pt-BR/Resources.resw @@ -960,4 +960,7 @@ Bem-vindo ao Traydio! + + Русский + diff --git a/Trdo/Strings/pt-PT/Resources.resw b/Trdo/Strings/pt-PT/Resources.resw index 2b01937..6a9f405 100644 --- a/Trdo/Strings/pt-PT/Resources.resw +++ b/Trdo/Strings/pt-PT/Resources.resw @@ -960,4 +960,7 @@ Bem-vindo ao Traydio! + + Русский + diff --git a/Trdo/Strings/ru-RU/Resources.resw b/Trdo/Strings/ru-RU/Resources.resw new file mode 100644 index 0000000..1be7cf8 --- /dev/null +++ b/Trdo/Strings/ru-RU/Resources.resw @@ -0,0 +1,1481 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + В приложении используются следующие библиотеки с открытым исходным кодом: + + + Библиотеки с открытым кодом + + + Связаться с разработчиком + + + Разработчик + + + Нравится Traydio? + + + Поддержка нескольких радиостанций + + + Запуск вместе с Windows + + + Управление из системного трея + + + Контроль потока и автоматическое переподключение + + + Возможности + + + Прежнее название: Trdo + + + Посмотрите краткое руководство по работе с Traydio. + + + Нужна помощь? + + + Возникла проблема или есть предложение? + + + Исходный код приложения открыт и доступен на GitHub. + + + Открытый исходный код + + + Каталог радиостанций + + + Оцените приложение: + + + Если вам нравится Traydio, вы можете поддержать разработчика! + + + Поддержать разработчика + + + О программе + + + Открыть руководство + + + Страница разработчика на GitHub + + + Отмена + + + Сохранить + + + Добавить радиостанцию + + + Изменить радиостанцию + + + Сайт станции (необязательно) + + + Например: https://example.com + + + Ссылка на логотип (необязательно) + + + Например: https://example.com/logo.png + + + Введите корректную ссылку HTTP или HTTPS. + + + Укажите название станции. + + + Добавить станцию + + + Название станции + + + Например: Моя любимая радиостанция + + + Ссылка на аудиопоток + + + Например: https://example.com/stream + + + Укажите ссылку на аудиопоток. + + + Добавить музыку с компьютера + + + Изменить источник музыки + + + Название + + + Например: Моя музыка, Избранное + + + Папка + + + Обзор… + + + Громкость + + + Папка не выбрана + + + В этой папке нет аудиофайлов. + + + Найден один трек. + + + Найдено треков: {0}. + + + Найдена одна вложенная папка с музыкой. Она будет добавлена как отдельная станция в новую группу. + + + Найдено вложенных папок с музыкой: {0}. Каждая будет добавлена как отдельная станция в новую группу. + + + Добавить шум + + + Синий + + + Яркий шум с преобладанием высоких частот. + + + Глубокий низкий гул почти без шипения. Подходит для отдыха. + + + Шум с одинаковой воспринимаемой громкостью на всех частотах. Подходит для сосредоточенной работы. + + + Мягкий шум с преобладанием низких частот, похожий на дождь. Подходит для сна. + + + Очень яркий шум с выраженными высокими частотами. + + + Ровное шипение во всём диапазоне частот. Подходит для маскировки посторонних звуков и проверки аудио. + + + Цвет шума + + + Коричневый + + + Белый шум + + + Изменить настройки шума + + + Серый + + + Добавить шум + + + Розовый + + + Название + + + Например: Дождь, Вентилятор, Морские волны + + + Фиолетовый + + + Громкость + + + Белый + + + Выйти из Traydio + + + Выход + + + Включить радио или поставить на паузу + + + Слушать + + + Показать или скрыть главное окно + + + Показать окно + + + Нажмите на звёздочку рядом с понравившейся песней, чтобы сохранить её здесь + + + В избранном пока нет песен + + + Экспортировать избранное в файл плейлиста + + + Не удалось выполнить экспорт + + + Экспорт… + + + Включить в следующий экспорт + + + Другие действия + + + Удалить из избранного + + + Избранные песни + + + Дополнительно + + + Уровень буфера + + + Больший буфер помогает при нестабильном интернете, но немного увеличивает задержку воспроизведения. Задавайте отдельное значение только для станции, которой это необходимо. + + + Задать отдельный буфер для этой станции + + + Общая настройка приложения + + + Отдельная настройка станции + + + Задержка сведений о треке + + + Некоторые станции присылают сведения о следующем треке до его начала. Задержите обновление названия, чтобы оно совпадало с музыкой. Величина опережения зависит от станции, поэтому задержка настраивается для каждой отдельно. + + + Задать отдельную задержку сведений для этой станции + + + Общая настройка приложения + + + Отдельная настройка станции + + + Traydio: добавление станции + + + Громкость + + + Громкость сохраняется для этой станции и восстанавливается, когда вы возвращаетесь к ней. + + + Включите воспроизведение, чтобы увидеть название и исполнителя + + + Радио не играет + + + Исполнитель + + + Название + + + Недавние треки + + + Треки + + + Сейчас играет + + + Добавить песню в избранное + + + Настройки поиска музыки + + + Не удалось включить станцию. Возможно, поток недоступен или ссылка указана неверно. + + + Нет подключения к интернету. Подключитесь к сети и повторите попытку. + + + Не удалось включить станцию после нескольких попыток. Возможно, поток недоступен или ссылка указана неверно. + + + Не удалось включить станцию после нескольких попыток ({0}). Возможно, поток недоступен или ссылка указана неверно. + + + Не удалось включить станцию: {0} + + + Не удалось включить шум: нет доступного устройства воспроизведения звука. + + + Не удалось включить станцию. {0} + + + Буферизация + + + Не удалось подготовить аудиопоток: {0} + + + Не удалось приостановить {0}: {1} + + + Не удалось включить {0}: {1} + + + Не удалось восстановить {0}: {1} + + + Не удалось переключиться на {0}: {1} + + + Не удалось обновить аудиопоток: {0} + + + Неверная ссылка на аудиопоток станции {0} + + + Пауза + + + Пауза и закрытие + + + Слушать + + + О программе Traydio + + + Добавить первую станцию + + + Удалить группу + + + Удалить группу со станциями + + + Изменить + + + Изменить подпись + + + Добавьте любимую радиостанцию, чтобы начать слушать. + + + Пока нет станций + + + Избранные песни + + + Группировать по + + + Скрыть регулятор громкости + + + Вставить разделитель выше + + + Вручную + + + Переместить в группу + + + Новый разделитель + + + Новая группа + + + Открыть папку + + + ОК + + + Ошибка воспроизведения + + + Выключить или включить звук + + + Открыть мини-плеер + + + Выход + + + Выйти из Traydio + + + Обновить сведения обо всех станциях + + + Обновить сведения о станции + + + Удалить + + + Удалить разделитель + + + Переименовать + + + Настройки + + + Показать регулятор громкости + + + Отменить таймер сна + + + Таймер сна + + + 15 минут + + + 20 минут + + + 30 минут + + + 45 минут + + + 50 минут + + + Сортировать по + + + Станция + + + Станции + + + Показать треки альбома + + + Открыть сайт + + + Разрешить компьютеру переходить в спящий режим по обычному расписанию во время работы радио. Windows может блокировать сон при воспроизведении любого звука. + + + Разрешить спящий режим + + + Разрешить Traydio автоматически увеличивать буфер при перебоях воспроизведения. + + + Увеличивать буфер при перебоях + + + Звук будет включаться сразу при запуске Traydio. Это может мешать. Включить эту настройку? + + + Следить за воспроизведением и автоматически восстанавливать поток при зависании, обрыве или пропадании звука. + + + Автовосстановление + + + Больший буфер помогает при нестабильном интернете, но может немного увеличить задержку воспроизведения. + + + Уровень буфера + + + Скопировать диагностику + + + Не удалось скопировать диагностику: {0} + + + Показать пример уведомления о песне + + + Показать пример уведомления + + + Пример + + + Traydio записывает в журнал сведения о воспроизведении: выбранный модуль, состояние потока и ошибки. Откройте папку журналов или скопируйте диагностику для сообщения о проблеме. + + + Диагностика + + + Включить + + + Ошибка экспорта: {0} + + + Экспорт станций + + + Сохраните резервную копию станций или перенесите их на другое устройство с помощью плейлистов M3U и PLS. + + + Импорт и экспорт станций + + + Ошибка импорта: {0} + + + Импорт станций + + + Английский + + + Как в системе + + + Испанский + + + Французский + + + Китайский + + + Немецкий + + + Португальский (Бразилия) + + + Португальский (Португалия) + + + Арабский + + + Польский + + + Выберите язык интерфейса приложения. + + + Язык + + + Выберите сервисы для поиска песен на экранах «Сейчас играет» и «Избранные песни». + + + Сервисы поиска музыки + + + Открыть папку журналов + + + Не удалось открыть папку журналов: {0} + + + Не удалось открыть папку журналов. Путь: {0} + + + Автоматически запускать Traydio при входе в Windows. + + + Запускать вместе с Windows + + + Автоматически (рекомендуется) + + + Только средства Windows + + + Предпочитать средства Windows + + + Traydio запоминает подходящий модуль воспроизведения для каждой станции. Сбросьте сохранённый выбор, если поведение станции изменилось. + + + Выберите модуль воспроизведения. В автоматическом режиме сначала используется LibVLC, при необходимости Windows Media Foundation. + + + Модуль воспроизведения + + + Автоматически включать последнюю станцию при открытии Traydio. + + + Включать радио при запуске + + + Время показа уведомления. Нажмите на него, чтобы закрыть раньше. + + + Длительность показа уведомления о песне, в секундах + + + Воспроизводить тихий радиошум, пока станция загружается. Шум плавно появляется и затихает, когда начинается музыка. + + + Послушать радиошум + + + Включить радиошум для проверки + + + Проверить + + + Радиошум при буферизации + + + Воспроизводить радиошум при буферизации + + + Не удалось сбросить сохранённый выбор модулей: {0} + + + Очистить журналы + + + Не удалось очистить журналы: {0} + + + Файлы журналов очищены. + + + Сбросить выбор модулей + + + Перезапустите Traydio, чтобы применить выбранный язык. + + + Укажите, сколько времени звук может отсутствовать до автоматического перезапуска потока. + + + Время ожидания при отсутствии звука + + + Показывать короткое уведомление рядом с панелью задач при смене песни. + + + Уведомление о смене песни + + + Уведомление о смене песни + + + Многие станции присылают сведения о треке заранее. Задержите обновление названия, чтобы оно совпадало с музыкой в главном окне, мини-плеере, системных элементах управления, истории и уведомлениях. Первый трек после включения показывается сразу. Отдельную задержку можно задать в дополнительных настройках станции или через правую кнопку мыши на уведомлении. + + + Задержка сведений о треке, в секундах + + + Настройки + + + Задержка сведений о треке + + + Добавить текущую песню в избранное + + + Выключить или включить звук + + + Ничего не делать + + + Воспроизведение или пауза + + + Открыть Traydio + + + Показать уведомление о текущей песне + + + Показать или скрыть мини-плеер + + + Выберите действия для левой и правой кнопок мыши на значке в трее. Одна из кнопок всегда открывает Traydio, чтобы настройки оставались доступны. + + + Действия по нажатию на значок в трее + + + Одиночное нажатие выполняется сразу. Если назначить действие на двойное нажатие, одиночное будет с небольшой задержкой, чтобы Windows могла различить их. + + + Левая кнопка мыши + + + Двойное нажатие левой кнопкой + + + Правая кнопка мыши + + + Двойное нажатие правой кнопкой + + + Traydio + + + Добавить песню в избранное + + + Сейчас играет + + + Нажмите, чтобы закрыть. Правая кнопка мыши открывает настройку задержки сведений о треке + + + Сейчас играет + + + Станция доступна, но ни один модуль не смог воспроизвести её на этом компьютере. + + + Станция доступна, но воспроизведение на этом компьютере не удалось: {0} + + + {0} Попробуйте указать {1} в качестве ссылки на аудиопоток. + + + Выключено + + + Включено + + + Traydio: добавьте станцию, чтобы начать слушать + + + Нажмите левой кнопкой, чтобы приостановить + + + Нажмите левой кнопкой, чтобы включить + + + Звук выключен + + + Traydio {0} (пауза) +{1} + + + Traydio {0} (воспроизведение) +{1} + + + Traydio {0} (воспроизведение) +{1} +{2} + + + Нажмите правой кнопкой, чтобы приостановить + + + Нажмите правой кнопкой, чтобы включить + + + • Лёгкое приложение с простым интерфейсом + + + • Тысячи радиостанций со всего мира + + + • Современный интерфейс с фоном Mica + + + • Незаметная работа в системном трее + + + ✨ Возможности Traydio: + + + Небольшое радио в системном трее Windows + + + Краткое руководство + + + Начать! + + + Выбирайте станции, меняйте настройки и знакомьтесь с возможностями + + + Правая кнопка открывает меню + + + Закрепите значок Traydio в видимой области системного трея + + + Закрепите значок в трее + + + Быстрое управление воспроизведением + + + Левая кнопка включает радио и паузу + + + Первый запуск Traydio + + + Traydio + + + Добро пожаловать в Traydio! + + + Русский + + + Громкость + + + Громкость станции + + + Уровень буфера станции + + + Задержка сведений о треке станции, в секундах + + + Отмена + + + Сохранить + + + Мини-плеер Traydio + + + Показать визуализацию + + + Поверх всех окон + + + Скрыть заголовок окна + + + Громкость + + + Другие действия с избранным + + + Очистить историю экспорта + + + Настройки поиска музыки + + + Добавить песню в избранное + + + Нажмите для просмотра сведений + + + Добавить песню в избранное + + + Поиск радиостанций + + + Название станции или фильтры… + + + Фильтры + + + Фильтры + + + Удалить этот фильтр + + + Ошибка поиска + + + Страна, язык или жанр… + + + Найти страну, язык или жанр + + + Подходящие страны, языки и жанры не найдены. + + + Качество и порядок + + + Формат аудио + + + Минимальный битрейт + + + Сортировать по + + + Скрыть неработающие станции + + + Сбросить всё + + + Показать результаты + + + Введите название для поиска станций… + + + Добавить станцию вручную + + + Добавить шум… + + + Добавить музыку с компьютера… + + + Послушать станцию + + + Изменить сведения перед добавлением + + + Добавить станцию + + + Отмена + + + Небольшой и удобный проигрыватель интернет-радио для Windows + + + Открытый каталог тысяч радиостанций со всего мира, который пополняют слушатели + + + Станция не выбрана + + + Любой формат + + + Любой битрейт + + + От 64 кбит/с + + + От 128 кбит/с + + + От 192 кбит/с + + + От 256 кбит/с + + + От 320 кбит/с + + + По оценкам + + + По популярности + + + По названию + + + Случайный порядок + + + Скрыть неработающие станции + + + Не удалось подключиться к каталогу станций. Проверьте подключение к интернету. + + + Только Windows Media Foundation + + + Сначала Windows, затем LibVLC + + + Сначала LibVLC, затем Windows + + + Станции не импортированы. + + + Станции экспортированы. + + + Экспорт отменён или список станций пуст. + + + Журналы пока пусты. + + + Папка журналов открыта. + + + Сохранённых модулей для сброса нет. + + + Диагностика скопирована в буфер обмена. + + + Добавить станцию + + + Добавление станции будет доступно в следующем обновлении. + + + ОК + + + Неизвестная станция + + + Совпадений не найдено + + + Нет связи с radio-browser.info + + + Проверьте подключение и повторите попытку. + + + Обновить также станции, у которых сведения уже заполнены + + + Сведения уже есть у всех станций. Можно проверить каталог ещё раз, чтобы получить изменения. + + + У одной станции не указан жанр или страна. + + + Найти сведения о станциях + + + Найти + + + Отмена + + + Нет станций для проверки + + + Все станции недавно проверены. Повторите попытку через несколько дней. + + + Начинаем… + + + Поиск сведений о станциях + + + Завершаем… + + + Сведения о станциях + + + Поиск остановлен: radio-browser.info перестал отвечать. + + + Поиск остановлен по вашему запросу. Найденные сведения сохранены. + + + Новая группа + + + Название группы + + + Подпись (необязательно) + + + Станция в этой группе тоже будет удалена. Отменить это действие нельзя. + + + Удалить + + + Нет данных для экспорта + + + Сначала добавьте песни в избранное. + + + Экспортировать всё избранное + + + Отмечать экспортированные треки + + + CSV подходит для сервисов переноса плейлистов, например Soundiiz и TuneMyMusic. + + + XSPF подходит для проигрывателей, например VLC. Выберите формат при сохранении. + + + Отмеченные треки остаются в избранном, но пропускаются при следующем экспорте. + + + Экспорт избранного + + + Экспортировать + + + Отмена + + + Нет новых треков для экспорта + + + Все избранные треки уже экспортированы. Включите «Экспортировать всё избранное» или очистите историю экспорта. + + + Экспорт завершён + + + История экспорта очищена + + + Задержка сведений о треке + + + Выключить уведомления о песнях + + + Страна + + + Язык + + + Жанр + + + Одна станция + + + Вручную + + + Название + + + Жанр + + + Страна + + + Недавно добавленные + + + Жанр не указан + + + Страна не указана + + + Вручную + + + Жанр + + + Страна + + + По умолчанию + + + Средний + + + Большой + + + Максимальный + + + Буфер: {0} (для станции) + + + Буфер: {0} (общая настройка) + + + Задержка сведений: {0} (для станции) + + + Задержка сведений: {0} (общая настройка) + + + Формат: {0} + + + Битрейт: {0} + + + Порядок: {0} + + + Ошибка сервера ({0}). Каталог станций временно недоступен. Повторите попытку позже. + + + Ошибка поиска: {0} + + + {0:0} с + + + {0} мин + + + Импортировано станций: {0}. + + + Сброшен выбор модуля для станций: {0}. + + + Станций: {0:N0} + + + Экспортировано {0:dd.MM.yyyy HH:mm} + + + Сортировка: {0}. Группы скрыты, перемещение выключено + + + Группировка: {0}. Перемещение выключено + + + Сейчас играет: {0} + + + Задержка сведений для {0} + + + Общая настройка ({0}) + + + Сейчас: {0} (станция не выбрана) + + + Только новые ({0} из {1}) + + + Всё избранное ({0}) + + + Экспортировано треков: {0}. Файл: {1}. + + + Отмечено для пропуска при следующем экспорте: {0}. + + + В следующий экспорт будет включено треков: {0}. + + + В каталоге radio-browser.info нет станции с такой ссылкой на поток. Добавить сведения не удалось. + + + Обновлено: {0} + + + Станций без жанра или страны: {0}. + + + {0} Traydio запросит сведения у radio-browser.info. Передаются только ссылки на аудиопотоки станций. + + + {0} из {1}. {2} + + + Обновлено станций: {0} из {1}. + + + Не найдено в radio-browser.info: {0}. + + + Найдено несколько совпадений: {0}. Выбраны наиболее подходящие. + + + Оставлено до следующего раза из-за ограничения числа запросов: {0}. + + + Удалить «{0}»? + + + Все станции в группе ({0}) тоже будут удалены. Отменить это действие нельзя. + + + Без задержки + + + 1 минута + + + 1 секунда + + + {0:0} с + + + {0:0.0} с + + + Без группы + + + Удалить из избранного + + + Добавить трек в избранное + + + Новая группа + + + Неизвестная станция + + + Неизвестная станция + + + Дополнительные сведения не найдены. + + + {0} кбит/с {1} + + + {0} кбит/с + + \ No newline at end of file diff --git a/Trdo/Strings/zh-CN/Resources.resw b/Trdo/Strings/zh-CN/Resources.resw index 82564ce..4e240c8 100644 --- a/Trdo/Strings/zh-CN/Resources.resw +++ b/Trdo/Strings/zh-CN/Resources.resw @@ -960,4 +960,7 @@ 欢迎使用 Traydio! + + Русский + diff --git a/Trdo/ViewModels/AboutViewModel.cs b/Trdo/ViewModels/AboutViewModel.cs index c7294e9..57993d7 100644 --- a/Trdo/ViewModels/AboutViewModel.cs +++ b/Trdo/ViewModels/AboutViewModel.cs @@ -1,3 +1,4 @@ +using Trdo.Services; using System; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -29,7 +30,7 @@ public int SelectedRating public bool ShowContactDeveloper => SelectedRating > 0 && SelectedRating <= 3; public string AppName => "Traydio"; - public string AppDescription => "A simple, elegant internet radio player for Windows"; + public string AppDescription => LocalizedText.Get("Ui_AboutViewModel_9a48ce922c", "A simple, elegant internet radio player for Windows"); public string Version { get @@ -48,7 +49,7 @@ public string Version public string RadioBrowserName => "Radio Browser"; public string RadioBrowserUrl => "https://www.radio-browser.info"; public string RadioBrowserDisplayText => "radio-browser.info"; - public string RadioBrowserDescription => "Community-driven internet radio database with thousands of stations worldwide"; + public string RadioBrowserDescription => LocalizedText.Get("Ui_AboutViewModel_5c23df2250", "Community-driven internet radio database with thousands of stations worldwide"); public string WinUIExUrl => "https://github.com/dotMorten/WinUIEx"; public string WinUIExDisplayText => "WinUIEx"; diff --git a/Trdo/ViewModels/AddStationViewModel.cs b/Trdo/ViewModels/AddStationViewModel.cs index 14e7e15..d86c7f0 100644 --- a/Trdo/ViewModels/AddStationViewModel.cs +++ b/Trdo/ViewModels/AddStationViewModel.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using System; using System.ComponentModel; @@ -238,8 +238,8 @@ public double BufferLevel /// collapsed Advanced header so the setting is discoverable without expanding. /// public string BufferSummary => _hasBufferOverride - ? $"Buffer: {BufferLevelDescription} (this station)" - : $"Buffer: {StreamWatchdogService.DescribeBufferLevel(GlobalBufferLevel)} (app setting)"; + ? LocalizedText.Format("Ui_AddStationViewModel_6782fbe204", "Buffer: {0} (this station)", BufferLevelDescription) + : LocalizedText.Format("Ui_AddStationViewModel_68753a79d1", "Buffer: {0} (app setting)", StreamWatchdogService.DescribeBufferLevel(GlobalBufferLevel)); /// /// Whether this station overrides the app-wide track info delay. When false the @@ -290,8 +290,8 @@ public double SongPopupDelaySeconds /// header so the setting is discoverable without expanding. /// public string SongPopupDelaySummary => _hasSongPopupDelayOverride - ? $"Track info delay: {SongPopupDelayDescription} (this station)" - : $"Track info delay: {SongChangeAnnouncementPolicy.DescribeDelay(GlobalSongPopupDelaySeconds)} (app setting)"; + ? LocalizedText.Format("Ui_AddStationViewModel_a3d6447529", "Track info delay: {0} (this station)", SongPopupDelayDescription) + : LocalizedText.Format("Ui_AddStationViewModel_758d997b51", "Track info delay: {0} (app setting)", SongChangeAnnouncementPolicy.DescribeDelay(GlobalSongPopupDelaySeconds)); public bool HasValidationError { diff --git a/Trdo/ViewModels/PlayerViewModel.cs b/Trdo/ViewModels/PlayerViewModel.cs index d466e5a..a8001d4 100644 --- a/Trdo/ViewModels/PlayerViewModel.cs +++ b/Trdo/ViewModels/PlayerViewModel.cs @@ -691,8 +691,8 @@ public async Task RefreshMetadataAsync(CancellationToken cancellationToken = def : Visibility.Collapsed; public string MiniPlayerFavoriteButtonText => IsCurrentTrackFavorited - ? "Remove favorite" - : "Favorite track"; + ? LocalizedText.Get("Ui_PlayerViewModel_5bc0aa08d6", "Remove favorite") + : LocalizedText.Get("Ui_PlayerViewModel_ee96487389", "Favorite track"); public string MiniPlayerFavoriteButtonGlyph => IsCurrentTrackFavorited ? "\uE735" @@ -802,7 +802,7 @@ private void LogAlbumArtChoice(string? trackArtUrl, string? faviconUrl) ? Visibility.Visible : Visibility.Collapsed; - public string SelectedStationDisplayName => SelectedStation?.Name ?? "No station selected"; + public string SelectedStationDisplayName => SelectedStation?.Name ?? LocalizedText.Get("Ui_PlayerViewModel_abdae1e25b", "No station selected"); public bool IsSpotifyEnabled => SettingsService.IsSpotifyEnabled; @@ -927,7 +927,7 @@ public void Toggle() } catch (Exception ex) { - string stationName = _selectedStation?.Name ?? "Unknown"; + string stationName = _selectedStation?.Name ?? LocalizedText.Get("Ui_PlayerViewModel_b764cdc0ea", "Unknown"); _lastError = string.Format( LocalizationService.GetString("PlayerViewModel_FailedToPlay", "Failed to play {0}: {1}"), stationName, ex.Message); @@ -959,7 +959,7 @@ public void Pause() } catch (Exception ex) { - string stationName = _selectedStation?.Name ?? "Unknown"; + string stationName = _selectedStation?.Name ?? LocalizedText.Get("Ui_PlayerViewModel_b764cdc0ea", "Unknown"); _lastError = string.Format( LocalizationService.GetString("PlayerViewModel_FailedToPause", "Failed to pause {0}: {1}"), stationName, ex.Message); @@ -988,7 +988,7 @@ private set : TimeSpan.Zero; /// The countdown control's label, rounded up so it reads "1m" until the timer actually elapses. - public string SleepTimerDisplayText => $"{Math.Max((int)Math.Ceiling(SleepTimerRemaining.TotalMinutes), 1)}m"; + public string SleepTimerDisplayText => LocalizedText.Format("Ui_PlayerViewModel_7f5ea98385", "{0}m", Math.Max((int)Math.Ceiling(SleepTimerRemaining.TotalMinutes), 1)); /// Fraction of the chosen duration still remaining, for the countdown ring. public double SleepTimerProgress => _sleepTimerDuration > TimeSpan.Zero @@ -1189,7 +1189,7 @@ public void ToggleCurrentTrackFavorite() if (CurrentMetadata?.HasMetadata != true) return; - string stationName = SelectedStation?.Name ?? "Unknown Station"; + string stationName = SelectedStation?.Name ?? LocalizedText.Get("Ui_PlayerViewModel_c647094d1f", "Unknown Station"); IsCurrentTrackFavorited = _favoritesService.ToggleFavorite(CurrentMetadata, stationName); } @@ -1275,7 +1275,7 @@ public void RestoreSelectedStationPlaybackTarget() } catch (Exception ex) { - string stationName = _selectedStation?.Name ?? "Unknown"; + string stationName = _selectedStation?.Name ?? LocalizedText.Get("Ui_PlayerViewModel_b764cdc0ea", "Unknown"); _lastError = string.Format( LocalizationService.GetString("PlayerViewModel_FailedToRestore", "Failed to restore {0}: {1}"), stationName, ex.Message); @@ -1345,7 +1345,7 @@ public StationGroup AddStationsToNewFolder(string folderName, IReadOnlyList Codecs { get; } = [ - new("Any codec", string.Empty), + new(LocalizedText.Get("Ui_SearchStationViewModel_30fb688956", "Any codec"), string.Empty), new("MP3", "MP3"), new("AAC", "AAC"), new("AAC+", "AAC+"), @@ -91,20 +91,20 @@ public SearchStationViewModel() public IReadOnlyList Bitrates { get; } = [ - new("Any bitrate", 0), - new("64 kbps+", 64), - new("128 kbps+", 128), - new("192 kbps+", 192), - new("256 kbps+", 256), - new("320 kbps+", 320), + new(LocalizedText.Get("Ui_SearchStationViewModel_ad7098d679", "Any bitrate"), 0), + new(LocalizedText.Get("Ui_SearchStationViewModel_1d74528823", "64 kbps+"), 64), + new(LocalizedText.Get("Ui_SearchStationViewModel_8c36825952", "128 kbps+"), 128), + new(LocalizedText.Get("Ui_SearchStationViewModel_085a07bf8e", "192 kbps+"), 192), + new(LocalizedText.Get("Ui_SearchStationViewModel_b37b0bde4c", "256 kbps+"), 256), + new(LocalizedText.Get("Ui_SearchStationViewModel_f0790b9846", "320 kbps+"), 320), ]; public IReadOnlyList SortOptions { get; } = [ - new("Most voted", "votes", true), - new("Most popular", "clickcount", true), - new("Name (A–Z)", "name", false), - new("Random", "random", false), + new(LocalizedText.Get("Ui_SearchStationViewModel_e5a1fae87d", "Most voted"), "votes", true), + new(LocalizedText.Get("Ui_SearchStationViewModel_ad2ea94850", "Most popular"), "clickcount", true), + new(LocalizedText.Get("Ui_SearchStationViewModel_4e2d9e7be4", "Name (A–Z)"), "name", false), + new(LocalizedText.Get("Ui_SearchStationViewModel_67bc484430", "Random"), "random", false), ]; // Search box and status --------------------------------------------- @@ -436,16 +436,16 @@ private void RefreshFilterChips() } if (!string.IsNullOrEmpty(SelectedCodec?.Value)) - FilterChips.Add(new QualityFilterChip(QualityChipKind.Codec, $"Codec: {SelectedCodec!.Display}")); + FilterChips.Add(new QualityFilterChip(QualityChipKind.Codec, LocalizedText.Format("Ui_SearchStationViewModel_0c4d21bd30", "Codec: {0}", SelectedCodec!.Display))); if ((SelectedBitrate?.Value ?? 0) > 0) - FilterChips.Add(new QualityFilterChip(QualityChipKind.Bitrate, $"Bitrate: {SelectedBitrate!.Display}")); + FilterChips.Add(new QualityFilterChip(QualityChipKind.Bitrate, LocalizedText.Format("Ui_SearchStationViewModel_c18962622a", "Bitrate: {0}", SelectedBitrate!.Display))); if (!Equals(SelectedSort, SortOptions[0])) - FilterChips.Add(new QualityFilterChip(QualityChipKind.Sort, $"Sort: {SelectedSort.Display}")); + FilterChips.Add(new QualityFilterChip(QualityChipKind.Sort, LocalizedText.Format("Ui_SearchStationViewModel_4d9eaf057a", "Sort: {0}", SelectedSort.Display))); if (HideBroken) - FilterChips.Add(new QualityFilterChip(QualityChipKind.HideBroken, "Hide broken stations")); + FilterChips.Add(new QualityFilterChip(QualityChipKind.HideBroken, LocalizedText.Get("Ui_SearchStationViewModel_49803a994d", "Hide broken stations"))); } private async Task PerformSearchAsync() @@ -505,14 +505,14 @@ private async Task PerformSearchAsync() Debug.WriteLine($"[SearchStationViewModel] Network error: {ex.Message}"); HasError = true; ErrorMessage = ex.StatusCode.HasValue - ? $"Server error ({(int)ex.StatusCode}): The radio station service is temporarily unavailable. Please try again later." - : "Network error: Unable to reach the radio station service. Please check your internet connection."; + ? LocalizedText.Format("Ui_SearchStationViewModel_3c5849e57a", "Server error ({0}): The radio station service is temporarily unavailable. Please try again later.", (int)ex.StatusCode) + : LocalizedText.Get("Ui_SearchStationViewModel_5b669f84ab", "Network error: Unable to reach the radio station service. Please check your internet connection."); } catch (Exception ex) { Debug.WriteLine($"[SearchStationViewModel] Search error: {ex.Message}"); HasError = true; - ErrorMessage = $"An error occurred while searching: {ex.Message}"; + ErrorMessage = LocalizedText.Format("Ui_SearchStationViewModel_df927f60b9", "An error occurred while searching: {0}", ex.Message); } finally { diff --git a/Trdo/ViewModels/SettingsViewModel.cs b/Trdo/ViewModels/SettingsViewModel.cs index 6e3c760..a6cd41d 100644 --- a/Trdo/ViewModels/SettingsViewModel.cs +++ b/Trdo/ViewModels/SettingsViewModel.cs @@ -516,9 +516,9 @@ public int PlaybackEngineModeIndex public string PlaybackEngineModeDescription => SettingsService.PlaybackEngineMode switch { - PlaybackEngineMode.NativeOnly => "Windows Media Foundation only", - PlaybackEngineMode.NativePreferred => "Windows first, LibVLC fallback", - _ => "LibVLC first, Windows fallback" + PlaybackEngineMode.NativeOnly => LocalizedText.Get("Ui_SettingsViewModel_3a7c5faee8", "Windows Media Foundation only"), + PlaybackEngineMode.NativePreferred => LocalizedText.Get("Ui_SettingsViewModel_806ed9fe76", "Windows first, LibVLC fallback"), + _ => LocalizedText.Get("Ui_SettingsViewModel_fca8b53a50", "LibVLC first, Windows fallback") }; public bool AllowSleepWhilePlaying @@ -628,7 +628,7 @@ public double SilenceTimeoutSeconds /// /// Gets a formatted display string for the current silence timeout value. /// - public string SilenceTimeoutDisplay => $"{SilenceTimeoutSeconds:0}s"; + public string SilenceTimeoutDisplay => LocalizedText.Format("Ui_SettingsViewModel_2054bec45c", "{0:0}s", SilenceTimeoutSeconds); private async Task InitializeStartupTaskAsync() {