feat(i18n): Add comprehensive translation support across all templates

- Replace hardcoded English strings with translation function calls in 36 template files
- Update translation files for all supported languages (ar, de, es, fi, fr, he, it, nb, nl, no)
- Add over 55,000 new translation entries across all language files
- Update extract_translations.py to use 'python -m babel.messages.frontend' instead of pybabel
- Improve internationalization coverage for UI elements including:
  * Skip to content links
  * Sidebar toggle buttons
  * Command palette placeholders
  * Admin dashboard elements
  * Form labels and buttons
  * Report templates
  * Payment and invoice views

This commit significantly improves the application's multilingual support
by making previously hardcoded strings translatable.
This commit is contained in:
Dries Peeters
2025-11-18 11:35:57 +01:00
parent 6ba233aa0e
commit 5ace391bd9
64 changed files with 85173 additions and 2050 deletions

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Script to complete Dutch translations by translating all empty msgstr entries.
Uses Babel's library for reliable .po file parsing and updates translations.
"""
import os
import sys
from pathlib import Path
try:
from babel.messages.pofile import read_po, write_po
from babel.messages.catalog import Message
except ImportError:
print("Error: Babel library not found. Please install it with: pip install Babel")
sys.exit(1)
def translate_to_dutch(text):
"""
Translate English text to Dutch.
This is a placeholder - in a real scenario, you might use a translation API
or manual translations. For now, we'll identify what needs translation.
"""
# Common translations mapping
translations = {
"Your session expired or the page was open too long. Please try again.":
"Uw sessie is verlopen of de pagina was te lang open. Probeer het opnieuw.",
"Administrator access required":
"Beheerdersrechten vereist",
"Could not update PDF layout due to a database error.":
"Kon PDF-lay-out niet bijwerken vanwege een databasefout.",
"PDF layout updated successfully":
"PDF-lay-out succesvol bijgewerkt",
"Could not reset PDF layout due to a database error.":
"Kon PDF-lay-out niet resetten vanwege een databasefout.",
"PDF layout reset to defaults":
"PDF-lay-out gereset naar standaardwaarden",
"Username is required":
"Gebruikersnaam is vereist",
"Could not create your account due to a database error. Please try again later.":
"Kon uw account niet aanmaken vanwege een databasefout. Probeer het later opnieuw.",
"Welcome! Your account has been created.":
"Welkom! Uw account is aangemaakt.",
"User not found. Please contact an administrator.":
"Gebruiker niet gevonden. Neem contact op met een beheerder.",
"Could not update your account role due to a database error.":
"Kon uw accountrol niet bijwerken vanwege een databasefout.",
"Account is disabled. Please contact an administrator.":
"Account is uitgeschakeld. Neem contact op met een beheerder.",
"Welcome back, %(username)s!":
"Welkom terug, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Onverwachte fout tijdens aanmelden. Probeer het opnieuw of controleer de serverlogs.",
"Goodbye, %(username)s!":
"Tot ziens, %(username)s!",
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Ongeldig avatarbestandstype. Toegestaan: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Ongeldig afbeeldingsbestand.",
"Failed to save avatar on server.":
"Kon avatar niet opslaan op server.",
"Profile updated successfully":
"Profiel succesvol bijgewerkt",
"Could not update your profile due to a database error.":
"Kon uw profiel niet bijwerken vanwege een databasefout.",
"Avatar removed":
"Avatar verwijderd",
"Failed to remove avatar.":
"Kon avatar niet verwijderen.",
"Single Sign-On is not configured yet. Please contact an administrator.":
"Single Sign-On is nog niet geconfigureerd. Neem contact op met een beheerder.",
"Single Sign-On is not configured.":
"Single Sign-On is niet geconfigureerd.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Authenticatie mislukt: ontbrekende issuer of subject claim. Controleer de OIDC-configuratie.",
"User account does not exist and self-registration is disabled.":
"Gebruikersaccount bestaat niet en zelfregistratie is uitgeschakeld.",
"Could not create your account due to a database error.":
"Kon uw account niet aanmaken vanwege een databasefout.",
"Unexpected error during SSO login. Please try again or contact support.":
"Onverwachte fout tijdens SSO-aanmelden. Probeer het opnieuw of neem contact op met ondersteuning.",
"Event created successfully":
"Gebeurtenis succesvol aangemaakt",
"Event updated successfully":
"Gebeurtenis succesvol bijgewerkt",
}
return translations.get(text, "")
def complete_dutch_translations():
"""Complete all missing Dutch translations."""
translations_dir = Path('translations')
nl_file = translations_dir / 'nl' / 'LC_MESSAGES' / 'messages.po'
en_file = translations_dir / 'en' / 'LC_MESSAGES' / 'messages.po'
if not nl_file.exists():
print(f"Error: Dutch translation file not found at {nl_file}")
return
if not en_file.exists():
print(f"Error: English translation file not found at {en_file}")
return
print("Reading translation files...")
nl_catalog = read_po(open(nl_file, 'r', encoding='utf-8'))
en_catalog = read_po(open(en_file, 'r', encoding='utf-8'))
print(f"Found {len(nl_catalog)} entries in Dutch file")
print(f"Found {len(en_catalog)} entries in English file")
# Find untranslated entries
untranslated = []
for message in nl_catalog:
if message.id:
# Check if translation is empty
is_empty = False
if isinstance(message.string, tuple):
# Plural form
is_empty = not message.string or all(not s for s in message.string)
else:
# Singular form
is_empty = not message.string or message.string == ""
if is_empty:
untranslated.append(message)
print(f"\nFound {len(untranslated)} untranslated entries")
if len(untranslated) == 0:
print("All translations are complete!")
return
# Show first 20 untranslated entries
print("\nFirst 20 untranslated entries:")
for i, msg in enumerate(untranslated[:20], 1):
msg_id = msg.id[:80] + "..." if len(msg.id) > 80 else msg.id
print(f"{i}. {msg_id}")
print(f"\n... and {len(untranslated) - 20} more entries")
# Ask for confirmation
response = input(f"\nDo you want to translate all {len(untranslated)} entries? (yes/no): ")
if response.lower() != 'yes':
print("Translation cancelled.")
return
# Translate entries using English as reference
translated_count = 0
for msg in untranslated:
# Try to find corresponding English message for context
en_msg = en_catalog.get(msg.id)
if en_msg and en_msg.string:
# For now, we'll use a simple approach: copy English as placeholder
# In production, you'd use a translation service or manual translation
# For this script, we'll mark them as needing translation
pass
print(f"\nTranslated {translated_count} entries")
print("Note: This script identifies untranslated entries.")
print("For actual translation, use a translation service or manual translation.")
if __name__ == '__main__':
complete_dutch_translations()

View File

@@ -0,0 +1,515 @@
#!/usr/bin/env python3
"""
Complete Dutch translations by translating all empty msgstr entries.
Uses Babel library for reliable .po file parsing.
"""
import sys
from pathlib import Path
try:
from babel.messages.pofile import read_po, write_po
except ImportError:
print("Error: Babel library not found. Please install it with: pip install Babel")
sys.exit(1)
# Comprehensive translation dictionary
TRANSLATIONS = {
# Session and authentication
"Your session expired or the page was open too long. Please try again.":
"Uw sessie is verlopen of de pagina was te lang open. Probeer het opnieuw.",
"Administrator access required":
"Beheerdersrechten vereist",
# PDF layout
"Could not update PDF layout due to a database error.":
"Kon PDF-lay-out niet bijwerken vanwege een databasefout.",
"PDF layout updated successfully":
"PDF-lay-out succesvol bijgewerkt",
"Could not reset PDF layout due to a database error.":
"Kon PDF-lay-out niet resetten vanwege een databasefout.",
"PDF layout reset to defaults":
"PDF-lay-out gereset naar standaardwaarden",
# User account
"Username is required":
"Gebruikersnaam is vereist",
"Could not create your account due to a database error. Please try again later.":
"Kon uw account niet aanmaken vanwege een databasefout. Probeer het later opnieuw.",
"Welcome! Your account has been created.":
"Welkom! Uw account is aangemaakt.",
"User not found. Please contact an administrator.":
"Gebruiker niet gevonden. Neem contact op met een beheerder.",
"Could not update your account role due to a database error.":
"Kon uw accountrol niet bijwerken vanwege een databasefout.",
"Account is disabled. Please contact an administrator.":
"Account is uitgeschakeld. Neem contact op met een beheerder.",
"Welcome back, %(username)s!":
"Welkom terug, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Onverwachte fout tijdens aanmelden. Probeer het opnieuw of controleer de serverlogs.",
"Goodbye, %(username)s!":
"Tot ziens, %(username)s!",
# Avatar/Profile
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Ongeldig avatarbestandstype. Toegestaan: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Ongeldig afbeeldingsbestand.",
"Failed to save avatar on server.":
"Kon avatar niet opslaan op server.",
"Profile updated successfully":
"Profiel succesvol bijgewerkt",
"Could not update your profile due to a database error.":
"Kon uw profiel niet bijwerken vanwege een databasefout.",
"Avatar removed":
"Avatar verwijderd",
"Failed to remove avatar.":
"Kon avatar niet verwijderen.",
# SSO
"Single Sign-On is not configured yet. Please contact an administrator.":
"Single Sign-On is nog niet geconfigureerd. Neem contact op met een beheerder.",
"Single Sign-On is not configured.":
"Single Sign-On is niet geconfigureerd.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Authenticatie mislukt: ontbrekende issuer of subject claim. Controleer de OIDC-configuratie.",
"User account does not exist and self-registration is disabled.":
"Gebruikersaccount bestaat niet en zelfregistratie is uitgeschakeld.",
"Could not create your account due to a database error.":
"Kon uw account niet aanmaken vanwege een databasefout.",
"Unexpected error during SSO login. Please try again or contact support.":
"Onverwachte fout tijdens SSO-aanmelden. Probeer het opnieuw of neem contact op met ondersteuning.",
# Events
"Event created successfully":
"Gebeurtenis succesvol aangemaakt",
"Event updated successfully":
"Gebeurtenis succesvol bijgewerkt",
"You do not have permission to delete this event.":
"U heeft geen toestemming om deze gebeurtenis te verwijderen.",
"Failed to delete event":
"Kon gebeurtenis niet verwijderen",
"Event deleted successfully":
"Gebeurtenis succesvol verwijderd",
"Error deleting event: %(error)s":
"Fout bij verwijderen gebeurtenis: %(error)s",
"Event moved successfully":
"Gebeurtenis succesvol verplaatst",
"Event resized successfully":
"Gebeurtenis succesvol van grootte gewijzigd",
"You do not have permission to view this event.":
"U heeft geen toestemming om deze gebeurtenis te bekijken.",
"You do not have permission to edit this event.":
"U heeft geen toestemming om deze gebeurtenis te bewerken.",
# Notes
"Note content cannot be empty":
"Notitie-inhoud kan niet leeg zijn",
"Note added successfully":
"Notitie succesvol toegevoegd",
"Error adding note":
"Fout bij toevoegen notitie",
"Error adding note: %(error)s":
"Fout bij toevoegen notitie: %(error)s",
"Note does not belong to this client":
"Notitie behoort niet bij deze klant",
"You do not have permission to edit this note":
"U heeft geen toestemming om deze notitie te bewerken",
"Error updating note":
"Fout bij bijwerken notitie",
"Note updated successfully":
"Notitie succesvol bijgewerkt",
"Error updating note: %(error)s":
"Fout bij bijwerken notitie: %(error)s",
"You do not have permission to delete this note":
"U heeft geen toestemming om deze notitie te verwijderen",
"Error deleting note":
"Fout bij verwijderen notitie",
"Note deleted successfully":
"Notitie succesvol verwijderd",
"Error deleting note: %(error)s":
"Fout bij verwijderen notitie: %(error)s",
# Clients
"You do not have permission to create clients":
"U heeft geen toestemming om klanten aan te maken",
# Comments
"Comment content cannot be empty":
"Reactie-inhoud kan niet leeg zijn",
"Comment must be associated with a project or task":
"Reactie moet gekoppeld zijn aan een project of taak",
"Comment cannot be associated with both a project and a task":
"Reactie kan niet tegelijk gekoppeld zijn aan een project en een taak",
"Invalid parent comment":
"Ongeldige bovenliggende reactie",
"Comment added successfully":
"Reactie succesvol toegevoegd",
"Error adding comment":
"Fout bij toevoegen reactie",
"Error adding comment: %(error)s":
"Fout bij toevoegen reactie: %(error)s",
"You do not have permission to edit this comment":
"U heeft geen toestemming om deze reactie te bewerken",
"Comment updated successfully":
"Reactie succesvol bijgewerkt",
"Error updating comment: %(error)s":
"Fout bij bijwerken reactie: %(error)s",
"You do not have permission to delete this comment":
"U heeft geen toestemming om deze reactie te verwijderen",
"Comment deleted successfully":
"Reactie succesvol verwijderd",
"Error deleting comment: %(error)s":
"Fout bij verwijderen reactie: %(error)s",
# Expenses
"Category name is required":
"Categorienaam is vereist",
"Expense category created successfully":
"Uitgavencategorie succesvol aangemaakt",
"Error creating expense category":
"Fout bij aanmaken uitgavencategorie",
"Expense category updated successfully":
"Uitgavencategorie succesvol bijgewerkt",
"Error updating expense category":
"Fout bij bijwerken uitgavencategorie",
"Expense category deactivated successfully":
"Uitgavencategorie succesvol gedeactiveerd",
"Error deactivating expense category":
"Fout bij deactiveren uitgavencategorie",
"Title is required":
"Titel is vereist",
"Category is required":
"Categorie is vereist",
"Amount is required":
"Bedrag is vereist",
"Expense date is required":
"Uitgavedatum is vereist",
"Invalid date format":
"Ongeldig datumformaat",
"Invalid amount format":
"Ongeldig bedragsformaat",
"Expense created successfully":
"Uitgave succesvol aangemaakt",
"Error creating expense":
"Fout bij aanmaken uitgave",
"You do not have permission to view this expense":
"U heeft geen toestemming om deze uitgave te bekijken",
"You do not have permission to edit this expense":
"U heeft geen toestemming om deze uitgave te bewerken",
"Cannot edit approved or reimbursed expenses":
"Kan goedgekeurde of terugbetaalde uitgaven niet bewerken",
"Please fill in all required fields":
"Vul alle verplichte velden in",
"Expense updated successfully":
"Uitgave succesvol bijgewerkt",
"Error updating expense":
"Fout bij bijwerken uitgave",
"You do not have permission to delete this expense":
"U heeft geen toestemming om deze uitgave te verwijderen",
"Cannot delete approved or invoiced expenses":
"Kan goedgekeurde of gefactureerde uitgaven niet verwijderen",
"Expense deleted successfully":
"Uitgave succesvol verwijderd",
"Error deleting expense":
"Fout bij verwijderen uitgave",
"Only administrators can approve expenses":
"Alleen beheerders kunnen uitgaven goedkeuren",
"Only pending expenses can be approved":
"Alleen openstaande uitgaven kunnen worden goedgekeurd",
"Expense approved successfully":
"Uitgave succesvol goedgekeurd",
"Error approving expense":
"Fout bij goedkeuren uitgave",
"Only administrators can reject expenses":
"Alleen beheerders kunnen uitgaven afwijzen",
"Only pending expenses can be rejected":
"Alleen openstaande uitgaven kunnen worden afgewezen",
"Rejection reason is required":
"Afwijzingsreden is vereist",
"Expense rejected":
"Uitgave afgewezen",
"Error rejecting expense":
"Fout bij afwijzen uitgave",
"Only administrators can mark expenses as reimbursed":
"Alleen beheerders kunnen uitgaven als terugbetaald markeren",
"Only approved expenses can be marked as reimbursed":
"Alleen goedgekeurde uitgaven kunnen als terugbetaald worden gemarkeerd",
"This expense is not marked as reimbursable":
"Deze uitgave is niet gemarkeerd als terugbetaalbaar",
"Expense marked as reimbursed":
"Uitgave gemarkeerd als terugbetaald",
"Error marking expense as reimbursed":
"Fout bij markeren uitgave als terugbetaald",
"OCR is not available. Please contact your administrator.":
"OCR is niet beschikbaar. Neem contact op met uw beheerder.",
"No file provided":
"Geen bestand opgegeven",
"No file selected":
"Geen bestand geselecteerd",
"Invalid file type. Allowed types: png, jpg, jpeg, gif, pdf":
"Ongeldig bestandstype. Toegestane typen: png, jpg, jpeg, gif, pdf",
"Receipt scanned successfully! You can now create an expense with the extracted data.":
"Bon succesvol gescand! U kunt nu een uitgave aanmaken met de geëxtraheerde gegevens.",
"Error scanning receipt. Please try again or enter the expense manually.":
"Fout bij scannen bon. Probeer het opnieuw of voer de uitgave handmatig in.",
"No scanned receipt data found. Please scan a receipt first.":
"Geen gescande bongegevens gevonden. Scan eerst een bon.",
"Expense created successfully from scanned receipt":
"Uitgave succesvol aangemaakt van gescande bon",
"You do not have permission to export this invoice":
"U heeft geen toestemming om deze factuur te exporteren",
"PDF generation failed: %(err)s. Fallback also failed: %(fb)s":
"PDF-generatie mislukt: %(err)s. Fallback ook mislukt: %(fb)s",
# Mileage
"Mileage entry created successfully":
"Kilometergeregistratie succesvol aangemaakt",
"Error creating mileage entry":
"Fout bij aanmaken kilometergeregistratie",
"You do not have permission to view this mileage entry":
"U heeft geen toestemming om deze kilometergeregistratie te bekijken",
"You do not have permission to edit this mileage entry":
"U heeft geen toestemming om deze kilometergeregistratie te bewerken",
"Cannot edit approved or reimbursed mileage entries":
"Kan goedgekeurde of terugbetaalde kilometergeregistraties niet bewerken",
"Mileage entry updated successfully":
"Kilometergeregistratie succesvol bijgewerkt",
"Error updating mileage entry":
"Fout bij bijwerken kilometergeregistratie",
"You do not have permission to delete this mileage entry":
"U heeft geen toestemming om deze kilometergeregistratie te verwijderen",
"Mileage entry deleted successfully":
"Kilometergeregistratie succesvol verwijderd",
"Error deleting mileage entry":
"Fout bij verwijderen kilometergeregistratie",
"Only administrators can approve mileage entries":
"Alleen beheerders kunnen kilometergeregistraties goedkeuren",
"Only pending mileage entries can be approved":
"Alleen openstaande kilometergeregistraties kunnen worden goedgekeurd",
"Mileage entry approved successfully":
"Kilometergeregistratie succesvol goedgekeurd",
"Error approving mileage entry":
"Fout bij goedkeuren kilometergeregistratie",
"Only administrators can reject mileage entries":
"Alleen beheerders kunnen kilometergeregistraties afwijzen",
"Only pending mileage entries can be rejected":
"Alleen openstaande kilometergeregistraties kunnen worden afgewezen",
"Mileage entry rejected":
"Kilometergeregistratie afgewezen",
"Error rejecting mileage entry":
"Fout bij afwijzen kilometergeregistratie",
"Only administrators can mark mileage entries as reimbursed":
"Alleen beheerders kunnen kilometergeregistraties als terugbetaald markeren",
"Only approved mileage entries can be marked as reimbursed":
"Alleen goedgekeurde kilometergeregistraties kunnen als terugbetaald worden gemarkeerd",
"Mileage entry marked as reimbursed":
"Kilometergeregistratie gemarkeerd als terugbetaald",
"Error marking mileage entry as reimbursed":
"Fout bij markeren kilometergeregistratie als terugbetaald",
# Per diem
"Start date must be before end date":
"Startdatum moet voor einddatum liggen",
"No per diem rate found for this location. Please configure rates first.":
"Geen per diem-tarief gevonden voor deze locatie. Configureer eerst de tarieven.",
"Per diem claim created successfully":
"Per diem-claim succesvol aangemaakt",
"Error creating per diem claim":
"Fout bij aanmaken per diem-claim",
"You do not have permission to view this per diem claim":
"U heeft geen toestemming om deze per diem-claim te bekijken",
"You do not have permission to edit this per diem claim":
"U heeft geen toestemming om deze per diem-claim te bewerken",
"Cannot edit approved or reimbursed per diem claims":
"Kan goedgekeurde of terugbetaalde per diem-claims niet bewerken",
"Per diem claim updated successfully":
"Per diem-claim succesvol bijgewerkt",
"Error updating per diem claim":
"Fout bij bijwerken per diem-claim",
"You do not have permission to delete this per diem claim":
"U heeft geen toestemming om deze per diem-claim te verwijderen",
"Per diem claim deleted successfully":
"Per diem-claim succesvol verwijderd",
"Error deleting per diem claim":
"Fout bij verwijderen per diem-claim",
"Only administrators can approve per diem claims":
"Alleen beheerders kunnen per diem-claims goedkeuren",
"Only pending per diem claims can be approved":
"Alleen openstaande per diem-claims kunnen worden goedgekeurd",
"Per diem claim approved successfully":
"Per diem-claim succesvol goedgekeurd",
"Error approving per diem claim":
"Fout bij goedkeuren per diem-claim",
"Only administrators can reject per diem claims":
"Alleen beheerders kunnen per diem-claims afwijzen",
"Only pending per diem claims can be rejected":
"Alleen openstaande per diem-claims kunnen worden afgewezen",
"Per diem claim rejected":
"Per diem-claim afgewezen",
"Error rejecting per diem claim":
"Fout bij afwijzen per diem-claim",
"Per diem rate created successfully":
"Per diem-tarief succesvol aangemaakt",
"Error creating per diem rate":
"Fout bij aanmaken per diem-tarief",
# Roles
"You do not have permission to access this page":
"U heeft geen toestemming om deze pagina te openen",
"Role name is required":
"Rolnaam is vereist",
"A role with this name already exists":
"Een rol met deze naam bestaat al",
"Could not create role due to a database error":
"Kon rol niet aanmaken vanwege een databasefout",
"Role created successfully":
"Rol succesvol aangemaakt",
"System roles cannot be edited":
"Systeemrollen kunnen niet worden bewerkt",
"Could not update role due to a database error":
"Kon rol niet bijwerken vanwege een databasefout",
"Role updated successfully":
"Rol succesvol bijgewerkt",
"You do not have permission to perform this action":
"U heeft geen toestemming om deze actie uit te voeren",
"System roles cannot be deleted":
"Systeemrollen kunnen niet worden verwijderd",
"Cannot delete role that is assigned to users. Please reassign users first.":
"Kan rol niet verwijderen die aan gebruikers is toegewezen. Wijs eerst gebruikers opnieuw toe.",
"Could not delete role due to a database error":
"Kon rol niet verwijderen vanwege een databasefout",
"Role \"%(name)s\" deleted successfully":
"Rol \"%(name)s\" succesvol verwijderd",
"Could not update user roles due to a database error":
"Kon gebruikersrollen niet bijwerken vanwege een databasefout",
"User roles updated successfully":
"Gebruikersrollen succesvol bijgewerkt",
# Projects
"Project code already in use":
"Projectcode is al in gebruik",
"Project is already in favorites":
"Project staat al in favorieten",
"Project added to favorites":
"Project toegevoegd aan favorieten",
"Failed to add project to favorites":
"Kon project niet toevoegen aan favorieten",
"Project is not in favorites":
"Project staat niet in favorieten",
"Project removed from favorites":
"Project verwijderd uit favorieten",
"Failed to remove project from favorites":
"Kon project niet verwijderen uit favorieten",
# Costs
"Description, category, amount, and date are required":
"Beschrijving, categorie, bedrag en datum zijn vereist",
"Could not add cost due to a database error. Please check server logs.":
"Kon kosten niet toevoegen vanwege een databasefout. Controleer de serverlogs.",
"Cost added successfully":
"Kosten succesvol toegevoegd",
"Cost not found":
"Kosten niet gevonden",
"You do not have permission to edit this cost":
"U heeft geen toestemming om deze kosten te bewerken",
"Could not update cost due to a database error. Please check server logs.":
"Kon kosten niet bijwerken vanwege een databasefout. Controleer de serverlogs.",
"Cost updated successfully":
"Kosten succesvol bijgewerkt",
"You do not have permission to delete this cost":
"U heeft geen toestemming om deze kosten te verwijderen",
"Cannot delete cost that has been invoiced":
"Kan kosten niet verwijderen die zijn gefactureerd",
"Could not delete cost due to a database error. Please check server logs.":
"Kon kosten niet verwijderen vanwege een databasefout. Controleer de serverlogs.",
# Invoice items
"Name and unit price are required":
"Naam en eenheidsprijs zijn vereist",
"Invalid quantity format":
"Ongeldig hoeveelheidsformaat",
"Invalid unit price format":
"Ongeldig eenheidsprijsformaat",
}
def translate_message(msgid):
"""Get Dutch translation for a message ID."""
return TRANSLATIONS.get(msgid, "")
def complete_translations():
"""Complete all missing Dutch translations."""
po_file = Path('translations/nl/LC_MESSAGES/messages.po')
if not po_file.exists():
print(f"Error: File not found: {po_file}")
return
print(f"Reading {po_file}...")
with open(po_file, 'r', encoding='utf-8') as f:
catalog = read_po(f)
print(f"Found {len(catalog)} entries in catalog")
# Find untranslated entries
untranslated = []
for message in catalog:
if message.id:
is_empty = False
if isinstance(message.string, tuple):
is_empty = not message.string or all(not s for s in message.string)
else:
is_empty = not message.string or message.string == ""
if is_empty:
untranslated.append(message)
print(f"Found {len(untranslated)} untranslated entries")
if len(untranslated) == 0:
print("All translations are complete!")
return
# Translate entries
translated_count = 0
for message in untranslated:
translation = translate_message(message.id)
if translation:
if isinstance(message.string, tuple):
# Plural form - set first form
message.string = (translation, message.string[1] if len(message.string) > 1 else "")
else:
message.string = translation
translated_count += 1
print(f"Translated {translated_count} entries")
if translated_count > 0:
# Backup
backup_file = po_file.with_suffix('.po.bak3')
if backup_file.exists():
backup_file.unlink()
po_file.rename(backup_file)
# Write updated file
with open(po_file, 'wb') as f:
write_po(f, catalog, width=79)
print(f"Backup saved to {backup_file}")
print(f"Updated {po_file}")
print(f"\nRemaining untranslated entries: {len(untranslated) - translated_count}")
if len(untranslated) - translated_count > 0:
print("\nFirst 10 remaining untranslated entries:")
for msg in untranslated[translated_count:translated_count+10]:
print(f" - {msg.id[:80]}...")
else:
print("No translations found in dictionary. Manual translation needed.")
if __name__ == '__main__':
complete_translations()

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Script to complete all Spanish translations by translating empty msgstr entries.
Uses Babel for proper .po file parsing and handles all translation cases.
"""
import sys
from pathlib import Path
try:
from babel.messages.pofile import read_po, write_po
from babel.messages.catalog import Message
except ImportError:
print("Error: Babel library not found. Please install it with: pip install Babel")
sys.exit(1)
def translate_string(english_text):
"""
Translate English text to Spanish.
Returns the Spanish translation or empty string if translation not available.
"""
# Comprehensive translation dictionary
translations = {
"Your session expired or the page was open too long. Please try again.":
"Su sesión expiró o la página estuvo abierta demasiado tiempo. Por favor, intente nuevamente.",
"Administrator access required":
"Se requiere acceso de administrador",
"Could not update PDF layout due to a database error.":
"No se pudo actualizar el diseño del PDF debido a un error de base de datos.",
"PDF layout updated successfully":
"Diseño del PDF actualizado correctamente",
"Could not reset PDF layout due to a database error.":
"No se pudo restablecer el diseño del PDF debido a un error de base de datos.",
"PDF layout reset to defaults":
"Diseño del PDF restablecido a los valores predeterminados",
"Username is required":
"Se requiere nombre de usuario",
"Could not create your account due to a database error. Please try again later.":
"No se pudo crear su cuenta debido a un error de base de datos. Por favor, intente nuevamente más tarde.",
"Welcome! Your account has been created.":
"¡Bienvenido! Su cuenta ha sido creada.",
"User not found. Please contact an administrator.":
"Usuario no encontrado. Por favor, contacte a un administrador.",
"Could not update your account role due to a database error.":
"No se pudo actualizar el rol de su cuenta debido a un error de base de datos.",
"Account is disabled. Please contact an administrator.":
"La cuenta está deshabilitada. Por favor, contacte a un administrador.",
"Welcome back, %(username)s!":
"¡Bienvenido de nuevo, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Error inesperado durante el inicio de sesión. Por favor, intente nuevamente o revise los registros del servidor.",
"Goodbye, %(username)s!":
"¡Hasta luego, %(username)s!",
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Tipo de archivo de avatar no válido. Permitidos: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Archivo de imagen no válido.",
"Failed to save avatar on server.":
"Error al guardar el avatar en el servidor.",
"Profile updated successfully":
"Perfil actualizado correctamente",
"Could not update your profile due to a database error.":
"No se pudo actualizar su perfil debido a un error de base de datos.",
"Avatar removed":
"Avatar eliminado",
"Failed to remove avatar.":
"Error al eliminar el avatar.",
"Single Sign-On is not configured yet. Please contact an administrator.":
"El inicio de sesión único aún no está configurado. Por favor, contacte a un administrador.",
"Single Sign-On is not configured.":
"El inicio de sesión único no está configurado.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Error de autenticación: falta el emisor o la reclamación del sujeto. Por favor, verifique la configuración OIDC.",
"User account does not exist and self-registration is disabled.":
"La cuenta de usuario no existe y el auto-registro está deshabilitado.",
"Could not create your account due to a database error.":
"No se pudo crear su cuenta debido a un error de base de datos.",
"Unexpected error during SSO login. Please try again or contact support.":
"Error inesperado durante el inicio de sesión SSO. Por favor, intente nuevamente o contacte al soporte.",
"Event created successfully":
"Evento creado correctamente",
"Event updated successfully":
"Evento actualizado correctamente",
"You do not have permission to delete this event.":
"No tiene permiso para eliminar este evento.",
"Failed to delete event":
"Error al eliminar el evento",
"Event deleted successfully":
"Evento eliminado correctamente",
"Error deleting event: %(error)s":
"Error al eliminar el evento: %(error)s",
"Event moved successfully":
"Evento movido correctamente",
"Event resized successfully":
"Evento redimensionado correctamente",
"You do not have permission to view this event.":
"No tiene permiso para ver este evento.",
"You do not have permission to edit this event.":
"No tiene permiso para editar este evento.",
"Note content cannot be empty":
"El contenido de la nota no puede estar vacío",
"Note added successfully":
"Nota agregada correctamente",
"Error adding note":
"Error al agregar la nota",
"Error adding note: %(error)s":
"Error al agregar la nota: %(error)s",
"Note does not belong to this client":
"La nota no pertenece a este cliente",
"You do not have permission to edit this note":
"No tiene permiso para editar esta nota",
"Error updating note":
"Error al actualizar la nota",
"Note updated successfully":
"Nota actualizada correctamente",
"Error updating note: %(error)s":
"Error al actualizar la nota: %(error)s",
"You do not have permission to delete this note":
"No tiene permiso para eliminar esta nota",
"Error deleting note":
"Error al eliminar la nota",
"Note deleted successfully":
"Nota eliminada correctamente",
"Error deleting note: %(error)s":
"Error al eliminar la nota: %(error)s",
"You do not have permission to create clients":
"No tiene permiso para crear clientes",
"Comment content cannot be empty":
"El contenido del comentario no puede estar vacío",
"Comment must be associated with a project or task":
"El comentario debe estar asociado con un proyecto o tarea",
"Comment cannot be associated with both a project and a task":
"El comentario no puede estar asociado con un proyecto y una tarea",
"Invalid parent comment":
"Comentario padre no válido",
"Comment added successfully":
"Comentario agregado correctamente",
"Error adding comment":
"Error al agregar el comentario",
"Error adding comment: %(error)s":
"Error al agregar el comentario: %(error)s",
"You do not have permission to edit this comment":
"No tiene permiso para editar este comentario",
"Comment updated successfully":
"Comentario actualizado correctamente",
"Error updating comment: %(error)s":
"Error al actualizar el comentario: %(error)s",
"You do not have permission to delete this comment":
"No tiene permiso para eliminar este comentario",
"Comment deleted successfully":
"Comentario eliminado correctamente",
"Error deleting comment: %(error)s":
"Error al eliminar el comentario: %(error)s",
}
return translations.get(english_text, "")
def complete_spanish_translations():
"""Complete all Spanish translations in the messages.po file."""
translations_dir = Path('translations')
es_file = translations_dir / 'es' / 'LC_MESSAGES' / 'messages.po'
if not es_file.exists():
print(f"Error: Spanish translation file not found at {es_file}")
return
print("Reading Spanish translation file...")
with open(es_file, 'r', encoding='utf-8') as f:
catalog = read_po(f)
print(f"Found {len(catalog)} entries in Spanish file")
# Count and translate empty entries
translated_count = 0
untranslated_count = 0
for message in catalog:
if message.id and not message.string:
# Empty translation found
translation = translate_string(message.id)
if translation:
message.string = translation
translated_count += 1
else:
untranslated_count += 1
print(f"\nTranslated: {translated_count} entries")
print(f"Still untranslated: {untranslated_count} entries")
if translated_count > 0:
# Backup original
backup_file = es_file.with_suffix('.po.bak2')
if backup_file.exists():
backup_file.unlink()
es_file.rename(backup_file)
print(f"Backup created: {backup_file}")
# Write updated file
with open(es_file, 'wb') as f:
write_po(f, catalog, width=79)
print(f"Updated: {es_file}")
else:
print("No translations to update")
if untranslated_count > 0:
print(f"\nWarning: {untranslated_count} entries still need manual translation")
print("These entries will need to be translated manually or added to the translation dictionary")
if __name__ == '__main__':
complete_spanish_translations()

View File

@@ -0,0 +1,713 @@
#!/usr/bin/env python3
"""
Complete all Spanish translations by processing the .po file and translating
all empty msgstr entries. This script handles both single-line and multi-line msgid entries.
"""
import re
import sys
from pathlib import Path
def translate_po_file():
"""Main function to translate the PO file."""
# Read the Spanish translation file
po_file = Path('translations/es/LC_MESSAGES/messages.po')
if not po_file.exists():
print(f"Error: File not found: {po_file}")
return
print("Reading Spanish translation file...")
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
# Comprehensive translation dictionary - all remaining translations
translations = {
"Could not update your account role due to a database error.":
"No se pudo actualizar el rol de su cuenta debido a un error de base de datos.",
"Account is disabled. Please contact an administrator.":
"La cuenta está deshabilitada. Por favor, contacte a un administrador.",
"Welcome back, %(username)s!":
"¡Bienvenido de nuevo, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Error inesperado durante el inicio de sesión. Por favor, intente nuevamente o revise los registros del servidor.",
"Goodbye, %(username)s!":
"¡Hasta luego, %(username)s!",
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Tipo de archivo de avatar no válido. Permitidos: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Archivo de imagen no válido.",
"Failed to save avatar on server.":
"Error al guardar el avatar en el servidor.",
"Profile updated successfully":
"Perfil actualizado correctamente",
"Could not update your profile due to a database error.":
"No se pudo actualizar su perfil debido a un error de base de datos.",
"Avatar removed":
"Avatar eliminado",
"Failed to remove avatar.":
"Error al eliminar el avatar.",
"Single Sign-On is not configured yet. Please contact an administrator.":
"El inicio de sesión único aún no está configurado. Por favor, contacte a un administrador.",
"Single Sign-On is not configured.":
"El inicio de sesión único no está configurado.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Error de autenticación: falta el emisor o la reclamación del sujeto. Por favor, verifique la configuración OIDC.",
"User account does not exist and self-registration is disabled.":
"La cuenta de usuario no existe y el auto-registro está deshabilitado.",
"Could not create your account due to a database error.":
"No se pudo crear su cuenta debido a un error de base de datos.",
"Unexpected error during SSO login. Please try again or contact support.":
"Error inesperado durante el inicio de sesión SSO. Por favor, intente nuevamente o contacte al soporte.",
"Event created successfully":
"Evento creado correctamente",
"Event updated successfully":
"Evento actualizado correctamente",
"You do not have permission to delete this event.":
"No tiene permiso para eliminar este evento.",
"Failed to delete event":
"Error al eliminar el evento",
"Event deleted successfully":
"Evento eliminado correctamente",
"Error deleting event: %(error)s":
"Error al eliminar el evento: %(error)s",
"Event moved successfully":
"Evento movido correctamente",
"Event resized successfully":
"Evento redimensionado correctamente",
"You do not have permission to view this event.":
"No tiene permiso para ver este evento.",
"You do not have permission to edit this event.":
"No tiene permiso para editar este evento.",
"Note content cannot be empty":
"El contenido de la nota no puede estar vacío",
"Note added successfully":
"Nota agregada correctamente",
"Error adding note":
"Error al agregar la nota",
"Error adding note: %(error)s":
"Error al agregar la nota: %(error)s",
"Note does not belong to this client":
"La nota no pertenece a este cliente",
"You do not have permission to edit this note":
"No tiene permiso para editar esta nota",
"Error updating note":
"Error al actualizar la nota",
"Note updated successfully":
"Nota actualizada correctamente",
"Error updating note: %(error)s":
"Error al actualizar la nota: %(error)s",
"You do not have permission to delete this note":
"No tiene permiso para eliminar esta nota",
"Error deleting note":
"Error al eliminar la nota",
"Note deleted successfully":
"Nota eliminada correctamente",
"Error deleting note: %(error)s":
"Error al eliminar la nota: %(error)s",
"You do not have permission to create clients":
"No tiene permiso para crear clientes",
"Comment content cannot be empty":
"El contenido del comentario no puede estar vacío",
"Comment must be associated with a project or task":
"El comentario debe estar asociado con un proyecto o tarea",
"Comment cannot be associated with both a project and a task":
"El comentario no puede estar asociado con un proyecto y una tarea",
"Invalid parent comment":
"Comentario padre no válido",
"Comment added successfully":
"Comentario agregado correctamente",
"Error adding comment":
"Error al agregar el comentario",
"Error adding comment: %(error)s":
"Error al agregar el comentario: %(error)s",
"You do not have permission to edit this comment":
"No tiene permiso para editar este comentario",
"Comment updated successfully":
"Comentario actualizado correctamente",
"Error updating comment: %(error)s":
"Error al actualizar el comentario: %(error)s",
"You do not have permission to delete this comment":
"No tiene permiso para eliminar este comentario",
"Comment deleted successfully":
"Comentario eliminado correctamente",
"Error deleting comment: %(error)s":
"Error al eliminar el comentario: %(error)s",
"Category name is required":
"Se requiere el nombre de la categoría",
"Expense category created successfully":
"Categoría de gasto creada correctamente",
"Error creating expense category":
"Error al crear la categoría de gasto",
"Expense category updated successfully":
"Categoría de gasto actualizada correctamente",
"Error updating expense category":
"Error al actualizar la categoría de gasto",
"Expense category deactivated successfully":
"Categoría de gasto desactivada correctamente",
"Error deactivating expense category":
"Error al desactivar la categoría de gasto",
"Title is required":
"Se requiere el título",
"Category is required":
"Se requiere la categoría",
"Amount is required":
"Se requiere el monto",
"Expense date is required":
"Se requiere la fecha del gasto",
"Invalid date format":
"Formato de fecha no válido",
"Invalid amount format":
"Formato de monto no válido",
"Expense created successfully":
"Gasto creado correctamente",
"Error creating expense":
"Error al crear el gasto",
"You do not have permission to view this expense":
"No tiene permiso para ver este gasto",
"You do not have permission to edit this expense":
"No tiene permiso para editar este gasto",
"Cannot edit approved or reimbursed expenses":
"No se pueden editar gastos aprobados o reembolsados",
"Please fill in all required fields":
"Por favor, complete todos los campos requeridos",
"Expense updated successfully":
"Gasto actualizado correctamente",
"Error updating expense":
"Error al actualizar el gasto",
"You do not have permission to delete this expense":
"No tiene permiso para eliminar este gasto",
"Cannot delete approved or invoiced expenses":
"No se pueden eliminar gastos aprobados o facturados",
"Expense deleted successfully":
"Gasto eliminado correctamente",
"Error deleting expense":
"Error al eliminar el gasto",
"Only administrators can approve expenses":
"Solo los administradores pueden aprobar gastos",
"Only pending expenses can be approved":
"Solo se pueden aprobar gastos pendientes",
"Expense approved successfully":
"Gasto aprobado correctamente",
"Error approving expense":
"Error al aprobar el gasto",
"Only administrators can reject expenses":
"Solo los administradores pueden rechazar gastos",
"Only pending expenses can be rejected":
"Solo se pueden rechazar gastos pendientes",
"Rejection reason is required":
"Se requiere la razón del rechazo",
"Expense rejected":
"Gasto rechazado",
"Error rejecting expense":
"Error al rechazar el gasto",
"Only administrators can mark expenses as reimbursed":
"Solo los administradores pueden marcar gastos como reembolsados",
"Only approved expenses can be marked as reimbursed":
"Solo los gastos aprobados pueden marcarse como reembolsados",
"This expense is not marked as reimbursable":
"Este gasto no está marcado como reembolsable",
"Expense marked as reimbursed":
"Gasto marcado como reembolsado",
"Error marking expense as reimbursed":
"Error al marcar el gasto como reembolsado",
"OCR is not available. Please contact your administrator.":
"OCR no está disponible. Por favor, contacte a su administrador.",
"No file provided":
"No se proporcionó archivo",
"No file selected":
"No se seleccionó archivo",
"Invalid file type. Allowed types: png, jpg, jpeg, gif, pdf":
"Tipo de archivo no válido. Tipos permitidos: png, jpg, jpeg, gif, pdf",
"Receipt scanned successfully! You can now create an expense with the extracted data.":
"¡Recibo escaneado correctamente! Ahora puede crear un gasto con los datos extraídos.",
"Error scanning receipt. Please try again or enter the expense manually.":
"Error al escanear el recibo. Por favor, intente nuevamente o ingrese el gasto manualmente.",
"No scanned receipt data found. Please scan a receipt first.":
"No se encontraron datos del recibo escaneado. Por favor, escanee un recibo primero.",
"Expense created successfully from scanned receipt":
"Gasto creado correctamente desde el recibo escaneado",
"You do not have permission to export this invoice":
"No tiene permiso para exportar esta factura",
"PDF generation failed: %(err)s. Fallback also failed: %(fb)s":
"Error en la generación del PDF: %(err)s. El respaldo también falló: %(fb)s",
"Mileage entry created successfully":
"Entrada de kilometraje creada correctamente",
"Error creating mileage entry":
"Error al crear la entrada de kilometraje",
"You do not have permission to view this mileage entry":
"No tiene permiso para ver esta entrada de kilometraje",
"You do not have permission to edit this mileage entry":
"No tiene permiso para editar esta entrada de kilometraje",
"Cannot edit approved or reimbursed mileage entries":
"No se pueden editar entradas de kilometraje aprobadas o reembolsadas",
"Mileage entry updated successfully":
"Entrada de kilometraje actualizada correctamente",
"Error updating mileage entry":
"Error al actualizar la entrada de kilometraje",
"You do not have permission to delete this mileage entry":
"No tiene permiso para eliminar esta entrada de kilometraje",
"Mileage entry deleted successfully":
"Entrada de kilometraje eliminada correctamente",
"Error deleting mileage entry":
"Error al eliminar la entrada de kilometraje",
"Only administrators can approve mileage entries":
"Solo los administradores pueden aprobar entradas de kilometraje",
"Only pending mileage entries can be approved":
"Solo se pueden aprobar entradas de kilometraje pendientes",
"Mileage entry approved successfully":
"Entrada de kilometraje aprobada correctamente",
"Error approving mileage entry":
"Error al aprobar la entrada de kilometraje",
"Only administrators can reject mileage entries":
"Solo los administradores pueden rechazar entradas de kilometraje",
"Only pending mileage entries can be rejected":
"Solo se pueden rechazar entradas de kilometraje pendientes",
"Mileage entry rejected":
"Entrada de kilometraje rechazada",
"Error rejecting mileage entry":
"Error al rechazar la entrada de kilometraje",
"Only administrators can mark mileage entries as reimbursed":
"Solo los administradores pueden marcar entradas de kilometraje como reembolsadas",
"Only approved mileage entries can be marked as reimbursed":
"Solo las entradas de kilometraje aprobadas pueden marcarse como reembolsadas",
"Mileage entry marked as reimbursed":
"Entrada de kilometraje marcada como reembolsada",
"Error marking mileage entry as reimbursed":
"Error al marcar la entrada de kilometraje como reembolsada",
"Start date must be before end date":
"La fecha de inicio debe ser anterior a la fecha de fin",
"No per diem rate found for this location. Please configure rates first.":
"No se encontró tarifa de viáticos para esta ubicación. Por favor, configure las tarifas primero.",
"Per diem claim created successfully":
"Reclamación de viáticos creada correctamente",
"Error creating per diem claim":
"Error al crear la reclamación de viáticos",
"You do not have permission to view this per diem claim":
"No tiene permiso para ver esta reclamación de viáticos",
"You do not have permission to edit this per diem claim":
"No tiene permiso para editar esta reclamación de viáticos",
"Cannot edit approved or reimbursed per diem claims":
"No se pueden editar reclamaciones de viáticos aprobadas o reembolsadas",
"Per diem claim updated successfully":
"Reclamación de viáticos actualizada correctamente",
"Error updating per diem claim":
"Error al actualizar la reclamación de viáticos",
"You do not have permission to delete this per diem claim":
"No tiene permiso para eliminar esta reclamación de viáticos",
"Per diem claim deleted successfully":
"Reclamación de viáticos eliminada correctamente",
"Error deleting per diem claim":
"Error al eliminar la reclamación de viáticos",
"Only administrators can approve per diem claims":
"Solo los administradores pueden aprobar reclamaciones de viáticos",
"Only pending per diem claims can be approved":
"Solo se pueden aprobar reclamaciones de viáticos pendientes",
"Per diem claim approved successfully":
"Reclamación de viáticos aprobada correctamente",
"Error approving per diem claim":
"Error al aprobar la reclamación de viáticos",
"Only administrators can reject per diem claims":
"Solo los administradores pueden rechazar reclamaciones de viáticos",
"Only pending per diem claims can be rejected":
"Solo se pueden rechazar reclamaciones de viáticos pendientes",
"Per diem claim rejected":
"Reclamación de viáticos rechazada",
"Error rejecting per diem claim":
"Error al rechazar la reclamación de viáticos",
"Per diem rate created successfully":
"Tarifa de viáticos creada correctamente",
"Error creating per diem rate":
"Error al crear la tarifa de viáticos",
"You do not have permission to access this page":
"No tiene permiso para acceder a esta página",
"Role name is required":
"Se requiere el nombre del rol",
"A role with this name already exists":
"Ya existe un rol con este nombre",
"Could not create role due to a database error":
"No se pudo crear el rol debido a un error de base de datos",
"Role created successfully":
"Rol creado correctamente",
"System roles cannot be edited":
"Los roles del sistema no se pueden editar",
"Could not update role due to a database error":
"No se pudo actualizar el rol debido a un error de base de datos",
"Role updated successfully":
"Rol actualizado correctamente",
"You do not have permission to perform this action":
"No tiene permiso para realizar esta acción",
"System roles cannot be deleted":
"Los roles del sistema no se pueden eliminar",
"Cannot delete role that is assigned to users. Please reassign users first.":
"No se puede eliminar un rol que está asignado a usuarios. Por favor, reasigne los usuarios primero.",
"Could not delete role due to a database error":
"No se pudo eliminar el rol debido a un error de base de datos",
'Role "%(name)s" deleted successfully':
'Rol "%(name)s" eliminado correctamente',
"Could not update user roles due to a database error":
"No se pudo actualizar los roles de usuario debido a un error de base de datos",
"User roles updated successfully":
"Roles de usuario actualizados correctamente",
"Project code already in use":
"El código del proyecto ya está en uso",
"Project is already in favorites":
"El proyecto ya está en favoritos",
"Project added to favorites":
"Proyecto agregado a favoritos",
"Failed to add project to favorites":
"Error al agregar el proyecto a favoritos",
"Project is not in favorites":
"El proyecto no está en favoritos",
"Project removed from favorites":
"Proyecto eliminado de favoritos",
"Failed to remove project from favorites":
"Error al eliminar el proyecto de favoritos",
"Description, category, amount, and date are required":
"Se requieren descripción, categoría, monto y fecha",
"Could not add cost due to a database error. Please check server logs.":
"No se pudo agregar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Cost added successfully":
"Costo agregado correctamente",
"Cost not found":
"Costo no encontrado",
"You do not have permission to edit this cost":
"No tiene permiso para editar este costo",
"Could not update cost due to a database error. Please check server logs.":
"No se pudo actualizar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Cost updated successfully":
"Costo actualizado correctamente",
"You do not have permission to delete this cost":
"No tiene permiso para eliminar este costo",
"Cannot delete cost that has been invoiced":
"No se puede eliminar un costo que ha sido facturado",
"Could not delete cost due to a database error. Please check server logs.":
"No se pudo eliminar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Name and unit price are required":
"Se requieren nombre y precio unitario",
"Invalid quantity format":
"Formato de cantidad no válido",
"Invalid unit price format":
"Formato de precio unitario no válido",
"Could not add extra good due to a database error. Please check server logs.":
"No se pudo agregar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Extra good added successfully":
"Artículo extra agregado correctamente",
"Extra good not found":
"Artículo extra no encontrado",
"You do not have permission to edit this extra good":
"No tiene permiso para editar este artículo extra",
"Could not update extra good due to a database error. Please check server logs.":
"No se pudo actualizar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Extra good updated successfully":
"Artículo extra actualizado correctamente",
"You do not have permission to delete this extra good":
"No tiene permiso para eliminar este artículo extra",
"Cannot delete extra good that has been added to an invoice":
"No se puede eliminar un artículo extra que ha sido agregado a una factura",
"Could not delete extra good due to a database error. Please check server logs.":
"No se pudo eliminar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Invalid project selected":
"Proyecto seleccionado no válido",
"Cannot start timer for an archived project. Please unarchive the project first.":
"No se puede iniciar el temporizador para un proyecto archivado. Por favor, desarchive el proyecto primero.",
"Cannot start timer for an inactive project":
"No se puede iniciar el temporizador para un proyecto inactivo",
"Cannot create time entries for an archived project. Please unarchive the project first.":
"No se pueden crear entradas de tiempo para un proyecto archivado. Por favor, desarchive el proyecto primero.",
"Cannot create time entries for an inactive project":
"No se pueden crear entradas de tiempo para un proyecto inactivo",
"Invalid timezone selected":
"Zona horaria seleccionada no válida",
"Standard hours per day must be between 0.5 and 24":
"Las horas estándar por día deben estar entre 0.5 y 24",
"Settings saved successfully":
"Configuración guardada correctamente",
"Error saving settings":
"Error al guardar la configuración",
"Error saving settings: %(error)s":
"Error al guardar la configuración: %(error)s",
"Preferences updated":
"Preferencias actualizadas",
"Language updated successfully":
"Idioma actualizado correctamente",
"Invalid language":
"Idioma no válido",
"Language updated to %(language)s":
"Idioma actualizado a %(language)s",
"Please enter a valid target hours (greater than 0)":
"Por favor, ingrese un objetivo de horas válido (mayor que 0)",
"A goal already exists for this week. Please edit the existing goal instead.":
"Ya existe un objetivo para esta semana. Por favor, edite el objetivo existente.",
"Weekly time goal created successfully!":
"¡Objetivo de tiempo semanal creado correctamente!",
"Failed to create goal. Please try again.":
"Error al crear el objetivo. Por favor, intente nuevamente.",
"You do not have permission to view this goal":
"No tiene permiso para ver este objetivo",
"You do not have permission to edit this goal":
"No tiene permiso para editar este objetivo",
"Weekly time goal updated successfully!":
"¡Objetivo de tiempo semanal actualizado correctamente!",
"Failed to update goal. Please try again.":
"Error al actualizar el objetivo. Por favor, intente nuevamente.",
"You do not have permission to delete this goal":
"No tiene permiso para eliminar este objetivo",
"Weekly time goal deleted successfully":
"Objetivo de tiempo semanal eliminado correctamente",
"Failed to delete goal. Please try again.":
"Error al eliminar el objetivo. Por favor, intente nuevamente.",
"remaining":
"restante",
"Success":
"Éxito",
"Error":
"Error",
"Warning":
"Advertencia",
"Information":
"Información",
"Saving...":
"Guardando...",
"Save":
"Guardar",
"Edit":
"Editar",
"Add":
"Agregar",
"Remove":
"Eliminar",
"Yes":
"",
"No":
"No",
"OK":
"Aceptar",
"Are you sure you want to delete this?":
"¿Está seguro de que desea eliminar esto?",
"You have unsaved changes. Are you sure you want to leave?":
"Tiene cambios sin guardar. ¿Está seguro de que desea salir?",
"Operation failed":
"Operación fallida",
"Operation completed successfully":
"Operación completada correctamente",
"No items selected":
"No hay elementos seleccionados",
"Invalid input":
"Entrada no válida",
"This field is required":
"Este campo es obligatorio",
"No active timer":
"No hay temporizador activo",
"Timer stopped":
"Temporizador detenido",
"Failed to stop timer":
"Error al detener el temporizador",
"Error stopping timer":
"Error al detener el temporizador",
"No form to save":
"No hay formulario para guardar",
"No timer found":
"No se encontró temporizador",
"Timer stopped due to inactivity":
"Temporizador detenido por inactividad",
"Navigation":
"Navegación",
"Time Tracking":
"Seguimiento de Tiempo",
"Kanban Board":
"Tablero Kanban",
"Weekly Goals":
"Objetivos Semanales",
"Templates":
"Plantillas",
"Finance & Expenses":
"Finanzas y Gastos",
"Payments":
"Pagos",
"Expenses":
"Gastos",
"Mileage":
"Kilometraje",
"Per Diem":
"Viáticos",
"Budget Alerts":
"Alertas de Presupuesto",
"Tools & Data":
"Herramientas y Datos",
"Import / Export":
"Importar / Exportar",
"Saved Filters":
"Filtros Guardados",
"Admin Dashboard":
"Panel de Administración",
"Users":
"Usuarios",
"API Tokens":
"Tokens de API",
"Roles & Permissions":
"Roles y Permisos",
"System Settings":
"Configuración del Sistema",
"PDF Layout":
"Diseño de PDF",
"Expense Categories":
"Categorías de Gastos",
"Per Diem Rates":
"Tarifas de Viáticos",
"System Info":
"Información del Sistema",
"Backups":
"Copias de Seguridad",
"OIDC Settings":
"Configuración OIDC",
"Support TimeTracker":
"Apoyar TimeTracker",
"Enjoying TimeTracker? Consider buying me a coffee to support continued development!":
"¿Disfrutando de TimeTracker? ¡Considere invitarme un café para apoyar el desarrollo continuo!",
"Made with":
"Hecho con",
"by":
"por",
"Support TimeTracker development":
"Apoyar el desarrollo de TimeTracker",
"Support":
"Soporte",
"Enjoying TimeTracker?":
"¿Disfrutando de TimeTracker?",
"Support continued development with a coffee":
"Apoye el desarrollo continuo con un café",
"Dismiss":
"Descartar",
"Toggle dark mode":
"Alternar modo oscuro",
"Change language":
"Cambiar idioma",
"User menu":
"Menú de usuario",
"Guest":
"Invitado",
"My Profile":
"Mi Perfil",
"My Settings":
"Mis Configuraciones",
"Are you sure you want to":
"¿Está seguro de que desea",
"deactivate":
"desactivar",
"activate":
"activar",
"this token?":
"este token?",
"Deactivate Token":
"Desactivar Token",
"Activate Token":
"Activar Token",
"Deactivate":
"Desactivar",
"Activate":
"Activar",
"Are you sure you want to delete this token? This action cannot be undone.":
"¿Está seguro de que desea eliminar este token? Esta acción no se puede deshacer.",
"Delete Token":
"Eliminar Token",
"Email Configuration & Testing":
"Configuración y Prueba de Correo Electrónico",
"Configure and test email delivery":
"Configurar y probar la entrega de correo electrónico",
"Back to Admin":
"Volver a Administración",
"Email Configuration":
"Configuración de Correo Electrónico",
"Configure email settings here to save them in the database. Database settings take precedence over environment variables.":
"Configure la configuración de correo electrónico aquí para guardarla en la base de datos. La configuración de la base de datos tiene prioridad sobre las variables de entorno.",
"Enable Database Email Configuration":
"Habilitar Configuración de Correo Electrónico en Base de Datos",
"Mail Server":
"Servidor de Correo",
"Mail Port":
"Puerto de Correo",
"Use TLS":
"Usar TLS",
"Use SSL":
"Usar SSL",
}
# Process single-line msgid entries
translated_count = 0
untranslated = []
def replace_single_line(match):
nonlocal translated_count
msgid_text = match.group(1)
translation = translations.get(msgid_text, "")
if translation:
translated_count += 1
# Escape quotes in translation
translation_escaped = translation.replace('"', '\\"')
return f'msgid "{msgid_text}"\nmsgstr "{translation_escaped}"'
else:
untranslated.append(msgid_text)
return match.group(0)
# Pattern for single-line msgid with empty msgstr
pattern = r'msgid\s+"([^"]+)"\s*\nmsgstr\s+""'
new_content = re.sub(pattern, replace_single_line, content)
# Handle multi-line msgid entries
multiline_pattern = r'msgid\s+""\s*\n((?:"[^"]*"\s*\n)+)msgstr\s+""'
def replace_multiline(match):
nonlocal translated_count
# Extract the full msgid text from continuation lines
lines = [line.strip().strip('"') for line in match.group(1).strip().split('\n') if line.strip().startswith('"')]
msgid_text = ''.join(lines)
translation = translations.get(msgid_text, "")
if translation:
translated_count += 1
# Format translation for multi-line if needed (PO format)
if len(translation) > 70:
# Split into multiple lines following PO format
translation_lines = []
remaining = translation
while len(remaining) > 70:
translation_lines.append(f'"{remaining[:70]}"')
remaining = remaining[70:]
if remaining:
translation_lines.append(f'"{remaining}"')
translation_formatted = '\n'.join(translation_lines)
return f'msgid ""\n{match.group(1)}msgstr ""\n{translation_formatted}'
else:
return f'msgid ""\n{match.group(1)}msgstr "{translation}"'
else:
untranslated.append(msgid_text)
return match.group(0)
new_content = re.sub(multiline_pattern, replace_multiline, new_content, flags=re.MULTILINE)
# Write back if changes were made
if new_content != content:
# Backup
backup_path = po_file.with_suffix('.po.bak_final')
if backup_path.exists():
backup_path.unlink()
po_file.rename(backup_path)
print(f"Backup created: {backup_path}")
# Write new content
with open(po_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated: {po_file}")
print(f"Translated: {translated_count} entries")
if untranslated:
print(f"\nStill untranslated: {len(untranslated)} entries")
print("First 20 untranslated:")
for msg in untranslated[:20]:
print(f" - {msg}")
else:
print("No changes made")
if __name__ == '__main__':
translate_po_file()

View File

@@ -4,6 +4,9 @@ import subprocess
def run(cmd: list[str]) -> int:
print("$", " ".join(cmd))
# Use python -m babel instead of pybabel directly
if cmd[0] == 'pybabel':
cmd = ['python', '-m', 'babel.messages.frontend'] + cmd[1:]
return subprocess.call(cmd)

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Script to sync translation files by adding missing msgid entries from English
to all other language files. This ensures all languages have the same structure.
Uses Babel's library for reliable .po file parsing.
"""
import os
import sys
from pathlib import Path
try:
from babel.messages.pofile import read_po, write_po
from babel.messages.catalog import Message
except ImportError:
print("Error: Babel library not found. Please install it with: pip install Babel")
sys.exit(1)
def read_po_catalog(filepath):
"""Read a .po file and return the catalog."""
with open(filepath, 'r', encoding='utf-8') as f:
return read_po(f)
def sync_translations():
"""Sync all translation files with English as the reference."""
translations_dir = Path('translations')
en_file = translations_dir / 'en' / 'LC_MESSAGES' / 'messages.po'
if not en_file.exists():
print(f"Error: English translation file not found at {en_file}")
return
print("Reading English translation file...")
en_catalog = read_po_catalog(en_file)
print(f"Found {len(en_catalog)} entries in English file")
# Languages to update (excluding English)
languages = ['de', 'nl', 'fr', 'it', 'fi', 'es', 'ar', 'he', 'nb', 'no']
for lang in languages:
lang_file = translations_dir / lang / 'LC_MESSAGES' / 'messages.po'
if not lang_file.exists():
print(f"Warning: {lang} translation file not found, skipping...")
continue
print(f"\nProcessing {lang}...")
lang_catalog = read_po_catalog(lang_file)
# Add missing entries from English
added = 0
for message in en_catalog:
if message.id and message.id not in lang_catalog:
# Create new message with empty translation
new_msg = Message(message.id, '', context=message.context)
lang_catalog[message.id] = new_msg
added += 1
if added > 0:
print(f" Added {added} missing entries")
# Backup original
backup_file = lang_file.with_suffix('.po.bak')
if backup_file.exists():
backup_file.unlink()
lang_file.rename(backup_file)
# Write updated file
with open(lang_file, 'wb') as f:
write_po(f, lang_catalog, width=79)
print(f" Updated {lang_file}")
print(f" Backup saved to {backup_file}")
else:
print(f" No missing entries, already up to date")
print("\nSync complete!")
if __name__ == '__main__':
sync_translations()

View File

@@ -0,0 +1,786 @@
#!/usr/bin/env python3
"""
Complete Spanish translation script - translates all empty msgstr entries.
Reads the .po file and replaces all empty translations with Spanish translations.
"""
import re
from pathlib import Path
# Comprehensive Spanish translations dictionary
TRANSLATIONS = {
# Session and authentication
"Your session expired or the page was open too long. Please try again.":
"Su sesión expiró o la página estuvo abierta demasiado tiempo. Por favor, intente nuevamente.",
"Administrator access required":
"Se requiere acceso de administrador",
# PDF Layout
"Could not update PDF layout due to a database error.":
"No se pudo actualizar el diseño del PDF debido a un error de base de datos.",
"PDF layout updated successfully":
"Diseño del PDF actualizado correctamente",
"Could not reset PDF layout due to a database error.":
"No se pudo restablecer el diseño del PDF debido a un error de base de datos.",
"PDF layout reset to defaults":
"Diseño del PDF restablecido a los valores predeterminados",
# User account
"Username is required":
"Se requiere nombre de usuario",
"Could not create your account due to a database error. Please try again later.":
"No se pudo crear su cuenta debido a un error de base de datos. Por favor, intente nuevamente más tarde.",
"Welcome! Your account has been created.":
"¡Bienvenido! Su cuenta ha sido creada.",
"User not found. Please contact an administrator.":
"Usuario no encontrado. Por favor, contacte a un administrador.",
"Could not update your account role due to a database error.":
"No se pudo actualizar el rol de su cuenta debido a un error de base de datos.",
"Account is disabled. Please contact an administrator.":
"La cuenta está deshabilitada. Por favor, contacte a un administrador.",
"Welcome back, %(username)s!":
"¡Bienvenido de nuevo, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Error inesperado durante el inicio de sesión. Por favor, intente nuevamente o revise los registros del servidor.",
"Goodbye, %(username)s!":
"¡Hasta luego, %(username)s!",
# Avatar and profile
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Tipo de archivo de avatar no válido. Permitidos: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Archivo de imagen no válido.",
"Failed to save avatar on server.":
"Error al guardar el avatar en el servidor.",
"Profile updated successfully":
"Perfil actualizado correctamente",
"Could not update your profile due to a database error.":
"No se pudo actualizar su perfil debido a un error de base de datos.",
"Avatar removed":
"Avatar eliminado",
"Failed to remove avatar.":
"Error al eliminar el avatar.",
# SSO
"Single Sign-On is not configured yet. Please contact an administrator.":
"El inicio de sesión único aún no está configurado. Por favor, contacte a un administrador.",
"Single Sign-On is not configured.":
"El inicio de sesión único no está configurado.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Error de autenticación: falta el emisor o la reclamación del sujeto. Por favor, verifique la configuración OIDC.",
"User account does not exist and self-registration is disabled.":
"La cuenta de usuario no existe y el auto-registro está deshabilitado.",
"Could not create your account due to a database error.":
"No se pudo crear su cuenta debido a un error de base de datos.",
"Unexpected error during SSO login. Please try again or contact support.":
"Error inesperado durante el inicio de sesión SSO. Por favor, intente nuevamente o contacte al soporte.",
# Events
"Event created successfully":
"Evento creado correctamente",
"Event updated successfully":
"Evento actualizado correctamente",
"You do not have permission to delete this event.":
"No tiene permiso para eliminar este evento.",
"Failed to delete event":
"Error al eliminar el evento",
"Event deleted successfully":
"Evento eliminado correctamente",
"Error deleting event: %(error)s":
"Error al eliminar el evento: %(error)s",
"Event moved successfully":
"Evento movido correctamente",
"Event resized successfully":
"Evento redimensionado correctamente",
"You do not have permission to view this event.":
"No tiene permiso para ver este evento.",
"You do not have permission to edit this event.":
"No tiene permiso para editar este evento.",
# Notes
"Note content cannot be empty":
"El contenido de la nota no puede estar vacío",
"Note added successfully":
"Nota agregada correctamente",
"Error adding note":
"Error al agregar la nota",
"Error adding note: %(error)s":
"Error al agregar la nota: %(error)s",
"Note does not belong to this client":
"La nota no pertenece a este cliente",
"You do not have permission to edit this note":
"No tiene permiso para editar esta nota",
"Error updating note":
"Error al actualizar la nota",
"Note updated successfully":
"Nota actualizada correctamente",
"Error updating note: %(error)s":
"Error al actualizar la nota: %(error)s",
"You do not have permission to delete this note":
"No tiene permiso para eliminar esta nota",
"Error deleting note":
"Error al eliminar la nota",
"Note deleted successfully":
"Nota eliminada correctamente",
"Error deleting note: %(error)s":
"Error al eliminar la nota: %(error)s",
# Clients
"You do not have permission to create clients":
"No tiene permiso para crear clientes",
# Comments
"Comment content cannot be empty":
"El contenido del comentario no puede estar vacío",
"Comment must be associated with a project or task":
"El comentario debe estar asociado con un proyecto o tarea",
"Comment cannot be associated with both a project and a task":
"El comentario no puede estar asociado con un proyecto y una tarea",
"Invalid parent comment":
"Comentario padre no válido",
"Comment added successfully":
"Comentario agregado correctamente",
"Error adding comment":
"Error al agregar el comentario",
"Error adding comment: %(error)s":
"Error al agregar el comentario: %(error)s",
"You do not have permission to edit this comment":
"No tiene permiso para editar este comentario",
"Comment updated successfully":
"Comentario actualizado correctamente",
"Error updating comment: %(error)s":
"Error al actualizar el comentario: %(error)s",
"You do not have permission to delete this comment":
"No tiene permiso para eliminar este comentario",
"Comment deleted successfully":
"Comentario eliminado correctamente",
"Error deleting comment: %(error)s":
"Error al eliminar el comentario: %(error)s",
# Expense categories
"Category name is required":
"Se requiere el nombre de la categoría",
"Expense category created successfully":
"Categoría de gasto creada correctamente",
"Error creating expense category":
"Error al crear la categoría de gasto",
"Expense category updated successfully":
"Categoría de gasto actualizada correctamente",
"Error updating expense category":
"Error al actualizar la categoría de gasto",
"Expense category deactivated successfully":
"Categoría de gasto desactivada correctamente",
"Error deactivating expense category":
"Error al desactivar la categoría de gasto",
# Expenses
"Title is required":
"Se requiere el título",
"Category is required":
"Se requiere la categoría",
"Amount is required":
"Se requiere el monto",
"Expense date is required":
"Se requiere la fecha del gasto",
"Invalid date format":
"Formato de fecha no válido",
"Invalid amount format":
"Formato de monto no válido",
"Expense created successfully":
"Gasto creado correctamente",
"Error creating expense":
"Error al crear el gasto",
"You do not have permission to view this expense":
"No tiene permiso para ver este gasto",
"You do not have permission to edit this expense":
"No tiene permiso para editar este gasto",
"Cannot edit approved or reimbursed expenses":
"No se pueden editar gastos aprobados o reembolsados",
"Please fill in all required fields":
"Por favor, complete todos los campos requeridos",
"Expense updated successfully":
"Gasto actualizado correctamente",
"Error updating expense":
"Error al actualizar el gasto",
"You do not have permission to delete this expense":
"No tiene permiso para eliminar este gasto",
"Cannot delete approved or invoiced expenses":
"No se pueden eliminar gastos aprobados o facturados",
"Expense deleted successfully":
"Gasto eliminado correctamente",
"Error deleting expense":
"Error al eliminar el gasto",
"Only administrators can approve expenses":
"Solo los administradores pueden aprobar gastos",
"Only pending expenses can be approved":
"Solo se pueden aprobar gastos pendientes",
"Expense approved successfully":
"Gasto aprobado correctamente",
"Error approving expense":
"Error al aprobar el gasto",
"Only administrators can reject expenses":
"Solo los administradores pueden rechazar gastos",
"Only pending expenses can be rejected":
"Solo se pueden rechazar gastos pendientes",
"Rejection reason is required":
"Se requiere la razón del rechazo",
"Expense rejected":
"Gasto rechazado",
"Error rejecting expense":
"Error al rechazar el gasto",
"Only administrators can mark expenses as reimbursed":
"Solo los administradores pueden marcar gastos como reembolsados",
"Only approved expenses can be marked as reimbursed":
"Solo los gastos aprobados pueden marcarse como reembolsados",
"This expense is not marked as reimbursable":
"Este gasto no está marcado como reembolsable",
"Expense marked as reimbursed":
"Gasto marcado como reembolsado",
"Error marking expense as reimbursed":
"Error al marcar el gasto como reembolsado",
# OCR and receipts
"OCR is not available. Please contact your administrator.":
"OCR no está disponible. Por favor, contacte a su administrador.",
"No file provided":
"No se proporcionó archivo",
"No file selected":
"No se seleccionó archivo",
"Invalid file type. Allowed types: png, jpg, jpeg, gif, pdf":
"Tipo de archivo no válido. Tipos permitidos: png, jpg, jpeg, gif, pdf",
"Receipt scanned successfully! You can now create an expense with the extracted data.":
"¡Recibo escaneado correctamente! Ahora puede crear un gasto con los datos extraídos.",
"Error scanning receipt. Please try again or enter the expense manually.":
"Error al escanear el recibo. Por favor, intente nuevamente o ingrese el gasto manualmente.",
"No scanned receipt data found. Please scan a receipt first.":
"No se encontraron datos del recibo escaneado. Por favor, escanee un recibo primero.",
"Expense created successfully from scanned receipt":
"Gasto creado correctamente desde el recibo escaneado",
# Invoices
"You do not have permission to export this invoice":
"No tiene permiso para exportar esta factura",
"PDF generation failed: %(err)s. Fallback also failed: %(fb)s":
"Error en la generación del PDF: %(err)s. El respaldo también falló: %(fb)s",
# Mileage
"Mileage entry created successfully":
"Entrada de kilometraje creada correctamente",
"Error creating mileage entry":
"Error al crear la entrada de kilometraje",
"You do not have permission to view this mileage entry":
"No tiene permiso para ver esta entrada de kilometraje",
"You do not have permission to edit this mileage entry":
"No tiene permiso para editar esta entrada de kilometraje",
"Cannot edit approved or reimbursed mileage entries":
"No se pueden editar entradas de kilometraje aprobadas o reembolsadas",
"Mileage entry updated successfully":
"Entrada de kilometraje actualizada correctamente",
"Error updating mileage entry":
"Error al actualizar la entrada de kilometraje",
"You do not have permission to delete this mileage entry":
"No tiene permiso para eliminar esta entrada de kilometraje",
"Mileage entry deleted successfully":
"Entrada de kilometraje eliminada correctamente",
"Error deleting mileage entry":
"Error al eliminar la entrada de kilometraje",
"Only administrators can approve mileage entries":
"Solo los administradores pueden aprobar entradas de kilometraje",
"Only pending mileage entries can be approved":
"Solo se pueden aprobar entradas de kilometraje pendientes",
"Mileage entry approved successfully":
"Entrada de kilometraje aprobada correctamente",
"Error approving mileage entry":
"Error al aprobar la entrada de kilometraje",
"Only administrators can reject mileage entries":
"Solo los administradores pueden rechazar entradas de kilometraje",
"Only pending mileage entries can be rejected":
"Solo se pueden rechazar entradas de kilometraje pendientes",
"Mileage entry rejected":
"Entrada de kilometraje rechazada",
"Error rejecting mileage entry":
"Error al rechazar la entrada de kilometraje",
"Only administrators can mark mileage entries as reimbursed":
"Solo los administradores pueden marcar entradas de kilometraje como reembolsadas",
"Only approved mileage entries can be marked as reimbursed":
"Solo las entradas de kilometraje aprobadas pueden marcarse como reembolsadas",
"Mileage entry marked as reimbursed":
"Entrada de kilometraje marcada como reembolsada",
"Error marking mileage entry as reimbursed":
"Error al marcar la entrada de kilometraje como reembolsada",
# Per diem
"Start date must be before end date":
"La fecha de inicio debe ser anterior a la fecha de fin",
"No per diem rate found for this location. Please configure rates first.":
"No se encontró tarifa de viáticos para esta ubicación. Por favor, configure las tarifas primero.",
"Per diem claim created successfully":
"Reclamación de viáticos creada correctamente",
"Error creating per diem claim":
"Error al crear la reclamación de viáticos",
"You do not have permission to view this per diem claim":
"No tiene permiso para ver esta reclamación de viáticos",
"You do not have permission to edit this per diem claim":
"No tiene permiso para editar esta reclamación de viáticos",
"Cannot edit approved or reimbursed per diem claims":
"No se pueden editar reclamaciones de viáticos aprobadas o reembolsadas",
"Per diem claim updated successfully":
"Reclamación de viáticos actualizada correctamente",
"Error updating per diem claim":
"Error al actualizar la reclamación de viáticos",
"You do not have permission to delete this per diem claim":
"No tiene permiso para eliminar esta reclamación de viáticos",
"Per diem claim deleted successfully":
"Reclamación de viáticos eliminada correctamente",
"Error deleting per diem claim":
"Error al eliminar la reclamación de viáticos",
"Only administrators can approve per diem claims":
"Solo los administradores pueden aprobar reclamaciones de viáticos",
"Only pending per diem claims can be approved":
"Solo se pueden aprobar reclamaciones de viáticos pendientes",
"Per diem claim approved successfully":
"Reclamación de viáticos aprobada correctamente",
"Error approving per diem claim":
"Error al aprobar la reclamación de viáticos",
"Only administrators can reject per diem claims":
"Solo los administradores pueden rechazar reclamaciones de viáticos",
"Only pending per diem claims can be rejected":
"Solo se pueden rechazar reclamaciones de viáticos pendientes",
"Per diem claim rejected":
"Reclamación de viáticos rechazada",
"Error rejecting per diem claim":
"Error al rechazar la reclamación de viáticos",
"Per diem rate created successfully":
"Tarifa de viáticos creada correctamente",
"Error creating per diem rate":
"Error al crear la tarifa de viáticos",
# Permissions
"You do not have permission to access this page":
"No tiene permiso para acceder a esta página",
"Role name is required":
"Se requiere el nombre del rol",
"A role with this name already exists":
"Ya existe un rol con este nombre",
"Could not create role due to a database error":
"No se pudo crear el rol debido a un error de base de datos",
"Role created successfully":
"Rol creado correctamente",
"System roles cannot be edited":
"Los roles del sistema no se pueden editar",
"Could not update role due to a database error":
"No se pudo actualizar el rol debido a un error de base de datos",
"Role updated successfully":
"Rol actualizado correctamente",
"You do not have permission to perform this action":
"No tiene permiso para realizar esta acción",
"System roles cannot be deleted":
"Los roles del sistema no se pueden eliminar",
"Cannot delete role that is assigned to users. Please reassign users first.":
"No se puede eliminar un rol que está asignado a usuarios. Por favor, reasigne los usuarios primero.",
"Could not delete role due to a database error":
"No se pudo eliminar el rol debido a un error de base de datos",
'Role "%(name)s" deleted successfully':
'Rol "%(name)s" eliminado correctamente',
"Could not update user roles due to a database error":
"No se pudo actualizar los roles de usuario debido a un error de base de datos",
"User roles updated successfully":
"Roles de usuario actualizados correctamente",
# Projects
"Project code already in use":
"El código del proyecto ya está en uso",
"Project is already in favorites":
"El proyecto ya está en favoritos",
"Project added to favorites":
"Proyecto agregado a favoritos",
"Failed to add project to favorites":
"Error al agregar el proyecto a favoritos",
"Project is not in favorites":
"El proyecto no está en favoritos",
"Project removed from favorites":
"Proyecto eliminado de favoritos",
"Failed to remove project from favorites":
"Error al eliminar el proyecto de favoritos",
# Project costs
"Description, category, amount, and date are required":
"Se requieren descripción, categoría, monto y fecha",
"Could not add cost due to a database error. Please check server logs.":
"No se pudo agregar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Cost added successfully":
"Costo agregado correctamente",
"Cost not found":
"Costo no encontrado",
"You do not have permission to edit this cost":
"No tiene permiso para editar este costo",
"Could not update cost due to a database error. Please check server logs.":
"No se pudo actualizar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Cost updated successfully":
"Costo actualizado correctamente",
"You do not have permission to delete this cost":
"No tiene permiso para eliminar este costo",
"Cannot delete cost that has been invoiced":
"No se puede eliminar un costo que ha sido facturado",
"Could not delete cost due to a database error. Please check server logs.":
"No se pudo eliminar el costo debido a un error de base de datos. Por favor, revise los registros del servidor.",
# Extra goods
"Name and unit price are required":
"Se requieren nombre y precio unitario",
"Invalid quantity format":
"Formato de cantidad no válido",
"Invalid unit price format":
"Formato de precio unitario no válido",
"Could not add extra good due to a database error. Please check server logs.":
"No se pudo agregar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Extra good added successfully":
"Artículo extra agregado correctamente",
"Extra good not found":
"Artículo extra no encontrado",
"You do not have permission to edit this extra good":
"No tiene permiso para editar este artículo extra",
"Could not update extra good due to a database error. Please check server logs.":
"No se pudo actualizar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
"Extra good updated successfully":
"Artículo extra actualizado correctamente",
"You do not have permission to delete this extra good":
"No tiene permiso para eliminar este artículo extra",
"Cannot delete extra good that has been added to an invoice":
"No se puede eliminar un artículo extra que ha sido agregado a una factura",
"Could not delete extra good due to a database error. Please check server logs.":
"No se pudo eliminar el artículo extra debido a un error de base de datos. Por favor, revise los registros del servidor.",
# Timer and projects
"Invalid project selected":
"Proyecto seleccionado no válido",
"Cannot start timer for an archived project. Please unarchive the project first.":
"No se puede iniciar el temporizador para un proyecto archivado. Por favor, desarchive el proyecto primero.",
"Cannot start timer for an inactive project":
"No se puede iniciar el temporizador para un proyecto inactivo",
"Cannot create time entries for an archived project. Please unarchive the project first.":
"No se pueden crear entradas de tiempo para un proyecto archivado. Por favor, desarchive el proyecto primero.",
"Cannot create time entries for an inactive project":
"No se pueden crear entradas de tiempo para un proyecto inactivo",
"Invalid timezone selected":
"Zona horaria seleccionada no válida",
"Standard hours per day must be between 0.5 and 24":
"Las horas estándar por día deben estar entre 0.5 y 24",
# Settings
"Settings saved successfully":
"Configuración guardada correctamente",
"Error saving settings":
"Error al guardar la configuración",
"Error saving settings: %(error)s":
"Error al guardar la configuración: %(error)s",
"Preferences updated":
"Preferencias actualizadas",
"Language updated successfully":
"Idioma actualizado correctamente",
"Invalid language":
"Idioma no válido",
"Language updated to %(language)s":
"Idioma actualizado a %(language)s",
# Weekly goals
"Please enter a valid target hours (greater than 0)":
"Por favor, ingrese un objetivo de horas válido (mayor que 0)",
"A goal already exists for this week. Please edit the existing goal instead.":
"Ya existe un objetivo para esta semana. Por favor, edite el objetivo existente.",
"Weekly time goal created successfully!":
"¡Objetivo de tiempo semanal creado correctamente!",
"Failed to create goal. Please try again.":
"Error al crear el objetivo. Por favor, intente nuevamente.",
"You do not have permission to view this goal":
"No tiene permiso para ver este objetivo",
"You do not have permission to edit this goal":
"No tiene permiso para editar este objetivo",
"Weekly time goal updated successfully!":
"¡Objetivo de tiempo semanal actualizado correctamente!",
"Failed to update goal. Please try again.":
"Error al actualizar el objetivo. Por favor, intente nuevamente.",
"You do not have permission to delete this goal":
"No tiene permiso para eliminar este objetivo",
"Weekly time goal deleted successfully":
"Objetivo de tiempo semanal eliminado correctamente",
"Failed to delete goal. Please try again.":
"Error al eliminar el objetivo. Por favor, intente nuevamente.",
# Common UI strings
"remaining":
"restante",
"Success":
"Éxito",
"Error":
"Error",
"Warning":
"Advertencia",
"Information":
"Información",
"Saving...":
"Guardando...",
"Save":
"Guardar",
"Edit":
"Editar",
"Add":
"Agregar",
"Remove":
"Eliminar",
"Yes":
"",
"No":
"No",
"OK":
"Aceptar",
"Are you sure you want to delete this?":
"¿Está seguro de que desea eliminar esto?",
"You have unsaved changes. Are you sure you want to leave?":
"Tiene cambios sin guardar. ¿Está seguro de que desea salir?",
"Operation failed":
"Operación fallida",
"Operation completed successfully":
"Operación completada correctamente",
"No items selected":
"No hay elementos seleccionados",
"Invalid input":
"Entrada no válida",
"This field is required":
"Este campo es obligatorio",
# Timer
"No active timer":
"No hay temporizador activo",
"Timer stopped":
"Temporizador detenido",
"Failed to stop timer":
"Error al detener el temporizador",
"Error stopping timer":
"Error al detener el temporizador",
"No form to save":
"No hay formulario para guardar",
"No timer found":
"No se encontró temporizador",
"Timer stopped due to inactivity":
"Temporizador detenido por inactividad",
# Navigation
"Navigation":
"Navegación",
"Time Tracking":
"Seguimiento de Tiempo",
"Kanban Board":
"Tablero Kanban",
"Weekly Goals":
"Objetivos Semanales",
"Templates":
"Plantillas",
"Finance & Expenses":
"Finanzas y Gastos",
"Payments":
"Pagos",
"Expenses":
"Gastos",
"Mileage":
"Kilometraje",
"Per Diem":
"Viáticos",
"Budget Alerts":
"Alertas de Presupuesto",
"Tools & Data":
"Herramientas y Datos",
"Import / Export":
"Importar / Exportar",
"Saved Filters":
"Filtros Guardados",
"Admin Dashboard":
"Panel de Administración",
"Users":
"Usuarios",
"API Tokens":
"Tokens de API",
"Roles & Permissions":
"Roles y Permisos",
"System Settings":
"Configuración del Sistema",
"PDF Layout":
"Diseño de PDF",
"Expense Categories":
"Categorías de Gastos",
"Per Diem Rates":
"Tarifas de Viáticos",
"System Info":
"Información del Sistema",
"Backups":
"Copias de Seguridad",
"OIDC Settings":
"Configuración OIDC",
# Support
"Support TimeTracker":
"Apoyar TimeTracker",
"Enjoying TimeTracker? Consider buying me a coffee to support continued development!":
"¿Disfrutando de TimeTracker? ¡Considere invitarme un café para apoyar el desarrollo continuo!",
"Made with":
"Hecho con",
"by":
"por",
"Support TimeTracker development":
"Apoyar el desarrollo de TimeTracker",
"Support":
"Soporte",
"Enjoying TimeTracker?":
"¿Disfrutando de TimeTracker?",
"Support continued development with a coffee":
"Apoye el desarrollo continuo con un café",
"Dismiss":
"Descartar",
# UI elements
"Toggle dark mode":
"Alternar modo oscuro",
"Change language":
"Cambiar idioma",
"User menu":
"Menú de usuario",
"Guest":
"Invitado",
"My Profile":
"Mi Perfil",
"My Settings":
"Mis Configuraciones",
"Are you sure you want to":
"¿Está seguro de que desea",
"deactivate":
"desactivar",
"activate":
"activar",
"this token?":
"este token?",
"Deactivate Token":
"Desactivar Token",
"Activate Token":
"Activar Token",
"Deactivate":
"Desactivar",
"Activate":
"Activar",
"Are you sure you want to delete this token? This action cannot be undone.":
"¿Está seguro de que desea eliminar este token? Esta acción no se puede deshacer.",
"Delete Token":
"Eliminar Token",
# Email configuration
"Email Configuration & Testing":
"Configuración y Prueba de Correo Electrónico",
"Configure and test email delivery":
"Configurar y probar la entrega de correo electrónico",
"Back to Admin":
"Volver a Administración",
"Email Configuration":
"Configuración de Correo Electrónico",
"Configure email settings here to save them in the database. Database settings take precedence over environment variables.":
"Configure la configuración de correo electrónico aquí para guardarla en la base de datos. La configuración de la base de datos tiene prioridad sobre las variables de entorno.",
"Enable Database Email Configuration":
"Habilitar Configuración de Correo Electrónico en Base de Datos",
"Mail Server":
"Servidor de Correo",
"Mail Port":
"Puerto de Correo",
"Use TLS":
"Usar TLS",
"Use SSL":
"Usar SSL",
}
def translate_po_file():
"""Translate all empty msgstr entries in the Spanish .po file."""
po_file = Path('translations/es/LC_MESSAGES/messages.po')
if not po_file.exists():
print(f"Error: File not found: {po_file}")
return
# Read the file
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
# Pattern to match msgid followed by empty msgstr (single line)
# Handle both single-line and multi-line msgid
patterns = [
# Single line msgid with empty msgstr
(r'(msgid\s+"([^"]+)"\s*\nmsgstr\s+"")', r'msgid "\2"\nmsgstr "{}"'),
# Multi-line msgid (handled separately)
]
translated_count = 0
untranslated = []
# Process each translation
def replace_match(match):
nonlocal translated_count
msgid_text = match.group(2)
translation = TRANSLATIONS.get(msgid_text, "")
if translation:
translated_count += 1
return f'msgid "{msgid_text}"\nmsgstr "{translation}"'
else:
untranslated.append(msgid_text)
return match.group(0) # Keep original if no translation
# Replace single-line msgid patterns
new_content = re.sub(patterns[0][0], replace_match, content)
# Handle multi-line msgid (msgid "" followed by continuation lines)
# This is more complex - we'll need to handle it separately
multiline_pattern = r'msgid\s+""\s*\n((?:"[^"]*"\s*\n)+)msgstr\s+""'
def replace_multiline(match):
nonlocal translated_count
# Extract the full msgid text from continuation lines
lines = match.group(1).strip().split('\n')
msgid_text = ''.join(line.strip('"') for line in lines if line.strip().startswith('"'))
translation = TRANSLATIONS.get(msgid_text, "")
if translation:
translated_count += 1
# Format translation for multi-line if needed
if len(translation) > 70:
# Split into multiple lines
lines = [f'"{translation[i:i+70]}"' for i in range(0, len(translation), 70)]
translation_lines = '\\n"\n"'.join(lines)
return f'msgid ""\n{match.group(1)}msgstr ""\n"{translation_lines}"'
else:
return f'msgid ""\n{match.group(1)}msgstr "{translation}"'
else:
untranslated.append(msgid_text)
return match.group(0)
new_content = re.sub(multiline_pattern, replace_multiline, new_content, flags=re.MULTILINE)
# Write back if changes were made
if new_content != content:
# Backup
backup_path = po_file.with_suffix('.po.bak3')
if backup_path.exists():
backup_path.unlink()
po_file.rename(backup_path)
print(f"Backup created: {backup_path}")
# Write new content
with open(po_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated: {po_file}")
print(f"Translated: {translated_count} entries")
if untranslated:
print(f"Still untranslated: {len(untranslated)} entries")
print("First 10 untranslated:")
for msg in untranslated[:10]:
print(f" - {msg}")
else:
print("No changes made")
if __name__ == '__main__':
translate_po_file()

253
scripts/translate_dutch.py Normal file
View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Script to complete all Dutch translations by translating empty msgstr entries.
This script reads the Dutch .po file and translates all missing entries.
"""
import re
from pathlib import Path
def translate_text(text):
"""
Translate English text to Dutch.
This function contains common translations and patterns.
"""
# Common error messages and UI strings
translations = {
# Session and authentication
"Your session expired or the page was open too long. Please try again.":
"Uw sessie is verlopen of de pagina was te lang open. Probeer het opnieuw.",
"Administrator access required":
"Beheerdersrechten vereist",
# PDF layout
"Could not update PDF layout due to a database error.":
"Kon PDF-lay-out niet bijwerken vanwege een databasefout.",
"PDF layout updated successfully":
"PDF-lay-out succesvol bijgewerkt",
"Could not reset PDF layout due to a database error.":
"Kon PDF-lay-out niet resetten vanwege een databasefout.",
"PDF layout reset to defaults":
"PDF-lay-out gereset naar standaardwaarden",
# User account
"Username is required":
"Gebruikersnaam is vereist",
"Could not create your account due to a database error. Please try again later.":
"Kon uw account niet aanmaken vanwege een databasefout. Probeer het later opnieuw.",
"Welcome! Your account has been created.":
"Welkom! Uw account is aangemaakt.",
"User not found. Please contact an administrator.":
"Gebruiker niet gevonden. Neem contact op met een beheerder.",
"Could not update your account role due to a database error.":
"Kon uw accountrol niet bijwerken vanwege een databasefout.",
"Account is disabled. Please contact an administrator.":
"Account is uitgeschakeld. Neem contact op met een beheerder.",
"Welcome back, %(username)s!":
"Welkom terug, %(username)s!",
"Unexpected error during login. Please try again or check server logs.":
"Onverwachte fout tijdens aanmelden. Probeer het opnieuw of controleer de serverlogs.",
"Goodbye, %(username)s!":
"Tot ziens, %(username)s!",
# Avatar/Profile
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP":
"Ongeldig avatarbestandstype. Toegestaan: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.":
"Ongeldig afbeeldingsbestand.",
"Failed to save avatar on server.":
"Kon avatar niet opslaan op server.",
"Profile updated successfully":
"Profiel succesvol bijgewerkt",
"Could not update your profile due to a database error.":
"Kon uw profiel niet bijwerken vanwege een databasefout.",
"Avatar removed":
"Avatar verwijderd",
"Failed to remove avatar.":
"Kon avatar niet verwijderen.",
# SSO
"Single Sign-On is not configured yet. Please contact an administrator.":
"Single Sign-On is nog niet geconfigureerd. Neem contact op met een beheerder.",
"Single Sign-On is not configured.":
"Single Sign-On is niet geconfigureerd.",
"Authentication failed: missing issuer or subject claim. Please check OIDC configuration.":
"Authenticatie mislukt: ontbrekende issuer of subject claim. Controleer de OIDC-configuratie.",
"User account does not exist and self-registration is disabled.":
"Gebruikersaccount bestaat niet en zelfregistratie is uitgeschakeld.",
"Could not create your account due to a database error.":
"Kon uw account niet aanmaken vanwege een databasefout.",
"Unexpected error during SSO login. Please try again or contact support.":
"Onverwachte fout tijdens SSO-aanmelden. Probeer het opnieuw of neem contact op met ondersteuning.",
# Events
"Event created successfully":
"Gebeurtenis succesvol aangemaakt",
"Event updated successfully":
"Gebeurtenis succesvol bijgewerkt",
"You do not have permission to delete this event.":
"U heeft geen toestemming om deze gebeurtenis te verwijderen.",
"Failed to delete event":
"Kon gebeurtenis niet verwijderen",
"Event deleted successfully":
"Gebeurtenis succesvol verwijderd",
"Error deleting event: %(error)s":
"Fout bij verwijderen gebeurtenis: %(error)s",
"Event moved successfully":
"Gebeurtenis succesvol verplaatst",
"Event resized successfully":
"Gebeurtenis succesvol van grootte gewijzigd",
"You do not have permission to view this event.":
"U heeft geen toestemming om deze gebeurtenis te bekijken.",
"You do not have permission to edit this event.":
"U heeft geen toestemming om deze gebeurtenis te bewerken.",
# Notes
"Note content cannot be empty":
"Notitie-inhoud kan niet leeg zijn",
"Note added successfully":
"Notitie succesvol toegevoegd",
"Error adding note":
"Fout bij toevoegen notitie",
"Error adding note: %(error)s":
"Fout bij toevoegen notitie: %(error)s",
"Note does not belong to this client":
"Notitie behoort niet bij deze klant",
"You do not have permission to edit this note":
"U heeft geen toestemming om deze notitie te bewerken",
"Error updating note":
"Fout bij bijwerken notitie",
"Note updated successfully":
"Notitie succesvol bijgewerkt",
"Error updating note: %(error)s":
"Fout bij bijwerken notitie: %(error)s",
"You do not have permission to delete this note":
"U heeft geen toestemming om deze notitie te verwijderen",
"Error deleting note":
"Fout bij verwijderen notitie",
"Note deleted successfully":
"Notitie succesvol verwijderd",
"Error deleting note: %(error)s":
"Fout bij verwijderen notitie: %(error)s",
# Clients
"You do not have permission to create clients":
"U heeft geen toestemming om klanten aan te maken",
# Comments
"Comment content cannot be empty":
"Reactie-inhoud kan niet leeg zijn",
"Comment must be associated with a project or task":
"Reactie moet gekoppeld zijn aan een project of taak",
"Comment cannot be associated with both a project and a task":
"Reactie kan niet tegelijk gekoppeld zijn aan een project en een taak",
"Invalid parent comment":
"Ongeldige bovenliggende reactie",
"Comment added successfully":
"Reactie succesvol toegevoegd",
"Error adding comment":
"Fout bij toevoegen reactie",
"Error adding comment: %(error)s":
"Fout bij toevoegen reactie: %(error)s",
"You do not have permission to edit this comment":
"U heeft geen toestemming om deze reactie te bewerken",
"Comment updated successfully":
"Reactie succesvol bijgewerkt",
"Error updating comment: %(error)s":
"Fout bij bijwerken reactie: %(error)s",
"You do not have permission to delete this comment":
"U heeft geen toestemming om deze reactie te verwijderen",
}
# Direct translation
if text in translations:
return translations[text]
# Pattern-based translations for common phrases
patterns = [
(r"^(.+) created successfully$", r"\1 succesvol aangemaakt"),
(r"^(.+) updated successfully$", r"\1 succesvol bijgewerkt"),
(r"^(.+) deleted successfully$", r"\1 succesvol verwijderd"),
(r"^Failed to (.+)$", r"Kon \1 niet"),
(r"^Error (.+)$", r"Fout \1"),
(r"^You do not have permission to (.+)$", r"U heeft geen toestemming om \1"),
(r"^Could not (.+) due to a database error\.$", r"Kon \1 niet vanwege een databasefout."),
(r"^(.+) cannot be empty$", r"\1 kan niet leeg zijn"),
(r"^(.+) is required$", r"\1 is vereist"),
(r"^(.+) is not configured\.$", r"\1 is niet geconfigureerd."),
(r"^(.+) is not configured yet\. Please contact an administrator\.$", r"\1 is nog niet geconfigureerd. Neem contact op met een beheerder."),
]
for pattern, replacement in patterns:
match = re.match(pattern, text, re.IGNORECASE)
if match:
# Simple word-by-word translation for common words
words = {
"create": "aanmaken", "update": "bijwerken", "delete": "verwijderen",
"save": "opslaan", "remove": "verwijderen", "add": "toevoegen",
"edit": "bewerken", "view": "bekijken", "access": "toegang",
"permission": "toestemming", "error": "fout", "failed": "mislukt",
}
translated = replacement
for en, nl in words.items():
translated = translated.replace(en, nl)
return translated.capitalize() if text[0].isupper() else translated
# Fallback: return empty string (needs manual translation)
return ""
def translate_po_file():
"""Translate all empty msgstr entries in the Dutch .po file."""
po_file = Path('translations/nl/LC_MESSAGES/messages.po')
if not po_file.exists():
print(f"Error: File not found: {po_file}")
return
print(f"Reading {po_file}...")
content = po_file.read_text(encoding='utf-8')
# Find all empty msgstr entries
# Pattern 1: Simple msgstr ""
pattern1 = r'(msgid "([^"]+)"\nmsgstr "")'
# Pattern 2: Multi-line msgid with empty msgstr
pattern2 = r'(msgid ""\n"([^"]+)"\nmsgstr "")'
matches1 = list(re.finditer(pattern1, content, re.MULTILINE))
matches2 = list(re.finditer(pattern2, content, re.MULTILINE))
print(f"Found {len(matches1)} simple untranslated entries")
print(f"Found {len(matches2)} multi-line untranslated entries")
# Translate and replace
translated_count = 0
for match in matches1 + matches2:
msgid = match.group(2) if match.group(2) else match.group(1)
translation = translate_text(msgid)
if translation:
# Replace empty msgstr with translation
old_str = match.group(0)
new_str = old_str.replace('msgstr ""', f'msgstr "{translation}"')
content = content.replace(old_str, new_str, 1)
translated_count += 1
if translated_count > 0:
# Backup original
backup_file = po_file.with_suffix('.po.bak2')
if backup_file.exists():
backup_file.unlink()
po_file.rename(backup_file)
# Write updated content
po_file.write_text(content, encoding='utf-8')
print(f"\nTranslated {translated_count} entries")
print(f"Backup saved to {backup_file}")
else:
print("No translations applied (all entries already translated or no matches found)")
if __name__ == '__main__':
translate_po_file()

View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Script to complete Spanish translations by translating all empty msgstr entries.
"""
import re
from pathlib import Path
def translate_to_spanish(english_text):
"""
Translate English text to Spanish.
This is a basic translation dictionary for common phrases.
For a complete solution, you would use a translation API or manual translation.
"""
# Common translations dictionary
translations = {
"Administrator access required": "Se requiere acceso de administrador",
"Could not update PDF layout due to a database error.": "No se pudo actualizar el diseño del PDF debido a un error de base de datos.",
"PDF layout updated successfully": "Diseño del PDF actualizado correctamente",
"Could not reset PDF layout due to a database error.": "No se pudo restablecer el diseño del PDF debido a un error de base de datos.",
"PDF layout reset to defaults": "Diseño del PDF restablecido a los valores predeterminados",
"Username is required": "Se requiere nombre de usuario",
"Welcome! Your account has been created.": "¡Bienvenido! Su cuenta ha sido creada.",
"User not found. Please contact an administrator.": "Usuario no encontrado. Por favor, contacte a un administrador.",
"Could not update your account role due to a database error.": "No se pudo actualizar el rol de su cuenta debido a un error de base de datos.",
"Account is disabled. Please contact an administrator.": "La cuenta está deshabilitada. Por favor, contacte a un administrador.",
"Unexpected error during login. Please try again or check server logs.": "Error inesperado durante el inicio de sesión. Por favor, intente nuevamente o revise los registros del servidor.",
"Invalid avatar file type. Allowed: PNG, JPG, JPEG, GIF, WEBP": "Tipo de archivo de avatar no válido. Permitidos: PNG, JPG, JPEG, GIF, WEBP",
"Invalid image file.": "Archivo de imagen no válido.",
"Failed to save avatar on server.": "Error al guardar el avatar en el servidor.",
"Profile updated successfully": "Perfil actualizado correctamente",
"Could not update your profile due to a database error.": "No se pudo actualizar su perfil debido a un error de base de datos.",
"Avatar removed": "Avatar eliminado",
"Failed to remove avatar.": "Error al eliminar el avatar.",
"Single Sign-On is not configured yet. Please contact an administrator.": "El inicio de sesión único aún no está configurado. Por favor, contacte a un administrador.",
"Single Sign-On is not configured.": "El inicio de sesión único no está configurado.",
"User account does not exist and self-registration is disabled.": "La cuenta de usuario no existe y el auto-registro está deshabilitado.",
"Could not create your account due to a database error.": "No se pudo crear su cuenta debido a un error de base de datos.",
"Unexpected error during SSO login. Please try again or contact support.": "Error inesperado durante el inicio de sesión SSO. Por favor, intente nuevamente o contacte al soporte.",
"Event created successfully": "Evento creado correctamente",
"Event updated successfully": "Evento actualizado correctamente",
"You do not have permission to delete this event.": "No tiene permiso para eliminar este evento.",
"Failed to delete event": "Error al eliminar el evento",
"Event deleted successfully": "Evento eliminado correctamente",
"Event moved successfully": "Evento movido correctamente",
"Event resized successfully": "Evento redimensionado correctamente",
"You do not have permission to view this event.": "No tiene permiso para ver este evento.",
"You do not have permission to edit this event.": "No tiene permiso para editar este evento.",
"Note content cannot be empty": "El contenido de la nota no puede estar vacío",
"Note added successfully": "Nota agregada correctamente",
"Error adding note": "Error al agregar la nota",
"Note does not belong to this client": "La nota no pertenece a este cliente",
"You do not have permission to edit this note": "No tiene permiso para editar esta nota",
"Error updating note": "Error al actualizar la nota",
"Note updated successfully": "Nota actualizada correctamente",
"You do not have permission to delete this note": "No tiene permiso para eliminar esta nota",
"Error deleting note": "Error al eliminar la nota",
"Note deleted successfully": "Nota eliminada correctamente",
"You do not have permission to create clients": "No tiene permiso para crear clientes",
"Comment content cannot be empty": "El contenido del comentario no puede estar vacío",
"Comment must be associated with a project or task": "El comentario debe estar asociado con un proyecto o tarea",
"Comment cannot be associated with both a project and a task": "El comentario no puede estar asociado con un proyecto y una tarea",
"Invalid parent comment": "Comentario padre no válido",
"Comment added successfully": "Comentario agregado correctamente",
"Error adding comment": "Error al agregar el comentario",
"You do not have permission to edit this comment": "No tiene permiso para editar este comentario",
"Comment updated successfully": "Comentario actualizado correctamente",
"You do not have permission to delete this comment": "No tiene permiso para eliminar este comentario",
"Comment deleted successfully": "Comentario eliminado correctamente",
"Category name is required": "Se requiere el nombre de la categoría",
"Expense category created successfully": "Categoría de gasto creada correctamente",
"Error creating expense category": "Error al crear la categoría de gasto",
"Expense category updated successfully": "Categoría de gasto actualizada correctamente",
"Error updating expense category": "Error al actualizar la categoría de gasto",
"Expense category deactivated successfully": "Categoría de gasto desactivada correctamente",
"Error deactivating expense category": "Error al desactivar la categoría de gasto",
"Title is required": "Se requiere el título",
"Category is required": "Se requiere la categoría",
"Amount is required": "Se requiere el monto",
"Expense date is required": "Se requiere la fecha del gasto",
"Invalid date format": "Formato de fecha no válido",
"Invalid amount format": "Formato de monto no válido",
"Expense created successfully": "Gasto creado correctamente",
"Error creating expense": "Error al crear el gasto",
"You do not have permission to view this expense": "No tiene permiso para ver este gasto",
"You do not have permission to edit this expense": "No tiene permiso para editar este gasto",
"Cannot edit approved or reimbursed expenses": "No se pueden editar gastos aprobados o reembolsados",
"Please fill in all required fields": "Por favor, complete todos los campos requeridos",
"Expense updated successfully": "Gasto actualizado correctamente",
"Error updating expense": "Error al actualizar el gasto",
"You do not have permission to delete this expense": "No tiene permiso para eliminar este gasto",
"Cannot delete approved or invoiced expenses": "No se pueden eliminar gastos aprobados o facturados",
"Expense deleted successfully": "Gasto eliminado correctamente",
"Error deleting expense": "Error al eliminar el gasto",
"Only administrators can approve expenses": "Solo los administradores pueden aprobar gastos",
"Only pending expenses can be approved": "Solo se pueden aprobar gastos pendientes",
"Expense approved successfully": "Gasto aprobado correctamente",
"Error approving expense": "Error al aprobar el gasto",
"Only administrators can reject expenses": "Solo los administradores pueden rechazar gastos",
"Only pending expenses can be rejected": "Solo se pueden rechazar gastos pendientes",
"Rejection reason is required": "Se requiere la razón del rechazo",
"Expense rejected": "Gasto rechazado",
"Error rejecting expense": "Error al rechazar el gasto",
"Only administrators can mark expenses as reimbursed": "Solo los administradores pueden marcar gastos como reembolsados",
"Only approved expenses can be marked as reimbursed": "Solo los gastos aprobados pueden marcarse como reembolsados",
"This expense is not marked as reimbursable": "Este gasto no está marcado como reembolsable",
"Expense marked as reimbursed": "Gasto marcado como reembolsado",
"Error marking expense as reimbursed": "Error al marcar el gasto como reembolsado",
"OCR is not available. Please contact your administrator.": "OCR no está disponible. Por favor, contacte a su administrador.",
"No file provided": "No se proporcionó archivo",
"No file selected": "No se seleccionó archivo",
"Invalid file type. Allowed types: png, jpg, jpeg, gif, pdf": "Tipo de archivo no válido. Tipos permitidos: png, jpg, jpeg, gif, pdf",
"Error scanning receipt. Please try again or enter the expense manually.": "Error al escanear el recibo. Por favor, intente nuevamente o ingrese el gasto manualmente.",
"No scanned receipt data found. Please scan a receipt first.": "No se encontraron datos del recibo escaneado. Por favor, escanee un recibo primero.",
"Expense created successfully from scanned receipt": "Gasto creado correctamente desde el recibo escaneado",
"You do not have permission to export this invoice": "No tiene permiso para exportar esta factura",
"Mileage entry created successfully": "Entrada de kilometraje creada correctamente",
"Error creating mileage entry": "Error al crear la entrada de kilometraje",
"You do not have permission to view this mileage entry": "No tiene permiso para ver esta entrada de kilometraje",
"You do not have permission to edit this mileage entry": "No tiene permiso para editar esta entrada de kilometraje",
"Cannot edit approved or reimbursed mileage entries": "No se pueden editar entradas de kilometraje aprobadas o reembolsadas",
"Mileage entry updated successfully": "Entrada de kilometraje actualizada correctamente",
"Error updating mileage entry": "Error al actualizar la entrada de kilometraje",
"You do not have permission to delete this mileage entry": "No tiene permiso para eliminar esta entrada de kilometraje",
"Mileage entry deleted successfully": "Entrada de kilometraje eliminada correctamente",
"Error deleting mileage entry": "Error al eliminar la entrada de kilometraje",
"Only administrators can approve mileage entries": "Solo los administradores pueden aprobar entradas de kilometraje",
"Only pending mileage entries can be approved": "Solo se pueden aprobar entradas de kilometraje pendientes",
"Mileage entry approved successfully": "Entrada de kilometraje aprobada correctamente",
"Error approving mileage entry": "Error al aprobar la entrada de kilometraje",
"Only administrators can reject mileage entries": "Solo los administradores pueden rechazar entradas de kilometraje",
"Only pending mileage entries can be rejected": "Solo se pueden rechazar entradas de kilometraje pendientes",
"Mileage entry rejected": "Entrada de kilometraje rechazada",
"Error rejecting mileage entry": "Error al rechazar la entrada de kilometraje",
"Only administrators can mark mileage entries as reimbursed": "Solo los administradores pueden marcar entradas de kilometraje como reembolsadas",
"Only approved mileage entries can be marked as reimbursed": "Solo las entradas de kilometraje aprobadas pueden marcarse como reembolsadas",
"Mileage entry marked as reimbursed": "Entrada de kilometraje marcada como reembolsada",
"Error marking mileage entry as reimbursed": "Error al marcar la entrada de kilometraje como reembolsada",
}
# Check if we have a direct translation
if english_text in translations:
return translations[english_text]
# Handle format strings with %(variable)s
if '%(' in english_text:
# For format strings, we need to preserve the format specifiers
# This is a simplified approach - in production you'd want more sophisticated handling
return english_text # Return as-is for now, will need manual translation
# For other strings, return empty to indicate manual translation needed
return ""
def translate_po_file(po_file_path):
"""Translate all empty msgstr entries in a .po file."""
po_file = Path(po_file_path)
if not po_file.exists():
print(f"Error: File not found: {po_file_path}")
return
# Read the file
with open(po_file, 'r', encoding='utf-8') as f:
content = f.read()
# Pattern to match msgid followed by empty msgstr
# Handle both single-line and multi-line msgid
pattern = r'(msgid\s+"[^"]*"\s*(?:\n"[^"]*")*)\n(msgstr\s+"")'
def replace_empty_translation(match):
msgid_block = match.group(1)
# Extract the actual msgid text
msgid_lines = msgid_block.split('\n')
msgid_text = ''
for line in msgid_lines:
if line.startswith('msgid '):
msgid_text += line[6:].strip('"')
elif line.startswith('"'):
msgid_text += line.strip('"')
# Translate
translation = translate_to_spanish(msgid_text)
if translation:
return f'{msgid_block}\nmsgstr "{translation}"'
else:
# Return as-is if no translation found
return match.group(0)
# Replace empty translations
new_content = re.sub(pattern, replace_empty_translation, content)
# Also handle multiline msgid with empty msgstr
# This is more complex and might need a proper PO file parser
# Write back
if new_content != content:
# Backup
backup_path = po_file.with_suffix('.po.backup')
po_file.rename(backup_path)
print(f"Backup created: {backup_path}")
# Write new content
with open(po_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Updated: {po_file_path}")
else:
print("No changes made")
if __name__ == '__main__':
translate_po_file('translations/es/LC_MESSAGES/messages.po')