diff --git a/README.md b/README.md index 78eb428..0ca5701 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You can download and install Inscriptions from various sources: ## 🛣️ Roadmap --Switch backends +-Switch BackendType ## 💝 Donations diff --git a/data/inscriptions.gschema.xml b/data/inscriptions.gschema.xml index 828c18c..f6ae757 100644 --- a/data/inscriptions.gschema.xml +++ b/data/inscriptions.gschema.xml @@ -7,6 +7,11 @@ + + + + + @@ -69,5 +74,89 @@ Whether to highlight both source and target Highlights alternating colours on both source and target in the TextView + + 'deepl' + Level of formality + For supported languages, how format the output should be + + + + + + "source" + Language code to translate from + Represents a language code. "idk" is to autodetect, and "system" for the system language. + + + "echo" + Language code to translate to + Represents a language code. "system" for the system language. + + + + + + "idk" + Language code to translate from + Represents a language code. "idk" is to autodetect, and "system" for the system language. + + + "system" + Language code to translate to + Represents a language code. "system" for the system language. + + + "" + context for translations + Passed as context parameter to the DeepL API + + + 'default' + Level of formality + For supported languages, how format the output should be + + + 0 + Current API usage + How many characters have been translated + + + 0 + Max API usage + How many characters can be translated + + + + + + "idk" + Language code to translate from + Represents a language code. "idk" is to autodetect, and "system" for the system language. + + + "system" + Language code to translate to + Represents a language code. "system" for the system language. + + + "" + context for translations + Passed as context parameter to the DeepL API + + + 'default' + Level of formality + For supported languages, how format the output should be + + + 0 + Current API usage + How many characters have been translated + + + 0 + Max API usage + How many characters can be translated + \ No newline at end of file diff --git a/po/POTFILES b/po/POTFILES index 0c8f812..f356ed8 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -15,6 +15,7 @@ src/Widgets/Panes/TargetPane.vala src/Widgets/Buttons/RetryButton.vala src/Widgets/LogToolbar.vala src/Widgets/TextView.vala +src/Enums/PossibleBackendType.vala src/Backend/DeepL/DeepL.vala src/Constants.vala src/Application.vala \ No newline at end of file diff --git a/src/Application.vala b/src/Application.vala index e8ad9e3..ddcae1d 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -6,7 +6,7 @@ public class Inscriptions.Application : Gtk.Application { internal static Settings settings; - internal static DeepL backend; + internal static DeepL.Backend backend; internal static MainWindow main_window; public const string ACTION_PREFIX = "app."; @@ -36,7 +36,7 @@ public class Inscriptions.Application : Gtk.Application { // Backend takes care of the async for us. We give it the text // And it will emit a signal whenever finished, which we can connect to - backend = new DeepL (); + backend = new DeepL.Backend (); } protected override void startup () { diff --git a/src/Backend/BackendTemplate.vala b/src/Backend/BackendTemplate.vala index 7eb94a8..976ffd3 100644 --- a/src/Backend/BackendTemplate.vala +++ b/src/Backend/BackendTemplate.vala @@ -29,7 +29,7 @@ The object has two signals: If you want to write your own backend, everything would pretty much work if you do a drop in replacement with send_request (text) and the two signals to retrieve - i may open up a bit more the possibilities to do other backends in the future + i may open up a bit more the possibilities to do other BackendType in the future public void send_request (text); @@ -44,19 +44,16 @@ public const SUPPORTED_TARGET */ // Translation service that use translate -public abstract class MrWorldWide.DeepL : Object { +public interface Inscriptions.BackendTemplate : Object { - private string source_lang; - private string target_lang; - private string api_key; - private string base_url; - public string system_language; - private string context; + public abstract string source_lang {get; set;} + public abstract string target_lang {get; set;} + public abstract uint status_code {get; set;} - /** + /** * Connect to this signal to receive translated text */ - public signal void answer_received (string translated_text = ""); + public signal void answer_received (uint status_code, string translated_text = ""); /** * Connect to this signal to know when language is detected @@ -64,18 +61,16 @@ public abstract class MrWorldWide.DeepL : Object { public signal void language_detected (string? detected_language_code = null); /** - * Connect to this signal to get usage + * Call this */ - public signal void usage_retrieved (int current_usage, int max_usage); - - public const string[] SUPPORTED_FORMALITY = {"DE", "FR", "IT", "ES", "NL", "PL", "PT-BR", "PT-PT", "JA", "RU"}; - public int current_usage = 0; - public int max_usage = 0; + public abstract void check_usage (); - /** - * Anything to prepare should go here + /** + * Connect to this signal to get usage */ - public abstract void init (); + public signal void usage_retrieved (uint status_code, int current_usage, int max_usage); + public abstract int current_usage {get; set; default = 0;} + public abstract int max_usage {get; set; default = 0;} /** * Call this method to send asynchronously a request. @@ -86,7 +81,6 @@ public abstract class MrWorldWide.DeepL : Object { /** * Call this */ - public abstract void check_usage (); - - + public abstract Lang[] supported_source_languages {get; internal set;} + public abstract Lang[] supported_target_languages {get; internal set;} } diff --git a/src/Backend/DeepL/DeepL.vala b/src/Backend/DeepL/DeepL.vala index dcf3bec..0959f38 100644 --- a/src/Backend/DeepL/DeepL.vala +++ b/src/Backend/DeepL/DeepL.vala @@ -7,7 +7,7 @@ * The backend, responsible for requests and answers. * This needs to be standardized into a template, and broken up in several files. */ -public class Inscriptions.DeepL : Object { +public class Inscriptions.DeepL.Backend : Object, BackendTemplate { private const uint TIMEOUT = 3000; @@ -15,27 +15,26 @@ public class Inscriptions.DeepL : Object { internal Soup.Logger logger; Secrets secrets; - string source_lang; - string target_lang; + public uint status_code { get; set; } + public int current_usage { get; set; } + public int max_usage { get; set; } + + public Lang[] supported_source_languages { get; set; } + public Lang[] supported_target_languages { get; set; } + + string source_lang { get; set; } + string target_lang { get; set; } string api_key; string base_url; public string system_language; string context; - public signal void answer_received (uint status, string? translated_text = null); - public signal void language_detected (string? detected_language_code = null); - public signal void usage_retrieved (uint status); - const string URL_DEEPL_FREE = "https://api-free.deepl.com"; const string URL_DEEPL_PRO = "https://api.deepl.com"; const string REST_OF_THE_URL = "/v2/translate"; const string URL_USAGE = "/v2/usage"; - public const string[] SUPPORTED_FORMALITY = {"DE", "FR", "IT", "ES", "NL", "PL", "PT-BR", "PT-PT", "JA", "RU"}; - public int current_usage = 0; - public int max_usage = 0; - // Private debounce to not constantly check usage on key change int interval = 1000; // ms uint debounce_timer_id = 0; @@ -52,9 +51,12 @@ public class Inscriptions.DeepL : Object { stderr.printf ("%c %s\n", dir, text); }); + supported_source_languages = DeepL.Utils.supported_source_languages (); + supported_target_languages = DeepL.Utils.supported_target_languages (); + secrets = Secrets.get_default (); - system_language = DeepLUtils.detect_system (); + system_language = DeepL.Utils.detect_system (); // Fallback this.current_usage = Application.settings.get_int ("current-usage"); @@ -138,7 +140,7 @@ public class Inscriptions.DeepL : Object { unwrapped_text = unwrap_json (answer); } else { - unwrapped_text = unwrap_error_message (answer); + unwrapped_text = DeepL.Utils.unwrap_error_message (answer); } answer_received (msg.status_code, unwrapped_text); @@ -229,20 +231,6 @@ public class Inscriptions.DeepL : Object { - public string unwrap_error_message (string text_json) { - - var parser = new Json.Parser (); - try { - parser.load_from_data (text_json); - } catch (Error e) { - print ("\nCannot: " + e.message); - return text_json; - } - - var root = parser.get_root (); - var objects = root.get_object (); - return objects.get_string_member_with_default ("message", _("Cannot retrieve error message text!")); - } @@ -272,12 +260,12 @@ public class Inscriptions.DeepL : Object { Application.settings.set_int ("max-usage", max_usage); var msg = session.get_async_result_message (res); - usage_retrieved (msg.status_code); + usage_retrieved (msg.status_code, current_usage, max_usage); string? error_message = null; if (msg.status_code != Soup.Status.OK) { - error_message = unwrap_error_message (answer); + error_message = DeepL.Utils.unwrap_error_message (answer); } answer_received (msg.status_code, error_message); @@ -288,4 +276,5 @@ public class Inscriptions.DeepL : Object { } + } diff --git a/src/Backend/DeepL/DeepLUtils.vala b/src/Backend/DeepL/DeepLUtils.vala index dd2a1f7..88c5be4 100644 --- a/src/Backend/DeepL/DeepLUtils.vala +++ b/src/Backend/DeepL/DeepLUtils.vala @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) */ -namespace Inscriptions.DeepLUtils { +namespace Inscriptions.DeepL.Utils { // FUCKY: DeepL is a bit weird with some codes // We have to hack at it for edge cases @@ -26,4 +26,105 @@ namespace Inscriptions.DeepLUtils { print ("\nBackend: Detected system language: " + minicode); return minicode; } + + + public string unwrap_error_message (string text_json) { + + var parser = new Json.Parser (); + try { + parser.load_from_data (text_json); + } catch (Error e) { + print ("\nCannot: " + e.message); + return text_json; + } + + var root = parser.get_root (); + var objects = root.get_object (); + return objects.get_string_member_with_default ("message", _("Cannot retrieve error message text!")); + } + + + public Lang[] supported_source_languages () { + return { + //TRANSLATORS: The following are all languages user can select as source or target for translation + new Lang ("idk",_("Detect automatically")), + new Lang ("system",_("System language")), + new Lang ("AR",_("Arabic")), + new Lang ("BG",_("Bulgarian")), + new Lang ("CS",_("Czech")), + new Lang ("DA",_("Danish")), + new Lang ("DE",_("German")), + new Lang ("EL",_("Greek")), + new Lang ("EN",_("English (All)")), + new Lang ("ES",_("Spanish (All)")), + new Lang ("ET",_("Estonian")), + new Lang ("FI",_("Finnish")), + new Lang ("FR",_("French")), + new Lang ("HE",_("Hebrew")), + new Lang ("HU",_("Hungarian")), + new Lang ("ID",_("Indonesian")), + new Lang ("IT",_("Italian")), + new Lang ("JA",_("Japanese")), + new Lang ("KO",_("Korean")), + new Lang ("LT",_("Lithuanian")), + new Lang ("LV",_("Latvian")), + new Lang ("NB",_("Norwegian Bokmål")), + new Lang ("NL",_("Dutch")), + new Lang ("PL",_("Polish")), + new Lang ("PT",_("Portuguese (All)")), + new Lang ("RO",_("Romanian")), + new Lang ("RU",_("Russian")), + new Lang ("SK",_("Slovak")), + new Lang ("SL",_("Slovenian")), + new Lang ("SV",_("Swedish")), + new Lang ("TH",_("Thai")), + new Lang ("TR",_("Turkish")), + new Lang ("UK",_("Ukrainian")), + new Lang ("VI",_("Vietnamese")), + new Lang ("ZH",_("Chinese (All)")) + }; + } + + public Lang[] supported_target_languages () { + return { + new Lang ("system",_("System language")), + new Lang ("AR",_("Arabic")), + new Lang ("BG",_("Bulgarian")), + new Lang ("CS",_("Czech")), + new Lang ("DA",_("Danish")), + new Lang ("DE",_("German")), + new Lang ("EL",_("Greek")), + new Lang ("EN",_("English (GB)")), + new Lang ("EN",_("English (US)")), + new Lang ("ES",_("Spanish (All)")), + new Lang ("ES-419",_("Spanish (Latin American)")), + new Lang ("ET",_("Estonian")), + new Lang ("FI",_("Finnish")), + new Lang ("FR",_("French")), + new Lang ("HE",_("Hebrew")), + new Lang ("HU",_("Hungarian")), + new Lang ("ID",_("Indonesian")), + new Lang ("IT",_("Italian")), + new Lang ("JA",_("Japanese")), + new Lang ("KO",_("Korean")), + new Lang ("LT",_("Lithuanian")), + new Lang ("LV",_("Latvian")), + new Lang ("NB",_("Norwegian Bokmål")), + new Lang ("NL",_("Dutch")), + new Lang ("PL",_("Polish")), + new Lang ("PT-PT",_("Portuguese (Portugal)")), + new Lang ("PT-BR",_("Portuguese (Brazilian)")), + new Lang ("RO",_("Romanian")), + new Lang ("RU",_("Russian")), + new Lang ("SK",_("Slovak")), + new Lang ("SL",_("Slovenian")), + new Lang ("SV",_("Swedish")), + new Lang ("TH",_("Thai")), + new Lang ("TR",_("Turkish")), + new Lang ("UK",_("Ukrainian")), + new Lang ("VI",_("Vietnamese")), + new Lang ("ZH-HANS",_("Chinese (Simplified)")), + new Lang ("ZH-HANT",_("Chinese (Traditional)")) + }; + } } diff --git a/src/Backend/Dummy.vala b/src/Backend/Dummy.vala new file mode 100644 index 0000000..11969e9 --- /dev/null +++ b/src/Backend/Dummy.vala @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + +// Translation service that use translate +public class Inscriptions.Dummy : Object, BackendTemplate { + + string source_lang {get; set;} + string target_lang {get; set;} + public uint status_code { get; set; } + public Lang[] supported_source_languages { get; set; } + public Lang[] supported_target_languages { get; set; } + + public int current_usage { get; set; } + public int max_usage { get; set; } + + construct { + supported_source_languages = { new Lang ("source", _("Source"))}; + supported_target_languages = { + new Lang ("echo", _("Echo")), + new Lang ("up", _("Echo Up")), + new Lang ("down", _("Echo Down")), + }; + + max_usage = 100; + } + + public void check_usage () { + current_usage = Random.int_range (0, 101); + usage_retrieved (200, current_usage, max_usage); + } + + public void send_request (string text) { + switch (target_lang) { + case "echo": answer_received (200, text); return; + case "up": answer_received (200, text.ascii_up ()); return; + case "down": answer_received (200, text.ascii_down ()); return; + } + } +} diff --git a/src/Constants.vala b/src/Constants.vala index a238eca..e81542b 100644 --- a/src/Constants.vala +++ b/src/Constants.vala @@ -45,6 +45,10 @@ namespace Inscriptions { public const string KEY_FORMALITY = "formality"; public const string KEY_CURRENT_USAGE = "current-usage"; public const string KEY_MAX_USAGE = "max-usage"; + + public const string KEY_BACKEND = "backend"; + + } namespace Inscriptions { diff --git a/src/Enums/BackendType.vala b/src/Enums/BackendType.vala new file mode 100644 index 0000000..a49f132 --- /dev/null +++ b/src/Enums/BackendType.vala @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * A convenient way to track all existing BackendType + */ +public enum Inscriptions.BackendType { + DEEPL, + LIBRETRANSLATE, + DUMMY; + + public string to_name () { + switch (this) { + case DUMMY: return _("Dummy"); + case DEEPL: return _("DeepL"); + case LIBRETRANSLATE: return _("LibreTranslate"); + default: return _("DeepL"); + } + } + + public string to_secrets_label () { + switch (this) { + case DUMMY: return "Super-Secret-Key"; + case DEEPL: return "DeepL-Auth-Key"; + case LIBRETRANSLATE: return "API_KEY"; + default: return "DeepL-Auth-Key"; + } + } + + public string to_settings_prefix () { + switch (this) { + case DUMMY: return ".dummy"; + case DEEPL: return ".deepl"; + case LIBRETRANSLATE: return ".libretranslate"; + default: return ".deepl"; + } + } + + public static BackendType from_int (int number) { + switch (number) { + case 0: return DEEPL; + case 1: return LIBRETRANSLATE; + case 3: return DUMMY; + default: return DEEPL; + } + } + + public const BackendType[] ALL = {DUMMY, DEEPL, LIBRETRANSLATE}; + public const string[] STRING_ALL = {N_("DeepL"), N_("LibreTranslate"), N_("Dummy")}; +} \ No newline at end of file diff --git a/src/Services/BackendController.vala b/src/Services/BackendController.vala index c74032e..6d75038 100644 --- a/src/Services/BackendController.vala +++ b/src/Services/BackendController.vala @@ -6,9 +6,68 @@ /** * a */ -public class Inscriptions.BackendController : Object { +public class Inscriptions.BackendController : Object, BackendTemplate { + + private BackendType current_backend_type {get; set;} + private BackendTemplate current_backend {get; set;} + public signal void backend_changed (BackendType new_backend); + + public Lang[] supported_source_languages { get; set; } + public Lang[] supported_target_languages { get; set; } + + // Ensure only once instance, accessible whenever needed. + private static BackendController? instance; + public static BackendController get_default () { + if (instance == null) { + instance = new BackendController (); + } + return instance; + } construct { + on_backend_changed (); + Application.settings.changed[KEY_BACKEND].connect (on_backend_changed); + } + + private void on_backend_changed () { + var int_new_backend_type = Application.settings.get_enum (KEY_BACKEND); + var new_backend = BackendType.from_int (int_new_backend_type); + + supported_source_languages = current_backend.supported_source_languages; + supported_target_languages = current_backend.supported_target_languages; + + backend_changed (new_backend); } + + + + public string source_lang { + get {return current_backend.source_lang;} + set {current_backend.source_lang = value;} +} + + public string target_lang { + get {return current_backend.target_lang;} + set {current_backend.target_lang = value;} +} + public uint status_code { + get {return current_backend.status_code;} + set {current_backend.status_code = value;} +} + + public void check_usage () {current_backend.check_usage ();} + + int current_usage { + get {return current_backend.current_usage;} + set {current_backend.current_usage = value;} +} + int max_usage { + get {return current_backend.max_usage;} + set {current_backend.max_usage = value;} +} + + public void send_request (string text) {current_backend.send_request (text);} + + } diff --git a/src/Services/Secrets.vala b/src/Services/Secrets.vala index d69d5bc..865ed85 100644 --- a/src/Services/Secrets.vala +++ b/src/Services/Secrets.vala @@ -34,15 +34,15 @@ public class Inscriptions.Secrets : Object { "label", Secret.SchemaAttributeType.STRING); attributes = new GLib.HashTable (str_hash, str_equal); - attributes["label"] = "DeepL-Auth-Key"; - // try { - // _cached = Secret.password_lookupv_sync (schema, attributes, null); - // print ("retrieved password!"); - // } catch (Error e) { - // warning (e.message); - // } + switch_backend_attribute (); + Application.settings.changed [KEY_BACKEND].connect (switch_backend_attribute); + } + private void switch_backend_attribute () { + var number = Application.settings.get_enum (KEY_BACKEND); + var new_backend = BackendType.from_int (number); + attributes["label"] = new_backend.to_secrets_label (); } public void store_key (string new_key) { @@ -50,7 +50,8 @@ public class Inscriptions.Secrets : Object { changed (); Secret.password_storev.begin (schema, attributes, Secret.COLLECTION_DEFAULT, - "DeepL-Auth-Key", new_key, null, (obj, async_res) => { + "API Key for translation backend", + new_key, null, (obj, async_res) => { try { bool res = Secret.password_store.end (async_res); diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index 7159fc0..30d1933 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -14,6 +14,7 @@ public class Inscriptions.HeaderBar : Granite.Bin { Gtk.HeaderBar headerbar; Gtk.Stack title_stack; Gtk.Label title_label; + Gtk.DropDown title_dropdown; Gtk.StackSwitcher title_switcher; Gtk.Revealer back_revealer; @@ -68,6 +69,12 @@ public class Inscriptions.HeaderBar : Granite.Bin { title_label = new Gtk.Label (_("Inscriptions")); title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + title_dropdown = new Gtk.DropDown.from_strings (BackendType.STRING_ALL); + title_dropdown.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + title_dropdown.add_css_class (Granite.STYLE_CLASS_FLAT); + title_dropdown.halign = Gtk.Align.CENTER; + title_dropdown.show_arrow = false; + title_switcher = new Gtk.StackSwitcher () { stack = stack_window_view }; @@ -77,8 +84,9 @@ public class Inscriptions.HeaderBar : Granite.Bin { }; title_stack.add_child (title_label); + title_stack.add_child (title_dropdown); title_stack.add_child (title_switcher); - title_stack.visible_child = title_label; + title_stack.visible_child = title_dropdown; //TRANSLATORS: Do not translate the name itself. You can write it in your writing system if that is usually done for your language headerbar = new Gtk.HeaderBar () { @@ -175,6 +183,13 @@ public class Inscriptions.HeaderBar : Granite.Bin { translate_revealer, "reveal_child", SettingsBindFlags.INVERT_BOOLEAN ); + + title_dropdown.selected = Application.settings.get_enum (KEY_BACKEND); + title_dropdown.notify["selected"].connect (on_dropdown_selected); + } + + private void on_dropdown_selected () { + Application.settings.set_enum (KEY_BACKEND, (int)title_dropdown.selected); } private void on_menu () { @@ -193,7 +208,7 @@ public class Inscriptions.HeaderBar : Granite.Bin { title_stack.visible_child = title_switcher; } else { - title_stack.visible_child = title_label; + title_stack.visible_child = title_dropdown; } } } \ No newline at end of file diff --git a/src/Widgets/Popovers/OptionsPopover.vala b/src/Widgets/Popovers/OptionsPopover.vala index 2f678bb..e830a4b 100644 --- a/src/Widgets/Popovers/OptionsPopover.vala +++ b/src/Widgets/Popovers/OptionsPopover.vala @@ -127,7 +127,7 @@ public class Inscriptions.OptionsPopover : Gtk.Popover { } // I know this could be a cool one liner, but the one liner is ugly and unreadable - if (target in DeepL.SUPPORTED_FORMALITY) { + if (target in DeepL.Backend.SUPPORTED_FORMALITY) { formalbox.sensitive = true; formalbox.tooltip_text = _("Set how formal the translation should be"); diff --git a/src/meson.build b/src/meson.build index 8d8ba29..d877124 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,15 +6,19 @@ sources = files ( 'Enums' / 'Formality.vala', 'Enums' / 'StatusCode.vala', 'Enums' / 'HighlightColor.vala', + 'Enums' / 'BackendType.vala', 'Objects' / 'Lang.vala', 'Objects' / 'DDModel.vala', - 'Services' / 'Secrets.vala', - + 'Backend' / 'BackendTemplate.vala', + 'Backend' / 'Dummy.vala', 'Backend' / 'DeepL' / 'DeepL.vala', 'Backend' / 'DeepL' / 'DeepLUtils.vala', + 'Services' / 'Secrets.vala', + 'Services' / 'BackendController.vala', + 'Widgets' / 'Panes' / 'Pane.vala', 'Widgets' / 'Panes' / 'SourcePane.vala', 'Widgets' / 'Panes' / 'TargetPane.vala',