All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m49s
Frontend:
- Fix Framer Motion / motion-dom build error by pinning framer-motion to
11.18.2 (compatible with React 19 and Next.js 16).
- Add cross-env and build:local script to bypass standalone symlink errors
on Windows without Developer Mode.
- Allow NEXT_OUTPUT=default to disable standalone output for local builds.
- Refactor i18n: split 14,177-line src/lib/i18n.tsx into per-locale,
per-namespace JSON files under src/lib/i18n/messages/.
- Load English synchronously; other locales loaded on demand via dynamic
imports (reduces initial bundle, improves maintainability).
- Remove unused next-intl message files src/messages/en.json and fr.json.
Backend:
- Remove insecure legacy /api/v1/download/{filename} and /api/v1/cleanup/{filename}
endpoints. The job-based /api/v1/download/{job_id} already enforces ownership.
- Deduplicate texts in TranslationService.translate_batch before sending them
to the provider, reducing API calls for repeated strings.
- Pin httpx to <0.28 to fix TestClient incompatibility with starlette 0.35.1.
- Add pytest-cov and ruff dev dependencies/config.
DevOps:
- Remove hardcoded Grafana password from docker-compose.yml and
docker-compose.monitoring.yml; use GRAFANA_PASSWORD env var.
- Change default TRANSLATION_SERVICE from ollama to google in
docker-compose.yml (Ollama is an optional profile).
- Add GRAFANA_PASSWORD to .env.example.
- Add .coverage and frontend/pnpm-workspace.yaml to .gitignore.
Tests:
- Update API versioning tests for removed legacy endpoints.
- Add tests/test_translation_service.py for deduplication behavior.
Verified:
- pnpm run build:local passes.
- uv run pytest tests/test_providers/* tests/test_translation_service.py
tests/test_story_3_5_api_versioning.py tests/test_download_endpoint.py
tests/test_translators/test_excel_translator.py: provider/translator tests
pass; one pre-existing French error-message test still fails (message is
returned in English, unrelated to this change).
190 lines
13 KiB
JSON
190 lines
13 KiB
JSON
{
|
||
"glossaries.yourGlossaries": "معاجمك",
|
||
"glossaries.title": "المعاجم والسياق",
|
||
"glossaries.description": "قم بإدارة معاجمك وتعليمات السياق للحصول على ترجمات أكثر دقة.",
|
||
"glossaries.createNew": "إنشاء جديد",
|
||
"glossaries.empty": "لا توجد معاجم بعد",
|
||
"glossaries.emptyDesc": "أنشئ أول معجم لك أو حمّل إعدادًا مسبقًا احترافيًا أعلاه",
|
||
"glossaries.defineTerms": "مصطلحات",
|
||
"glossaries.aboutTitle": "حول المعاجم",
|
||
"glossaries.aboutDesc": "تتيح لك المعاجم تحديد ترجمات دقيقة لمصطلحات معينة. عند الترجمة، يتم استخدام مصطلحات المعجم لضمان ترجمات متسقة ودقيقة.",
|
||
"glossaries.aboutFormat": "كل مصطلح له كلمة مصدر وترجمات بلغات متعددة. حدد معجمًا في صفحة الترجمة لتطبيقه.",
|
||
"glossaries.toast.created": "تم إنشاء المعجم",
|
||
"glossaries.toast.createdDesc": "تم إنشاء المعجم \\\"{name}\\\".",
|
||
"glossaries.toast.imported": "تم استيراد المعجم",
|
||
"glossaries.toast.importedDesc": "تم استيراد المعجم \\\"{name}\\\".",
|
||
"glossaries.toast.updated": "تم تحديث المعجم",
|
||
"glossaries.toast.updatedDesc": "تم تحديث المعجم \\\"{name}\\\".",
|
||
"glossaries.toast.deleted": "تم حذف المعجم",
|
||
"glossaries.toast.deletedDesc": "تم حذف المعجم.",
|
||
"glossaries.toast.error": "خطأ",
|
||
"glossaries.toast.errorCreate": "فشل إنشاء المعجم",
|
||
"glossaries.toast.errorImport": "فشل استيراد المعجم",
|
||
"glossaries.toast.errorUpdate": "فشل تحديث المعجم",
|
||
"glossaries.toast.errorDelete": "فشل حذف المعجم",
|
||
"glossaries.dialog.title": "معجم جديد",
|
||
"glossaries.dialog.description": "أنشئ معجمًا لترجماتك",
|
||
"glossaries.dialog.nameLabel": "الاسم",
|
||
"glossaries.dialog.namePlaceholder": "معجمي",
|
||
"glossaries.dialog.tabTemplates": "القوالب",
|
||
"glossaries.dialog.tabFile": "ملف",
|
||
"glossaries.dialog.tabManual": "يدوي",
|
||
"glossaries.dialog.cancel": "إلغاء",
|
||
"glossaries.dialog.creating": "جارٍ الإنشاء…",
|
||
"glossaries.dialog.importing": "جارٍ الاستيراد…",
|
||
"glossaries.dialog.importBtn": "استيراد",
|
||
"glossaries.dialog.selectPrompt": "اختيار",
|
||
"glossaries.dialog.createBtn": "إنشاء",
|
||
"glossaries.dialog.createEmpty": "إنشاء فارغ",
|
||
"glossaries.dialog.terms": "مصطلحات",
|
||
"glossaries.dialog.templatesDesc": "اختر قالبًا مُعدًّا مسبقًا",
|
||
"glossaries.dialog.templatesEmpty": "لا توجد قوالب متاحة",
|
||
"glossaries.dialog.dropTitle": "اسحب ملف CSV هنا",
|
||
"glossaries.dialog.dropOr": "أو",
|
||
"glossaries.dialog.dropFormats": "CSV, TSV, TXT",
|
||
"glossaries.termEditor.addTerm": "إضافة مصطلح",
|
||
"glossaries.termEditor.maxReached": "تم بلوغ الحد الأقصى {max} من المصطلحات لكل قاموس.",
|
||
"glossaries.dialog.formatTitle": "التنسيق",
|
||
"glossaries.dialog.formatDesc": "المصدر،الهدف (واحدة لكل سطر)",
|
||
"glossaries.dialog.formatNote": "يُتجاهل السطر الأول عند اكتشاف ترويسة",
|
||
"glossaries.dialog.errorFormat": "تنسيق غير مدعوم",
|
||
"glossaries.dialog.errorSize": "الملف كبير جدًا",
|
||
"glossaries.dialog.errorEmpty": "ملف فارغ",
|
||
"glossaries.dialog.errorRead": "خطأ في القراءة",
|
||
"glossaries.dialog.parsing": "جارٍ التحليل…",
|
||
"glossaries.dialog.termsImported": "مصطلحات مستوردة",
|
||
"glossaries.dialog.changeFile": "تغيير الملف",
|
||
"glossaries.dialog.retry": "إعادة المحاولة",
|
||
"glossaries.edit.title": "Modifier le glossaire",
|
||
"glossaries.edit.description": "Modifiez le nom, la paire de langues et les termes du glossaire.",
|
||
"glossaries.edit.nameLabel": "Nom du glossaire",
|
||
"glossaries.edit.namePlaceholder": "Entrez le nom du glossaire...",
|
||
"glossaries.edit.sourceLang": "Langue source",
|
||
"glossaries.edit.targetLang": "Langue cible",
|
||
"glossaries.edit.termsLabel": "Termes ({count} valides)",
|
||
"glossaries.edit.exportCsv": "Exporter CSV",
|
||
"glossaries.edit.importCsv": "Importer CSV",
|
||
"glossaries.edit.cancel": "Annuler",
|
||
"glossaries.edit.saving": "Enregistrement...",
|
||
"glossaries.edit.saveChanges": "Enregistrer les modifications",
|
||
"glossaries.edit.importFailedTitle": "Échec de l'importation",
|
||
"glossaries.edit.importFailedMaxDesc": "Le CSV contient {count} termes, le maximum est de {max}. Veuillez réduire le nombre de termes.",
|
||
"glossaries.edit.importSuccessTitle": "Importation réussie",
|
||
"glossaries.edit.importSuccessDesc": "{count} termes importés avec succès.",
|
||
"glossaries.edit.importFailedEmptyDesc": "Aucun terme valide trouvé dans le fichier CSV.",
|
||
"glossaries.edit.importFailedReadDesc": "Impossible de lire le fichier CSV.",
|
||
"glossaries.delete.title": "Supprimer le glossaire",
|
||
"glossaries.delete.description": "Êtes-vous sûr de vouloir supprimer ce glossaire ?",
|
||
"glossaries.delete.warning": "Cette action est irréversible",
|
||
"glossaries.delete.warningDesc": "Toutes les paires de termes seront définitivement supprimées.",
|
||
"glossaries.delete.cancel": "Annuler",
|
||
"glossaries.delete.deleting": "Suppression...",
|
||
"glossaries.delete.deleteBtn": "Supprimer",
|
||
"glossaries.upgrade.title": "Glossaires",
|
||
"glossaries.upgrade.description": "Personnalisez vos traductions avec une terminologie personnalisée",
|
||
"glossaries.upgrade.feature1": "Créez plusieurs glossaires",
|
||
"glossaries.upgrade.feature2": "Définissez des paires de termes source→cible",
|
||
"glossaries.upgrade.feature3": "Importez/exportez via CSV",
|
||
"glossaries.upgrade.feature4": "Appliquez aux traductions LLM",
|
||
"glossaries.upgrade.proFeatureBefore": "Les glossaires sont une fonctionnalité ",
|
||
"glossaries.upgrade.proFeatureAfter": ". Passez à un forfait supérieur pour débloquer la terminologie personnalisée.",
|
||
"glossaries.upgrade.proLabel": "Pro",
|
||
"glossaries.upgrade.upgradeBtn": "Passer à Pro",
|
||
"glossaries.loading": "Chargement...",
|
||
"glossaries.howItWorks.title": "Comment ces paramètres sont utilisés",
|
||
"glossaries.howItWorks.step1Title": "Configurez ici",
|
||
"glossaries.howItWorks.step1Desc": "Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.",
|
||
"glossaries.howItWorks.step2Title": "Activez dans Traduire",
|
||
"glossaries.howItWorks.step2Desc": "Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.",
|
||
"glossaries.howItWorks.warning": "Les instructions de contexte s'appliquent automatiquement à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être sélectionnés manuellement sur la page Traduire.",
|
||
"glossaries.howItWorks.goToTranslate": "Aller à Traduire",
|
||
"glossaries.status.unsaved": "Non enregistré",
|
||
"glossaries.status.active": "Actif · s'applique à toutes les traductions IA",
|
||
"glossaries.status.inactive": "Inactif",
|
||
"glossaries.instructions.whatForBold": "À quoi ça sert ?",
|
||
"glossaries.instructions.whatForDesc": "Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.",
|
||
"glossaries.instructions.example": "Exemple : « Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres. »",
|
||
"glossaries.instructions.charCount": "{count} caractères",
|
||
"glossaries.instructions.emptyHint": "Vide — aucune instruction envoyée à l'IA",
|
||
"glossaries.instructions.clearAll": "Tout effacer",
|
||
"glossaries.instructions.saving": "Enregistrement…",
|
||
"glossaries.instructions.saved": "Enregistré",
|
||
"glossaries.presets.whatForBold": "À quoi ça sert ?",
|
||
"glossaries.presets.whatForDesc": "Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le sélectionner manuellement sur la page Traduire pour forcer des traductions de termes précis.",
|
||
"glossaries.presets.clickHint": "Cliquez sur une carte → glossaire créé → sélectionnez-le dans Traduire",
|
||
"glossaries.presets.creating": "Création…",
|
||
"glossaries.presets.alreadyImported": "Déjà importé",
|
||
"glossaries.presets.it.title": "IT / Logiciel",
|
||
"glossaries.presets.it.desc": "Développement, infrastructure, DevOps",
|
||
"glossaries.presets.legal.title": "Juridique / Contrats",
|
||
"glossaries.presets.legal.desc": "Droit des affaires, contentieux",
|
||
"glossaries.presets.medical.title": "Médical / Santé",
|
||
"glossaries.presets.medical.desc": "Pharmacologie, chirurgie, diagnostic",
|
||
"glossaries.presets.finance.title": "Finance / Comptabilité",
|
||
"glossaries.presets.finance.desc": "IFRS, bilans, fiscalité",
|
||
"glossaries.presets.marketing.title": "Marketing / Publicité",
|
||
"glossaries.presets.marketing.desc": "Digital, branding, analytics",
|
||
"glossaries.presets.hr.title": "RH / Ressources Humaines",
|
||
"glossaries.presets.hr.desc": "Contrats, politiques, recrutement",
|
||
"glossaries.presets.scientific.title": "Scientifique / Recherche",
|
||
"glossaries.presets.scientific.desc": "Publications, thèses, articles",
|
||
"glossaries.presets.ecommerce.title": "E-commerce / Vente",
|
||
"glossaries.presets.ecommerce.desc": "Boutiques en ligne, catalogues, CRM",
|
||
"glossaries.grid.title": "Vos",
|
||
"glossaries.grid.titleHighlight": "glossaires",
|
||
"glossaries.grid.countWithAction": "{count} glossaire({plural}) — cliquez sur une carte pour la modifier",
|
||
"glossaries.grid.emptyAction": "Créez votre premier glossaire ou importez un preset ci-dessus",
|
||
"glossaries.grid.activeTranslation": "Traduction active :",
|
||
"glossaries.grid.goToTranslate": "Aller à Traduire pour activer",
|
||
"glossaries.badge.compatible": "Compatible",
|
||
"glossaries.badge.otherTarget": "Autre cible",
|
||
"glossaries.card.editTerms": "Modifier les termes",
|
||
"glossaries.card.created": "تم الإنشاء",
|
||
"glossaries.card.term": "مصطلح",
|
||
"glossaries.card.delete": "Supprimer",
|
||
"glossaries.grid.searchPlaceholder": "Search a glossary…",
|
||
"glossaries.grid.noResults": "No results for this search.",
|
||
"glossaries.detail.backToList": "Back to glossaries",
|
||
"glossaries.detail.save": "Save",
|
||
"glossaries.detail.savedTitle": "Saved",
|
||
"glossaries.detail.savedDesc": "The glossary has been updated.",
|
||
"glossaries.detail.settingsTitle": "Settings",
|
||
"glossaries.detail.sourceLang": "Source language",
|
||
"glossaries.detail.targetLang": "Target language",
|
||
"glossaries.detail.termsTitle": "Terms",
|
||
"glossaries.detail.terms": "terms",
|
||
"glossaries.detail.searchTerms": "Filter…",
|
||
"glossaries.detail.noTerms": "No terms yet.",
|
||
"glossaries.detail.addFirstTerm": "Add the first term",
|
||
"glossaries.detail.addTerm": "Add a term",
|
||
"glossaries.detail.maxReached": "Maximum limit reached",
|
||
"glossaries.detail.source": "Source",
|
||
"glossaries.detail.target": "Target",
|
||
"glossaries.detail.sourcePlaceholder": "source term",
|
||
"glossaries.detail.targetPlaceholder": "target term",
|
||
"glossaries.detail.csvTitle": "CSV",
|
||
"glossaries.detail.csvDesc": "Export your terms as CSV or import new ones (replaces the current list).",
|
||
"glossaries.detail.export": "Export",
|
||
"glossaries.detail.import": "Import",
|
||
"glossaries.detail.dangerTitle": "Danger zone",
|
||
"glossaries.detail.dangerDesc": "Deletion is permanent. All associated terms will be lost.",
|
||
"glossaries.detail.deleteGlossary": "Delete this glossary",
|
||
"glossaries.detail.confirmDelete": "Confirm deletion?",
|
||
"glossaries.detail.confirm": "Confirm",
|
||
"glossaries.detail.cancel": "Cancel",
|
||
"glossaries.detail.sourceLangNote": "'The original source is in French. For other languages, we read the term's translations field (if available).'",
|
||
"glossaries.detail.sourceLocked": "fixed",
|
||
"glossaries.detail.sourceLockedNote": "Templates only store the source in French. Multilingual source is on the roadmap.",
|
||
"glossaries.detail.targetLangNote": "Pick a language to see the matching translations, or « Multilingual » for the default value.",
|
||
"glossaries.detail.notFoundTitle": "Glossary not found",
|
||
"glossaries.detail.notFoundDesc": "This glossary does not exist or you don't have access to it.",
|
||
"glossaries.detail.maxTermsTitle": "Limit reached",
|
||
"glossaries.detail.maxTermsDesc": "Maximum {max} terms per glossary.",
|
||
"glossaries.detail.importEmptyTitle": "Empty file",
|
||
"glossaries.detail.importEmptyDesc": "No terms detected in this file.",
|
||
"glossaries.detail.importedTitle": "Imported",
|
||
"glossaries.detail.importedDesc": "{count} terms imported.",
|
||
"glossaries.detail.importErrorTitle": "Read error",
|
||
"glossaries.detail.importErrorDesc": "Unable to read the file."
|
||
}
|