From 12d98e9ab06f2e37f5fdb9a9d860b457da088995 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:49:32 +0200 Subject: [PATCH] Add localization (#1482) Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- DiscordChatExporter.Gui/App.axaml.cs | 4 + .../Converters/MarkdownToInlinesConverter.cs | 176 ++++++++++++++ .../DiscordChatExporter.Gui.csproj | 1 + .../Localization/Language.cs | 11 + .../LocalizationManager.English.cs | 151 ++++++++++++ .../LocalizationManager.French.cs | 153 ++++++++++++ .../LocalizationManager.German.cs | 157 +++++++++++++ .../LocalizationManager.Spanish.cs | 151 ++++++++++++ .../LocalizationManager.Ukrainian.cs | 150 ++++++++++++ .../Localization/LocalizationManager.cs | 170 ++++++++++++++ .../Services/SettingsService.cs | 4 + .../Utils/Extensions/MarkdigExtensions.cs | 19 ++ .../Components/DashboardViewModel.cs | 28 +-- .../Dialogs/ExportSetupViewModel.cs | 6 +- .../ViewModels/Dialogs/SettingsViewModel.cs | 17 +- .../ViewModels/MainViewModel.cs | 44 ++-- .../Views/Components/DashboardView.axaml | 222 +++++------------- .../Views/Controls/HyperLink.axaml.cs | 32 ++- .../Views/Dialogs/ExportSetupView.axaml | 112 +++------ .../Views/Dialogs/SettingsView.axaml | 49 ++-- 20 files changed, 1346 insertions(+), 311 deletions(-) create mode 100644 DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs create mode 100644 DiscordChatExporter.Gui/Localization/Language.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.English.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.French.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.German.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.Spanish.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.Ukrainian.cs create mode 100644 DiscordChatExporter.Gui/Localization/LocalizationManager.cs create mode 100644 DiscordChatExporter.Gui/Utils/Extensions/MarkdigExtensions.cs diff --git a/DiscordChatExporter.Gui/App.axaml.cs b/DiscordChatExporter.Gui/App.axaml.cs index 6fb013d2..0673e3f4 100644 --- a/DiscordChatExporter.Gui/App.axaml.cs +++ b/DiscordChatExporter.Gui/App.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Platform; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Utils; using DiscordChatExporter.Gui.Utils.Extensions; @@ -39,6 +40,9 @@ public class App : Application, IDisposable services.AddSingleton(); services.AddSingleton(); + // Localization + services.AddSingleton(); + // View models services.AddTransient(); services.AddTransient(); diff --git a/DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs b/DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs new file mode 100644 index 00000000..cb740fee --- /dev/null +++ b/DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs @@ -0,0 +1,176 @@ +using System; +using System.Globalization; +using System.Linq; +using Avalonia.Controls.Documents; +using Avalonia.Data.Converters; +using Avalonia.Layout; +using Avalonia.Media; +using DiscordChatExporter.Gui.Utils.Extensions; +using DiscordChatExporter.Gui.Views.Controls; +using Markdig; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using MarkdownInline = Markdig.Syntax.Inlines.Inline; + +namespace DiscordChatExporter.Gui.Converters; + +public class MarkdownToInlinesConverter : IValueConverter +{ + public static readonly MarkdownToInlinesConverter Instance = new(); + + private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder() + .UseEmphasisExtras() + .Build(); + + private static void ProcessInline( + InlineCollection inlines, + MarkdownInline markdownInline, + FontWeight? fontWeight = null, + FontStyle? fontStyle = null, + TextDecorationCollection? textDecorations = null + ) + { + switch (markdownInline) + { + case LiteralInline literal: + { + var run = new Run(literal.Content.ToString()); + + if (fontWeight is not null) + run.FontWeight = fontWeight.Value; + if (fontStyle is not null) + run.FontStyle = fontStyle.Value; + if (textDecorations is not null) + run.TextDecorations = textDecorations; + + inlines.Add(run); + break; + } + + case LineBreakInline: + { + inlines.Add(new LineBreak()); + break; + } + + case EmphasisInline emphasis: + { + var newWeight = fontWeight; + var newStyle = fontStyle; + var newDecorations = textDecorations; + + switch (emphasis.DelimiterChar) + { + case '*' or '_' when emphasis.DelimiterCount == 2: + newWeight = FontWeight.SemiBold; + break; + case '*' or '_': + newStyle = FontStyle.Italic; + break; + case '~': + newDecorations = TextDecorations.Strikethrough; + break; + case '+': + newDecorations = TextDecorations.Underline; + break; + } + + foreach (var child in emphasis) + ProcessInline(inlines, child, newWeight, newStyle, newDecorations); + + break; + } + + case LinkInline link: + { + inlines.Add( + new InlineUIContainer( + new HyperLink + { + Text = link.GetInnerText(), + Url = link.Url, + VerticalAlignment = VerticalAlignment.Bottom, + } + ) + ); + + break; + } + + case ContainerInline container: + { + foreach (var child in container) + ProcessInline(inlines, child, fontWeight, fontStyle, textDecorations); + + break; + } + } + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var inlines = new InlineCollection(); + if (value is not string { Length: > 0 } text) + return inlines; + + var isFirst = true; + + foreach (var block in Markdown.Parse(text, MarkdownPipeline)) + { + switch (block) + { + case ParagraphBlock { Inline: not null } paragraph: + { + if (!isFirst) + { + // Insert a blank line between paragraphs + inlines.Add(new LineBreak()); + inlines.Add(new LineBreak()); + } + + isFirst = false; + + foreach (var markdownInline in paragraph.Inline!) + ProcessInline(inlines, markdownInline); + + break; + } + + case ListBlock list: + { + var itemOrder = 1; + if (list.IsOrdered && int.TryParse(list.OrderedStart, out var startNum)) + itemOrder = startNum; + + foreach (var listItem in list.OfType()) + { + if (!isFirst) + inlines.Add(new LineBreak()); + isFirst = false; + + var prefix = list.IsOrdered ? $"{itemOrder++}. " : $"{list.BulletType} "; + inlines.Add(new Run(prefix)); + + foreach (var subBlock in listItem.OfType()) + { + if (subBlock is { Inline: not null } p) + foreach (var markdownInline in p.Inline) + ProcessInline(inlines, markdownInline); + } + } + + break; + } + } + } + + return inlines; + } + + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) => throw new NotSupportedException(); +} diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index d52770a3..c2bca8c4 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -37,6 +37,7 @@ + diff --git a/DiscordChatExporter.Gui/Localization/Language.cs b/DiscordChatExporter.Gui/Localization/Language.cs new file mode 100644 index 00000000..faebe5bf --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/Language.cs @@ -0,0 +1,11 @@ +namespace DiscordChatExporter.Gui.Localization; + +public enum Language +{ + System, + English, + Ukrainian, + German, + French, + Spanish, +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.English.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.English.cs new file mode 100644 index 00000000..9cb79162 --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.English.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager +{ + private static readonly IReadOnlyDictionary EnglishLocalization = + new Dictionary + { + // Dashboard + [nameof(PullGuildsTooltip)] = "Pull available servers and channels (Enter)", + [nameof(SettingsTooltip)] = "Settings", + [nameof(LastMessageSentTooltip)] = "Last message sent:", + [nameof(TokenWatermark)] = "Token", + // Token instructions (personal account) + [nameof(TokenPersonalHeader)] = "To get the token for your personal account:", + [nameof(TokenPersonalTosWarning)] = + "* Automating user accounts is technically against TOS — **use at your own risk**!", + [nameof(TokenPersonalInstructions)] = """ + 1. Open Discord in your web browser and login + 2. Open any server or direct message channel + 3. Press **Ctrl+Shift+I** to show developer tools + 4. Navigate to the **Network** tab + 5. Press **Ctrl+R** to reload + 6. Switch between random channels to trigger network requests + 7. Search for a request that starts with **messages** + 8. Select the **Headers** tab on the right + 9. Scroll down to the **Request Headers** section + 10. Copy the value of the **authorization** header + """, + // Token instructions (bot) + [nameof(TokenBotHeader)] = "To get the token for your bot:", + [nameof(TokenBotInstructions)] = """ + The token is generated during bot creation. If you lost it, generate a new one: + + 1. Open Discord [developer portal](https://discord.com/developers/applications) + 2. Open your application's settings + 3. Navigate to the **Bot** section on the left + 4. Under **Token** click **Reset Token** + 5. Click **Yes, do it!** and authenticate to confirm + * Integrations using the previous token will stop working until updated + * Your bot needs to have the **Message Content Intent** enabled to read messages + """, + [nameof(TokenHelpText)] = + "If you have questions or issues, please refer to the [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)", + // Settings + [nameof(SettingsTitle)] = "Settings", + [nameof(ThemeLabel)] = "Theme", + [nameof(ThemeTooltip)] = "Preferred user interface theme", + [nameof(LanguageLabel)] = "Language", + [nameof(LanguageTooltip)] = "Preferred user interface language", + [nameof(AutoUpdateLabel)] = "Auto-update", + [nameof(AutoUpdateTooltip)] = "Perform automatic updates on every launch", + [nameof(PersistTokenLabel)] = "Persist token", + [nameof(PersistTokenTooltip)] = + "Save the last used token to a file so that it can be persisted between sessions", + [nameof(RateLimitPreferenceLabel)] = "Rate limit preference", + [nameof(RateLimitPreferenceTooltip)] = + "Whether to respect advisory rate limits. If disabled, only hard rate limits (i.e. 429 responses) will be respected.", + [nameof(ShowThreadsLabel)] = "Show threads", + [nameof(ShowThreadsTooltip)] = "Which types of threads to show in the channel list", + [nameof(LocaleLabel)] = "Locale", + [nameof(LocaleTooltip)] = "Locale to use when formatting dates and numbers", + [nameof(NormalizeToUtcLabel)] = "Normalize to UTC", + [nameof(NormalizeToUtcTooltip)] = "Normalize all timestamps to UTC+0", + [nameof(ParallelLimitLabel)] = "Parallel limit", + [nameof(ParallelLimitTooltip)] = "How many channels can be exported at the same time", + // Export Setup + [nameof(ChannelsSelectedText)] = "channels selected", + [nameof(OutputPathLabel)] = "Output path", + [nameof(OutputPathTooltip)] = """ + Output file or directory path. + + If a directory is specified, file names will be generated automatically based on the channel names and export parameters. + + Directory paths must end with a slash to avoid ambiguity. + + Available template tokens: + - **%g** — server ID + - **%G** — server name + - **%t** — category ID + - **%T** — category name + - **%c** — channel ID + - **%C** — channel name + - **%p** — channel position + - **%P** — category position + - **%a** — after date + - **%b** — before date + - **%d** — current date + """, + [nameof(FormatLabel)] = "Format", + [nameof(FormatTooltip)] = "Export format", + [nameof(AfterDateLabel)] = "After (date)", + [nameof(AfterDateTooltip)] = "Only include messages sent after this date", + [nameof(BeforeDateLabel)] = "Before (date)", + [nameof(BeforeDateTooltip)] = "Only include messages sent before this date", + [nameof(AfterTimeLabel)] = "After (time)", + [nameof(AfterTimeTooltip)] = "Only include messages sent after this time", + [nameof(BeforeTimeLabel)] = "Before (time)", + [nameof(BeforeTimeTooltip)] = "Only include messages sent before this time", + [nameof(PartitionLimitLabel)] = "Partition limit", + [nameof(PartitionLimitTooltip)] = + "Split the output into partitions, each limited to the specified number of messages (e.g. '100') or file size (e.g. '10mb')", + [nameof(MessageFilterLabel)] = "Message filter", + [nameof(MessageFilterTooltip)] = + "Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image'). See the documentation for more info.", + [nameof(FormatMarkdownLabel)] = "Format markdown", + [nameof(FormatMarkdownTooltip)] = + "Process markdown, mentions, and other special tokens", + [nameof(DownloadAssetsLabel)] = "Download assets", + [nameof(DownloadAssetsTooltip)] = + "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)", + [nameof(ReuseAssetsLabel)] = "Reuse assets", + [nameof(ReuseAssetsTooltip)] = + "Reuse previously downloaded assets to avoid redundant requests", + [nameof(AssetsDirPathLabel)] = "Assets directory path", + [nameof(AssetsDirPathTooltip)] = + "Download assets to this directory. If not specified, the asset directory path will be derived from the output path.", + [nameof(AdvancedOptionsTooltip)] = "Toggle advanced options", + [nameof(ExportButton)] = "EXPORT", + // Common buttons + [nameof(CloseButton)] = "CLOSE", + [nameof(CancelButton)] = "CANCEL", + // Dialog messages + [nameof(UkraineSupportTitle)] = "Thank you for supporting Ukraine!", + [nameof(UkraineSupportMessage)] = """ + As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom. + + Click LEARN MORE to find ways that you can help. + """, + [nameof(LearnMoreButton)] = "LEARN MORE", + [nameof(UnstableBuildTitle)] = "Unstable build warning", + [nameof(UnstableBuildMessage)] = """ + You're using a development build of {0}. These builds are not thoroughly tested and may contain bugs. + + Auto-updates are disabled for development builds. + + Click SEE RELEASES if you want to download a stable release instead. + """, + [nameof(SeeReleasesButton)] = "SEE RELEASES", + [nameof(UpdateDownloadingMessage)] = "Downloading update to {0} v{1}...", + [nameof(UpdateReadyMessage)] = + "Update has been downloaded and will be installed when you exit", + [nameof(UpdateInstallNowButton)] = "INSTALL NOW", + [nameof(UpdateFailedMessage)] = "Failed to perform application update", + [nameof(ErrorPullingGuildsTitle)] = "Error pulling servers", + [nameof(ErrorPullingChannelsTitle)] = "Error pulling channels", + [nameof(ErrorExportingTitle)] = "Error exporting channel(s)", + [nameof(SuccessfulExportMessage)] = "Successfully exported {0} channel(s)", + }; +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.French.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.French.cs new file mode 100644 index 00000000..3a61de17 --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.French.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager +{ + private static readonly IReadOnlyDictionary FrenchLocalization = new Dictionary< + string, + string + > + { + // Dashboard + [nameof(PullGuildsTooltip)] = "Charger les serveurs et canaux disponibles (Entrée)", + [nameof(SettingsTooltip)] = "Paramètres", + [nameof(LastMessageSentTooltip)] = "Dernier message envoyé :", + [nameof(TokenWatermark)] = "Token", + // Token instructions (personal account) + [nameof(TokenPersonalHeader)] = "Obtenir le token pour votre compte personnel :", + [nameof(TokenPersonalTosWarning)] = + "* L'automatisation des comptes est techniquement contraire aux CGU — **à vos risques et périls**!", + [nameof(TokenPersonalInstructions)] = """ + 1. Ouvrez Discord dans votre navigateur web et connectez-vous + 2. Ouvrez n'importe quel serveur ou canal de message direct + 3. Appuyez sur **Ctrl+Shift+I** pour afficher les outils de développement + 4. Naviguez vers l'onglet **Network** + 5. Appuyez sur **Ctrl+R** pour recharger + 6. Changez de canal pour déclencher des requêtes réseau + 7. Cherchez une requête commençant par **messages** + 8. Sélectionnez l'onglet **Headers** à droite + 9. Faites défiler jusqu'à la section **Request Headers** + 10. Copiez la valeur de l'en-tête **authorization** + """, + // Token instructions (bot) + [nameof(TokenBotHeader)] = "Obtenir le token pour votre bot :", + [nameof(TokenBotInstructions)] = """ + Le token est généré lors de la création du bot. Si vous l'avez perdu, générez-en un nouveau : + + 1. Ouvrez Discord [portail développeur](https://discord.com/developers/applications) + 2. Ouvrez les paramètres de votre application + 3. Naviguez vers la section **Bot** à gauche + 4. Sous **Token**, cliquez sur **Reset Token** + 5. Cliquez sur **Yes, do it!** et confirmez + * Les intégrations utilisant l'ancien token cesseront de fonctionner jusqu'à leur mise à jour + * Votre bot doit avoir l'option **Message Content Intent** activée pour lire les messages + """, + [nameof(TokenHelpText)] = + "Pour les questions ou problèmes, veuillez consulter la [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)", + // Settings + [nameof(SettingsTitle)] = "Paramètres", + [nameof(ThemeLabel)] = "Thème", + [nameof(ThemeTooltip)] = "Thème d'interface préféré", + [nameof(LanguageLabel)] = "Langue", + [nameof(LanguageTooltip)] = "Langue d'interface préférée", + [nameof(AutoUpdateLabel)] = "Mise à jour automatique", + [nameof(AutoUpdateTooltip)] = "Effectuer des mises à jour automatiques à chaque lancement", + [nameof(PersistTokenLabel)] = "Conserver le token", + [nameof(PersistTokenTooltip)] = + "Enregistrer le dernier token utilisé dans un fichier pour le conserver entre les sessions", + [nameof(RateLimitPreferenceLabel)] = "Préférence de limite de débit", + [nameof(RateLimitPreferenceTooltip)] = + "Indique s'il faut respecter les limites de débit recommandées. Si désactivé, seules les limites strictes (réponses 429) seront respectées.", + [nameof(ShowThreadsLabel)] = "Afficher les fils", + [nameof(ShowThreadsTooltip)] = "Quels types de fils afficher dans la liste des canaux", + [nameof(LocaleLabel)] = "Locale", + [nameof(LocaleTooltip)] = "Locale à utiliser pour le formatage des dates et des nombres", + [nameof(NormalizeToUtcLabel)] = "Normaliser en UTC", + [nameof(NormalizeToUtcTooltip)] = "Normaliser tous les horodatages en UTC+0", + [nameof(ParallelLimitLabel)] = "Limite parallèle", + [nameof(ParallelLimitTooltip)] = "Combien de canaux peuvent être exportés simultanément", + // Export Setup + [nameof(ChannelsSelectedText)] = "canaux sélectionnés", + [nameof(OutputPathLabel)] = "Chemin de sortie", + [nameof(OutputPathTooltip)] = """ + Chemin du fichier ou répertoire de sortie. + + Si un répertoire est spécifié, les noms de fichiers seront générés automatiquement en fonction des noms de canaux et des paramètres d'exportation. + + Les chemins de répertoire doivent se terminer par un slash pour éviter toute ambiguïté. + + Jetons de modèle disponibles : + - **%g** — ID du serveur + - **%G** — nom du serveur + - **%t** — ID de la catégorie + - **%T** — nom de la catégorie + - **%c** — ID du canal + - **%C** — nom du canal + - **%p** — position du canal + - **%P** — position de la catégorie + - **%a** — date après + - **%b** — date avant + - **%d** — date actuelle + """, + [nameof(FormatLabel)] = "Format", + [nameof(FormatTooltip)] = "Format d'exportation", + [nameof(AfterDateLabel)] = "Après (date)", + [nameof(AfterDateTooltip)] = "Inclure uniquement les messages envoyés après cette date", + [nameof(BeforeDateLabel)] = "Avant (date)", + [nameof(BeforeDateTooltip)] = "Inclure uniquement les messages envoyés avant cette date", + [nameof(AfterTimeLabel)] = "Après (heure)", + [nameof(AfterTimeTooltip)] = "Inclure uniquement les messages envoyés après cette heure", + [nameof(BeforeTimeLabel)] = "Avant (heure)", + [nameof(BeforeTimeTooltip)] = "Inclure uniquement les messages envoyés avant cette heure", + [nameof(PartitionLimitLabel)] = "Limite de partition", + [nameof(PartitionLimitTooltip)] = + "Diviser la sortie en partitions, chacune limitée au nombre de messages spécifié (ex. '100') ou à la taille de fichier (ex. '10mb')", + [nameof(MessageFilterLabel)] = "Filtre de messages", + [nameof(MessageFilterTooltip)] = + "Inclure uniquement les messages satisfaisant ce filtre (ex. 'from:foo#1234' ou 'has:image'). Voir la documentation pour plus d'informations.", + [nameof(FormatMarkdownLabel)] = "Formater le markdown", + [nameof(FormatMarkdownTooltip)] = + "Traiter le markdown, les mentions et autres tokens spéciaux", + [nameof(DownloadAssetsLabel)] = "Télécharger les ressources", + [nameof(DownloadAssetsTooltip)] = + "Télécharger les ressources référencées par l'export (avatars, fichiers joints, images intégrées, etc.)", + [nameof(ReuseAssetsLabel)] = "Réutiliser les ressources", + [nameof(ReuseAssetsTooltip)] = + "Réutiliser les ressources précédemment téléchargées pour éviter les requêtes redondantes", + [nameof(AssetsDirPathLabel)] = "Chemin du dossier des ressources", + [nameof(AssetsDirPathTooltip)] = + "Télécharger les ressources dans ce dossier. Si non spécifié, le chemin sera dérivé du chemin de sortie.", + [nameof(AdvancedOptionsTooltip)] = "Basculer les options avancées", + [nameof(ExportButton)] = "EXPORTER", + // Common buttons + [nameof(CloseButton)] = "FERMER", + [nameof(CancelButton)] = "ANNULER", + // Dialog messages + [nameof(UkraineSupportTitle)] = "Merci de soutenir l'Ukraine !", + [nameof(UkraineSupportMessage)] = """ + Alors que la Russie mène une guerre génocidaire contre mon pays, je suis reconnaissant envers tous ceux qui continuent à soutenir l'Ukraine dans notre lutte pour la liberté. + + Cliquez sur EN SAVOIR PLUS pour trouver des moyens d'aider. + """, + [nameof(LearnMoreButton)] = "EN SAVOIR PLUS", + [nameof(UnstableBuildTitle)] = "Avertissement : version instable", + [nameof(UnstableBuildMessage)] = """ + Vous utilisez une version de développement de {0}. Ces versions ne sont pas rigoureusement testées et peuvent contenir des bugs. + + Les mises à jour automatiques sont désactivées pour les versions de développement. + + Cliquez sur VOIR LES VERSIONS pour télécharger une version stable. + """, + [nameof(SeeReleasesButton)] = "VOIR LES VERSIONS", + [nameof(UpdateDownloadingMessage)] = "Téléchargement de la mise à jour vers {0} v{1}...", + [nameof(UpdateReadyMessage)] = + "La mise à jour a été téléchargée et sera installée à la fermeture", + [nameof(UpdateInstallNowButton)] = "INSTALLER MAINTENANT", + [nameof(UpdateFailedMessage)] = "Échec de la mise à jour de l'application", + [nameof(ErrorPullingGuildsTitle)] = "Erreur lors du chargement des serveurs", + [nameof(ErrorPullingChannelsTitle)] = "Erreur lors du chargement des canaux", + [nameof(ErrorExportingTitle)] = "Erreur lors de l'exportation des canaux", + [nameof(SuccessfulExportMessage)] = "{0} canal(-aux) exporté(s) avec succès", + }; +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.German.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.German.cs new file mode 100644 index 00000000..fd3f1439 --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.German.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager +{ + private static readonly IReadOnlyDictionary GermanLocalization = new Dictionary< + string, + string + > + { + // Dashboard + [nameof(PullGuildsTooltip)] = "Verfügbare Server und Kanäle laden (Enter)", + [nameof(SettingsTooltip)] = "Einstellungen", + [nameof(LastMessageSentTooltip)] = "Letzte Nachricht gesendet:", + [nameof(TokenWatermark)] = "Token", + // Token instructions (personal account) + [nameof(TokenPersonalHeader)] = "Token für Ihr persönliches Konto abrufen:", + [nameof(TokenPersonalTosWarning)] = + "* Das Automatisieren von Benutzerkonten verstößt technisch gegen die AGB — **auf eigene Gefahr**!", + [nameof(TokenPersonalInstructions)] = """ + 1. Öffnen Sie Discord in Ihrem Webbrowser und melden Sie sich an + 2. Öffnen Sie einen Server oder einen direkten Nachrichtenkanal + 3. Drücken Sie **Ctrl+Shift+I**, um die Entwicklertools anzuzeigen + 4. Navigieren Sie zum Reiter **Network** + 5. Drücken Sie **Ctrl+R** zum Neuladen + 6. Wechseln Sie zwischen Kanälen, um Netzwerkanfragen auszulösen + 7. Suchen Sie nach einer Anfrage, die mit **messages** beginnt + 8. Wählen Sie den Reiter **Headers** auf der rechten Seite + 9. Scrollen Sie nach unten zum Abschnitt **Request Headers** + 10. Kopieren Sie den Wert des Headers **authorization** + """, + // Token instructions (bot) + [nameof(TokenBotHeader)] = "Token für Ihren Bot abrufen:", + [nameof(TokenBotInstructions)] = """ + Der Token wird bei der Bot-Erstellung generiert. Falls er verloren gegangen ist, generieren Sie einen neuen: + + 1. Öffnen Sie Discord [Entwicklerportal](https://discord.com/developers/applications) + 2. Öffnen Sie die Einstellungen Ihrer Anwendung + 3. Navigieren Sie zum Abschnitt **Bot** auf der linken Seite + 4. Klicken Sie unter **Token** auf **Reset Token** + 5. Klicken Sie auf **Yes, do it!** und bestätigen Sie + * Integrationen, die den alten Token verwenden, hören auf zu funktionieren, bis sie aktualisiert werden + * Ihr Bot benötigt die aktivierte **Message Content Intent**, um Nachrichten zu lesen + """, + [nameof(TokenHelpText)] = + "Bei Fragen oder Problemen lesen Sie die [Dokumentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)", + // Settings + [nameof(SettingsTitle)] = "Einstellungen", + [nameof(ThemeLabel)] = "Design", + [nameof(ThemeTooltip)] = "Bevorzugtes Oberflächendesign", + [nameof(LanguageLabel)] = "Sprache", + [nameof(LanguageTooltip)] = "Bevorzugte Sprache der Benutzeroberfläche", + [nameof(AutoUpdateLabel)] = "Automatische Updates", + [nameof(AutoUpdateTooltip)] = "Automatische Updates bei jedem Start durchführen", + [nameof(PersistTokenLabel)] = "Token speichern", + [nameof(PersistTokenTooltip)] = + "Den zuletzt verwendeten Token in einer Datei speichern, damit er zwischen Sitzungen erhalten bleibt", + [nameof(RateLimitPreferenceLabel)] = "Ratenlimit-Einstellung", + [nameof(RateLimitPreferenceTooltip)] = + "Ob empfohlene Ratenlimits eingehalten werden sollen. Wenn deaktiviert, werden nur harte Ratenlimits (d. h. 429-Antworten) eingehalten.", + [nameof(ShowThreadsLabel)] = "Threads anzeigen", + [nameof(ShowThreadsTooltip)] = "Welche Thread-Typen in der Kanalliste angezeigt werden", + [nameof(LocaleLabel)] = "Gebietsschema", + [nameof(LocaleTooltip)] = "Gebietsschema für die Formatierung von Daten und Zahlen", + [nameof(NormalizeToUtcLabel)] = "Auf UTC normalisieren", + [nameof(NormalizeToUtcTooltip)] = "Alle Zeitstempel auf UTC+0 normalisieren", + [nameof(ParallelLimitLabel)] = "Paralleles Limit", + [nameof(ParallelLimitTooltip)] = "Wie viele Kanäle gleichzeitig exportiert werden können", + // Export Setup + [nameof(ChannelsSelectedText)] = "Kanäle ausgewählt", + [nameof(OutputPathLabel)] = "Ausgabepfad", + [nameof(OutputPathTooltip)] = """ + Ausgabedatei- oder Verzeichnispfad. + + Wenn ein Verzeichnis angegeben wird, werden Dateinamen automatisch basierend auf den Kanalnamen und Exportparametern generiert. + + Verzeichnispfade müssen mit einem Schrägstrich enden, um Mehrdeutigkeiten zu vermeiden. + + Verfügbare Vorlagen-Token: + - **%g** — Server-ID + - **%G** — Servername + - **%t** — Kategorie-ID + - **%T** — Kategoriename + - **%c** — Kanal-ID + - **%C** — Kanalname + - **%p** — Kanalposition + - **%P** — Kategorieposition + - **%a** — Datum ab + - **%b** — Datum bis + - **%d** — aktuelles Datum + """, + [nameof(FormatLabel)] = "Format", + [nameof(FormatTooltip)] = "Exportformat", + [nameof(AfterDateLabel)] = "Nach (Datum)", + [nameof(AfterDateTooltip)] = + "Nur Nachrichten einschließen, die nach diesem Datum gesendet wurden", + [nameof(BeforeDateLabel)] = "Vor (Datum)", + [nameof(BeforeDateTooltip)] = + "Nur Nachrichten einschließen, die vor diesem Datum gesendet wurden", + [nameof(AfterTimeLabel)] = "Nach (Uhrzeit)", + [nameof(AfterTimeTooltip)] = + "Nur Nachrichten einschließen, die nach dieser Uhrzeit gesendet wurden", + [nameof(BeforeTimeLabel)] = "Vor (Uhrzeit)", + [nameof(BeforeTimeTooltip)] = + "Nur Nachrichten einschließen, die vor dieser Uhrzeit gesendet wurden", + [nameof(PartitionLimitLabel)] = "Partitionslimit", + [nameof(PartitionLimitTooltip)] = + "Die Ausgabe in Partitionen aufteilen, jede begrenzt auf die angegebene Anzahl von Nachrichten (z. B. '100') oder Dateigröße (z. B. '10mb')", + [nameof(MessageFilterLabel)] = "Nachrichtenfilter", + [nameof(MessageFilterTooltip)] = + "Nur Nachrichten einschließen, die diesem Filter entsprechen (z. B. 'from:foo#1234' oder 'has:image'). Weitere Informationen finden Sie in der Dokumentation.", + [nameof(FormatMarkdownLabel)] = "Markdown formatieren", + [nameof(FormatMarkdownTooltip)] = + "Markdown, Erwähnungen und andere spezielle Token verarbeiten", + [nameof(DownloadAssetsLabel)] = "Assets herunterladen", + [nameof(DownloadAssetsTooltip)] = + "Vom Export referenzierte Assets herunterladen (Benutzeravatare, angehängte Dateien, eingebettete Bilder usw.)", + [nameof(ReuseAssetsLabel)] = "Assets wiederverwenden", + [nameof(ReuseAssetsTooltip)] = + "Zuvor heruntergeladene Assets wiederverwenden, um redundante Anfragen zu vermeiden", + [nameof(AssetsDirPathLabel)] = "Asset-Verzeichnispfad", + [nameof(AssetsDirPathTooltip)] = + "Assets in dieses Verzeichnis herunterladen. Wenn nicht angegeben, wird der Asset-Verzeichnispfad vom Ausgabepfad abgeleitet.", + [nameof(AdvancedOptionsTooltip)] = "Erweiterte Optionen umschalten", + [nameof(ExportButton)] = "EXPORTIEREN", + // Common buttons + [nameof(CloseButton)] = "SCHLIESSEN", + [nameof(CancelButton)] = "ABBRECHEN", + // Dialog messages + [nameof(UkraineSupportTitle)] = "Danke für Ihre Unterstützung der Ukraine!", + [nameof(UkraineSupportMessage)] = """ + Während Russland einen Vernichtungskrieg gegen mein Land führt, bin ich jedem dankbar, der weiterhin an der Seite der Ukraine in unserem Kampf für die Freiheit steht. + + Klicken Sie auf MEHR ERFAHREN, um Möglichkeiten der Unterstützung zu finden. + """, + [nameof(LearnMoreButton)] = "MEHR ERFAHREN", + [nameof(UnstableBuildTitle)] = "Warnung: Instabile Version", + [nameof(UnstableBuildMessage)] = """ + Sie verwenden eine Entwicklungsversion von {0}. Diese Versionen wurden nicht gründlich getestet und können Fehler enthalten. + + Automatische Updates sind für Entwicklungsversionen deaktiviert. + + Klicken Sie auf RELEASES ANZEIGEN, wenn Sie stattdessen eine stabile Version herunterladen möchten. + """, + [nameof(SeeReleasesButton)] = "RELEASES ANZEIGEN", + [nameof(UpdateDownloadingMessage)] = "Update auf {0} v{1} wird heruntergeladen...", + [nameof(UpdateReadyMessage)] = + "Update wurde heruntergeladen und wird beim Beenden installiert", + [nameof(UpdateInstallNowButton)] = "JETZT INSTALLIEREN", + [nameof(UpdateFailedMessage)] = "Anwendungsupdate konnte nicht durchgeführt werden", + [nameof(ErrorPullingGuildsTitle)] = "Fehler beim Laden der Server", + [nameof(ErrorPullingChannelsTitle)] = "Fehler beim Laden der Kanäle", + [nameof(ErrorExportingTitle)] = "Fehler beim Exportieren der Kanäle", + [nameof(SuccessfulExportMessage)] = "{0} Kanal/-äle erfolgreich exportiert", + }; +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.Spanish.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.Spanish.cs new file mode 100644 index 00000000..e4d8e46d --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.Spanish.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager +{ + private static readonly IReadOnlyDictionary SpanishLocalization = + new Dictionary + { + // Dashboard + [nameof(PullGuildsTooltip)] = "Cargar servidores y canales disponibles (Enter)", + [nameof(SettingsTooltip)] = "Ajustes", + [nameof(LastMessageSentTooltip)] = "Último mensaje enviado:", + [nameof(TokenWatermark)] = "Token", + // Token instructions (personal account) + [nameof(TokenPersonalHeader)] = "Cómo obtener el token para tu cuenta personal:", + [nameof(TokenPersonalTosWarning)] = + "* Automatizar cuentas de usuario técnicamente va en contra de los ToS — **bajo tu propio riesgo**!", + [nameof(TokenPersonalInstructions)] = """ + 1. Abre Discord en tu navegador web e inicia sesión + 2. Abre cualquier servidor o canal de mensaje directo + 3. Presiona **Ctrl+Shift+I** para mostrar las herramientas de desarrollo + 4. Navega a la pestaña **Network** + 5. Presiona **Ctrl+R** para recargar + 6. Cambia entre canales para activar solicitudes de red + 7. Busca una solicitud que comience con **messages** + 8. Selecciona la pestaña **Headers** a la derecha + 9. Desplázate hasta la sección **Request Headers** + 10. Copia el valor del encabezado **authorization** + """, + // Token instructions (bot) + [nameof(TokenBotHeader)] = "Cómo obtener el token para tu bot:", + [nameof(TokenBotInstructions)] = """ + El token se genera al crear el bot. Si lo perdiste, genera uno nuevo: + + 1. Abre Discord [portal de desarrolladores](https://discord.com/developers/applications) + 2. Abre la configuración de tu aplicación + 3. Navega a la sección **Bot** en el lado izquierdo + 4. En **Token**, haz clic en **Reset Token** + 5. Haz clic en **Yes, do it!** y autentica para confirmar + * Las integraciones que usen el token anterior dejarán de funcionar hasta que se actualicen + * Tu bot necesita tener habilitado **Message Content Intent** para leer mensajes + """, + [nameof(TokenHelpText)] = + "Si tienes preguntas o problemas, consulta la [documentación](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)", + // Settings + [nameof(SettingsTitle)] = "Ajustes", + [nameof(ThemeLabel)] = "Tema", + [nameof(ThemeTooltip)] = "Tema de interfaz preferido", + [nameof(LanguageLabel)] = "Idioma", + [nameof(LanguageTooltip)] = "Idioma de interfaz preferido", + [nameof(AutoUpdateLabel)] = "Actualización automática", + [nameof(AutoUpdateTooltip)] = "Realizar actualizaciones automáticas en cada inicio", + [nameof(PersistTokenLabel)] = "Guardar token", + [nameof(PersistTokenTooltip)] = + "Guardar el último token utilizado en un archivo para conservarlo entre sesiones", + [nameof(RateLimitPreferenceLabel)] = "Preferencia de límite de velocidad", + [nameof(RateLimitPreferenceTooltip)] = + "Si se deben respetar los límites de velocidad recomendados. Si está desactivado, solo se respetarán los límites estrictos (respuestas 429).", + [nameof(ShowThreadsLabel)] = "Mostrar hilos", + [nameof(ShowThreadsTooltip)] = "Qué tipos de hilos mostrar en la lista de canales", + [nameof(LocaleLabel)] = "Configuración regional", + [nameof(LocaleTooltip)] = "Configuración regional para el formato de fechas y números", + [nameof(NormalizeToUtcLabel)] = "Normalizar a UTC", + [nameof(NormalizeToUtcTooltip)] = "Normalizar todas las marcas de tiempo a UTC+0", + [nameof(ParallelLimitLabel)] = "Límite paralelo", + [nameof(ParallelLimitTooltip)] = "Cuántos canales pueden exportarse al mismo tiempo", + // Export Setup + [nameof(ChannelsSelectedText)] = "canales seleccionados", + [nameof(OutputPathLabel)] = "Ruta de salida", + [nameof(OutputPathTooltip)] = """ + Ruta del archivo o directorio de salida. + + Si se especifica un directorio, los nombres de archivo se generarán automáticamente según los nombres de los canales y los parámetros de exportación. + + Las rutas de directorio deben terminar con una barra diagonal para evitar ambigüedades. + + Tokens de plantilla disponibles: + - **%g** — ID del servidor + - **%G** — nombre del servidor + - **%t** — ID de categoría + - **%T** — nombre de categoría + - **%c** — ID del canal + - **%C** — nombre del canal + - **%p** — posición del canal + - **%P** — posición de la categoría + - **%a** — fecha desde + - **%b** — fecha hasta + - **%d** — fecha actual + """, + [nameof(FormatLabel)] = "Formato", + [nameof(FormatTooltip)] = "Formato de exportación", + [nameof(AfterDateLabel)] = "Después (fecha)", + [nameof(AfterDateTooltip)] = "Solo incluir mensajes enviados después de esta fecha", + [nameof(BeforeDateLabel)] = "Antes (fecha)", + [nameof(BeforeDateTooltip)] = "Solo incluir mensajes enviados antes de esta fecha", + [nameof(AfterTimeLabel)] = "Después (hora)", + [nameof(AfterTimeTooltip)] = "Solo incluir mensajes enviados después de esta hora", + [nameof(BeforeTimeLabel)] = "Antes (hora)", + [nameof(BeforeTimeTooltip)] = "Solo incluir mensajes enviados antes de esta hora", + [nameof(PartitionLimitLabel)] = "Límite de partición", + [nameof(PartitionLimitTooltip)] = + "Dividir la salida en particiones, cada una limitada al número de mensajes especificado (p. ej. '100') o tamaño de archivo (p. ej. '10mb')", + [nameof(MessageFilterLabel)] = "Filtro de mensajes", + [nameof(MessageFilterTooltip)] = + "Solo incluir mensajes que satisfagan este filtro (p. ej. 'from:foo#1234' o 'has:image'). Consulte la documentación para más información.", + [nameof(FormatMarkdownLabel)] = "Formatear markdown", + [nameof(FormatMarkdownTooltip)] = + "Procesar markdown, menciones y otros tokens especiales", + [nameof(DownloadAssetsLabel)] = "Descargar recursos", + [nameof(DownloadAssetsTooltip)] = + "Descargar los recursos referenciados por la exportación (avatares, archivos adjuntos, imágenes incrustadas, etc.)", + [nameof(ReuseAssetsLabel)] = "Reutilizar recursos", + [nameof(ReuseAssetsTooltip)] = + "Reutilizar recursos previamente descargados para evitar solicitudes redundantes", + [nameof(AssetsDirPathLabel)] = "Ruta del directorio de recursos", + [nameof(AssetsDirPathTooltip)] = + "Descargar recursos en este directorio. Si no se especifica, la ruta se derivará de la ruta de salida.", + [nameof(AdvancedOptionsTooltip)] = "Alternar opciones avanzadas", + [nameof(ExportButton)] = "EXPORTAR", + // Common buttons + [nameof(CloseButton)] = "CERRAR", + [nameof(CancelButton)] = "CANCELAR", + // Dialog messages + [nameof(UkraineSupportTitle)] = "¡Gracias por apoyar a Ucrania!", + [nameof(UkraineSupportMessage)] = """ + Mientras Rusia libra una guerra genocida contra mi país, estoy agradecido con todos los que continúan apoyando a Ucrania en nuestra lucha por la libertad. + + Haga clic en MÁS INFORMACIÓN para encontrar formas de ayudar. + """, + [nameof(LearnMoreButton)] = "MÁS INFORMACIÓN", + [nameof(UnstableBuildTitle)] = "Advertencia de versión inestable", + [nameof(UnstableBuildMessage)] = """ + Está usando una versión de desarrollo de {0}. Estas versiones no han sido probadas exhaustivamente y pueden contener errores. + + Las actualizaciones automáticas están desactivadas para las versiones de desarrollo. + + Haga clic en VER VERSIONES si desea descargar una versión estable. + """, + [nameof(SeeReleasesButton)] = "VER VERSIONES", + [nameof(UpdateDownloadingMessage)] = "Descargando actualización a {0} v{1}...", + [nameof(UpdateReadyMessage)] = + "La actualización se ha descargado y se instalará al salir", + [nameof(UpdateInstallNowButton)] = "INSTALAR AHORA", + [nameof(UpdateFailedMessage)] = "Error al realizar la actualización de la aplicación", + [nameof(ErrorPullingGuildsTitle)] = "Error al cargar servidores", + [nameof(ErrorPullingChannelsTitle)] = "Error al cargar canales", + [nameof(ErrorExportingTitle)] = "Error al exportar canal(es)", + [nameof(SuccessfulExportMessage)] = "{0} canal(es) exportado(s) con éxito", + }; +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.Ukrainian.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.Ukrainian.cs new file mode 100644 index 00000000..068cf8f9 --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.Ukrainian.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager +{ + private static readonly IReadOnlyDictionary UkrainianLocalization = + new Dictionary + { + // Dashboard + [nameof(PullGuildsTooltip)] = "Завантажити доступні сервери та канали (Enter)", + [nameof(SettingsTooltip)] = "Налаштування", + [nameof(LastMessageSentTooltip)] = "Останнє повідомлення:", + [nameof(TokenWatermark)] = "Токен", + // Token instructions (personal account) + [nameof(TokenPersonalHeader)] = "Як отримати токен для персонального акаунту:", + [nameof(TokenPersonalTosWarning)] = + "* Автоматизація облікових записів технічно порушує Умови обслуговування — **на власний ризик**!", + [nameof(TokenPersonalInstructions)] = """ + 1. Відкрийте Discord у вашому веб-браузері та увійдіть + 2. Відкрийте будь-який сервер або канал особистих повідомлень + 3. Натисніть **Ctrl+Shift+I**, щоб відкрити інструменти розробника + 4. Перейдіть на вкладку **Network** + 5. Натисніть **Ctrl+R** для перезавантаження + 6. Перемикайтеся між каналами, щоб викликати мережеві запити + 7. Знайдіть запит, що починається з **messages** + 8. Виберіть вкладку **Headers** праворуч + 9. Прокрутіть до розділу **Request Headers** + 10. Скопіюйте значення заголовка **authorization** + """, + // Token instructions (bot) + [nameof(TokenBotHeader)] = "Як отримати токен для бота:", + [nameof(TokenBotInstructions)] = """ + Токен генерується під час створення бота. Якщо ви його втратили, згенеруйте новий: + + 1. Відкрийте Discord [портал розробника](https://discord.com/developers/applications) + 2. Відкрийте налаштування вашого застосунку + 3. Перейдіть до розділу **Bot** ліворуч + 4. В розділі **Token** натисніть **Reset Token** + 5. Натисніть **Yes, do it!** та підтвердьте + * Інтеграції, що використовують попередній токен, перестануть працювати + * Ваш бот повинен мати включений **Message Content Intent** для читання повідомлень + """, + [nameof(TokenHelpText)] = + "Якщо у вас є запитання або проблеми, зверніться до [документації](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)", + // Settings + [nameof(SettingsTitle)] = "Налаштування", + [nameof(ThemeLabel)] = "Тема", + [nameof(ThemeTooltip)] = "Бажана тема інтерфейсу", + [nameof(LanguageLabel)] = "Мова", + [nameof(LanguageTooltip)] = "Бажана мова інтерфейсу", + [nameof(AutoUpdateLabel)] = "Авто-оновлення", + [nameof(AutoUpdateTooltip)] = "Виконувати автоматичні оновлення при кожному запуску", + [nameof(PersistTokenLabel)] = "Зберігати токен", + [nameof(PersistTokenTooltip)] = + "Зберігати останній використаний токен у файлі для збереження між сеансами", + [nameof(RateLimitPreferenceLabel)] = "Ліміт запитів", + [nameof(RateLimitPreferenceTooltip)] = + "Чи дотримуватись рекомендованих лімітів запитів. Якщо вимкнено, будуть дотримуватись лише жорсткі ліміти (тобто відповіді 429).", + [nameof(ShowThreadsLabel)] = "Показувати гілки", + [nameof(ShowThreadsTooltip)] = "Які типи гілок показувати у списку каналів", + [nameof(LocaleLabel)] = "Локаль", + [nameof(LocaleTooltip)] = "Локаль для форматування дат та чисел", + [nameof(NormalizeToUtcLabel)] = "Нормалізувати до UTC", + [nameof(NormalizeToUtcTooltip)] = "Нормалізувати всі часові мітки до UTC+0", + [nameof(ParallelLimitLabel)] = "Ліміт паралелізації", + [nameof(ParallelLimitTooltip)] = "Скільки каналів може експортуватись одночасно", + // Export Setup + [nameof(ChannelsSelectedText)] = "каналів вибрано", + [nameof(OutputPathLabel)] = "Шлях збереження", + [nameof(OutputPathTooltip)] = """ + Шлях до файлу або директорії виводу. + + Якщо вказано директорію, імена файлів генеруватимуться автоматично на основі назв каналів та параметрів експорту. + + Шляхи до директорій повинні закінчуватись слешем для уникнення неоднозначності. + + Доступні шаблонні токени: + - **%g** — ID сервера + - **%G** — назва сервера + - **%t** — ID категорії + - **%T** — назва категорії + - **%c** — ID каналу + - **%C** — назва каналу + - **%p** — позиція каналу + - **%P** — позиція категорії + - **%a** — дата після + - **%b** — дата до + - **%d** — поточна дата + """, + [nameof(FormatLabel)] = "Формат", + [nameof(FormatTooltip)] = "Формат експорту", + [nameof(AfterDateLabel)] = "Після (дата)", + [nameof(AfterDateTooltip)] = "Включати лише повідомлення, надіслані після цієї дати", + [nameof(BeforeDateLabel)] = "До (дата)", + [nameof(BeforeDateTooltip)] = "Включати лише повідомлення, надіслані до цієї дати", + [nameof(AfterTimeLabel)] = "Після (час)", + [nameof(AfterTimeTooltip)] = "Включати лише повідомлення, надіслані після цього часу", + [nameof(BeforeTimeLabel)] = "До (час)", + [nameof(BeforeTimeTooltip)] = "Включати лише повідомлення, надіслані до цього часу", + [nameof(PartitionLimitLabel)] = "Розділяти експорт", + [nameof(PartitionLimitTooltip)] = + "Розділити вивід на частини, кожна обмежена вказаною кількістю повідомлень (напр. '100') або розміром файлу (напр. '10mb')", + [nameof(MessageFilterLabel)] = "Фільтр повідомлень", + [nameof(MessageFilterTooltip)] = + "Включати лише повідомлення, що відповідають цьому фільтру (напр. 'from:foo#1234' або 'has:image'). Дивіться документацію для більш детальної інформації.", + [nameof(FormatMarkdownLabel)] = "Форматувати markdown", + [nameof(FormatMarkdownTooltip)] = + "Обробляти markdown, згадки та інші спеціальні токени", + [nameof(DownloadAssetsLabel)] = "Завантажувати ресурси", + [nameof(DownloadAssetsTooltip)] = + "Завантажувати ресурси, на які посилається експорт (аватари, вкладені файли, вбудовані зображення тощо)", + [nameof(ReuseAssetsLabel)] = "Повторно використовувати ресурси", + [nameof(ReuseAssetsTooltip)] = + "Повторно використовувати раніше завантажені ресурси, щоб уникнути зайвих запитів", + [nameof(AssetsDirPathLabel)] = "Шлях до директорії ресурсів", + [nameof(AssetsDirPathTooltip)] = + "Завантажувати ресурси до цієї директорії. Якщо не вказано, шлях до директорії ресурсів буде визначено з шляху збереження.", + [nameof(AdvancedOptionsTooltip)] = "Перемкнути розширені параметри", + [nameof(ExportButton)] = "ЕКСПОРТУВАТИ", + // Common buttons + [nameof(CloseButton)] = "ЗАКРИТИ", + [nameof(CancelButton)] = "СКАСУВАТИ", + // Dialog messages + [nameof(UkraineSupportTitle)] = "Дякуємо за підтримку України!", + [nameof(UkraineSupportMessage)] = """ + Поки Росія веде геноцидну війну проти моєї країни, я вдячний кожному, хто продовжує підтримувати Україну у нашій боротьбі за свободу. + + Натисніть ДІЗНАТИСЬ БІЛЬШЕ, щоб знайти способи допомогти. + """, + [nameof(LearnMoreButton)] = "ДІЗНАТИСЬ БІЛЬШЕ", + [nameof(UnstableBuildTitle)] = "Попередження про нестабільну збірку", + [nameof(UnstableBuildMessage)] = """ + Ви використовуєте збірку розробки {0}. Ці збірки не пройшли ретельного тестування та можуть містити помилки. + + Авто-оновлення вимкнено для збірок розробки. + + Натисніть ПЕРЕГЛЯНУТИ РЕЛІЗИ, щоб завантажити стабільний реліз. + """, + [nameof(SeeReleasesButton)] = "ПЕРЕГЛЯНУТИ РЕЛІЗИ", + [nameof(UpdateDownloadingMessage)] = "Завантаження оновлення {0} v{1}...", + [nameof(UpdateReadyMessage)] = "Оновлення завантажено та буде встановлено після виходу", + [nameof(UpdateInstallNowButton)] = "ВСТАНОВИТИ ЗАРАЗ", + [nameof(UpdateFailedMessage)] = "Не вдалося виконати оновлення програми", + [nameof(ErrorPullingGuildsTitle)] = "Помилка завантаження серверів", + [nameof(ErrorPullingChannelsTitle)] = "Помилка завантаження каналів", + [nameof(ErrorExportingTitle)] = "Помилка експорту каналу(-ів)", + [nameof(SuccessfulExportMessage)] = "Успішно експортовано {0} канал(-ів)", + }; +} diff --git a/DiscordChatExporter.Gui/Localization/LocalizationManager.cs b/DiscordChatExporter.Gui/Localization/LocalizationManager.cs new file mode 100644 index 00000000..7ab62e9f --- /dev/null +++ b/DiscordChatExporter.Gui/Localization/LocalizationManager.cs @@ -0,0 +1,170 @@ +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; +using DiscordChatExporter.Gui.Services; +using DiscordChatExporter.Gui.Utils; +using DiscordChatExporter.Gui.Utils.Extensions; + +namespace DiscordChatExporter.Gui.Localization; + +public partial class LocalizationManager : ObservableObject, IDisposable +{ + private readonly DisposableCollector _eventRoot = new(); + + public LocalizationManager(SettingsService settingsService) + { + _eventRoot.Add( + settingsService.WatchProperty( + o => o.Language, + () => Language = settingsService.Language, + true + ) + ); + + _eventRoot.Add( + this.WatchProperty( + o => o.Language, + () => + { + foreach (var propertyName in EnglishLocalization.Keys) + OnPropertyChanged(propertyName); + } + ) + ); + } + + [ObservableProperty] + public partial Language Language { get; set; } = Language.System; + + private string Get([CallerMemberName] string? key = null) + { + if (string.IsNullOrWhiteSpace(key)) + return string.Empty; + + var localization = Language switch + { + Language.System => + CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName.ToLowerInvariant() switch + { + "ukr" => UkrainianLocalization, + "deu" => GermanLocalization, + "fra" => FrenchLocalization, + "spa" => SpanishLocalization, + _ => EnglishLocalization, + }, + Language.Ukrainian => UkrainianLocalization, + Language.German => GermanLocalization, + Language.French => FrenchLocalization, + Language.Spanish => SpanishLocalization, + _ => EnglishLocalization, + }; + + if ( + localization.TryGetValue(key, out var value) + // English is used as a fallback + || EnglishLocalization.TryGetValue(key, out value) + ) + { + return value; + } + + return $"Missing localization for '{key}'"; + } + + public void Dispose() => _eventRoot.Dispose(); +} + +public partial class LocalizationManager +{ + // ---- Dashboard ---- + + public string PullGuildsTooltip => Get(); + public string SettingsTooltip => Get(); + public string LastMessageSentTooltip => Get(); + public string TokenWatermark => Get(); + + // Token instructions (personal account) + public string TokenPersonalHeader => Get(); + public string TokenPersonalTosWarning => Get(); + public string TokenPersonalInstructions => Get(); + + // Token instructions (bot) + public string TokenBotHeader => Get(); + public string TokenBotInstructions => Get(); + public string TokenHelpText => Get(); + + // ---- Settings ---- + + public string SettingsTitle => Get(); + public string ThemeLabel => Get(); + public string ThemeTooltip => Get(); + public string LanguageLabel => Get(); + public string LanguageTooltip => Get(); + public string AutoUpdateLabel => Get(); + public string AutoUpdateTooltip => Get(); + public string PersistTokenLabel => Get(); + public string PersistTokenTooltip => Get(); + public string RateLimitPreferenceLabel => Get(); + public string RateLimitPreferenceTooltip => Get(); + public string ShowThreadsLabel => Get(); + public string ShowThreadsTooltip => Get(); + public string LocaleLabel => Get(); + public string LocaleTooltip => Get(); + public string NormalizeToUtcLabel => Get(); + public string NormalizeToUtcTooltip => Get(); + public string ParallelLimitLabel => Get(); + public string ParallelLimitTooltip => Get(); + + // ---- Export Setup ---- + + public string ChannelsSelectedText => Get(); + public string OutputPathLabel => Get(); + public string OutputPathTooltip => Get(); + public string FormatLabel => Get(); + public string FormatTooltip => Get(); + public string AfterDateLabel => Get(); + public string AfterDateTooltip => Get(); + public string BeforeDateLabel => Get(); + public string BeforeDateTooltip => Get(); + public string AfterTimeLabel => Get(); + public string AfterTimeTooltip => Get(); + public string BeforeTimeLabel => Get(); + public string BeforeTimeTooltip => Get(); + public string PartitionLimitLabel => Get(); + public string PartitionLimitTooltip => Get(); + public string MessageFilterLabel => Get(); + public string MessageFilterTooltip => Get(); + public string FormatMarkdownLabel => Get(); + public string FormatMarkdownTooltip => Get(); + public string DownloadAssetsLabel => Get(); + public string DownloadAssetsTooltip => Get(); + public string ReuseAssetsLabel => Get(); + public string ReuseAssetsTooltip => Get(); + public string AssetsDirPathLabel => Get(); + public string AssetsDirPathTooltip => Get(); + public string AdvancedOptionsTooltip => Get(); + public string ExportButton => Get(); + + // ---- Common buttons ---- + + public string CloseButton => Get(); + public string CancelButton => Get(); + + // ---- Dialog messages ---- + + public string UkraineSupportTitle => Get(); + public string UkraineSupportMessage => Get(); + public string LearnMoreButton => Get(); + public string UnstableBuildTitle => Get(); + public string UnstableBuildMessage => Get(); + public string SeeReleasesButton => Get(); + public string UpdateDownloadingMessage => Get(); + public string UpdateReadyMessage => Get(); + public string UpdateInstallNowButton => Get(); + public string UpdateFailedMessage => Get(); + public string ErrorPullingGuildsTitle => Get(); + public string ErrorPullingChannelsTitle => Get(); + public string ErrorExportingTitle => Get(); + public string SuccessfulExportMessage => Get(); +} diff --git a/DiscordChatExporter.Gui/Services/SettingsService.cs b/DiscordChatExporter.Gui/Services/SettingsService.cs index 87fb914b..cca8a3aa 100644 --- a/DiscordChatExporter.Gui/Services/SettingsService.cs +++ b/DiscordChatExporter.Gui/Services/SettingsService.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Models; namespace DiscordChatExporter.Gui.Services; @@ -23,6 +24,9 @@ public partial class SettingsService() [ObservableProperty] public partial ThemeVariant Theme { get; set; } + [ObservableProperty] + public partial Language Language { get; set; } + [ObservableProperty] public partial bool IsAutoUpdateEnabled { get; set; } = true; diff --git a/DiscordChatExporter.Gui/Utils/Extensions/MarkdigExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/MarkdigExtensions.cs new file mode 100644 index 00000000..2c2c9ac3 --- /dev/null +++ b/DiscordChatExporter.Gui/Utils/Extensions/MarkdigExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Markdig.Syntax.Inlines; +using MarkdownInline = Markdig.Syntax.Inlines.Inline; + +namespace DiscordChatExporter.Gui.Utils.Extensions; + +internal static class MarkdigExtensions +{ + extension(MarkdownInline inline) + { + public string GetInnerText() => + inline switch + { + LiteralInline literal => literal.Content.ToString(), + ContainerInline container => string.Concat(container.Select(c => c.GetInnerText())), + _ => string.Empty, + }; + } +} diff --git a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs index ebd416c2..ecbead8f 100644 --- a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs @@ -13,6 +13,7 @@ using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Models; using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Utils; @@ -38,13 +39,15 @@ public partial class DashboardViewModel : ViewModelBase ViewModelManager viewModelManager, DialogManager dialogManager, SnackbarManager snackbarManager, - SettingsService settingsService + SettingsService settingsService, + LocalizationManager localizationManager ) { _viewModelManager = viewModelManager; _dialogManager = dialogManager; _snackbarManager = snackbarManager; _settingsService = settingsService; + LocalizationManager = localizationManager; _progressMuxer = Progress.CreateMuxer().WithAutoReset(); @@ -70,6 +73,8 @@ public partial class DashboardViewModel : ViewModelBase [NotifyCanExecuteChangedFor(nameof(ExportCommand))] public partial bool IsBusy { get; set; } + public LocalizationManager LocalizationManager { get; } + public ProgressContainer Progress { get; } = new(); public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1; @@ -102,9 +107,6 @@ public partial class DashboardViewModel : ViewModelBase private async Task ShowSettingsAsync() => await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel()); - [RelayCommand] - private void ShowHelp() => Process.StartShellExecute(Program.ProjectDocumentationUrl); - private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token); [RelayCommand(CanExecute = nameof(CanPullGuilds))] @@ -141,7 +143,7 @@ public partial class DashboardViewModel : ViewModelBase catch (Exception ex) { var dialog = _viewModelManager.CreateMessageBoxViewModel( - "Error pulling servers", + LocalizationManager.ErrorPullingGuildsTitle, ex.ToString() ); @@ -208,7 +210,7 @@ public partial class DashboardViewModel : ViewModelBase catch (Exception ex) { var dialog = _viewModelManager.CreateMessageBoxViewModel( - "Error pulling channels", + LocalizationManager.ErrorPullingChannelsTitle, ex.ToString() ); @@ -303,14 +305,17 @@ public partial class DashboardViewModel : ViewModelBase if (successfulExportCount > 0) { _snackbarManager.Notify( - $"Successfully exported {successfulExportCount} channel(s)" + string.Format( + LocalizationManager.SuccessfulExportMessage, + successfulExportCount + ) ); } } catch (Exception ex) { var dialog = _viewModelManager.CreateMessageBoxViewModel( - "Error exporting channel(s)", + LocalizationManager.ErrorExportingTitle, ex.ToString() ); @@ -322,13 +327,6 @@ public partial class DashboardViewModel : ViewModelBase } } - [RelayCommand] - private void OpenDiscord() => Process.StartShellExecute("https://discord.com/app"); - - [RelayCommand] - private void OpenDiscordDeveloperPortal() => - Process.StartShellExecute("https://discord.com/developers/applications"); - protected override void Dispose(bool disposing) { if (disposing) diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index 4574f6ec..afee2f00 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -12,15 +12,19 @@ using DiscordChatExporter.Core.Exporting.Filtering; using DiscordChatExporter.Core.Exporting.Partitioning; using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Services; namespace DiscordChatExporter.Gui.ViewModels.Dialogs; public partial class ExportSetupViewModel( DialogManager dialogManager, - SettingsService settingsService + SettingsService settingsService, + LocalizationManager localizationManager ) : DialogViewModelBase { + public LocalizationManager LocalizationManager { get; } = localizationManager; + [ObservableProperty] public partial Guild? Guild { get; set; } diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs index d3eaad9f..544f4ba1 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Models; using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Utils; @@ -16,13 +17,19 @@ public class SettingsViewModel : DialogViewModelBase private readonly DisposableCollector _eventRoot = new(); - public SettingsViewModel(SettingsService settingsService) + public SettingsViewModel( + SettingsService settingsService, + LocalizationManager localizationManager + ) { _settingsService = settingsService; + LocalizationManager = localizationManager; _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged)); } + public LocalizationManager LocalizationManager { get; } + public IReadOnlyList AvailableThemes { get; } = Enum.GetValues(); public ThemeVariant Theme @@ -31,6 +38,14 @@ public class SettingsViewModel : DialogViewModelBase set => _settingsService.Theme = value; } + public IReadOnlyList AvailableLanguages { get; } = Enum.GetValues(); + + public Language Language + { + get => _settingsService.Language; + set => _settingsService.Language = value; + } + public bool IsAutoUpdateEnabled { get => _settingsService.IsAutoUpdateEnabled; diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs index a40ca6fb..77060b28 100644 --- a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Avalonia; using CommunityToolkit.Mvvm.Input; using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Localization; using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Utils.Extensions; using DiscordChatExporter.Gui.ViewModels.Components; @@ -15,7 +16,8 @@ public partial class MainViewModel( DialogManager dialogManager, SnackbarManager snackbarManager, SettingsService settingsService, - UpdateService updateService + UpdateService updateService, + LocalizationManager localizationManager ) : ViewModelBase { public string Title { get; } = $"{Program.Name} v{Program.VersionString}"; @@ -28,14 +30,10 @@ public partial class MainViewModel( return; var dialog = viewModelManager.CreateMessageBoxViewModel( - "Thank you for supporting Ukraine!", - """ - As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom. - - Click LEARN MORE to find ways that you can help. - """, - "LEARN MORE", - "CLOSE" + localizationManager.UkraineSupportTitle, + localizationManager.UkraineSupportMessage, + localizationManager.LearnMoreButton, + localizationManager.CloseButton ); // Disable this message in the future @@ -56,16 +54,10 @@ public partial class MainViewModel( return; var dialog = viewModelManager.CreateMessageBoxViewModel( - "Unstable build warning", - $""" - You're using a development build of {Program.Name}. These builds are not thoroughly tested and may contain bugs. - - Auto-updates are disabled for development builds. - - Click SEE RELEASES if you want to download a stable release instead. - """, - "SEE RELEASES", - "CLOSE" + localizationManager.UnstableBuildTitle, + string.Format(localizationManager.UnstableBuildMessage, Program.Name), + localizationManager.SeeReleasesButton, + localizationManager.CloseButton ); if (await dialogManager.ShowDialogAsync(dialog) == true) @@ -80,12 +72,18 @@ public partial class MainViewModel( if (updateVersion is null) return; - snackbarManager.Notify($"Downloading update to {Program.Name} v{updateVersion}..."); + snackbarManager.Notify( + string.Format( + localizationManager.UpdateDownloadingMessage, + Program.Name, + updateVersion + ) + ); await updateService.PrepareUpdateAsync(updateVersion); snackbarManager.Notify( - "Update has been downloaded and will be installed when you exit", - "INSTALL NOW", + localizationManager.UpdateReadyMessage, + localizationManager.UpdateInstallNowButton, () => { updateService.FinalizeUpdate(true); @@ -98,7 +96,7 @@ public partial class MainViewModel( catch { // Failure to update shouldn't crash the application - snackbarManager.Notify("Failed to perform application update"); + snackbarManager.Notify(localizationManager.UpdateFailedMessage); } } diff --git a/DiscordChatExporter.Gui/Views/Components/DashboardView.axaml b/DiscordChatExporter.Gui/Views/Components/DashboardView.axaml index 350f6acf..6582ad8a 100644 --- a/DiscordChatExporter.Gui/Views/Components/DashboardView.axaml +++ b/DiscordChatExporter.Gui/Views/Components/DashboardView.axaml @@ -4,7 +4,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:components="clr-namespace:DiscordChatExporter.Gui.ViewModels.Components" - xmlns:controls="clr-namespace:DiscordChatExporter.Gui.Views.Controls" xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles" @@ -27,7 +26,7 @@ RevealPassword="{Binding $self.IsFocused}" Text="{Binding Token}" Theme="{DynamicResource SoloTextBox}" - Watermark="Token"> + Watermark="{Binding LocalizationManager.TokenWatermark}"> + ToolTip.Tip="{Binding LocalizationManager.PullGuildsTooltip}"> + ToolTip.Tip="{Binding LocalizationManager.SettingsTooltip}"> @@ -219,172 +218,67 @@ - + + - - - - - - + + + + + + + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs b/DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs index 08452c12..479303de 100644 --- a/DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs +++ b/DiscordChatExporter.Gui/Views/Controls/HyperLink.axaml.cs @@ -1,7 +1,9 @@ -using System.Windows.Input; +using System.Diagnostics; +using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using DiscordChatExporter.Gui.Utils.Extensions; namespace DiscordChatExporter.Gui.Views.Controls; @@ -16,6 +18,12 @@ public partial class HyperLink : UserControl public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); + // If Url is set and Command is not set, clicking will open this URL in the default browser. + public static readonly StyledProperty UrlProperty = AvaloniaProperty.Register< + HyperLink, + string? + >(nameof(Url)); + public HyperLink() => InitializeComponent(); public string? Text @@ -36,14 +44,22 @@ public partial class HyperLink : UserControl set => SetValue(CommandParameterProperty, value); } + public string? Url + { + get => GetValue(UrlProperty); + set => SetValue(UrlProperty, value); + } + private void TextBlock_OnPointerReleased(object? sender, PointerReleasedEventArgs args) { - if (Command is null) - return; - - if (!Command.CanExecute(CommandParameter)) - return; - - Command.Execute(CommandParameter); + if (Command is not null) + { + if (Command.CanExecute(CommandParameter)) + Command.Execute(CommandParameter); + } + else if (!string.IsNullOrWhiteSpace(Url)) + { + Process.StartShellExecute(Url); + } } } diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml index f5c999e0..ea0acc72 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.axaml @@ -38,7 +38,7 @@ IsVisible="{Binding !IsSingleChannel}" TextTrimming="CharacterEllipsis"> - + @@ -69,64 +69,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +