Files
ObsiGate/_all_li.py
T
bruno 661989d85d
CI / lint (push) Failing after 19s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 10s
fix: wrap 73 LI descriptions across all help sections with data-i18n
Pattern: <li><strong>TERM</strong> : French desc → wrapped in <span data-i18n>
Covers: Navigation, Tabs, Search, Tags, Files, Editing, Graph,
AI Editor, Palette, Shortcuts, Sharing, Webhooks, Backups,
Security, Customization, Tips sections
901 locale keys, 442 data-i18n attributes
2026-06-18 15:59:19 -04:00

169 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Wrap ALL <li> French descriptions after <strong> in data-i18n spans across entire help guide."""
import re, json, hashlib
with open('frontend/index.html', 'r', encoding='utf-8') as f:
html = f.read()
with open('frontend/locales/fr.json', 'r', encoding='utf-8') as f: fr = json.load(f)
with open('frontend/locales/en.json', 'r', encoding='utf-8') as f: en = json.load(f)
# Find help body boundaries
start = html.find('id="help-modal"')
depth = 0
pos = start
while pos < len(html):
no = html.find('<div', pos+1)
nc = html.find('</div>', pos+1)
if nc == -1: break
if no != -1 and no < nc: depth += 1; pos = no
else:
if depth == 0: end = nc + 6; break
depth -= 1; pos = nc
help_body = html[start:end]
count = 0
# ── Find ALL patterns: <strong...>TERM</strong> FOLLOWED_BY FRENCH_TEXT ──
# Pattern 1: <strong...>TERM</strong> : French text\n
# Pattern 2: <strong...>TERM</strong>\n : French text\n
# Pattern 3: <strong...>TERM</strong> (context) : French text\n
# First, find all <li> blocks and extract <strong> + trailing text
li_pattern = re.compile(
r'(<li>\s*(<strong[^>]*>[^<]+</strong>)\s*'
r'(:?\s*(?:\([^)]+\)\s*:?\s*)?' # optional (context) :
r')([^<]{3,300})' # French description (until next <)
, re.DOTALL)
translations = {}
to_fix = []
for m in re.finditer(r'<li>\s*(<strong[^>]*>[^<]+</strong>)\s*(:?\s*(?:\([^)]+\)\s*:?\s*)?)([^<]{3,300})', help_body):
strong_tag = m.group(1)
connector = m.group(2) # e.g. " : " or " (desktop) : " or just " "
desc = m.group(3).strip()
# Skip if already wrapped in span
full_match = m.group(0)
if '<span data-i18n=' in full_match:
continue
# Skip if description has no French chars
if not any(c in desc for c in 'éèêëàâîïôùûçÉÈÊËÀÂÎÏÔÙÛÇ'):
continue
# Skip if description is English-only
if desc in ('Show/hide the sidebar', 'Return to home', 'Global search with autocomplete'):
continue
# Generate key
full_text = (connector + desc).strip()
h = hashlib.md5(full_text.encode()).hexdigest()[:8]
key = f'help.desc_{h}'
if key not in fr:
fr[key] = full_text
en[key] = full_text # placeholder
to_fix.append((strong_tag, connector, desc, key, full_text))
translations[key] = full_text
print(f"Found {len(to_fix)} LI descriptions to wrap")
# ── Apply fixes ──
for strong_tag, connector, desc, key, full_text in to_fix:
# Build the old pattern to replace
old = f'{strong_tag}{connector}{desc}'
new = f'{strong_tag}<span data-i18n="{key}">{connector}{desc}</span>'
if old in html:
html = html.replace(old, new)
count += 1
print(f"Applied {count} fixes")
# ── Translate all new keys ──
en_translations = {
'Recherche globale avec autocomplétion': 'Global search with autocomplete',
'Affiche le contexte actif (All, vault1, etc.)': 'Shows the active context (All, vault1, etc.)',
'Accès aux paramètres, thème, aide': 'Access to settings, theme, help',
'Arborescence des fichiers et dossiers': 'File and folder tree',
'Nuage de tags cliquables': 'Clickable tag cloud',
'Barre de recherche pour filtrer fichiers/tags': 'Search bar to filter files/tags',
'Dropdown pour changer de vault': 'Dropdown to change vault',
'Glissez le bord droit pour ajuster la largeur': 'Drag the right edge to adjust width',
"Cliquez sur l'icône sidebar dans le header": 'Click the sidebar icon in the header',
'Chemin du fichier (cliquable pour repositionner la sidebar)': 'File path (clickable to reposition sidebar)',
'Métadonnées du fichier': 'File metadata',
'Copier, Source, Télécharger, Éditer, Ouvrir dans nouvelle fenêtre': 'Copy, Source, Download, Edit, Open in new window',
'Markdown avec wikilinks et images': 'Markdown with wikilinks and images',
'Affiche/masque la sidebar': 'Show/hide the sidebar',
'Masque la sidebar pour plus d\'espace': 'Hide the sidebar for more space',
"Retour à l'accueil": 'Return to home',
'Recherche globale avec autocomplétion': 'Global search with autocomplete',
'Complète automatiquement le texte là où se trouve le curseur': 'Automatically completes text where the cursor is',
"Améliore la qualité, la clarté et le flux du texte": 'Improves quality, clarity and flow of the text',
"Corrige les fautes d'orthographe et de grammaire": 'Fixes spelling and grammar errors',
"Rend le texte plus concis tout en préservant l'information": 'Makes text more concise while preserving information',
'Ajoute des détails, exemples ou explications': 'Adds details, examples or explanations',
'Simplifie le langage, évite le jargon': 'Simplifies language, avoids jargon',
"Réécrit le texte selon vos instructions personnalisées": 'Rewrites the text according to your custom instructions',
'Explique le texte sélectionné de façon claire': 'Clearly explains the selected text',
'Résume le texte de façon concise': 'Concisely summarizes the text',
"Continue l'écriture à partir du texte": 'Continues writing from the text',
'Ton formel et professionnel': 'Formal and professional tone',
'Ton décontracté et conversationnel': 'Casual and conversational tone',
'Traduit le texte sélectionné vers une autre langue': 'Translates the selected text to another language',
'Génère du contenu à partir de la sélection': 'Generates content from the selection',
'Fichiers (carrés) et dossiers (cercles) avec icônes': 'Files (squares) and folders (circles) with icons',
'Molette pour zoomer, cliquer-glisser pour se déplacer': 'Scroll wheel to zoom, click-drag to move',
'Votre choix est sauvegardé en localStorage': 'Your choice is saved in localStorage',
"Dans la barre d'outils (le cas échéant)": 'In the toolbar (when available)',
"Les blocs de code s'adaptent au thème": 'Code blocks adapt to the theme',
'Les 50 dernières recherches (localStorage)': 'The 50 most recent searches (localStorage)',
'Restent ouverts jusqu\'à fermeture manuelle': 'Stay open until manually closed',
'Les tags s\'adaptent au vault sélectionné': 'Tags adapt to the selected vault',
'Stats index, mémoire, moteur de recherche': 'Index stats, memory, search engine',
'Délai debounce, résultats par page, timeout': 'Debounce delay, results per page, timeout',
'Workers, boosts TF-IDF, expansions préfixe': 'Workers, TF-IDF boosts, prefix expansions',
'Patterns pour masquer les tags template': 'Patterns to hide template tags',
'Forcer un rescan des vaults': 'Force a vault rescan',
'↑/↓ pour naviguer, Enter pour sélectionner': '↑/↓ to navigate, Enter to select',
'Chaque type de fichier a son icône (markdown, code, etc.)': 'Each file type has its icon (markdown, code, etc.)',
'Un onglet temporaire (italique) est remplacé': 'A temporary tab (italic) is replaced',
'L\'onglet "Récent" dans la sidebar liste vos': 'The "Recent" tab in the sidebar lists your',
'Ouvre le fichier dans un onglet temporaire (aperçu,': 'Opens the file in a temporary tab (preview,',
'Complétion, réécriture, traduction (voir': 'Completion, rewrite, translation (see',
'Résultats classés par pertinence TF-IDF': 'Results ranked by TF-IDF relevance',
'Auto-save silencieux après 2s d\'inactivité': 'Silent auto-save after 2s of inactivity',
'Contrôle la profondeur d\'exploration (0-3)': 'Controls exploration depth (0-3)',
"N'affiche que les nœuds avec un tag spécifique": 'Only shows nodes with a specific tag',
'Métadonnées du nœud (tags, liens, chemin)': 'Node metadata (tags, links, path)',
'Aperçu du contenu de la note sans naviguer': 'Preview of note content without navigating',
'Navigation facilitée dans les fichiers longs': 'Easier navigation in long files',
'Bouton "×" pour réinitialiser le filtre': '"×" button to reset the filter',
'Sélection rapide en haut de l\'onglet Vault': 'Quick select at the top of the Vault tab',
'Affichage horizontal avec le nom du fichier': 'Horizontal display with the file name',
}
for k, v in fr.items():
if k.startswith('help.desc_') and en.get(k) == v:
if v in en_translations:
en[k] = en_translations[v]
# Also try to match substrings
for k, v in fr.items():
if k.startswith('help.desc_') and en.get(k) == v:
for fr_t, en_t in en_translations.items():
if fr_t in v and len(fr_t) > 15:
en[k] = v.replace(fr_t, en_t)
break
with open('frontend/index.html', 'w', encoding='utf-8') as f: f.write(html)
with open('frontend/locales/fr.json', 'w', encoding='utf-8') as f: json.dump(fr, f, ensure_ascii=False, indent=2)
with open('frontend/locales/en.json', 'w', encoding='utf-8') as f: json.dump(en, f, ensure_ascii=False, indent=2)
total = len(re.findall(r'data-i18n(?:-[a-z]+)?="', html))
print(f'Total tags: {total}, Keys: {len(fr)}')
still_fr = sum(1 for k in en if k.startswith('help.desc_') and en[k] == fr.get(k, ''))
print(f'Untranslated help.desc: {still_fr}')