Skip to content

Idea: Cleaning up unused translations - #2635

Open
amazingphilippe wants to merge 7 commits into
mainfrom
debt/unused-translation-script
Open

Idea: Cleaning up unused translations#2635
amazingphilippe wants to merge 7 commits into
mainfrom
debt/unused-translation-script

Conversation

@amazingphilippe

Copy link
Copy Markdown
Contributor

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-translations as a safety measure.

Test instructions | Instructions pour tester la modification

run make cleanup-translations
It might find unused translations, and prompt you to confirm deletion
run make test-translations
It should not find missing translations

unused -= to_remove
if not unused:
return unused
except Exception:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

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).

Suggested changeset 1
scripts/cleanup_translations.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/cleanup_translations.py b/scripts/cleanup_translations.py
--- a/scripts/cleanup_translations.py
+++ b/scripts/cleanup_translations.py
@@ -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
 
EOF
@@ -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

Copilot is powered by AI and may make mistakes. Always verify output.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to 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-translations target 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.

Comment thread scripts/cleanup_translations.py Outdated
Comment on lines +52 to +57
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()

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment thread scripts/cleanup_translations.py Outdated
Comment on lines +27 to +36
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
)

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread scripts/cleanup_translations.py Outdated

# 3. Read extracted keys
extracted_keys = set()
with open("/tmp/cleanup_messages.csv", newline="") as csvfile:

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread scripts/cleanup_translations.py Outdated
Comment on lines +46 to +47
os.remove("/tmp/cleanup_messages.po")
os.remove("/tmp/cleanup_messages.csv")

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +19
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

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

print(f"Loading translations from {csv_path}...")
keys = get_translation_keys(csv_path)
print(f"Found {len(keys)} unique translation source strings.")

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
print(f"Found {len(keys)} unique translation source strings.")
print(f"Found {len(keys)} translation source strings.")

Copilot uses AI. Check for mistakes.
Comment thread scripts/cleanup_translations.py Outdated
Comment on lines +88 to +107
# 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

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread scripts/cleanup_translations.py Outdated
Comment on lines +59 to +82
# 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",
]
)

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@github-actions

Copy link
Copy Markdown

if temp_file and os.path.exists(temp_file):
try:
os.remove(temp_file)
except Exception:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

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, in get_keys_from_babel() finally block (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.

Suggested changeset 1
scripts/cleanup_translations.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/cleanup_translations.py b/scripts/cleanup_translations.py
--- a/scripts/cleanup_translations.py
+++ b/scripts/cleanup_translations.py
@@ -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):
EOF
@@ -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):
Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

@amazingphilippe
amazingphilippe enabled auto-merge (squash) July 15, 2026 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants