Skip to content

Commit 3cdbba0

Browse files
authored
Move hardcoded strings to localized files (#368)
Almost all strings are fetched from *.properties files. Remaining non-localized strings are used at times when the loc module may not be loaded (e.g throwing an error stack). When translations are being brought in, we can just paste in the whole file and let it delete the ones we don't need. * fix: use print_and_write consistently in check_strings.py * fix: add missing i18n keys, update stale key references * do a one-time cleanup to get rid of unused strings * Add script for double-checking messages found in source code vs properties files * document loc process, fix misreferenced string ids - cleaned up string ids that either added "tabcmd" as a prefix or took it off as a prefix incorrectly - incorporate check_strings into the localization build and pushed some output into a log file - added docs in Contributing.md and i18n/README
1 parent 1a13fcf commit 3cdbba0

57 files changed

Lines changed: 591 additions & 8800 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/i18n/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ These steps are separated for easier troubleshooting: each step is idempotent an
1111

1212
1. convert strings from .properties files to .mo for bundling
1313
This step combines the .properties files into a single file, discarding any strings that are not present in code and normalizing curly quotes and unrecognized characters in the strings it keeps. (These files are separate because they are pulled from separate translation sources internally.)
14-
> python -m doit properties
14+
> python -m doit combine_property_files
1515
1616
2. Convert the combined .properties file into a .po file (these are human readable)
1717
> python -m doit po
1818
1919
3. Convert the .po files into .mo files (these are not human readable)
2020
This also checks the .mo files for validity by loading them with gettext
21-
> python -m doit mo
21+
> python -m doit mo
22+
23+
## Optional reorganization task
24+
25+
Move all strings from extra.properties to the bottom of tabcmd_messages_xx.properties:
26+
> python -m doit move_tabcmd_strings
27+
28+
This consolidates all strings into the main tabcmd_messages files and clears the extra.properties files.

bin/i18n/check_strings.py

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
Usage:
99
python bin/i18n/check_strings.py # Dev mode: check against en/*.properties
10-
python bin/i18n/check_strings.py --mode build # Build mode: check against filtered.properties for all locales
10+
python bin/i18n/check_strings.py --mode build # Build mode: check against combined.tmp for all locales
1111
1212
Returns:
1313
0 if no missing strings found
@@ -188,107 +188,109 @@ def format_limited_list(items: List[str], prefix: str = " Missing: ", limit: in
188188

189189

190190
def check_build_mode(project_root: Path, locales: List[str]) -> int:
191-
"""Check all locales against filtered.properties files (build pipeline mode)."""
191+
"""Check all locales against combined.tmp files (build pipeline mode)."""
192192
tabcmd_dir = project_root / "tabcmd"
193193

194194
# Setup output file
195195
output_file = project_root / "localization_check_results.txt"
196196

197-
def print_and_write(message, file_handle=None):
198-
"""Print to console and write to file"""
197+
def print_and_write(message, file_handle=None, full_list=None, full_prefix=" "):
198+
"""Print message to console and file; if full_list is provided and longer than 10
199+
items, the console gets the truncated message and the file also gets the complete list."""
199200
print(message)
200201
if file_handle:
201202
file_handle.write(message + "\n")
202-
203+
if full_list and len(full_list) > 10:
204+
file_handle.write(f"{full_prefix}Complete list:\n")
205+
for key in sorted(full_list):
206+
file_handle.write(f"{full_prefix} {key}\n")
207+
203208
with open(output_file, "w", encoding="utf-8") as f:
204209
print_and_write(f"Build mode: Scanning Python files in: {tabcmd_dir}", f)
205210
print_and_write(f"Checking locales: {', '.join(locales)}", f)
206211
print_and_write("", f)
207-
212+
208213
# Find all Python files and extract string keys
209214
python_files = find_python_files(str(tabcmd_dir))
210215
print_and_write(f"Found {len(python_files)} Python files to scan", f)
211-
216+
212217
code_strings = set()
213218
for file_path in python_files:
214219
code_strings.update(extract_string_keys_from_file(file_path))
215-
220+
216221
print_and_write(f"Found {len(code_strings)} unique string keys in code", f)
217-
222+
218223
# Check each locale, starting with English as baseline
219224
english_success = True # Only track English success for exit code
220225
english_missing_keys = set()
221-
english_output = "" # Store English output to repeat at end
222226
locales_with_same_missing = []
223-
227+
224228
for locale in locales:
225-
filtered_file = project_root / "tabcmd" / "locales" / locale / "LC_MESSAGES" / "filtered.properties"
226-
229+
filtered_file = project_root / "tabcmd" / "locales" / locale / "LC_MESSAGES" / "combined.tmp"
230+
227231
if not filtered_file.exists():
228-
print_and_write(f"WARNING: No filtered.properties for locale '{locale}' at {filtered_file}", f)
232+
print_and_write(f"WARNING: No combined.tmp for locale '{locale}' at {filtered_file}", f)
229233
continue
230-
234+
231235
defined_keys = load_properties_file(str(filtered_file))
232236
missing_keys = code_strings - defined_keys
233-
237+
234238
if missing_keys:
235239
if locale == "en":
236-
# English has missing keys - this affects exit code
237240
english_success = False
238241
english_missing_keys = missing_keys
239-
english_output = f"\nERROR: Found {len(missing_keys)} missing string keys for locale '{locale}':\n"
240-
english_output += "=" * 60 + "\n"
242+
243+
msg = f"\nERROR: Found {len(missing_keys)} missing string keys for locale 'en':\n"
244+
msg += "=" * 60 + "\n"
241245
for line in format_limited_list(list(missing_keys)):
242-
english_output += line + "\n"
243-
print_and_write(english_output.rstrip(), f) # Print now for baseline
246+
msg += line + "\n"
247+
print_and_write(msg.rstrip(), f, full_list=list(missing_keys))
244248
else:
245249
# For other languages, only show if different from English
246250
if missing_keys == english_missing_keys:
247251
locales_with_same_missing.append(locale)
248252
else:
249253
print_and_write(f"\nERROR: Found {len(missing_keys)} missing string keys for locale '{locale}' (different from English):", f)
250254
print_and_write("=" * 60, f)
251-
252-
# Show keys unique to this locale
255+
253256
unique_to_locale = missing_keys - english_missing_keys
254257
if unique_to_locale:
255-
print_and_write(f" Additional missing keys in {locale}:", f)
256-
for line in format_limited_list(list(unique_to_locale), " Missing: "):
257-
print_and_write(line, f)
258-
259-
# Show keys missing in English but present in this locale
258+
summary = "\n".join(format_limited_list(list(unique_to_locale), " Missing: "))
259+
print_and_write(f" Additional missing keys in {locale}:\n{summary}", f,
260+
full_list=list(unique_to_locale), full_prefix=" ")
261+
260262
present_in_locale = english_missing_keys - missing_keys
261263
if present_in_locale:
262-
print_and_write(f" Keys present in {locale} but missing in English:", f)
263-
for line in format_limited_list(list(present_in_locale), " Present: "):
264-
print_and_write(line, f)
265-
266-
# Show common missing keys if both have missing keys
264+
summary = "\n".join(format_limited_list(list(present_in_locale), " Present: "))
265+
print_and_write(f" Keys present in {locale} but missing in English:\n{summary}", f,
266+
full_list=list(present_in_locale), full_prefix=" ")
267+
267268
common_missing = missing_keys & english_missing_keys
268269
if common_missing and (unique_to_locale or present_in_locale):
269270
print_and_write(f" Keys missing in both English and {locale}: {len(common_missing)}", f)
270271
else:
271-
if locale == "en":
272-
english_output = f"[OK] Locale '{locale}': All {len(code_strings)} string keys found"
273-
print_and_write(english_output, f) # Print now for baseline
274-
else:
275-
print_and_write(f"[OK] Locale '{locale}': All {len(code_strings)} string keys found", f)
276-
272+
print_and_write(f"[OK] Locale '{locale}': All {len(code_strings)} string keys found", f)
273+
277274
# Show summary for locales with same missing keys as English
278275
if locales_with_same_missing:
279276
print_and_write(f"\nNOTE: The following locales have the same missing keys as English:", f)
280277
print_and_write(f" {', '.join(locales_with_same_missing)}", f)
281278
print_and_write(f" Missing keys: {len(english_missing_keys)}", f)
282-
279+
283280
# Print English results again at the end for visibility
284-
if english_output and "en" in locales:
281+
if "en" in locales:
285282
print_and_write(f"\n--- English Results (repeated for visibility) ---", f)
286-
print_and_write(english_output.rstrip(), f)
287-
288-
# Summary message about file output
283+
if english_missing_keys:
284+
msg = f"ERROR: Found {len(english_missing_keys)} missing string keys for locale 'en':\n"
285+
msg += "=" * 60 + "\n"
286+
for line in format_limited_list(list(english_missing_keys)):
287+
msg += line + "\n"
288+
print_and_write(msg.rstrip(), f, full_list=list(english_missing_keys))
289+
else:
290+
print_and_write(f"[OK] Locale 'en': All {len(code_strings)} string keys found", f)
291+
289292
print_and_write(f"\nResults saved to: {output_file}", f)
290-
291-
# Only fail if English has missing strings
293+
292294
if english_success:
293295
print_and_write("\nSUCCESS: All required English strings are present", f)
294296
else:
@@ -333,8 +335,9 @@ def check_dev_mode(project_root: Path) -> int:
333335
rel_path = os.path.relpath(file_path, project_root)
334336
print(f"\nFile: {rel_path}")
335337
print("-" * 40)
336-
for line in format_limited_list(missing_by_file[file_path]):
337-
print(line)
338+
# In dev mode, show complete list since there's no file output
339+
for key in sorted(missing_by_file[file_path]):
340+
print(f" Missing: {key}")
338341

339342
print("\n" + "=" * 80)
340343
print("Please add the missing string keys to the appropriate .properties files.")
@@ -353,7 +356,7 @@ def main():
353356
"--mode",
354357
choices=["dev", "build"],
355358
default="dev",
356-
help="dev: check against en/*.properties (default), build: check against filtered.properties for all locales"
359+
help="dev: check against en/*.properties (default), build: check against combined.tmp for all locales"
357360
)
358361
parser.add_argument(
359362
"--locales",

contributing.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ The version reflected in the executable (tabcmd -v) is stored in a metadata file
137137
138138
139139
140+
141+
### Localization
142+
143+
Strings should be added/edited in /tabcmd/locales/en/{name}.properties by id and referred to in code as
144+
> string = _("string.id")
145+
146+
- regenerate updated strings for packaging as exe
147+
> python -m doit combine_property_files po mo
148+
149+
150+
### Versioning
151+
152+
Versioning is done with setuptools_scm and based on git tags. The version number will be x.y.dev0.dirty except for commits with a new version tag.
153+
This is pulled from the git state, and to get a clean version like "v2.1.0", you must be on a commit with the tag "v2.1.0" (Creating a Github release also creates a tag on the selected branch.)
154+
155+
The version reflected in the executable (tabcmd -v) is stored in a metadata file created by a .doit script:
156+
> python -m doit version
157+
158+
159+
140160
### Packaging
141161
Packaging for release is done in a github action and should not need to be done locally.
142162

0 commit comments

Comments
 (0)