mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-06 03:30:25 -06:00
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:
167
scripts/complete_dutch_translations.py
Normal file
167
scripts/complete_dutch_translations.py
Normal 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()
|
||||
|
||||
515
scripts/complete_nl_translations.py
Normal file
515
scripts/complete_nl_translations.py
Normal 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()
|
||||
|
||||
214
scripts/complete_spanish_translations.py
Normal file
214
scripts/complete_spanish_translations.py
Normal 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()
|
||||
|
||||
713
scripts/complete_spanish_translations_final.py
Normal file
713
scripts/complete_spanish_translations_final.py
Normal 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":
|
||||
"Sí",
|
||||
"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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
84
scripts/sync_translations.py
Normal file
84
scripts/sync_translations.py
Normal 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()
|
||||
|
||||
786
scripts/translate_all_spanish.py
Normal file
786
scripts/translate_all_spanish.py
Normal 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":
|
||||
"Sí",
|
||||
"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
253
scripts/translate_dutch.py
Normal 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()
|
||||
|
||||
212
scripts/translate_spanish.py
Normal file
212
scripts/translate_spanish.py
Normal 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')
|
||||
|
||||
Reference in New Issue
Block a user