Idea: Cleaning up unused translations - #2635
Conversation
| unused -= to_remove | ||
| if not unused: | ||
| return unused | ||
| except Exception: |
Check notice
Code scanning / CodeQL
Empty except Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
General fix: keep the non-fatal behavior (continue scanning) but replace the empty handler with explicit handling that records what failed and why.
Best fix without changing functionality: in find_unused_translations, change the empty except Exception: pass to print a warning to stderr containing the file path and exception message, then continue. This preserves current control flow (no re-raise, still skips bad files) while removing silent failure.
Specific edit needed:
- File:
scripts/cleanup_translations.py - Region: inside
find_unused_translations, around lines 87–100 - Change only the
except Exception:block to emit a warning and continue.
No new imports are needed (sys is already imported).
| @@ -96,8 +96,9 @@ | ||
|
|
||
| if not unused: | ||
| return unused | ||
| except Exception: | ||
| pass | ||
| except Exception as e: | ||
| print(f"Warning: could not process {file_path}: {e}", file=sys.stderr) | ||
| continue | ||
|
|
||
| return unused | ||
|
|
There was a problem hiding this comment.
Pull request overview
Adds an interactive utility to identify and remove unused French translation strings by combining Babel extraction results with a fallback text search, and wires it into the Makefile.
Changes:
- Introduces
scripts/cleanup_translations.pyto extract used strings via Babel and detect unused keys. - Adds an interactive deletion flow to remove unused rows from
app/translations/csv/fr.csv. - Adds
make cleanup-translationstarget to run the script.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| scripts/cleanup_translations.py | New script to detect and optionally delete unused translation strings using Babel + source search. |
| Makefile | Adds a new cleanup-translations target to run the cleanup script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return set() | ||
|
|
||
|
|
||
| def find_unused_translations(keys, search_dirs): | ||
| # First, get keys that are used in code via Babel (covers Python/HTML) | ||
| babel_keys = get_keys_from_babel() |
There was a problem hiding this comment.
On any Babel extraction error, get_keys_from_babel() returns an empty set, which makes unused = set(keys) - babel_keys - extra_keys treat almost everything as unused and can lead to mass deletion. Safer behavior is to fail fast (raise/exit non-zero) or return a sentinel (e.g., None) and have find_unused_translations abort without offering deletion when extraction fails.
| return set() | |
| def find_unused_translations(keys, search_dirs): | |
| # First, get keys that are used in code via Babel (covers Python/HTML) | |
| babel_keys = get_keys_from_babel() | |
| return None | |
| def find_unused_translations(keys, search_dirs): | |
| # First, get keys that are used in code via Babel (covers Python/HTML) | |
| babel_keys = get_keys_from_babel() | |
| if babel_keys is None: | |
| print("Aborting: unable to extract keys via Babel; not proceeding with cleanup.") | |
| sys.exit(1) |
| subprocess.run( | ||
| ["poetry", "run", "pybabel", "extract", "-F", "babel.cfg", "-k", "_l", "-o", "/tmp/cleanup_messages.po", "."], | ||
| check=True, | ||
| capture_output=True, | ||
| ) | ||
|
|
||
| # 2. Run po2csv | ||
| subprocess.run( | ||
| ["poetry", "run", "po2csv", "/tmp/cleanup_messages.po", "/tmp/cleanup_messages.csv"], check=True, capture_output=True | ||
| ) |
There was a problem hiding this comment.
The script uses fixed /tmp/cleanup_messages.* paths and only cleans them up on the success path. This can clash across concurrent runs (e.g., parallel CI/jobs) and can leak temp files when any subprocess step fails. Use tempfile to create unique temp paths and ensure cleanup in a finally block (also consider specifying encoding='utf-8' when reading the temp CSV for consistent behavior).
|
|
||
| # 3. Read extracted keys | ||
| extracted_keys = set() | ||
| with open("/tmp/cleanup_messages.csv", newline="") as csvfile: |
There was a problem hiding this comment.
The script uses fixed /tmp/cleanup_messages.* paths and only cleans them up on the success path. This can clash across concurrent runs (e.g., parallel CI/jobs) and can leak temp files when any subprocess step fails. Use tempfile to create unique temp paths and ensure cleanup in a finally block (also consider specifying encoding='utf-8' when reading the temp CSV for consistent behavior).
| os.remove("/tmp/cleanup_messages.po") | ||
| os.remove("/tmp/cleanup_messages.csv") |
There was a problem hiding this comment.
The script uses fixed /tmp/cleanup_messages.* paths and only cleans them up on the success path. This can clash across concurrent runs (e.g., parallel CI/jobs) and can leak temp files when any subprocess step fails. Use tempfile to create unique temp paths and ensure cleanup in a finally block (also consider specifying encoding='utf-8' when reading the temp CSV for consistent behavior).
| def get_translation_keys(csv_path): | ||
| keys = [] | ||
| if not os.path.exists(csv_path): | ||
| return keys | ||
|
|
||
| with open(csv_path, mode="r", encoding="utf-8") as f: | ||
| reader = csv.DictReader(f) | ||
| for row in reader: | ||
| if row["source"]: | ||
| if row["source"].startswith("!/!/"): | ||
| continue | ||
| keys.append(row["source"]) | ||
| return keys |
There was a problem hiding this comment.
The log message claims the script found 'unique' source strings, but get_translation_keys returns a list and can include duplicates. Either deduplicate (e.g., return a set or de-dupe before printing) or adjust the message to not claim uniqueness.
|
|
||
| print(f"Loading translations from {csv_path}...") | ||
| keys = get_translation_keys(csv_path) | ||
| print(f"Found {len(keys)} unique translation source strings.") |
There was a problem hiding this comment.
The log message claims the script found 'unique' source strings, but get_translation_keys returns a list and can include duplicates. Either deduplicate (e.g., return a set or de-dupe before printing) or adjust the message to not claim uniqueness.
| print(f"Found {len(keys)} unique translation source strings.") | |
| print(f"Found {len(keys)} translation source strings.") |
| # Second pass: search remaining unused keys in JS and CSS files | ||
| for directory in search_dirs: | ||
| for root, dirs, files in os.walk(directory): | ||
| for file in files: | ||
| if file.endswith((".js", ".css", ".scss", ".txt", ".md")): | ||
| file_path = os.path.join(root, file) | ||
| if "translations/csv" in file_path or "node_modules" in file_path: | ||
| continue | ||
| try: | ||
| with open(file_path, "r", encoding="utf-8") as f: | ||
| content = f.read() | ||
|
|
||
| to_remove = set() | ||
| for key in unused: | ||
| if key in content: | ||
| to_remove.add(key) | ||
|
|
||
| unused -= to_remove | ||
| if not unused: | ||
| return unused |
There was a problem hiding this comment.
This does an O(number_of_files × number_of_candidate_keys) substring scan, which can get expensive if unused is large. A more efficient approach is to aggregate searchable content once (or stream files) and check each key once, or use a multi-pattern search approach (e.g., building an index, or a compiled matcher) to avoid re-scanning the same content for every key.
| # Also need to consider hardcoded constant keys used in test-translations.py | ||
| extra_keys = set( | ||
| [ | ||
| "English Government of Canada signature", | ||
| "French Government of Canada signature", | ||
| "Empty", | ||
| "1 template", | ||
| "Number must have 10 digits", | ||
| "bad invitation link", | ||
| "invitation expired", | ||
| "password", | ||
| "Your service already uses ", | ||
| "Try again. Something’s wrong with this code", | ||
| "Code already sent, wait 10 seconds", | ||
| "You cannot delete a default email reply to address if other reply to addresses exist", | ||
| "Code has expired", | ||
| "Code already sent", | ||
| "Code has already been used", | ||
| "Code not found", | ||
| "as an email reply-to address.", | ||
| "You cannot remove the only user for a service", | ||
| "Cannot send to international mobile numbers", | ||
| ] | ||
| ) |
There was a problem hiding this comment.
Hardcoding extra_keys creates a drift risk against the real source of truth (the constants used by test-translations). Consider loading these from a shared module/config (or importing the list from the test-translations implementation if it’s in Python), so future translation key additions don’t require updating multiple places.
🧪 Review environmenthttps://bw5fulsda4vf5476xpfzrmclwy0yhdcl.lambda-url.ca-central-1.on.aws/ |
| if temp_file and os.path.exists(temp_file): | ||
| try: | ||
| os.remove(temp_file) | ||
| except Exception: |
Check notice
Code scanning / CodeQL
Empty except Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 2 months ago
Use non-empty exception handling in the temp-file cleanup block by catching OSError (the expected deletion failure type) and logging a warning to stderr. This keeps functionality unchanged (script continues even if cleanup fails) while preserving diagnostic information.
Best fix in this snippet:
- In
scripts/cleanup_translations.py, inget_keys_from_babel()finallyblock (lines ~62–65), replace:except Exception: pass
- With:
except OSError as cleanup_error: print(..., file=sys.stderr)
No new imports are required because sys is already imported.
| @@ -61,8 +61,8 @@ | ||
| if temp_file and os.path.exists(temp_file): | ||
| try: | ||
| os.remove(temp_file) | ||
| except Exception: | ||
| pass | ||
| except OSError as cleanup_error: | ||
| print(f"Warning: failed to remove temporary file '{temp_file}': {cleanup_error}", file=sys.stderr) | ||
|
|
||
|
|
||
| def find_unused_translations(keys, search_dirs): |
🧪 Review environmenthttps://taylefytzpreqhynysprh63jvy0eqazj.lambda-url.ca-central-1.on.aws/ |
Summary | Résumé
We have a lot of translated content strings that we don't use anymore. Mostly from the days where content was hardcoded. Do we want to clean this up? This script can do it automatically, and we can always run
make test-translationsas a safety measure.Test instructions | Instructions pour tester la modification
run
make cleanup-translationsIt might find unused translations, and prompt you to confirm deletion
run
make test-translationsIt should not find missing translations