diff --git a/po/POTFILES b/po/POTFILES index 81ffcb0..5df6141 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -4,6 +4,7 @@ src/Application.vala src/Backend/DeepL/DeepL.vala src/Constants.vala src/Enums/StatusCode.vala +src/Enums/ProviderType.vala src/Views/ErrorView.vala src/Views/LogView.vala src/Views/TranslationView.vala @@ -20,6 +21,7 @@ src/Widgets/Panes/Pane.vala src/Widgets/Panes/SourcePane.vala src/Widgets/Panes/TargetPane.vala src/Widgets/Popovers/OptionsPopover.vala +src/Widgets/Popovers/ProviderPopover.vala src/Widgets/Popovers/SettingsPopover.vala src/Widgets/PopoverWidgets/ApiEntry.vala src/Widgets/PopoverWidgets/ApiLevel.vala diff --git a/src/Backend/Backend.vala b/src/Backend/Backend.vala new file mode 100644 index 0000000..117c37b --- /dev/null +++ b/src/Backend/Backend.vala @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * a + */ +public class Inscriptions.Backend : Object { + + public signal void provider_changed (); + + private const uint TIMEOUT = 3000; //ms + public bool busy = false; // + + Secrets secrets; + Soup.Session session; + Inscriptions.Provider translation_provider; + + Inscriptions.ProviderType _provider_type; + public Inscriptions.ProviderType provider_type { + get {return _provider_type;} + set { + translation_provider = value.to_provider (); + _provider_type = value; + provider_changed (); + } + } + + // Ensure only once instance, accessible whenever needed. + private static Backend? instance; + public static Backend get_default () { + if (instance == null) { + instance = new Backend (); + } + return instance; + } + + // Only access through get_default () + private Backend () {} + + construct { + secrets = Secrets.get_default (); + + session = new Soup.Session () { + timeout = TIMEOUT + }; + + var logger = new Soup.Logger (Soup.LoggerLogLevel.BODY); + session.add_feature (logger); + + logger.set_printer ((_1, _2, dir, text) => { + stderr.printf ("%c %s\n", dir, text); + }); + + //translation_provider = Inscriptions.Providers.DeepL (); + } + + public async Inscriptions.BackendAnswer translate (Inscriptions.TranslationRequest translation_request) { + + busy = true; + + /* + // Ask Secret for API key + // Ask Provider for URL, giving it API Key + // Ask Provider for wrapped body + + // Create Msg + soup_translation_request = provider.prepare_translation_request (api_key, request); + + // send message, get answer + + // Ask provider to unwrap answer + + var answerdata = AnswerData (); + if (StatusCode == OK) { + var answer_data = translation_provider.unwrap_answer (json_answer); + var message = answer_data.message; + var language_detected = answer_data.detected_language_code; + + } else { + var message = translation_provider.unwrap_error (json_answer); + } + + + + */ + + busy = false; + + return BackendAnswer () { + status_code = StatusCode.OK, + message = "string message", + language_detected = "string? language_detected", + initial_request = translation_request + }; + } + + public Provider.Features get_supported_features () { + return translation_provider.get_supported_features (); + } + + public string[] get_supported_formality () { + return translation_provider.get_supported_formality (); + } + + public Lang[] get_source_languages () { + return translation_provider.get_source_languages (); + } + + public Lang[] get_target_languages () { + return translation_provider.get_target_languages (); + } +} diff --git a/src/Backend/BackendTemplate.vala b/src/Backend/BackendTemplate.vala deleted file mode 100644 index 46b6f72..0000000 --- a/src/Backend/BackendTemplate.vala +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) - */ - - - /* -The object has two signals: - answer_received (translated_text): This one tells us we have translated text - language_detected (detected_language_code): this one is to set language detected to detected language - - Handlers on the other side will know what to do with the signals. - - public void reload: - Set the various object properties - - public void send_request (string text) - allrounder for the service, takes a text to translate and takes care of the rest - - public string detect_system - Detects what system language code we do be do having - - public string prep_json (string text) - does the whole wrapping request into a json we can send - - public string unwrap_json (text_json) - does the whole unwrapping response from a json we get back - - - 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 - - - public void send_request (text); - public signal void answer_received (string translated_text); - public signal void language_detected (string? detected_language_code = null); - public signal void usage_retrieved (int current_word_usage, int max_word_usage); - - public const string SUPPORTED_FORMALITY -public const SUPPORTED_SOURCE -public const SUPPORTED_TARGET - - */ - -// Translation service that use translate -public abstract class MrWorldWide.DeepL : Object { - - private string source_lang; - private string target_lang; - private string api_key; - private string base_url; - public string system_language; - private string context; - - /** - * Connect to this signal to receive translated text - */ - public signal void answer_received (string translated_text = ""); - - /** - * Connect to this signal to know when language is detected - */ - public signal void language_detected (string? detected_language_code = null); - - /** - * Connect to this signal to get usage - */ - 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; - - /** - * Anything to prepare should go here - */ - public abstract void init (); - - /** - * Call this method to send asynchronously a request. - * Connect to answer_received to get a parsed answer - */ - public abstract void send_request (string text); - - /** - * Call this - */ - public abstract void check_usage (); - - -} diff --git a/src/Backend/DeepL/DeepL.vala b/src/Backend/DeepL/DeepL.vala index 424015b..09b7a17 100644 --- a/src/Backend/DeepL/DeepL.vala +++ b/src/Backend/DeepL/DeepL.vala @@ -63,7 +63,7 @@ public class Inscriptions.DeepL : Object { on_source_lang_changed (); on_target_lang_changed (); - secrets.changed.connect (debounce_check); + //secrets.changed.connect (debounce_check); Application.settings_translate.changed[KEY_SOURCE_LANGUAGE].connect (on_source_lang_changed); Application.settings_translate.changed[KEY_TARGET_LANGUAGE].connect (on_target_lang_changed); } @@ -98,7 +98,7 @@ public class Inscriptions.DeepL : Object { } public void on_key_changed () { - api_key = secrets.cached_key; + api_key = ""; if (api_key.chomp () == "") { answer_received (StatusCode.NO_KEY, _("Missing API Key")); diff --git a/src/Backend/DeepL/DeepLUtils.vala b/src/Backend/DeepL/DeepLUtils.vala index e59ad44..4d57e36 100644 --- a/src/Backend/DeepL/DeepLUtils.vala +++ b/src/Backend/DeepL/DeepLUtils.vala @@ -26,4 +26,7 @@ namespace Inscriptions.DeepLUtils { //print ("\nBackend: Detected system language: " + minicode); return minicode; } + + } + diff --git a/src/Backend/Provider.vala b/src/Backend/Provider.vala new file mode 100644 index 0000000..c228643 --- /dev/null +++ b/src/Backend/Provider.vala @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + + /** + * a + * + */ +public interface Inscriptions.Provider : Object { + + internal const string PROVIDER_SETTINGS_PREFIX = ""; + + [Flags] + public enum Features { + NONE, + CHECK_USAGE, + SET_FORMALITY, + SET_CONTEXT + } + + public struct Usage {int current; int max;} + public struct AnswerData {string message; string detected_language_code;} + + /** + * NONE is for providers who can only translate, nothing else + * + * CHECK_USAGE: Providers should have an implementation of {@link Provider.prepare_check_usage} and {@link Provider.unwrap_check_usage} + * + * SET_FORMALITY and SET_CONTEXT: {@link Inscriptions.TranslationRequest} contains both. Simply ignore it/null it if unsupported. + */ + public abstract string get_name (); + public abstract string get_auth_header (); + public abstract Features get_supported_features (); + public virtual string[] get_supported_formality () {return {};} + + /** + * Managing their internal settings is up to the provider + */ + internal abstract Settings? settings {get; set; default = null;} + + + + /** + * Provider gets as much info as possible, to allow it maximum flexibility + * + * The message is sent by the backend itself + */ + public abstract Soup.Message prepare_translation_request (string api_key, Inscriptions.TranslationRequest request); + + /** + * Errors and the explanation given by the provider should be handled and returned if the status code is not OK + * + */ + public abstract AnswerData unwrap_translation_answer (string json_answer); + + public abstract string unwrap_error (string json_answer); + + /** + * Override this if your backend supports CHECK_USAGE + * + */ + public virtual Soup.Message prepare_check_usage (string api_key) {return new Soup.Message ("", "");} + public virtual Usage unwrap_check_usage (string json_answer) {return Usage () {current = 0, max = 0};} + + + public abstract Lang[] get_source_languages (); + public abstract Lang[] get_target_languages (); +} diff --git a/src/Backend/Providers/DeepL.vala b/src/Backend/Providers/DeepL.vala new file mode 100644 index 0000000..11bc4f7 --- /dev/null +++ b/src/Backend/Providers/DeepL.vala @@ -0,0 +1,417 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ +/* + +/** + * Bespoke and soft like a bnuy + * + */ +public class Inscriptions.Providers.DeepL : Object, Provider { + + 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 string get_name () { + return "DeepL"; + } + + public string get_auth_header () { + return ProviderType.DEEPL.to_secrets_label (); + } + + public Inscriptions.Provider.Features get_supported_features () { + return CHECK_USAGE | SET_CONTEXT | SET_FORMALITY; + } + + public string[] get_supported_formality () { + return {"DE", "FR", "IT", "ES", "ES-419", "NL", "PL", "PT-BR", "PT-PT", "JA", "RU"}; + } + + private string get_correct_url (string api_key) { + + if (api_key.has_suffix (":fx")) { + return URL_DEEPL_FREE; + } + + return URL_DEEPL_PRO; + } + + + internal Bytes wrap_request_into_json (Inscriptions.TranslationRequest request) { + + var builder = new Json.Builder (); + builder.begin_object (); + builder.set_member_name ("text"); + builder.begin_array (); + builder.add_string_value (request.text_to_translate); + builder.end_array (); + + if (request.source_language_code != "idk") { + builder.set_member_name ("source_lang"); + builder.add_string_value (request.source_language_code); + } + + builder.set_member_name ("target_lang"); + builder.add_string_value (request.target_language_code); + + if (request.context != "") { + builder.set_member_name ("context"); + builder.add_string_value (request.context); + } + + if (request.target_language_code in get_supported_formality ()) { + builder.set_member_name ("formality"); + builder.add_string_value (request.formality_level.to_string ()); + } + + builder.set_member_name ("show_billed_characters"); + builder.add_boolean_value (true); + + builder.end_object (); + + Json.Generator generator = new Json.Generator (); + generator.set_root (builder.get_root ()); + string str = generator.to_data (null); + + return new Bytes (str.data); + } + + public Soup.Message prepare_translation_request (string api_key, Inscriptions.TranslationRequest request) { + + var base_url = get_correct_url (api_key); + var soup_message= new Soup.Message ("POST", base_url + REST_OF_THE_URL); + soup_message.request_headers.append ("Content-Type", "application/json"); + soup_message.request_headers.append ("User-Agent", USER_AGENT); + soup_message.request_headers.append ("Authorization", "%s %s".printf (get_auth_header (), api_key)); + soup_message.set_request_body_from_bytes ("application/json", wrap_request_into_json (request)); + + return soup_message; + } + + public Soup.Message prepare_check_usage (string api_key) { + + var base_url = get_correct_url (api_key); + var soup_message= new Soup.Message ("GET", base_url + URL_USAGE); + soup_message.request_headers.append ("Content-Type", "application/json"); + soup_message.request_headers.append ("User-Agent", USER_AGENT); + soup_message.request_headers.append ("Authorization", "%s %s".printf (get_auth_header (), api_key)); + + return soup_message; + } + + public AnswerData unwrap_translation_answer (string json_answer) { + + var parser = new Json.Parser (); + try { + parser.load_from_data (json_answer); + + } catch (Error e) { + print ("\nCannot: " + e.message); + return AnswerData () {message = json_answer, detected_language_code = ""}; + } + + var objects = parser.get_root ().get_object (); + var array = objects.get_array_member ("translations"); + var translation = array.get_object_element (0); + var billed_characters = (int)translation.get_int_member_with_default ("billed_characters", 0); + + //current_usage = current_usage + billed_characters; + //Application.settings_translate.set_int (KEY_CURRENT_USAGE, current_usage); + + var detected_language_code = translation.get_string_member_with_default ("detected_source_language", (_("Cannot detect!"))); + string translated_text = translation.get_string_member_with_default ("text", _("Cannot retrieve translated text!")); + + return AnswerData () {message = translated_text, detected_language_code = detected_language_code}; + } + + public Usage unwrap_check_usage (string json_answer) { + + var parser = new Json.Parser (); + try { + parser.load_from_data (json_answer); + + } catch (Error e) { + print ("\nCannot: " + e.message); + return Usage () {current = -1, max = -1}; + } + + var objects = parser.get_root ().get_object (); + var current_usage = (int)objects.get_int_member ("character_count"); + var max_usage = (int)objects.get_int_member ("character_limit"); + + return Usage () {current = current_usage, max = max_usage}; + } + + public string unwrap_error (string json_answer) { + + var parser = new Json.Parser (); + try { + parser.load_from_data (json_answer); + } catch (Error e) { + print ("\nCannot: " + e.message); + return json_answer; + } + + var objects = parser.get_root ().get_object (); + return objects.get_string_member_with_default ("message", _("Cannot retrieve error message text!")); + } + + internal GLib.Settings? settings { get; set; } + + public Lang[] get_source_languages () { + return { + //TRANSLATORS: The following are all languages user can select as source or target for translation + new Lang (AUTO_DETECT_LANGUAGE,_("Detect automatically")), + new Lang (SYSTEM_LANGUAGE,_("System language")), + new Lang ("ACE", dgettext (DOMAIN, "Acehnese")), + new Lang ("AF",dgettext (DOMAIN, "Afrikaans")), + new Lang ("SQ",dgettext (DOMAIN, "Albanian")), + new Lang ("AR",dgettext (DOMAIN, "Arabic")), + new Lang ("AN", dgettext (DOMAIN, "Aragonese")), + new Lang ("HY",dgettext (DOMAIN, "Armenian")), + new Lang ("AS",dgettext (DOMAIN, "Assamese")), + new Lang ("AY",dgettext (DOMAIN, "Aymara")), + new Lang ("AZ",dgettext (DOMAIN, "Azerbaijani")), + new Lang ("BA",dgettext (DOMAIN, "Bashkir")), + new Lang ("EU",dgettext (DOMAIN, "Basque")), + new Lang ("BE",dgettext (DOMAIN, "Belarusian")), + new Lang ("BN",dgettext (DOMAIN, "Bengali")), + new Lang ("BHO",dgettext (DOMAIN, "Bhojpuri")), + new Lang ("BS",dgettext (DOMAIN, "Bosnian")), + new Lang ("BR",dgettext (DOMAIN, "Breton")), + new Lang ("BG",dgettext (DOMAIN, "Bulgarian")), + new Lang ("MY",dgettext (DOMAIN, "Burmese")), + new Lang ("YUE",dgettext (DOMAIN, "Cantonese")), + new Lang ("CA",dgettext (DOMAIN, "Catalan")), + new Lang ("CEB",dgettext (DOMAIN, "Cebuano")), + new Lang ("ZH", _("%s (Unspecified variant)").printf (dgettext (DOMAIN, "Chinese"))), + new Lang ("HR",dgettext (DOMAIN, "Croatian")), + new Lang ("CS",dgettext (DOMAIN, "Czech")), + new Lang ("DA",dgettext (DOMAIN, "Danish")), + new Lang ("PRS",dgettext (DOMAIN, "Dari")), + new Lang ("NL",dgettext (DOMAIN, "Dutch")), + new Lang ("EN",_("%s (All variants)").printf (dgettext (DOMAIN, "English"))), + new Lang ("EO",dgettext (DOMAIN, "Esperanto")), + new Lang ("ET",dgettext (DOMAIN, "Estonian")), + new Lang ("FI",dgettext (DOMAIN, "Finnish")), + new Lang ("FR",dgettext (DOMAIN, "French")), + new Lang ("GL",dgettext (DOMAIN, "Galician")), + new Lang ("KA",dgettext (DOMAIN, "Georgian")), + new Lang ("DE",dgettext (DOMAIN, "German")), + new Lang ("EL",dgettext (DOMAIN, "Greek")), + new Lang ("GN",dgettext (DOMAIN, "Guarani")), + new Lang ("GU",dgettext (DOMAIN, "Gujarati")), + new Lang ("HT",dgettext (DOMAIN, "Haitian Creole")), + new Lang ("HA",dgettext (DOMAIN, "Hausa")), + new Lang ("HE",dgettext (DOMAIN, "Hebrew")), + new Lang ("HI",dgettext (DOMAIN, "Hindi")), + new Lang ("HU",dgettext (DOMAIN, "Hungarian")), + new Lang ("IS",dgettext (DOMAIN, "Icelandic")), + new Lang ("IG",dgettext (DOMAIN, "Igbo")), + new Lang ("ID",dgettext (DOMAIN, "Indonesian")), + new Lang ("GA",dgettext (DOMAIN, "Irish")), + new Lang ("IT",dgettext (DOMAIN, "Italian")), + new Lang ("JA",dgettext (DOMAIN, "Japanese")), + new Lang ("JV",dgettext (DOMAIN, "Javanese")), + new Lang ("PAM",dgettext (DOMAIN, "Kapampangan")), + new Lang ("KK",dgettext (DOMAIN, "Kazakh")), + new Lang ("GOM",dgettext (DOMAIN, "Konkani")), + new Lang ("KO",dgettext (DOMAIN, "Korean")), + new Lang ("KMR",_("%s (Kurmanji)").printf (dgettext (DOMAIN, "Kurdish"))), + new Lang ("CKB",_("%s (Sorani)").printf (dgettext (DOMAIN, "Kurdish"))), + new Lang ("KY",dgettext (DOMAIN, "Kyrgyz")), + new Lang ("LA",dgettext (DOMAIN, "Latin")), + new Lang ("LV",dgettext (DOMAIN, "Latvian")), + new Lang ("LN",dgettext (DOMAIN, "Lingala")), + new Lang ("LT",dgettext (DOMAIN, "Lithuanian")), + new Lang ("LMO",dgettext (DOMAIN, "Lombard")), + new Lang ("LB",dgettext (DOMAIN, "Luxembourgish")), + new Lang ("MK",dgettext (DOMAIN, "Macedonian")), + new Lang ("MAI",dgettext (DOMAIN, "Maithili")), + new Lang ("MG",dgettext (DOMAIN, "Malagasy")), + new Lang ("MS",dgettext (DOMAIN, "Malay")), + new Lang ("ML",dgettext (DOMAIN, "Malayalam")), + new Lang ("MT",dgettext (DOMAIN, "Maltese")), + new Lang ("MI",dgettext (DOMAIN, "Maori")), + new Lang ("MR",dgettext (DOMAIN, "Marathi")), + new Lang ("MN",dgettext (DOMAIN, "Mongolian")), + new Lang ("NE",dgettext (DOMAIN, "Nepali")), + new Lang ("NB",dgettext (DOMAIN, "Norwegian Bokmål")), + new Lang ("OC",dgettext (DOMAIN, "Occitan")), + new Lang ("OM",dgettext (DOMAIN, "Oromo")), + new Lang ("PAG",dgettext (DOMAIN, "Pangasinan")), + new Lang ("PS",dgettext (DOMAIN, "Pashto")), + new Lang ("FA",dgettext (DOMAIN, "Persian")), + new Lang ("PL",dgettext (DOMAIN, "Polish")), + new Lang ("PT",_("%s (Unspecified)").printf (dgettext (DOMAIN, "Portuguese"))), + new Lang ("PA",dgettext (DOMAIN, "Punjabi")), + new Lang ("QU",dgettext (DOMAIN, "Quechua")), + new Lang ("RO",dgettext (DOMAIN, "Romanian")), + new Lang ("RU",dgettext (DOMAIN, "Russian")), + new Lang ("SA",dgettext (DOMAIN, "Sanskrit")), + new Lang ("SR",dgettext (DOMAIN, "Serbian")), + new Lang ("ST",dgettext (DOMAIN, "Sesotho")), + new Lang ("SCN",dgettext (DOMAIN, "Sicilian")), + new Lang ("SK",dgettext (DOMAIN, "Slovak")), + new Lang ("SL",dgettext (DOMAIN, "Slovenian")), + new Lang ("ES",dgettext (DOMAIN, "Spanish")), + new Lang ("SU",dgettext (DOMAIN, "Sundanese")), + new Lang ("SW",dgettext (DOMAIN, "Swahili")), + new Lang ("SV",dgettext (DOMAIN, "Swedish")), + new Lang ("TL",dgettext (DOMAIN, "Tagalog")), + new Lang ("TG",dgettext (DOMAIN, "Tajik")), + new Lang ("TA",dgettext (DOMAIN, "Tamil")), + new Lang ("TT",dgettext (DOMAIN, "Tatar")), + new Lang ("TE",dgettext (DOMAIN, "Telugu")), + new Lang ("TH",dgettext (DOMAIN, "Thai")), + new Lang ("TS",dgettext (DOMAIN, "Tsonga")), + new Lang ("TN",dgettext (DOMAIN, "Tswana")), + new Lang ("TR",dgettext (DOMAIN, "Turkish")), + new Lang ("TK",dgettext (DOMAIN, "Turkmen")), + new Lang ("UK",dgettext (DOMAIN, "Ukrainian")), + new Lang ("UR",dgettext (DOMAIN, "Urdu")), + new Lang ("UZ",dgettext (DOMAIN, "Uzbek")), + new Lang ("VI",dgettext (DOMAIN, "Vietnamese")), + new Lang ("CY",dgettext (DOMAIN, "Welsh")), + new Lang ("WO",dgettext (DOMAIN, "Wolof")), + new Lang ("XH",dgettext (DOMAIN, "Xhosa")), + new Lang ("YI",dgettext (DOMAIN, "Yiddish")), + new Lang ("ZU",dgettext (DOMAIN, "Zulu")) + }; + } + + public Lang[] get_target_languages () { + return { + new Lang (SYSTEM_LANGUAGE,_("System language")), + new Lang ("ACE",dgettext (DOMAIN, "Acehnese")), + new Lang ("AF",dgettext (DOMAIN, "Afrikaans")), + new Lang ("SQ",dgettext (DOMAIN, "Albanian")), + new Lang ("AR",dgettext (DOMAIN, "Arabic")), + new Lang ("AN",dgettext (DOMAIN, "Aragonese")), + new Lang ("HY",dgettext (DOMAIN, "Armenian")), + new Lang ("AS",dgettext (DOMAIN, "Assamese")), + new Lang ("AY",dgettext (DOMAIN, "Aymara")), + new Lang ("AZ",dgettext (DOMAIN, "Azerbaijani")), + new Lang ("BA",dgettext (DOMAIN, "Bashkir")), + new Lang ("EU",dgettext (DOMAIN, "Basque")), + new Lang ("BE",dgettext (DOMAIN, "Belarusian")), + new Lang ("BN",dgettext (DOMAIN, "Bengali")), + new Lang ("BHO",dgettext (DOMAIN, "Bhojpuri")), + new Lang ("BS",dgettext (DOMAIN, "Bosnian")), + new Lang ("BR",dgettext (DOMAIN, "Breton")), + new Lang ("BG",dgettext (DOMAIN, "Bulgarian")), + new Lang ("MY",dgettext (DOMAIN, "Burmese")), + new Lang ("YUE",dgettext (DOMAIN, "Cantonese")), + new Lang ("CA",dgettext (DOMAIN, "Catalan")), + new Lang ("CEB",dgettext (DOMAIN, "Cebuano")), + new Lang ("ZH-HANS",_("%s (Simplified)").printf (dgettext (DOMAIN, "Chinese"))), + new Lang ("ZH-HANT",_("%s (Traditional)").printf (dgettext (DOMAIN, "Chinese"))), + new Lang ("ZH",_("%s (Unspecified variant)").printf (dgettext (DOMAIN, "Chinese"))), + new Lang ("HR",dgettext (DOMAIN, "Croatian")), + new Lang ("CS",dgettext (DOMAIN, "Czech")), + new Lang ("DA",dgettext (DOMAIN, "Danish")), + new Lang ("PRS",dgettext (DOMAIN, "Dari")), + new Lang ("NL",dgettext (DOMAIN, "Dutch")), + new Lang ("EN",_("%s (All variants)").printf (dgettext (DOMAIN, "English"))), + new Lang ("EN-US",_("%s (American)").printf (dgettext (DOMAIN, "English"))), + new Lang ("EN-GB",_("%s (British)").printf (dgettext (DOMAIN, "English"))), + new Lang ("EO",dgettext (DOMAIN, "Esperanto")), + new Lang ("ET",dgettext (DOMAIN, "Estonian")), + new Lang ("FI",dgettext (DOMAIN, "Finnish")), + new Lang ("FR",dgettext (DOMAIN, "French")), + new Lang ("GL",dgettext (DOMAIN, "Galician")), + new Lang ("KA",dgettext (DOMAIN, "Georgian")), + new Lang ("DE",dgettext (DOMAIN, "German")), + new Lang ("EL",dgettext (DOMAIN, "Greek")), + new Lang ("GN",dgettext (DOMAIN, "Guarani")), + new Lang ("GU",dgettext (DOMAIN, "Gujarati")), + new Lang ("HT",dgettext (DOMAIN, "Haitian Creole")), + new Lang ("HA",dgettext (DOMAIN, "Hausa")), + new Lang ("HE",dgettext (DOMAIN, "Hebrew")), + new Lang ("HI",dgettext (DOMAIN, "Hindi")), + new Lang ("HU",dgettext (DOMAIN, "Hungarian")), + new Lang ("IS",dgettext (DOMAIN, "Icelandic")), + new Lang ("IG",dgettext (DOMAIN, "Igbo")), + new Lang ("ID",dgettext (DOMAIN, "Indonesian")), + new Lang ("GA",dgettext (DOMAIN, "Irish")), + new Lang ("IT",dgettext (DOMAIN, "Italian")), + new Lang ("JA",dgettext (DOMAIN, "Japanese")), + new Lang ("JV",dgettext (DOMAIN, "Javanese")), + new Lang ("PAM",dgettext (DOMAIN, "Kapampangan")), + new Lang ("KK",dgettext (DOMAIN, "Kazakh")), + new Lang ("GOM",dgettext (DOMAIN, "Konkani")), + new Lang ("KO",dgettext (DOMAIN, "Korean")), + new Lang ("KMR",_("%s (Kurmanji)").printf (dgettext (DOMAIN, "Kurdish"))), + new Lang ("CKB",_("%s (Sorani)").printf (dgettext (DOMAIN, "Kurdish"))), + new Lang ("KY",dgettext (DOMAIN, "Kyrgyz")), + new Lang ("LA",dgettext (DOMAIN, "Latin")), + new Lang ("LV",dgettext (DOMAIN, "Latvian")), + new Lang ("LN",dgettext (DOMAIN, "Lingala")), + new Lang ("LT",dgettext (DOMAIN, "Lithuanian")), + new Lang ("LMO",dgettext (DOMAIN, "Lombard")), + new Lang ("LB",dgettext (DOMAIN, "Luxembourgish")), + new Lang ("MK",dgettext (DOMAIN, "Macedonian")), + new Lang ("MAI",dgettext (DOMAIN, "Maithili")), + new Lang ("MG",dgettext (DOMAIN, "Malagasy")), + new Lang ("MS",dgettext (DOMAIN, "Malay")), + new Lang ("ML",dgettext (DOMAIN, "Malayalam")), + new Lang ("MT",dgettext (DOMAIN, "Maltese")), + new Lang ("MI",dgettext (DOMAIN, "Maori")), + new Lang ("MR",dgettext (DOMAIN, "Marathi")), + new Lang ("MN",dgettext (DOMAIN, "Mongolian")), + new Lang ("NE",dgettext (DOMAIN, "Nepali")), + new Lang ("NB",dgettext (DOMAIN, "Norwegian Bokmål")), + new Lang ("OC",dgettext (DOMAIN, "Occitan")), + new Lang ("OM",dgettext (DOMAIN, "Oromo")), + new Lang ("PAG",dgettext (DOMAIN, "Pangasinan")), + new Lang ("PS",dgettext (DOMAIN, "Pashto")), + new Lang ("FA",dgettext (DOMAIN, "Persian")), + new Lang ("PL",dgettext (DOMAIN, "Polish")), + new Lang ("PT-BR",_("%s (Brazilian)").printf (dgettext (DOMAIN, "Portuguese"))), + new Lang ("PT-PT",_("%s (European)").printf (dgettext (DOMAIN, "Portuguese"))), + new Lang ("PT",_("%s (Unspecified)").printf (dgettext (DOMAIN, "Portuguese"))), + new Lang ("PA",dgettext (DOMAIN, "Punjabi")), + new Lang ("QU",dgettext (DOMAIN, "Quechua")), + new Lang ("RO",dgettext (DOMAIN, "Romanian")), + new Lang ("RU",dgettext (DOMAIN, "Russian")), + new Lang ("SA",dgettext (DOMAIN, "Sanskrit")), + new Lang ("SR",dgettext (DOMAIN, "Serbian")), + new Lang ("ST",dgettext (DOMAIN, "Sesotho")), + new Lang ("SCN",dgettext (DOMAIN, "Sicilian")), + new Lang ("SK",dgettext (DOMAIN, "Slovak")), + new Lang ("SL",dgettext (DOMAIN, "Slovenian")), + new Lang ("ES",dgettext (DOMAIN, "Spanish")), + new Lang ("ES-419",_("%s (Latin American)").printf (dgettext (DOMAIN, "Spanish"))), + new Lang ("SU",dgettext (DOMAIN, "Sundanese")), + new Lang ("SW",dgettext (DOMAIN, "Swahili")), + new Lang ("SV",dgettext (DOMAIN, "Swedish")), + new Lang ("TL",dgettext (DOMAIN, "Tagalog")), + new Lang ("TG",dgettext (DOMAIN, "Tajik")), + new Lang ("TA",dgettext (DOMAIN, "Tamil")), + new Lang ("TT",dgettext (DOMAIN, "Tatar")), + new Lang ("TE",dgettext (DOMAIN, "Telugu")), + new Lang ("TH",dgettext (DOMAIN, "Thai")), + new Lang ("TS",dgettext (DOMAIN, "Tsonga")), + new Lang ("TN",dgettext (DOMAIN, "Tswana")), + new Lang ("TR",dgettext (DOMAIN, "Turkish")), + new Lang ("TK",dgettext (DOMAIN, "Turkmen")), + new Lang ("UK",dgettext (DOMAIN, "Ukrainian")), + new Lang ("UR",dgettext (DOMAIN, "Urdu")), + new Lang ("UZ",dgettext (DOMAIN, "Uzbek")), + new Lang ("VI",dgettext (DOMAIN, "Vietnamese")), + new Lang ("CY",dgettext (DOMAIN, "Welsh")), + new Lang ("WO",dgettext (DOMAIN, "Wolof")), + new Lang ("XH",dgettext (DOMAIN, "Xhosa")), + new Lang ("YI",dgettext (DOMAIN, "Yiddish")), + new Lang ("ZU",dgettext (DOMAIN, "Zulu")) + }; + } + + construct { + settings = null; + } +} diff --git a/src/Backend/Providers/Dummy.vala b/src/Backend/Providers/Dummy.vala new file mode 100644 index 0000000..c025219 --- /dev/null +++ b/src/Backend/Providers/Dummy.vala @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ +/* + +/** + * + * Echo is intended as a test backend, and answers back whatever you send it, but modified + * + * Each "language code" is just if uppercase, lowercase, unmodified... So we can text if the plumbing works + * + * Funnily this could just be converted into some kind of text manipulation tool + */ + +/* +public class Inscriptions.Dummy : Object, Inscriptions.Provider { + + 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; + // } + //} +} + */ + +public class Inscriptions.Providers.Dummy : Object, Provider { + + public string get_name () { + return "Echo"; + } + + public string get_auth_header () { + return "Echo"; + } + + public Inscriptions.Provider.Features get_supported_features () { + return CHECK_USAGE | SET_CONTEXT | SET_FORMALITY; + } + + public string[] get_supported_formality () { + return {"same"}; + } + + public Soup.Message prepare_translation_request (string api_key, Inscriptions.TranslationRequest request) { + + return new Soup.Message ("", ""); + } + + public AnswerData unwrap_translation_answer (string json_answer) { + return AnswerData () {message = "", detected_language_code = ""}; + } + + public string unwrap_error (string json_answer) { + return "oopsie daisie!"; + } + + public Soup.Message prepare_check_usage (string api_key) { + return new Soup.Message ("", ""); + } + + public Usage unwrap_check_usage (string json_answer) { + return Usage () {current = Random.int_range (0, 101), max = 100}; + } + + internal GLib.Settings? settings { get; set; } + + public Lang[] get_source_languages () { + Lang[] langs = { + new Lang ("whatever", _("whatever")) + }; + return langs; + } + + public Lang[] get_target_languages () { + Lang[] langs = { + new Lang ("up", _("ECHO UP")), + new Lang ("same", _("Echo!")), + new Lang ("down", _("echo down")), + }; + return langs; + } + + construct { + settings = null; + } +} \ No newline at end of file diff --git a/src/Backend/Providers/LibreTranslate.vala b/src/Backend/Providers/LibreTranslate.vala new file mode 100644 index 0000000..4fcd57a --- /dev/null +++ b/src/Backend/Providers/LibreTranslate.vala @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + +/* + * + * + */ + +/* +public class Inscriptions.Dummy : Object, Inscriptions.Provider { + +} + */ \ No newline at end of file diff --git a/src/Constants.vala b/src/Constants.vala index b2b84e8..ac09ef8 100644 --- a/src/Constants.vala +++ b/src/Constants.vala @@ -62,6 +62,14 @@ namespace Inscriptions { const string DOMAIN = "iso_639_3"; +#if WINDOWS + const string USER_AGENT = APP_ID + "-" + APP_VERSION + " (Windows)"; +#else + const string USER_AGENT = APP_ID + "-" + APP_VERSION + " (Linux)"; +#endif + + + // https://developers.deepl.com/docs/getting-started/supported-languages // TODO: In the far future people might declare their own in a backend file public Lang[] SourceLang () { diff --git a/src/Enums/ProviderType.vala b/src/Enums/ProviderType.vala new file mode 100644 index 0000000..8573d1e --- /dev/null +++ b/src/Enums/ProviderType.vala @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Stella & Charlie (teamcons.carrd.co) + */ + + +// vala-lint=skip-file + +/** + * A convenient way to track all existing BackendType + */ +public enum Inscriptions.ProviderType { + DEEPL, + LIBRETRANSLATE, + DUMMY; + + public Provider to_provider () { + switch (this) { + case DUMMY: return new Providers.Dummy (); + case DEEPL: return new Providers.Dummy (); + case LIBRETRANSLATE: return new Providers.Dummy (); + default: return new Providers.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 ProviderType from_int (int number) { + switch (number) { + case 0: return DEEPL; + case 1: return LIBRETRANSLATE; + case 3: return DUMMY; + default: return DEEPL; + } + } + + public const ProviderType[] ALL = {DEEPL, LIBRETRANSLATE, DUMMY}; + public const string[] STRING_ALL = {N_("DeepL"), N_("LibreTranslate"), N_("Dummy")}; +} \ No newline at end of file diff --git a/src/Enums/StatusCode.vala b/src/Enums/StatusCode.vala index e923de7..1290425 100644 --- a/src/Enums/StatusCode.vala +++ b/src/Enums/StatusCode.vala @@ -26,7 +26,7 @@ public enum Inscriptions.StatusCode { FORBIDDEN = Soup.Status.FORBIDDEN, REQUEST_TIMEOUT = Soup.Status.REQUEST_TIMEOUT, TOO_MANY_REQUESTS = 429, - QUOTA = 456, + QUOTA_REACHED = 456, // 500 INTERNAL_SERVER_ERROR = Soup.Status.INTERNAL_SERVER_ERROR, @@ -92,7 +92,7 @@ public enum Inscriptions.StatusCode { icon_name = "dialog-warning"; return; - case StatusCode.QUOTA: + case StatusCode.QUOTA_REACHED: explanation_title = _("Your monthly quota has been exceeded"); explanation_text = _("If you are a Pro API user, this corresponds to your Cost Control limit"); icon_name = "dialog-warning"; diff --git a/src/Objects/Structs.vala b/src/Objects/Structs.vala new file mode 100644 index 0000000..5e5f2b0 --- /dev/null +++ b/src/Objects/Structs.vala @@ -0,0 +1,32 @@ + +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * The GUI sends this to the Backend, which will read relevant fields and process + */ +public struct Inscriptions.TranslationRequest { + int provider; + string source_language_code; + string target_language_code; + string text_to_translate; + Inscriptions.Formality? formality_level; + string? context; +} + +/** + * The Backend will answer with this and let the GUI decide wtf to do with it. + * + * If the status code is not a success, message will be an error code, and the initial request will be non-null + * + * language_detected will be null if irrelevant + * The + */ +public struct Inscriptions.BackendAnswer { + StatusCode status_code; + string message; + string? language_detected; + TranslationRequest? initial_request; +} diff --git a/src/Services/BackendController.vala b/src/Services/BackendController.vala deleted file mode 100644 index af84274..0000000 --- a/src/Services/BackendController.vala +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) - */ - -/** - * a - */ -public class Inscriptions.BackendController : Object { - - construct { - - } -} diff --git a/src/Services/Secrets.vala b/src/Services/Secrets.vala index bc61913..43e5377 100644 --- a/src/Services/Secrets.vala +++ b/src/Services/Secrets.vala @@ -4,13 +4,12 @@ */ /** - * Wrapper to handle loading/Saving the DeepL API Key safely. + * Wrapper to handle loading/Saving each key in a quick and smart manner + * * It is done asynchronously to not have the UI hang and freeze. */ public class Inscriptions.Secrets : Object { - public signal void changed (); - // Ensure only once instance, accessible whenever needed. private static Secrets? instance; public static Secrets get_default () { @@ -20,37 +19,35 @@ public class Inscriptions.Secrets : Object { return instance; } - private string _cached = ""; - public string cached_key { - get { return _cached ?? "";} - set { store_key (value);} - } - - Secret.Schema schema; - GLib.HashTable attributes; + // Only access through get_default () + private Secrets () {} + private Secret.Schema schema; + private GLib.HashTable cached_keys; construct { schema = new Secret.Schema (APP_ID, Secret.SchemaFlags.NONE, "label", Secret.SchemaAttributeType.STRING); - attributes = new GLib.HashTable (str_hash, str_equal); - attributes["label"] = "DeepL-Auth-Key"; + cached_keys = new GLib.HashTable (str_hash, str_equal); + } - // try { - // _cached = Secret.password_lookupv_sync (schema, attributes, null); - // print ("retrieved password!"); - // } catch (Error e) { - // warning (e.message); - // } + /** + * Save a specific key according to its provider + * + * The function is sync, because it saves a cache real quick, then does the stuff in the background without blocking the UI + */ + public void store_key (ProviderType provider, string new_key) { - } + // Save a cache first before doing the heavy lifting + cached_keys[provider.to_secrets_label ()] = new_key; - public void store_key (string new_key) { - _cached = new_key; - changed (); + // Lets go lesbians + var attributes = new GLib.HashTable (str_hash, str_equal); + attributes["label"] = provider.to_secrets_label (); Secret.password_storev.begin (schema, attributes, Secret.COLLECTION_DEFAULT, - "DeepL-Auth-Key", new_key, null, (obj, async_res) => { + provider.to_secrets_label (), new_key, + null, (obj, async_res) => { try { bool res = Secret.password_store.end (async_res); @@ -62,16 +59,34 @@ public class Inscriptions.Secrets : Object { }); } - public async string load_secret () { - var key = ""; + /** + * Load a specific key according to its provider + * + * If it answers null, then theres none we have and the user will get an error for the provider they chose + * + * Despite being async, if the key has been retrieved or stored at least once since app start, this may return very quick + */ + public async string? load_key (ProviderType provider) { + + // Do we have it already? + var? cached = cached_keys[provider.to_secrets_label ()]; + if (cached != null) { + return cached; + } + + // We dont. Do it now. + var attributes = new GLib.HashTable (str_hash, str_equal); + attributes["label"] = provider.to_secrets_label (); + string? key = null; + try { key = yield Secret.password_lookupv (schema, attributes, null); + } catch (Error e) { print (e.message); - } - _cached = key ?? ""; - return key ?? ""; + + return key; } diff --git a/src/Widgets/HeaderBar.vala b/src/Widgets/HeaderBar.vala index ee72053..070bfa3 100644 --- a/src/Widgets/HeaderBar.vala +++ b/src/Widgets/HeaderBar.vala @@ -67,11 +67,25 @@ public class Inscriptions.HeaderBar : Granite.Bin { }; #endif + var provider_popover = new ProviderPopover (); + + var provider_menu = new Gtk.MenuButton () { + label = "", + has_frame = false, + popover = provider_popover + }; + + provider_popover.bind_property ("selected-name", + provider_menu, "label", + GLib.BindingFlags.DEFAULT | SYNC_CREATE); + + + title_switcher = new Gtk.StackSwitcher () { stack = stack_window_view }; - switchwidget= new Inscriptions.SwitchWidget (title_label, title_switcher) { + switchwidget= new Inscriptions.SwitchWidget (provider_menu, title_switcher) { transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN }; @@ -144,7 +158,7 @@ public class Inscriptions.HeaderBar : Granite.Bin { #if DEVEL //menu_popover.autohide = false; - switcher_state (true); + //switcher_state (true); #endif /* -------------------- CONNECTS AND BINDS -------------------- */ diff --git a/src/Widgets/PopoverWidgets/ApiEntry.vala b/src/Widgets/PopoverWidgets/ApiEntry.vala index f51c729..64278b3 100644 --- a/src/Widgets/PopoverWidgets/ApiEntry.vala +++ b/src/Widgets/PopoverWidgets/ApiEntry.vala @@ -39,21 +39,20 @@ public class Inscriptions.ApiEntry : Gtk.Box { } private async void fill_key () { - api_entry.text = yield secrets.load_secret (); + api_entry.text = ""; //yield secrets.load_secret (); // Connects only once we set up, to avoid the app doing a request on start up - secrets.changed.connect (on_key_changed); api_entry.changed.connect (on_entry_changed); } private void on_key_changed () { api_entry.changed.disconnect (on_entry_changed); - api_entry.text = secrets.cached_key; + api_entry.text = ""; api_entry.changed.connect (on_entry_changed); } private void on_entry_changed () { - secrets.store_key (api_entry.text); + return; //"secrets.store_key (api_entry.text);" } private void paste_from_clipboard () { diff --git a/src/Widgets/Popovers/ProviderPopover.vala b/src/Widgets/Popovers/ProviderPopover.vala new file mode 100644 index 0000000..424c751 --- /dev/null +++ b/src/Widgets/Popovers/ProviderPopover.vala @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025-2026 Stella & Charlie (teamcons.carrd.co) + */ + +/** + * Popover for the Options button, in the SourcePane. Displays advanced options for translation + * Formality and Context are connected to settings, Formality depends on target language. + */ +public class Inscriptions.ProviderPopover : Gtk.Popover { + + public string selected_name {get; private set;} + SimpleAction selected_provider_action; + + construct { + width_request = 260; + //halign = Gtk.Align.START; + + var box = new Gtk.Box (VERTICAL,0) { + margin_top = MARGIN_MENU_BIG, + margin_bottom = MARGIN_MENU_BIG, + margin_start = MARGIN_MENU_BIG, + margin_end = MARGIN_MENU_BIG + }; + + var select_provider = new Gtk.CheckButton.with_label (_("DeepL")); + + var libretranslate_checkbutton = new Gtk.CheckButton.with_label (_("Libretranslate")) { + group = select_provider + }; + + var dummy_checkbutton = new Gtk.CheckButton.with_label (_("Echo")) { + group = select_provider + }; + + box.append (select_provider); + box.append (libretranslate_checkbutton); + box.append (dummy_checkbutton); + + child = box; + + selected_provider_action = new SimpleAction.stateful ("provider", GLib.VariantType.INT32, new Variant.int32 (0)); + + var action_group = new SimpleActionGroup (); + action_group.add_action (selected_provider_action); + insert_action_group ("select_provider", action_group); + + selected_provider_action.activate.connect (set_broadcast); + } + + private void set_broadcast (GLib.Variant? value) { + if (!selected_provider_action.get_state ().equal (value)) { + selected_provider_action.set_state (value); + selected_name = ((ProviderType)value).to_name (); + } + } +} diff --git a/src/Windows/MainWindow.vala b/src/Windows/MainWindow.vala index e33363f..180c9f4 100644 --- a/src/Windows/MainWindow.vala +++ b/src/Windows/MainWindow.vala @@ -116,7 +116,7 @@ public class Inscriptions.MainWindow : Gtk.ApplicationWindow { * Load the API key asyncally, and complain if there is none */ private async bool check_up_key () { - string key = yield Secrets.get_default ().load_secret (); + string key = ""; //yield Secrets.get_default ().load_secret (); if (key.chomp () == "") { on_error (Inscriptions.StatusCode.NO_KEY, _("No saved API Key")); diff --git a/src/meson.build b/src/meson.build index b56c559..dbec5b8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -35,13 +35,19 @@ sources = files ( 'Enums' / 'StatusCode.vala', 'Enums' / 'HighlightColor.vala', # 'Enums' / 'HeatmapLevel.vala', + 'Enums' / 'ProviderType.vala', + 'Objects' / 'Structs.vala', 'Objects' / 'Lang.vala', 'Objects' / 'DDModel.vala', 'Services' / 'Secrets.vala', 'Services' / 'ZoomController.vala', + 'Backend' / 'Backend.vala', + 'Backend' / 'Provider.vala', + 'Backend' / 'Providers' / 'DeepL.vala', + 'Backend' / 'Providers' / 'Dummy.vala', 'Backend' / 'DeepL' / 'DeepL.vala', 'Backend' / 'DeepL' / 'DeepLUtils.vala', @@ -66,6 +72,7 @@ sources = files ( 'Widgets' / 'Popovers' / 'SettingsPopover.vala', 'Widgets' / 'Popovers' / 'OptionsPopover.vala', + 'Widgets' / 'Popovers' / 'ProviderPopover.vala', 'Widgets' / 'PopoverWidgets' / 'ApiEntry.vala', 'Widgets' / 'PopoverWidgets' / 'ApiLevel.vala',