wordflow-cli 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ # ==============================================================================
2
+ # Configuration for wordflow CLI
3
+ # Default Location: ~/.config/wordflow/config.toml
4
+ # ==============================================================================
5
+ # ------------------------------------------------------------------------------
6
+ [global]
7
+ # ISO 639-1 language codes (e.g., "en", "nl", "fr", "es")
8
+ # only exception is chinese: you can choose between zh-cn and zh-tw for simplified or traditional translation
9
+ # full list at: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
10
+ source_language = "auto" # translate from
11
+ target_language = "en" # translate to
12
+
13
+ enable_notifications = true # Send system notifications
14
+
15
+ # ------------------------------------------------------------------------------
16
+ # DEFAULT ANKI CONFIGURATION
17
+ # Used by default for all languages unless language-based overrides are specified
18
+ # ------------------------------------------------------------------------------
19
+ [anki]
20
+ [anki.default]
21
+ # default AnkiConnect URL
22
+ url = "http://localhost:8765"
23
+
24
+ # Target deck name.
25
+ # Supports dynamic placeholders: {source_language}, {target_language}
26
+ # Example: "Languages::{source_language}" creates cards in the deck: "Languages::es" if 'es' is your source_language
27
+ deck = "Languages::{source_language}"
28
+
29
+ # The name of the note model in Anki
30
+ card_model = "wordflow cloze"
31
+
32
+ # Map your personal field names
33
+ #field_names = ["front", "translation", "additional notes"]
34
+
35
+ # Allow duplicate cards if the same word/cloze already exists in the deck?
36
+ allow_duplicates = false
37
+
38
+ # Default tags automatically attached to every card added by wordflow
39
+ tags = ["wordflow", "auto-generated"]
40
+
41
+ # Audio: choose between none|word|sentence|both.
42
+ # Adds automatic audio recordings of the selection
43
+ audio_mode = "sentence"
44
+ # Dictionnary url
45
+ # if a dict_url scheme is specified, it will add a hyperlink to the cloze field
46
+ # dynamic placeholders are: {source_language} {target_language} {word}
47
+ # examples:
48
+ # dict_url=https://context.reverso.net/translation/{source_language}-{target_language}/{word}
49
+ # dict_url=https://en.wiktionary.org/wiki/{word}
50
+ dict_url = "https://glosbe.com/{source_language}/{target_language}/{word}"
51
+
52
+ # feel free to tweak those values if you use these fields
53
+ max_alternates = 3
54
+ max_definitions = 2
55
+ max_synonyms = 2
56
+ max_examples = 1
57
+ # ------------------------------------------------------------------------------
58
+ # FIELD MAPPINGS
59
+ # ------------------------------------------------------------------------------
60
+ # Map your personal Anki field names (left) to Wordflow data placeholders (right).
61
+ # You can combine multiple placeholders and use HTML tags like <br> or <b>.
62
+ # missing fields are automatically left blank
63
+ #
64
+ # Available placeholders:
65
+ # {cloze} - The sentence with the {{c1::word}} deletion
66
+ # {translation} - The translation of the full sentence
67
+ # {source_word} - The isolated highlighted word
68
+ # {word_audio}
69
+ # {sentence_audio}
70
+ # {sentence_phonetic} - Pinyin / Romaji / Phonetics (if available)
71
+ # {word_phonetic} - Pinyin / Romaji / Phonetics (if available)
72
+ # {alternates} - Alternative translations of the word (if available)
73
+ # {definitions} - Dictionary definitions of the word (if available)
74
+ # {synonyms} - Synonyms (if available)
75
+ # {examples} - Example sentences containing the word (if available)
76
+ [anki.default.fields]
77
+ "Sentence" = "{cloze}"
78
+ "Translation" = "{translation}"
79
+ "Audio" = "{word_audio} &nbsp;&nbsp;&nbsp; {sentence_audio}"
80
+ "Phonetics" = "{word_phonetic} &nbsp;&nbsp;&nbsp; {sentence_phonetic}"
81
+ "Definitions" = "{definitions}"
82
+ "Synonyms" = "{synonyms}"
83
+ "Alternates" = "{alternates}"
84
+ "Examples" = "{examples}"
85
+ # ------------------------------------------------------------------------------
86
+ # LANGUAGE-SPECIFIC OVERRIDES
87
+ # Triggered when the source language is detected, or when passing -l/--lang <language_iso_code>
88
+ # use it to change the deck, card model, fields, etc. dynamically based on detected language
89
+ # ------------------------------------------------------------------------------
90
+
91
+ # Override for Chinese (-l zh-cn)
92
+ [anki.zh-cn]
93
+ deck = "Languages::Chinese"
94
+
95
+ # Override for Dutch (-l nl)
96
+ [anki.nl]
97
+ deck = "Languages::Nederlands"
98
+
99
+ [anki.es]
100
+ # experimental: change the accent by changing the top-level domain of the tts request. Please refer to https://gtts.readthedocs.io/en/latest/module.html#localized-accents for more info
101
+ audio_accent = "com.mx"
102
+
103
+
wordflow/data/logo.txt ADDED
@@ -0,0 +1,11 @@
1
+ $$\ $$\ $$\ $$$$$$$$\ $$\
2
+ $$ | $\ $$ | $$ | $$ _____|$$ |
3
+ $$ |$$$\ $$ | $$$$$$\ $$$$$$\ $$$$$$$ | $$ | $$ | $$$$$$\ $$\ $$\ $$\
4
+ $$ $$ $$\$$ |$$ __$$\ $$ __$$\ $$ __$$ | $$$$$\ $$ |$$ __$$\ $$ | $$ | $$ |
5
+ $$$$ _$$$$ |$$ / $$ |$$ | \__|$$ / $$ | $$ __| $$ |$$ / $$ |$$ | $$ | $$ |
6
+ $$$ / \$$$ |$$ | $$ |$$ | $$ | $$ | $$ | $$ |$$ | $$ |$$ | $$ | $$ |
7
+ $$ / \$$ |\$$$$$$ |$$ | \$$$$$$$ | $$ | $$ |\$$$$$$ |\$$$$$\$$$$ |
8
+ \__/ \__| \______/ \__| \_______| \__| \__| \______/ \_____\____/
9
+
10
+
11
+
@@ -0,0 +1,6 @@
1
+ _ __ __ ________
2
+ | | / /___ _________/ / / ____/ /___ _ __
3
+ | | /| / / __ \/ ___/ __ / / /_ / / __ \ | /| / /
4
+ | |/ |/ / /_/ / / / /_/ / / __/ / / /_/ / |/ |/ /
5
+ |__/|__/\____/_/ \__,_/ /_/ /_/\____/|__/|__/
6
+
wordflow/my_classes.py ADDED
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class AnkiConfig:
6
+ url: str
7
+ deck: str
8
+ card_model: str
9
+ tags: list[str]
10
+ dict_url: str | None
11
+ allow_duplicates: bool
12
+ fields: dict
13
+ audio_mode: str = "none"
14
+ audio_accent: str = "com"
15
+ max_synonyms: int = 3
16
+ max_alternates: int = 3
17
+ max_definitions: int = 2
18
+ max_examples: int = 1
19
+
20
+
21
+ @dataclass
22
+ class GlobalConfig:
23
+ source_language: str
24
+ target_language: str
25
+ enable_notifications: bool = True
26
+
27
+
28
+ @dataclass
29
+ class TranslationData:
30
+ source_data: str
31
+ translated_data: str
32
+ source_language: str
33
+ target_language: str
34
+ phonetic: str | None
35
+ alternate_translations: list[str] | None
36
+ synonyms: list[str] | None
37
+ definitions: list[str] | None
38
+ examples: list[str] | None
@@ -0,0 +1,77 @@
1
+ import subprocess
2
+ import shutil
3
+ import sys
4
+
5
+ from wordflow.clipboard import copy_to_clipboard
6
+
7
+
8
+ def notify(
9
+ title: str,
10
+ message: str,
11
+ urgency: str = "normal",
12
+ timeout: int = 5000,
13
+ enable_notifications=True,
14
+ copy_output_to_clipboard=False,
15
+ ):
16
+ """
17
+ Sends a Linux desktop notification using notify-send, or simply prints message to terminal
18
+ """
19
+ if not enable_notifications:
20
+ print(title, "\n", message)
21
+ return
22
+
23
+ # Check if notify-send is installed on the user's system
24
+ if not shutil.which("notify-send"):
25
+ print(
26
+ f"[Warning] 'notify-send' is not installed. Missing notification: {title}",
27
+ file=sys.stderr,
28
+ )
29
+ return
30
+
31
+ try:
32
+ if not copy_output_to_clipboard:
33
+ subprocess.run(
34
+ [
35
+ "notify-send",
36
+ "-a",
37
+ "Wordflow", # App name shown in notification center
38
+ "-u",
39
+ urgency, # Urgency: low, normal, critical
40
+ "-t",
41
+ str(timeout), # Timeout in milliseconds
42
+ title,
43
+ message,
44
+ ],
45
+ check=True,
46
+ capture_output=True,
47
+ text=True,
48
+ )
49
+ else:
50
+ process = subprocess.run(
51
+ [
52
+ "notify-send",
53
+ "-a",
54
+ "Wordflow",
55
+ "--action=default=Copy",
56
+ "--wait",
57
+ "-u",
58
+ urgency,
59
+ "-t",
60
+ str(timeout),
61
+ title,
62
+ message,
63
+ ],
64
+ check=True,
65
+ capture_output=True,
66
+ text=True,
67
+ )
68
+ if process.stdout.strip() == "default":
69
+ # Push the translation to the Wayland clipboard
70
+ copy_to_clipboard(message)
71
+
72
+ except subprocess.CalledProcessError as e:
73
+ print(
74
+ f"[Warning] Notification daemon error: {e.stderr.strip()}", file=sys.stderr
75
+ )
76
+ except Exception as e:
77
+ print(f"[Warning] Failed to send notification '{title}': {e}", file=sys.stderr)
@@ -0,0 +1,49 @@
1
+ import base64
2
+ import requests
3
+
4
+
5
+ class AudioError(Exception):
6
+ """Custom exception raised when TTS generation fails."""
7
+
8
+ pass
9
+
10
+
11
+ def get_audio(text: str, language: str, accent: str = ""):
12
+ """
13
+ Fetches TTS audio directly from Google's widget API.
14
+ Returns a base64 UTF-8 encoded string ready for AnkiConnect.
15
+
16
+ :param text: The text to be spoken.
17
+ :param source_language: ISO code (e.g., 'en', 'fr', 'zh').
18
+ :param accent: Optional TLD (e.g., 'co.uk', 'com.au', 'ca') to change the accent.
19
+ """
20
+ text_to_speak = text.strip()
21
+ if not text_to_speak:
22
+ return ""
23
+ # Safety mechanism: Google's TTS endpoint has a strict 200-character limit.
24
+ # Truncate to prevent HTTP 400 Bad Request errors on massive clipboard copies.
25
+ if len(text_to_speak) > 200:
26
+ text_to_speak = text_to_speak[:200]
27
+ # Accent trick
28
+ tld = accent.strip() if accent else "com"
29
+ url = f"https://translate.google.{tld}/translate_tts"
30
+ params = {
31
+ "ie": "UTF-8",
32
+ "q": text_to_speak,
33
+ "tl": language,
34
+ "client": "tw-ob", # Bypasses the token generator
35
+ }
36
+
37
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
38
+ try:
39
+ response = requests.get(url, params=params, headers=headers, timeout=5)
40
+ response.raise_for_status()
41
+
42
+ audio_bytes = response.content
43
+
44
+ audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
45
+
46
+ return audio_b64
47
+
48
+ except requests.RequestException as e:
49
+ raise AudioError(f"Failed to fetch audio from Google TTS: {str(e)}")
wordflow/translator.py ADDED
@@ -0,0 +1,170 @@
1
+ import requests
2
+ from time import sleep
3
+
4
+ from wordflow.my_classes import TranslationData
5
+
6
+
7
+ class TranslationError(Exception):
8
+ """Custom exception raised when translation fails to prevent bad Anki cards."""
9
+
10
+ pass
11
+
12
+
13
+ def normalize_lang_code(google_code: str) -> str:
14
+ """Converts Google's legacy or dialect-specific codes into ISO 639-1."""
15
+ if not google_code:
16
+ return ""
17
+
18
+ code = google_code.lower()
19
+
20
+ legacy_map = {
21
+ "iw": "he", # Hebrew
22
+ "in": "id", # Indonesian
23
+ "jw": "jv", # Javanese
24
+ "ji": "yi", # Yiddish
25
+ }
26
+ if code in legacy_map:
27
+ return legacy_map[code]
28
+
29
+ if code in ["zh-cn", "zh-tw"]:
30
+ return code
31
+
32
+ # 2. Strip regional subtags (e.g., 'zh-cn' -> 'zh', 'en-us' -> 'en')
33
+ if "-" in code:
34
+ code = code.split("-")[0]
35
+
36
+ return code
37
+
38
+
39
+ def translate(
40
+ text: str,
41
+ source_language: str = "auto",
42
+ target_language: str = "en",
43
+ max_retries: int = 3,
44
+ ) -> TranslationData:
45
+ """
46
+ Takes text in any language and translates it into target_language.
47
+ Returns a dictionnary containing the translation, source language used, and additional info in case of singe word translation: pronounciation, alternate translations, definition, synonyms, examples
48
+ """
49
+ text_to_translate = text.strip()
50
+ if not text_to_translate:
51
+ raise TranslationError("No text was provided for translation.")
52
+
53
+ url = "https://translate.googleapis.com/translate_a/single"
54
+ params = {
55
+ "client": "gtx", # The magical bypass client
56
+ "sl": source_language, # (auto is supported)
57
+ "tl": target_language,
58
+ "q": text, # The text to translate
59
+ "dt": [
60
+ "t",
61
+ "bd",
62
+ "rm",
63
+ "md",
64
+ "ss",
65
+ "ex",
66
+ ], # Request the "translation", pronounciation, alternate translations, definition, synonyms, and examples
67
+ }
68
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
69
+
70
+ last_error = None
71
+
72
+ for _ in range(max_retries):
73
+ try:
74
+ response = requests.get(url, params=params, headers=headers, timeout=5)
75
+ # FAIL FAST: If Google rate-limits, abort instantly to avoid extending the ban.
76
+ if response.status_code == 429:
77
+ raise TranslationError(
78
+ "Google temporarily rate-limited your IP (HTTP 429). Please wait a few minutes."
79
+ )
80
+ # other errors
81
+ response.raise_for_status()
82
+
83
+ data = parse_translation_data(response.json())
84
+ translation_data = TranslationData(
85
+ source_data=text,
86
+ translated_data=data["translation"],
87
+ source_language=data["source_language"],
88
+ target_language=target_language,
89
+ phonetic=data.get("phonetic"),
90
+ alternate_translations=data.get("alternates", ""),
91
+ synonyms=data.get("synonyms"),
92
+ definitions=data.get("definitions"),
93
+ examples=data.get("examples"),
94
+ )
95
+ # DEBUG
96
+ print("json data: ", response.json())
97
+ print("translation data: ", translation_data)
98
+ return translation_data
99
+
100
+ except requests.RequestException as e:
101
+ last_error = e
102
+ sleep(1) # wait ane retry
103
+
104
+ raise TranslationError(f"Failed to translate. Last error: {last_error}")
105
+
106
+
107
+ def parse_translation_data(data) -> dict:
108
+ # parse TRANSLATION
109
+ translation = "".join([sentence[0] for sentence in data[0] if sentence[0]])
110
+ # parse PHONETIC
111
+ phonetic = ""
112
+ for item in data[0]:
113
+ # Romanizations are placed in the 3rd index of a specific sub-array
114
+ if len(item) > 3 and item[3]:
115
+ phonetic = item[3]
116
+ break
117
+ # 3. parse DICTIONARY (Alternate translations)
118
+ # data[1] exists if it's a single word and dt=bd was requested
119
+ alternates = []
120
+ if len(data) > 1 and data[1]:
121
+ for pos_group in data[1]:
122
+ # pos_group[0] is the part of speech (e.g., "noun")
123
+ # pos_group[1] is a list of translated words
124
+ if len(pos_group) > 1:
125
+ alternates.extend(pos_group[1])
126
+ # SOURCE LANGUAGE
127
+ detected_source_language = data[2]
128
+ iso_source_language = normalize_lang_code(detected_source_language)
129
+
130
+ definitions = []
131
+ synonyms = []
132
+ examples = []
133
+ # 4. parse SYNONYMS
134
+ ## Located at data[11]. Structure: [ ["noun", [ [ ["syn1", "syn2"], "id" ] ] ], ... ]
135
+ if len(data) > 11 and data[11]:
136
+ for pos_group in data[11]:
137
+ if len(pos_group) > 1 and isinstance(pos_group[1], list):
138
+ for entry in pos_group[1]:
139
+ if (
140
+ isinstance(entry, list)
141
+ and len(entry) > 0
142
+ and isinstance(entry[0], list)
143
+ ):
144
+ synonyms.extend(entry[0])
145
+
146
+ # 5. parse DEFINITIONS
147
+ # Located at data[12]. Structure: [ ["noun", [ ["definition1", "id", "example"], ... ] ], ... ]
148
+ if len(data) > 12 and data[12]:
149
+ for pos_group in data[12]:
150
+ if len(pos_group) > 1 and isinstance(pos_group[1], list):
151
+ for entry in pos_group[1]:
152
+ if isinstance(entry, list) and len(entry) > 0:
153
+ definitions.append(entry[0])
154
+
155
+ # 6. parse EXAMPLES
156
+ # Located at data[13]. Structure: [ [ ["example 1 with <b>tags</b>", ...], ... ] ]
157
+ if len(data) > 13 and data[13]:
158
+ if isinstance(data[13], list) and len(data[13]) > 0:
159
+ for ex_group in data[13][0]:
160
+ if isinstance(ex_group, list) and len(ex_group) > 0:
161
+ examples.append(str(ex_group[0]))
162
+ return {
163
+ "translation": translation,
164
+ "phonetic": phonetic,
165
+ "alternates": list(set(alternates)),
166
+ "definitions": definitions,
167
+ "synonyms": list(set(synonyms)),
168
+ "examples": examples,
169
+ "source_language": iso_source_language,
170
+ }
wordflow/wizard.py ADDED
@@ -0,0 +1,191 @@
1
+ import os
2
+ from pathlib import Path
3
+ from importlib.resources import files
4
+
5
+
6
+ def _prompt(text: str, default: str) -> str:
7
+ """Helper to prompt for a string with a fallback default."""
8
+ ans = input(f"{text} [{default}]: ").strip()
9
+ return ans if ans else default
10
+
11
+
12
+ def _prompt_bool(text: str, default: bool) -> bool:
13
+ """Helper to prompt for a boolean (y/n)."""
14
+ default_str = "Y/n" if default else "y/N"
15
+ ans = input(f"{text} [{default_str}]: ").strip().lower()
16
+ if not ans:
17
+ return default
18
+ return ans in ("y", "yes", "true")
19
+
20
+
21
+ def _prompt_int(text: str, default: int) -> int:
22
+ """Helper to prompt for an integer."""
23
+ while True:
24
+ ans = input(f"{text} [{default}]: ").strip()
25
+ if not ans:
26
+ return default
27
+ if ans.isdigit():
28
+ return int(ans)
29
+ print("[-] Invalid input. Please type a valid number.\n")
30
+
31
+
32
+ def _prompt_list(text: str, default: list[str]) -> list[str]:
33
+ """Helper to prompt for a comma-separated list."""
34
+ default_str = ", ".join(default)
35
+ ans = input(f"{text} [{default_str}]: ").strip()
36
+ if not ans:
37
+ return default
38
+ return [item.strip() for item in ans.split(",") if item.strip()]
39
+
40
+
41
+ def _prompt_numbered_choice(text: str, choices: list[str], default: str) -> str:
42
+ """Prompts the user to choose from a numbered list of options."""
43
+ print(f"{text} [Default: {default}]")
44
+
45
+ for i, choice in enumerate(choices, 1):
46
+ print(f" {i}. {choice}")
47
+
48
+ while True:
49
+ ans = input("Select a number (or press ENTER for default): ").strip()
50
+
51
+ if not ans:
52
+ return default
53
+
54
+ if ans.isdigit():
55
+ idx = int(ans)
56
+ if 1 <= idx <= len(choices):
57
+ return choices[idx - 1]
58
+
59
+ for choice in choices:
60
+ if ans.lower() == choice.lower():
61
+ return choice
62
+
63
+ print(
64
+ f"[-] Invalid input. Please type a number between 1 and {len(choices)}.\n"
65
+ )
66
+
67
+
68
+ def _display_ascii_art():
69
+ """Attempts to load and print the ASCII art logo from the package data."""
70
+ try:
71
+ art = files("wordflow").joinpath("data", "logo.txt").read_text(encoding="utf-8")
72
+ print(f"\033[94m{art}\033[0m")
73
+ except Exception:
74
+ pass
75
+
76
+
77
+ def launch_wizard() -> dict:
78
+ """
79
+ Launches an interactive command-line wizard to configure Wordflow.
80
+ Returns a dictionary structured for the config.toml file.
81
+ """
82
+ config_path = Path(os.path.expanduser("~/.config/wordflow/config.toml"))
83
+
84
+ _display_ascii_art()
85
+ print("=" * 60)
86
+ print(" Welcome to the Wordflow Setup Wizard!")
87
+ print(" This will generate your configuration file at:")
88
+ print(f" {config_path}")
89
+ print(" Press ENTER to accept the [default] values.")
90
+ print("=" * 60 + "\n")
91
+
92
+ # --- GLOBAL ---
93
+ print("--- 1. Language & Global Settings ---")
94
+ source_language = _prompt(
95
+ "Source language (translate from) [auto, en, zh-cn...]", "auto"
96
+ )
97
+ target_language = _prompt("Target language (translate to) [en, fr, nl...]", "en")
98
+ enable_notifs = _prompt_bool("Enable desktop notifications?", True)
99
+ print()
100
+
101
+ # --- ANKI ---
102
+ print("--- 2. Anki Settings ---")
103
+ print("Make sure Anki is open with AnkiConnect installed.")
104
+ anki_url = _prompt("AnkiConnect URL", "http://localhost:8765")
105
+ deck = _prompt(
106
+ "Default Deck name (use {source_language} for dynamic)",
107
+ "Languages::{source_language}",
108
+ )
109
+ card_model = _prompt("Default Card Model name", "wordflow cloze")
110
+
111
+ # Inform the user about the new dictionary-based field layout instead of asking them to type it
112
+ print(
113
+ "\n -> Wordflow will automatically configure a rich 8-field layout (Sentence, Translation, Audio, Dictionary, etc.)."
114
+ )
115
+ print(
116
+ " -> You can easily customize these mappings or add HTML later in the config file.\n"
117
+ )
118
+
119
+ tags = _prompt_list(
120
+ "Default tags to apply to cards", ["wordflow", "auto-generated"]
121
+ )
122
+
123
+ allow_duplicates = _prompt_bool("Allow duplicate cards?", False)
124
+
125
+ audio_mode = _prompt_numbered_choice(
126
+ "Audio generation mode", ["none", "word", "sentence", "both"], "sentence"
127
+ )
128
+
129
+ add_dic_url = _prompt_bool(
130
+ "Add automatic dictionary hyperlinks to flashcards?", False
131
+ )
132
+ if add_dic_url:
133
+ dict_url = _prompt(
134
+ "Dictionary URL. Supported dynamic tags: {source_language}, {target_language}, {word}",
135
+ "https://glosbe.com/{source_language}/{target_language}/{word}",
136
+ )
137
+ else:
138
+ dict_url = ""
139
+
140
+ # --- DICTIONARY DATA ---
141
+ print("--- 3. Dictionary & Card Richness ---")
142
+ print("Set the maximum number of items to display on your cards if available.")
143
+ max_alternates = _prompt_int("Max alternate translations", 3)
144
+ max_definitions = _prompt_int("Max monolingual definitions", 2)
145
+ max_synonyms = _prompt_int("Max synonyms", 2)
146
+ max_examples = _prompt_int("Max example sentences", 1)
147
+
148
+ print("\n" + "=" * 60)
149
+ print(" Configuration complete!")
150
+ print(
151
+ " Note: You can tweak dictionary limits, field mappings, and language-specific"
152
+ )
153
+ print(f" overrides by editing {config_path} manually later.")
154
+ print("=" * 60)
155
+
156
+ # Construct the dictionary perfectly matching the new TOML schema
157
+ return {
158
+ "global": {
159
+ "source_language": source_language,
160
+ "target_language": target_language,
161
+ "enable_notifications": enable_notifs,
162
+ },
163
+ "anki": {
164
+ "default": {
165
+ "url": anki_url,
166
+ "deck": deck,
167
+ "card_model": card_model,
168
+ "allow_duplicates": allow_duplicates,
169
+ "tags": tags,
170
+ "audio_mode": audio_mode,
171
+ "dict_url": dict_url,
172
+ "max_alternates": 3,
173
+ "max_definitions": 2,
174
+ "max_synonyms": 2,
175
+ "max_examples": 1,
176
+ "fields": {
177
+ "Sentence": "{cloze}",
178
+ "Translation": "{translation}",
179
+ "Audio": "{word_audio} &nbsp;&nbsp;&nbsp; {sentence_audio}",
180
+ "Phonetics": "{word_phonetic} &nbsp;&nbsp;&nbsp; {sentence_phonetic}",
181
+ "Definitions": "{definitions}",
182
+ "Synonyms": "{synonyms}",
183
+ "Alternates": "{alternates}",
184
+ "Examples": "{examples}",
185
+ },
186
+ },
187
+ # Examples of override
188
+ "nl": {"deck": "Languages::Nederlands"},
189
+ "es": {"audio_accent": "com.mx"},
190
+ },
191
+ }