feat: implement translation management system and enhance localization support

- Added a new translation management system to automate synchronization, validation, and translation of internationalization files.
- Introduced scripts for running translation operations, including checking status, synchronizing keys, and auto-translating strings.
- Updated package.json with new translation-related commands for easier access.
- Enhanced localization files across multiple languages with new keys and improved translations.
- Integrated download functionality for share files in the UI, allowing users to download multiple files seamlessly.
- Refactored components to support new download features and improved user experience.
This commit is contained in:
Daniel Luiz Alves
2025-06-23 12:11:52 -03:00
parent 6af10c6f33
commit 4e841b272c
29 changed files with 2389 additions and 124 deletions

View File

@@ -22,6 +22,7 @@
"architecture",
"github-architecture",
"api",
"translation-management",
"contribute",
"open-an-issue",
"---Sponsor this project---",

View File

@@ -0,0 +1,525 @@
---
title: Translation Management
icon: Languages
---
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Callout } from "fumadocs-ui/components/callout";
Palmr includes a comprehensive translation management system that automates synchronization, validation, and translation of the application's internationalization files.
## Overview
The translation management system consists of Python scripts that help maintain consistency across all supported languages:
- **Synchronization**: Automatically add missing translation keys
- **Validation**: Check translation status and completeness
- **Auto-translation**: Use Google Translate API for initial translations
- **Reporting**: Generate detailed translation reports
## Quick Start
<Tabs items={['npm/pnpm', 'Python Direct']}>
<Tab value="npm/pnpm">
```bash
# Complete workflow (recommended)
pnpm run translations
# Check translation status
pnpm run translations:check
# Synchronize missing keys
pnpm run translations:sync
# Auto-translate [TO_TRANSLATE] strings
pnpm run translations:translate
# Dry run mode (test without changes)
pnpm run translations:dry-run
# Show help
pnpm run translations:help
```
</Tab>
<Tab value="Python Direct">
```bash
cd apps/web/scripts
# Complete workflow (recommended)
python3 run_translations.py all
# Individual commands
python3 run_translations.py check
python3 run_translations.py sync
python3 run_translations.py translate
python3 run_translations.py help
```
</Tab>
</Tabs>
## Available Scripts
### Main Commands (npm/pnpm)
| Command | Description |
| --------------------------------- | ------------------------------------------- |
| `pnpm run translations` | Complete workflow: sync + translate + check |
| `pnpm run translations:check` | Check translation status and completeness |
| `pnpm run translations:sync` | Synchronize missing keys from en-US.json |
| `pnpm run translations:translate` | Auto-translate [TO_TRANSLATE] strings |
| `pnpm run translations:dry-run` | Test workflow without making changes |
| `pnpm run translations:help` | Show detailed help and examples |
## Workflow
### 1. Adding New Translation Keys
When you add new text to the application:
1. **Add to English**: Update `apps/web/messages/en-US.json` with your new keys
2. **Sync translations**: Run `pnpm run translations:sync` to add missing keys to all languages
3. **Auto-translate**: Run `pnpm run translations:translate` for automatic translations
4. **Review translations**: **Mandatory step** - Check all auto-generated translations for accuracy
5. **Test in UI**: Verify translations work correctly in the application interface
<Callout type="important">
**Never skip step 4**: Auto-generated translations must be reviewed before
committing to production. They are a starting point, not a final solution.
</Callout>
### 2. Checking Translation Status
<Callout>
Always run `pnpm run translations:check` before releases to ensure
completeness.
</Callout>
```bash
# Generate detailed translation report
pnpm run translations:check
```
The report shows:
- **Completeness percentage** for each language
- **Untranslated strings** marked with `[TO_TRANSLATE]`
- **Identical strings** that may need localization
- **Missing keys** compared to English reference
### 3. Manual Translation Process
For critical strings or when automatic translation isn't sufficient:
1. **Find untranslated strings**: Look for `[TO_TRANSLATE] Original text` in language files
2. **Replace with translation**: Remove the prefix and add proper translation
3. **Validate**: Run `pnpm run translations:check` to verify completeness
## File Structure
```
apps/web/
├── messages/ # Translation files
│ ├── en-US.json # Reference language (English)
│ ├── pt-BR.json # Portuguese (Brazil)
│ ├── es-ES.json # Spanish
│ └── ... # Other languages
└── scripts/ # Management scripts
├── run_translations.py # Main wrapper
├── sync_translations.py # Synchronization
├── check_translations.py # Status checking
└── translate_missing.py # Auto-translation
```
## Prerequisites
- **Python 3.6 or higher** - Required for running the translation scripts
- **No external dependencies** - Scripts use only Python standard libraries (googletrans auto-installs when needed)
- **UTF-8 support** - Ensure your terminal supports UTF-8 for proper display of translations
## Script Details
### Main Wrapper (`run_translations.py`)
The main script provides a unified interface for all translation operations:
#### Available Commands
- `check` - Check translation status and generate reports
- `sync` - Synchronize missing keys from reference language
- `translate` - Automatically translate marked strings
- `all` - Run complete workflow (sync + translate + check)
- `help` - Show detailed help with examples
#### How it Works
1. Validates parameters and working directory
2. Calls appropriate individual scripts with passed parameters
3. Provides unified error handling and progress reporting
4. Supports all parameters from individual scripts
### Synchronization Script (`sync_translations.py`)
Maintains consistency across all language files:
#### Process
1. **Load reference**: Reads `en-US.json` as source of truth
2. **Scan languages**: Finds all `*.json` files in messages directory
3. **Compare keys**: Identifies missing keys in each language file
4. **Add missing keys**: Copies structure from reference with `[TO_TRANSLATE]` prefix
5. **Save updates**: Maintains JSON formatting and UTF-8 encoding
#### Key Features
- **Recursive key detection**: Handles nested JSON objects
- **Safe updates**: Preserves existing translations
- **Consistent formatting**: Maintains proper JSON structure
- **Progress reporting**: Shows detailed sync results
### Status Check Script (`check_translations.py`)
Provides comprehensive translation analysis:
#### Generated Reports
- **Completion percentage**: How much of each language is translated
- **Untranslated count**: Strings still marked with `[TO_TRANSLATE]`
- **Identical strings**: Text identical to English (may need localization)
- **Missing keys**: Keys present in reference but not in target language
#### Analysis Features
- **Visual indicators**: Icons show completion status (✅ 🟡 🔴)
- **Detailed breakdowns**: Per-language analysis with specific keys
- **Quality insights**: Identifies potential translation issues
- **Export friendly**: Output can be redirected to files for reports
### Auto-Translation Script (`translate_missing.py`)
Automated translation using Google Translate API:
#### Process
1. **Dependency check**: Auto-installs `googletrans` if needed
2. **Scan for work**: Finds all `[TO_TRANSLATE]` prefixed strings
3. **Language mapping**: Maps file names to Google Translate language codes
4. **Batch translation**: Processes strings with rate limiting
5. **Update files**: Replaces marked strings with translations
6. **Error handling**: Retries failed translations, reports results
#### Safety Features
- **Rate limiting**: Configurable delay between requests
- **Retry logic**: Multiple attempts for failed translations
- **Dry run mode**: Preview changes without modifications
- **Language skipping**: Exclude specific languages from processing
- **Progress tracking**: Real-time status of translation progress
## Advanced Usage
### Custom Parameters
You can pass additional parameters to the underlying Python scripts for more control:
#### Synchronization Parameters (`sync`)
```bash
# Sync without marking new keys as [TO_TRANSLATE]
python3 scripts/run_translations.py sync --no-mark-untranslated
# Use a different reference file (default: en-US.json)
python3 scripts/run_translations.py sync --reference pt-BR.json
# Specify custom messages directory
python3 scripts/run_translations.py sync --messages-dir /path/to/messages
# Dry run mode - see what would be changed
python3 scripts/run_translations.py sync --dry-run
```
#### Translation Parameters (`translate`)
```bash
# Custom delay between translation requests (avoid rate limiting)
python3 scripts/run_translations.py translate --delay 2.0
# Skip specific languages from translation
python3 scripts/run_translations.py translate --skip-languages pt-BR.json fr-FR.json
# Dry run - see what would be translated
python3 scripts/run_translations.py translate --dry-run
# Specify custom messages directory
python3 scripts/run_translations.py translate --messages-dir /path/to/messages
```
#### Check Parameters (`check`)
```bash
# Use different reference file for comparison
python3 scripts/run_translations.py check --reference pt-BR.json
# Check translations in custom directory
python3 scripts/run_translations.py check --messages-dir /path/to/messages
```
### Parameter Reference
| Parameter | Commands | Description |
| ------------------------ | -------------------------- | --------------------------------------------- |
| `--dry-run` | `sync`, `translate`, `all` | Preview changes without modifying files |
| `--messages-dir` | All | Custom directory containing translation files |
| `--reference` | `sync`, `check` | Reference file to use (default: en-US.json) |
| `--no-mark-untranslated` | `sync` | Don't add [TO_TRANSLATE] prefix to new keys |
| `--delay` | `translate` | Delay in seconds between translation requests |
| `--skip-languages` | `translate` | List of language files to skip |
### Dry Run Mode
Always test changes first:
```bash
# Test complete workflow
pnpm run translations:dry-run
# Test individual commands
python3 scripts/run_translations.py sync --dry-run
python3 scripts/run_translations.py translate --dry-run
```
### Common Use Cases
#### Scenario 1: Careful Translation with Review
```bash
# 1. Sync new keys without auto-marking for translation
python3 scripts/run_translations.py sync --no-mark-untranslated
# 2. Manually mark specific keys that need translation
# Edit files to add [TO_TRANSLATE] prefix where needed
# 3. Translate only marked strings with slower rate
python3 scripts/run_translations.py translate --delay 2.0
```
#### Scenario 2: Skip Already Reviewed Languages
```bash
# Skip languages that were already manually reviewed
python3 scripts/run_translations.py translate --skip-languages pt-BR.json es-ES.json
```
#### Scenario 3: Custom Project Structure
```bash
# Work with translations in different directory
python3 scripts/run_translations.py all --messages-dir ../custom-translations
```
#### Scenario 4: Quality Assurance
```bash
# Use different language as reference for comparison
python3 scripts/run_translations.py check --reference pt-BR.json
# This helps identify inconsistencies when you have high-quality translations
```
## Translation Keys Format
Translation files use nested JSON structure:
```json
{
"common": {
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"messages": {
"success": "Operation completed successfully",
"error": "An error occurred"
}
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome to Palmr"
}
}
```
## Automatic Translation
<Callout type="warning">
**Review Required**: Automatic translations are provided for convenience but
**must be reviewed** before production use. They serve as a starting point,
not a final solution.
</Callout>
The system uses Google Translate (free API) to automatically translate strings marked with `[TO_TRANSLATE]`:
```json
{
"key": "[TO_TRANSLATE] Original English text"
}
```
After auto-translation:
```json
{
"key": "Texto original em inglês"
}
```
### Translation Review Process
1. **Generate**: Use `pnpm run translations:translate` to auto-translate
2. **Review**: Check each translation for:
- **Context accuracy**: Does it make sense in the UI?
- **Technical terms**: Are they correctly translated?
- **Tone consistency**: Matches the application's voice?
- **Cultural appropriateness**: Suitable for target audience?
3. **Test**: Verify translations in the actual application interface
4. **Document**: Note any translation decisions for future reference
### Common Review Points
- **Button labels**: Ensure they fit within UI constraints
- **Error messages**: Must be clear and helpful to users
- **Navigation items**: Should be intuitive in target language
- **Technical terms**: Some may be better left in English
- **Placeholders**: Maintain formatting and variable names
## Development Guidelines
<Callout type="important">
**Primary Language**: Always use `en-US.json` as the parent language for
development. All new translation keys must be added to English first.
</Callout>
### Translation Workflow for Development
1. **English First**: Add all new text to `apps/web/messages/en-US.json`
2. **Auto-generate**: Use scripts to generate translations for other languages
3. **Review Required**: All auto-generated translations must be reviewed before production use
4. **Quality Check**: Run translation validation before merging PRs
### Why English as Parent Language?
- **Consistency**: Ensures all languages have the same keys structure
- **Reference**: English serves as the source of truth for meaning
- **Auto-translation**: Scripts use English as source for automatic translation
- **Documentation**: Most technical documentation is in English
## Best Practices
### For Developers
1. **Always use English as reference**: Add new keys to `en-US.json` first - never add keys directly to other languages
2. **Use semantic key names**: `dashboard.welcome` instead of `text1`
3. **Test translations**: Run `pnpm run translations:dry-run` before committing
4. **Review auto-translations**: All generated translations must be reviewed before production
5. **Maintain consistency**: Use existing patterns for similar UI elements
### For Translators
1. **Focus on [TO_TRANSLATE] strings**: These need immediate attention
2. **Check identical strings**: May need localization even if identical to English
3. **Use proper formatting**: Maintain HTML tags and placeholders
4. **Test in context**: Verify translations work in the actual UI
## Troubleshooting
### Common Issues
**Python not found**: Ensure Python 3.6+ is installed and in PATH
**googletrans errors**: The system auto-installs dependencies, but you can manually install:
```bash
pip install googletrans==4.0.0rc1
```
**Rate limiting**: Increase delay between requests:
```bash
python3 scripts/run_translations.py translate --delay 2.0
```
### Getting Help
- Run `pnpm run translations:help` for detailed command examples
- Review generated translation reports for specific issues
- Check the official documentation for complete reference
## Output Examples
### Synchronization Output
```
Loading reference file: en-US.json
Reference file contains 980 keys
Processing 14 translation files...
Processing: pt-BR.json
🔍 Found 12 missing keys
✅ Updated successfully (980/980 keys)
============================================================
SUMMARY
============================================================
✅ ar-SA.json - 980/980 keys
🔄 pt-BR.json - 980/980 keys (+12 added)
```
### Translation Status Report
```
📊 TRANSLATION REPORT
Reference: en-US.json (980 strings)
================================================================================
LANGUAGE COMPLETENESS STRINGS UNTRANSLATED POSSIBLE MATCHES
--------------------------------------------------------------------------------
✅ pt-BR 100.0% 980/980 0 (0.0%) 5
⚠️ fr-FR 100.0% 980/980 12 (2.5%) 3
🟡 de-DE 95.2% 962/980 0 (0.0%) 8
================================================================================
```
### Auto-Translation Progress
```
🌍 Translating 3 languages...
⏱️ Delay between requests: 1.0s
[1/3] 🌐 Language: PT
🔍 Processing: pt-BR.json
📝 Found 12 strings to translate
📍 (1/12) Translating: dashboard.welcome
✅ "Welcome to Palmr" → "Bem-vindo ao Palmr"
💾 File saved with 12 translations
📊 FINAL SUMMARY
================================================================================
✅ Translations performed:
• 34 successes
• 2 failures
• 36 total processed
• Success rate: 94.4%
```
## Contributing
When contributing translations:
1. **Follow the workflow**: Use the provided scripts for consistency
2. **Test thoroughly**: Run complete checks before submitting
3. **Document changes**: Note any significant translation decisions
4. **Maintain quality**: Review auto-translations for accuracy
The translation management system ensures consistency and makes it easy to maintain high-quality localization across all supported languages.

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "فشل في تحديث إعدادات الأمان",
"expirationUpdateError": "فشل في تحديث إعدادات انتهاء الصلاحية",
"securityUpdateSuccess": "تم تحديث إعدادات الأمان بنجاح",
"expirationUpdateSuccess": "تم تحديث إعدادات انتهاء الصلاحية بنجاح"
"expirationUpdateSuccess": "تم تحديث إعدادات انتهاء الصلاحية بنجاح",
"creatingZip": "إنشاء ملف zip ...",
"defaultShareName": "يشارك",
"downloadError": "فشل تنزيل ملفات المشاركة",
"downloadSuccess": "بدأ التنزيل بنجاح",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "لا توجد ملفات متاحة للتنزيل",
"singleShareZipName": "{ShareName} _files.zip",
"zipDownloadError": "فشل في إنشاء ملف مضغوط",
"zipDownloadSuccess": "تم تنزيل ملف zip بنجاح"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "تحرير الرابط",
"copyLink": "نسخ الرابط",
"notifyRecipients": "إشعار المستقبلين",
"delete": "حذف"
"delete": "حذف",
"downloadShareFiles": "قم بتنزيل جميع الملفات"
},
"bulkActions": {
"delete": "حذف",
"selected": "{count, plural, =1 {مشاركة واحدة محددة} other {# مشاركات محددة}}"
"selected": "{count, plural, =1 {مشاركة واحدة محددة} other {# مشاركات محددة}}",
"actions": "الإجراءات",
"download": "تنزيل محدد"
},
"selectAll": "تحديد الكل",
"selectShare": "تحديد المشاركة {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "معاينة",
"download": "تحميل"
"download": "تحميل",
"copyToMyFiles": "انسخ إلى ملفاتي",
"copying": "نسخ ..."
},
"uploadedBy": "تم الرفع بواسطة {name}",
"anonymous": "مجهول",
@@ -1165,7 +1179,9 @@
"downloadError": "خطأ في تحميل الملف",
"editSuccess": "تم تحديث الملف بنجاح",
"editError": "خطأ في تحديث الملف",
"previewNotAvailable": "المعاينة غير متوفرة"
"previewNotAvailable": "المعاينة غير متوفرة",
"copyError": "خطأ نسخ الملف إلى ملفاتك",
"copySuccess": "تم نسخ الملف إلى ملفاتك بنجاح"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "إلغاء",
"preview": "معاينة",
"download": "تحميل",
"delete": "حذف"
"delete": "حذف",
"copyToMyFiles": "انسخ إلى ملفاتي",
"copying": "نسخ ..."
},
"editField": {
"saveChanges": "حفظ التغييرات",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Sicherheitseinstellungen konnten nicht aktualisiert werden",
"expirationUpdateError": "Ablaufeinstellungen konnten nicht aktualisiert werden",
"securityUpdateSuccess": "Sicherheitseinstellungen erfolgreich aktualisiert",
"expirationUpdateSuccess": "Ablaufeinstellungen erfolgreich aktualisiert"
"expirationUpdateSuccess": "Ablaufeinstellungen erfolgreich aktualisiert",
"creatingZip": "ZIP -Datei erstellen ...",
"defaultShareName": "Aktie",
"downloadError": "Download Share -Dateien nicht herunterladen",
"downloadSuccess": "Download begann erfolgreich",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "Keine Dateien zum Download zur Verfügung stehen",
"singleShareZipName": "{ShareName} _files.zip",
"zipDownloadError": "Die ZIP -Datei erstellt nicht",
"zipDownloadSuccess": "ZIP -Datei erfolgreich heruntergeladen"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Link Bearbeiten",
"copyLink": "Link Kopieren",
"notifyRecipients": "Empfänger Benachrichtigen",
"delete": "Löschen"
"delete": "Löschen",
"downloadShareFiles": "Laden Sie alle Dateien herunter"
},
"bulkActions": {
"delete": "Löschen",
"selected": "{count, plural, =1 {1 Freigabe ausgewählt} other {# Freigaben ausgewählt}}"
"selected": "{count, plural, =1 {1 Freigabe ausgewählt} other {# Freigaben ausgewählt}}",
"actions": "Aktionen",
"download": "Download ausgewählt"
},
"selectAll": "Alle auswählen",
"selectShare": "Freigabe {shareName} auswählen"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Vorschau",
"download": "Herunterladen"
"download": "Herunterladen",
"copyToMyFiles": "Kopieren Sie in meine Dateien",
"copying": "Kopieren..."
},
"uploadedBy": "Hochgeladen von {name}",
"anonymous": "Anonym",
@@ -1165,7 +1179,9 @@
"downloadError": "Fehler beim Herunterladen der Datei",
"editSuccess": "Datei erfolgreich aktualisiert",
"editError": "Fehler beim Aktualisieren der Datei",
"previewNotAvailable": "Vorschau nicht verfügbar"
"previewNotAvailable": "Vorschau nicht verfügbar",
"copyError": "Fehler beim Kopieren der Datei in Ihre Dateien",
"copySuccess": "Datei erfolgreich in Ihre Dateien kopiert"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Abbrechen",
"preview": "Vorschau",
"download": "Herunterladen",
"delete": "Löschen"
"delete": "Löschen",
"copyToMyFiles": "Kopieren Sie in meine Dateien",
"copying": "Kopieren..."
},
"editField": {
"saveChanges": "Änderungen speichern",

View File

@@ -714,7 +714,16 @@
"notifyError": "Failed to notify recipients",
"bulkDeleteError": "Failed to delete shares",
"bulkDeleteLoading": "Deleting {count, plural, =1 {1 share} other {# shares}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 share deleted successfully} other {# shares deleted successfully}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 share deleted successfully} other {# shares deleted successfully}}",
"downloadSuccess": "Download started successfully",
"downloadError": "Failed to download share files",
"noFilesToDownload": "No files available to download",
"creatingZip": "Creating ZIP file...",
"zipDownloadSuccess": "ZIP file downloaded successfully",
"zipDownloadError": "Failed to create ZIP file",
"singleShareZipName": "{shareName}_files.zip",
"multipleSharesZipName": "{count}_shares_files.zip",
"defaultShareName": "Share"
},
"shares": {
"errors": {
@@ -777,9 +786,12 @@
"editLink": "Edit Link",
"copyLink": "Copy Link",
"notifyRecipients": "Notify Recipients",
"downloadShareFiles": "Download All Files",
"delete": "Delete"
},
"bulkActions": {
"actions": "Actions",
"download": "Download Selected",
"delete": "Delete",
"selected": "{count, plural, =1 {1 share selected} other {# shares selected}}"
},

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Error al actualizar configuración de seguridad",
"expirationUpdateError": "Error al actualizar configuración de expiración",
"securityUpdateSuccess": "Configuración de seguridad actualizada exitosamente",
"expirationUpdateSuccess": "Configuración de expiración actualizada exitosamente"
"expirationUpdateSuccess": "Configuración de expiración actualizada exitosamente",
"creatingZip": "Creación de archivo zip ...",
"defaultShareName": "Compartir",
"downloadError": "No se pudo descargar archivos compartidos",
"downloadSuccess": "Descargar comenzó con éxito",
"multipleSharesZipName": "{Count} _shares_files.zip",
"noFilesToDownload": "No hay archivos disponibles para descargar",
"singleShareZipName": "{Sharename} _files.zip",
"zipDownloadError": "No se pudo crear un archivo zip",
"zipDownloadSuccess": "Archivo zip descargado correctamente"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Editar Enlace",
"copyLink": "Copiar Enlace",
"notifyRecipients": "Notificar Destinatarios",
"delete": "Eliminar"
"delete": "Eliminar",
"downloadShareFiles": "Descargar todos los archivos"
},
"bulkActions": {
"delete": "Eliminar",
"selected": "{count, plural, =1 {1 compartido seleccionado} other {# compartidos seleccionados}}"
"selected": "{count, plural, =1 {1 compartido seleccionado} other {# compartidos seleccionados}}",
"actions": "Comportamiento",
"download": "Descargar Seleccionados"
},
"selectAll": "Seleccionar todo",
"selectShare": "Seleccionar compartido {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Vista previa",
"download": "Descargar"
"download": "Descargar",
"copyToMyFiles": "Copiar a mis archivos",
"copying": "Proceso de copiar..."
},
"uploadedBy": "Enviado por {name}",
"anonymous": "Anónimo",
@@ -1165,7 +1179,9 @@
"downloadError": "Error al descargar archivo",
"editSuccess": "Archivo actualizado con éxito",
"editError": "Error al actualizar archivo",
"previewNotAvailable": "Vista previa no disponible"
"previewNotAvailable": "Vista previa no disponible",
"copyError": "Error de copiar el archivo a sus archivos",
"copySuccess": "Archivo copiado en sus archivos correctamente"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Cancelar",
"preview": "Vista previa",
"download": "Descargar",
"delete": "Eliminar"
"delete": "Eliminar",
"copyToMyFiles": "Copiar a mis archivos",
"copying": "Proceso de copiar..."
},
"editField": {
"saveChanges": "Guardar cambios",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Échec de mise à jour des paramètres de sécurité",
"expirationUpdateError": "Échec de mise à jour des paramètres d'expiration",
"securityUpdateSuccess": "Paramètres de sécurité mis à jour avec succès",
"expirationUpdateSuccess": "Paramètres d'expiration mis à jour avec succès"
"expirationUpdateSuccess": "Paramètres d'expiration mis à jour avec succès",
"creatingZip": "Création d'un fichier zip ...",
"defaultShareName": "Partager",
"downloadError": "Échec de téléchargement des fichiers de partage",
"downloadSuccess": "Le téléchargement a commencé avec succès",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "Aucun fichier disponible en téléchargement",
"singleShareZipName": "{sharename} _files.zip",
"zipDownloadError": "Échec de la création du fichier zip",
"zipDownloadSuccess": "Fichier zip téléchargé avec succès"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Modifier le Lien",
"copyLink": "Copier le Lien",
"notifyRecipients": "Notifier les Destinataires",
"delete": "Supprimer"
"delete": "Supprimer",
"downloadShareFiles": "Télécharger tous les fichiers"
},
"bulkActions": {
"delete": "Supprimer",
"selected": "{count, plural, =1 {1 partage sélectionné} other {# partages sélectionnés}}"
"selected": "{count, plural, =1 {1 partage sélectionné} other {# partages sélectionnés}}",
"actions": "Actes",
"download": "Télécharger sélectionné"
},
"selectAll": "Tout sélectionner",
"selectShare": "Sélectionner le partage {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Aperçu",
"download": "Télécharger"
"download": "Télécharger",
"copyToMyFiles": "Copier dans mes fichiers",
"copying": "Copier..."
},
"uploadedBy": "Envoyé par {name}",
"anonymous": "Anonyme",
@@ -1165,7 +1179,9 @@
"downloadError": "Erreur lors du téléchargement",
"editSuccess": "Fichier mis à jour avec succès",
"editError": "Erreur lors de la mise à jour du fichier",
"previewNotAvailable": "Aperçu non disponible"
"previewNotAvailable": "Aperçu non disponible",
"copyError": "Erreur de copie du fichier dans vos fichiers",
"copySuccess": "Fichier copié dans vos fichiers avec succès"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Annuler",
"preview": "Aperçu",
"download": "Télécharger",
"delete": "Supprimer"
"delete": "Supprimer",
"copyToMyFiles": "Copier dans mes fichiers",
"copying": "Copier..."
},
"editField": {
"saveChanges": "Enregistrer les modifications",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "सुरक्षा सेटिंग्स अपडेट करने में विफल",
"expirationUpdateError": "समाप्ति सेटिंग्स अपडेट करने में विफल",
"securityUpdateSuccess": "सुरक्षा सेटिंग्स सफलतापूर्वक अपडेट हुईं",
"expirationUpdateSuccess": "समाप्ति सेटिंग्स सफलतापूर्वक अपडेट हुईं"
"expirationUpdateSuccess": "समाप्ति सेटिंग्स सफलतापूर्वक अपडेट हुईं",
"creatingZip": "ज़िप फ़ाइल बनाना ...",
"defaultShareName": "शेयर करना",
"downloadError": "शेयर फ़ाइलें डाउनलोड करने में विफल",
"downloadSuccess": "डाउनलोड सफलतापूर्वक शुरू हुआ",
"multipleSharesZipName": "{गिनती} _shares_files.zip",
"noFilesToDownload": "डाउनलोड करने के लिए कोई फाइलें उपलब्ध नहीं हैं",
"singleShareZipName": "{Sharename} _files.zip",
"zipDownloadError": "ज़िप फ़ाइल बनाने में विफल",
"zipDownloadSuccess": "ज़िप फ़ाइल सफलतापूर्वक डाउनलोड की गई"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "लिंक संपादित करें",
"copyLink": "लिंक कॉपी करें",
"notifyRecipients": "प्राप्तकर्ताओं को सूचित करें",
"delete": "हटाएं"
"delete": "हटाएं",
"downloadShareFiles": "सभी फ़ाइलें डाउनलोड करें"
},
"bulkActions": {
"delete": "हटाएं",
"selected": "{count, plural, =1 {1 साझाकरण चयनित} other {# साझाकरण चयनित}}"
"selected": "{count, plural, =1 {1 साझाकरण चयनित} other {# साझाकरण चयनित}}",
"actions": "कार्रवाई",
"download": "चयनित डाउनलोड करें"
},
"selectAll": "सभी चुनें",
"selectShare": "साझाकरण {shareName} चुनें"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "पूर्वावलोकन",
"download": "डाउनलोड"
"download": "डाउनलोड",
"copyToMyFiles": "मेरी फ़ाइलों में कॉपी करें",
"copying": "नकल ..."
},
"uploadedBy": "{name} द्वारा भेजा गया",
"anonymous": "अज्ञात",
@@ -1165,7 +1179,9 @@
"downloadError": "फ़ाइल डाउनलोड करने में त्रुटि",
"editSuccess": "फ़ाइल सफलतापूर्वक अपडेट की गई",
"editError": "फ़ाइल अपडेट करने में त्रुटि",
"previewNotAvailable": "पूर्वावलोकन उपलब्ध नहीं है"
"previewNotAvailable": "पूर्वावलोकन उपलब्ध नहीं है",
"copyError": "अपनी फ़ाइलों में फ़ाइल की नकल करने में त्रुटि",
"copySuccess": "आपकी फ़ाइलों को सफलतापूर्वक कॉपी की गई फ़ाइल"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "रद्द करें",
"preview": "पूर्वावलोकन",
"download": "डाउनलोड",
"delete": "हटाएं"
"delete": "हटाएं",
"copyToMyFiles": "मेरी फ़ाइलों में कॉपी करें",
"copying": "नकल ..."
},
"editField": {
"saveChanges": "परिवर्तन सहेजें",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Impossibile aggiornare le impostazioni di sicurezza",
"expirationUpdateError": "Impossibile aggiornare le impostazioni di scadenza",
"securityUpdateSuccess": "Impostazioni di sicurezza aggiornate con successo",
"expirationUpdateSuccess": "Impostazioni di scadenza aggiornate con successo"
"expirationUpdateSuccess": "Impostazioni di scadenza aggiornate con successo",
"creatingZip": "Creazione di file zip ...",
"defaultShareName": "Condividere",
"downloadError": "Impossibile scaricare i file di condivisione",
"downloadSuccess": "Download avviato con successo",
"multipleSharesZipName": "{Count} _Shares_files.zip",
"noFilesToDownload": "Nessun file disponibile per il download",
"singleShareZipName": "{Sharename} _files.zip",
"zipDownloadError": "Impossibile creare un file zip",
"zipDownloadSuccess": "File zip scaricato correttamente"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Modifica Link",
"copyLink": "Copia Link",
"notifyRecipients": "Notifica Destinatari",
"delete": "Elimina"
"delete": "Elimina",
"downloadShareFiles": "Scarica tutti i file"
},
"bulkActions": {
"delete": "Elimina",
"selected": "{count, plural, =1 {1 condivisione selezionata} other {# condivisioni selezionate}}"
"selected": "{count, plural, =1 {1 condivisione selezionata} other {# condivisioni selezionate}}",
"actions": "Azioni",
"download": "Scarica selezionato"
},
"selectAll": "Seleziona tutto",
"selectShare": "Seleziona condivisione {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Anteprima",
"download": "Scarica"
"download": "Scarica",
"copyToMyFiles": "Copia sui miei file",
"copying": "Copia ..."
},
"uploadedBy": "Inviato da {name}",
"anonymous": "Anonimo",
@@ -1165,7 +1179,9 @@
"downloadError": "Errore durante il download del file",
"editSuccess": "File aggiornato con successo",
"editError": "Errore durante l'aggiornamento del file",
"previewNotAvailable": "Anteprima non disponibile"
"previewNotAvailable": "Anteprima non disponibile",
"copyError": "Errore di copia del file sui tuoi file",
"copySuccess": "File copiato sui tuoi file correttamente"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Annulla",
"preview": "Anteprima",
"download": "Scarica",
"delete": "Elimina"
"delete": "Elimina",
"copyToMyFiles": "Copia sui miei file",
"copying": "Copia ..."
},
"editField": {
"saveChanges": "Salva modifiche",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "セキュリティ設定の更新に失敗しました",
"expirationUpdateError": "有効期限設定の更新に失敗しました",
"securityUpdateSuccess": "セキュリティ設定が正常に更新されました",
"expirationUpdateSuccess": "有効期限設定が正常に更新されました"
"expirationUpdateSuccess": "有効期限設定が正常に更新されました",
"creatingZip": "zipファイルの作成...",
"defaultShareName": "共有",
"downloadError": "共有ファイルをダウンロードできませんでした",
"downloadSuccess": "ダウンロードは正常に開始されました",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "ダウンロードできるファイルはありません",
"singleShareZipName": "{sharename} _files.zip",
"zipDownloadError": "zipファイルの作成に失敗しました",
"zipDownloadSuccess": "zipファイルは正常にダウンロードされました"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "リンク編集",
"copyLink": "リンクコピー",
"notifyRecipients": "受信者に通知",
"delete": "削除"
"delete": "削除",
"downloadShareFiles": "すべてのファイルをダウンロードします"
},
"bulkActions": {
"delete": "削除",
"selected": "{count, plural, =1 {1つの共有が選択されました} other {#つの共有が選択されました}}"
"selected": "{count, plural, =1 {1つの共有が選択されました} other {#つの共有が選択されました}}",
"actions": "アクション",
"download": "選択したダウンロード"
},
"selectAll": "すべて選択",
"selectShare": "共有{shareName}を選択"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "プレビュー",
"download": "ダウンロード"
"download": "ダウンロード",
"copyToMyFiles": "私のファイルにコピーします",
"copying": "コピー..."
},
"uploadedBy": "{name}が送信",
"anonymous": "匿名",
@@ -1165,7 +1179,9 @@
"downloadError": "ファイルのダウンロードに失敗しました",
"editSuccess": "ファイルを更新しました",
"editError": "ファイルの更新に失敗しました",
"previewNotAvailable": "プレビューは利用できません"
"previewNotAvailable": "プレビューは利用できません",
"copyError": "ファイルにファイルをコピーするエラー",
"copySuccess": "ファイルに正常にコピーされたファイル"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "キャンセル",
"preview": "プレビュー",
"download": "ダウンロード",
"delete": "削除"
"delete": "削除",
"copyToMyFiles": "私のファイルにコピーします",
"copying": "コピー..."
},
"editField": {
"saveChanges": "変更を保存",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "보안 설정 업데이트에 실패했습니다",
"expirationUpdateError": "만료 설정 업데이트에 실패했습니다",
"securityUpdateSuccess": "보안 설정이 성공적으로 업데이트되었습니다",
"expirationUpdateSuccess": "만료 설정이 성공적으로 업데이트되었습니다"
"expirationUpdateSuccess": "만료 설정이 성공적으로 업데이트되었습니다",
"creatingZip": "zip 파일 만들기 ...",
"defaultShareName": "공유하다",
"downloadError": "공유 파일을 다운로드하지 못했습니다",
"downloadSuccess": "다운로드가 성공적으로 시작되었습니다",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "다운로드 할 수있는 파일이 없습니다",
"singleShareZipName": "{sharename} _files.zip",
"zipDownloadError": "zip 파일을 만들지 못했습니다",
"zipDownloadSuccess": "zip 파일이 성공적으로 다운로드되었습니다"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "링크 편집",
"copyLink": "링크 복사",
"notifyRecipients": "받는 사람에게 알림",
"delete": "삭제"
"delete": "삭제",
"downloadShareFiles": "모든 파일을 다운로드하십시오"
},
"bulkActions": {
"delete": "삭제",
"selected": "{count, plural, =1 {1개의 공유가 선택됨} other {#개의 공유가 선택됨}}"
"selected": "{count, plural, =1 {1개의 공유가 선택됨} other {#개의 공유가 선택됨}}",
"actions": "행위",
"download": "선택한 다운로드"
},
"selectAll": "모두 선택",
"selectShare": "공유 {shareName} 선택"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "미리보기",
"download": "다운로드"
"download": "다운로드",
"copyToMyFiles": "내 파일에 복사하십시오",
"copying": "사자..."
},
"uploadedBy": "{name}님이 업로드함",
"anonymous": "익명",
@@ -1165,7 +1179,9 @@
"downloadError": "파일 다운로드 오류",
"editSuccess": "파일이 성공적으로 업데이트됨",
"editError": "파일 업데이트 오류",
"previewNotAvailable": "미리보기 불가"
"previewNotAvailable": "미리보기 불가",
"copyError": "파일에 파일을 복사합니다",
"copySuccess": "파일을 파일에 성공적으로 복사했습니다"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "취소",
"preview": "미리보기",
"download": "다운로드",
"delete": "삭제"
"delete": "삭제",
"copyToMyFiles": "내 파일에 복사하십시오",
"copying": "사자..."
},
"editField": {
"saveChanges": "변경사항 저장",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Fout bij het bijwerken van beveiligingsinstellingen",
"expirationUpdateError": "Fout bij het bijwerken van verloop instellingen",
"securityUpdateSuccess": "Beveiligingsinstellingen succesvol bijgewerkt",
"expirationUpdateSuccess": "Verloop instellingen succesvol bijgewerkt"
"expirationUpdateSuccess": "Verloop instellingen succesvol bijgewerkt",
"creatingZip": "Zip -bestand maken ...",
"defaultShareName": "Deel",
"downloadError": "Kan niet downloaden Delen -bestanden downloaden",
"downloadSuccess": "Download begonnen met succes",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "Geen bestanden beschikbaar om te downloaden",
"singleShareZipName": "{Sharename} _files.zip",
"zipDownloadError": "Kan zip -bestand niet maken",
"zipDownloadSuccess": "Zipbestand met succes gedownload"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Link Bewerken",
"copyLink": "Link Kopiëren",
"notifyRecipients": "Ontvangers Informeren",
"delete": "Verwijderen"
"delete": "Verwijderen",
"downloadShareFiles": "Download alle bestanden"
},
"bulkActions": {
"delete": "Verwijderen",
"selected": "{count, plural, =1 {1 deel geselecteerd} other {# delen geselecteerd}}"
"selected": "{count, plural, =1 {1 deel geselecteerd} other {# delen geselecteerd}}",
"actions": "Acties",
"download": "Download geselecteerd"
},
"selectAll": "Alles selecteren",
"selectShare": "Deel {shareName} selecteren"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Voorvertoning",
"download": "Downloaden"
"download": "Downloaden",
"copyToMyFiles": "Kopieer naar mijn bestanden",
"copying": "Kopiëren ..."
},
"uploadedBy": "Verzonden door {name}",
"anonymous": "Anoniem",
@@ -1165,7 +1179,9 @@
"downloadError": "Fout bij downloaden bestand",
"editSuccess": "Bestand succesvol bijgewerkt",
"editError": "Fout bij bijwerken bestand",
"previewNotAvailable": "Voorvertoning niet beschikbaar"
"previewNotAvailable": "Voorvertoning niet beschikbaar",
"copyError": "Fout bij het kopiëren van bestand naar uw bestanden",
"copySuccess": "Bestand gekopieerd naar uw bestanden succesvol"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Annuleren",
"preview": "Voorvertoning",
"download": "Downloaden",
"delete": "Verwijderen"
"delete": "Verwijderen",
"copyToMyFiles": "Kopieer naar mijn bestanden",
"copying": "Kopiëren ..."
},
"editField": {
"saveChanges": "Wijzigingen opslaan",

View File

@@ -714,7 +714,16 @@
"notifyError": "Nie udało się powiadomić odbiorców",
"bulkDeleteError": "Nie udało się usunąć udostępnień",
"bulkDeleteLoading": "Usuwanie {count, plural, =1 {1 udostępnienia} other {# udostępnień}}...",
"bulkDeleteSuccess": "{count, plural, =1 {1 udostępnienie usunięte pomyślnie} other {# udostępnień usuniętych pomyślnie}}"
"bulkDeleteSuccess": "{count, plural, =1 {1 udostępnienie usunięte pomyślnie} other {# udostępnień usuniętych pomyślnie}}",
"creatingZip": "Tworzenie pliku zip ...",
"defaultShareName": "Udział",
"downloadError": "Nie udało się pobrać plików udostępniania",
"downloadSuccess": "Pobierz zaczęło się pomyślnie",
"multipleSharesZipName": "{Count} _Shares_files.zip",
"noFilesToDownload": "Brak plików do pobrania",
"singleShareZipName": "{ShaRename} _files.zip",
"zipDownloadError": "Nie udało się utworzyć pliku zip",
"zipDownloadSuccess": "Plik zip pobrany pomyślnie"
},
"shares": {
"errors": {
@@ -777,11 +786,14 @@
"editLink": "Edytuj link",
"copyLink": "Skopiuj link",
"notifyRecipients": "Powiadom odbiorców",
"delete": "Usuń"
"delete": "Usuń",
"downloadShareFiles": "Pobierz wszystkie pliki"
},
"bulkActions": {
"delete": "Usuń",
"selected": "{count, plural, =1 {Wybrano 1 udostępnienie} other {# wybranych udostępnień}}"
"selected": "{count, plural, =1 {Wybrano 1 udostępnienie} other {# wybranych udostępnień}}",
"actions": "Działania",
"download": "Pobierz wybrany"
},
"selectAll": "Zaznacz wszystko",
"selectShare": "Wybierz udostępnienie {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Podgląd",
"download": "Pobierz"
"download": "Pobierz",
"copyToMyFiles": "Skopiuj do moich plików",
"copying": "Biurowy..."
},
"uploadedBy": "Przesłane przez {name}",
"anonymous": "Anonimowy",
@@ -1165,7 +1179,9 @@
"downloadError": "Błąd pobierania pliku",
"editSuccess": "Plik zaktualizowany pomyślnie",
"editError": "Błąd aktualizacji pliku",
"previewNotAvailable": "Podgląd niedostępny"
"previewNotAvailable": "Podgląd niedostępny",
"copyError": "Plik kopiowania błędów do plików",
"copySuccess": "Plik skopiowany do plików pomyślnie"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Anuluj",
"preview": "Podgląd",
"download": "Pobierz",
"delete": "Usuń"
"delete": "Usuń",
"copyToMyFiles": "Skopiuj do moich plików",
"copying": "Biurowy..."
},
"editField": {
"saveChanges": "Zapisz zmiany",

View File

@@ -678,7 +678,16 @@
"securityUpdateError": "Falha ao atualizar configurações de segurança",
"expirationUpdateError": "Falha ao atualizar configurações de expiração",
"securityUpdateSuccess": "Configurações de segurança atualizadas com sucesso",
"expirationUpdateSuccess": "Configurações de expiração atualizadas com sucesso"
"expirationUpdateSuccess": "Configurações de expiração atualizadas com sucesso",
"creatingZip": "Criando arquivo zip ...",
"defaultShareName": "Compartilhar",
"downloadError": "Falha ao baixar arquivos de compartilhamento",
"downloadSuccess": "Download começou com sucesso",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "Nenhum arquivo disponível para download",
"singleShareZipName": "{sharename}.zip",
"zipDownloadError": "Falha ao criar o arquivo zip",
"zipDownloadSuccess": "FILE DE ZIP FILHADO COMBONHADO com sucesso"
},
"shares": {
"errors": {
@@ -713,7 +722,9 @@
"selectShare": "Selecionar compartilhamento {shareName}",
"bulkActions": {
"selected": "{count, plural, =1 {1 compartilhamento selecionado} other {# compartilhamentos selecionados}}",
"delete": "Excluir"
"delete": "Excluir",
"actions": "Ações",
"download": "Download selecionado"
},
"columns": {
"name": "NOME",
@@ -747,7 +758,8 @@
"editLink": "Editar Link",
"copyLink": "Copiar Link",
"notifyRecipients": "Notificar Destinatários",
"delete": "Excluir"
"delete": "Excluir",
"downloadShareFiles": "Baixar todos os arquivos"
}
},
"storageUsage": {
@@ -876,7 +888,11 @@
"passwordRequired": "Senha é obrigatória",
"passwordMinLength": "A senha deve ter pelo menos 6 caracteres",
"nameRequired": "Nome é obrigatório",
"required": "Este campo é obrigatório"
"required": "Este campo é obrigatório",
"firstNameRequired": "O primeiro nome é necessário",
"lastNameRequired": "O sobrenome é necessário",
"usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres",
"usernameSpaces": "O nome de usuário não pode conter espaços"
},
"bulkDownload": {
"title": "Download em Lote",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Не удалось обновить настройки безопасности",
"expirationUpdateError": "Не удалось обновить настройки истечения",
"securityUpdateSuccess": "Настройки безопасности успешно обновлены",
"expirationUpdateSuccess": "Настройки истечения успешно обновлены"
"expirationUpdateSuccess": "Настройки истечения успешно обновлены",
"creatingZip": "Создание файла Zip ...",
"defaultShareName": "Делиться",
"downloadError": "Не удалось скачать общие файлы",
"downloadSuccess": "Скачать началась успешно",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "Нет файлов для скачивания",
"singleShareZipName": "{shareme} _files.zip",
"zipDownloadError": "Не удалось создать zip -файл",
"zipDownloadSuccess": "Zip -файл успешно загружен"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Редактировать Ссылку",
"copyLink": "Скопировать Ссылку",
"notifyRecipients": "Уведомить Получателей",
"delete": "Удалить"
"delete": "Удалить",
"downloadShareFiles": "Загрузите все файлы"
},
"bulkActions": {
"delete": "Удалить",
"selected": "{count, plural, =1 {1 общая папка выбрана} other {# общих папок выбрано}}"
"selected": "{count, plural, =1 {1 общая папка выбрана} other {# общих папок выбрано}}",
"actions": "Действия",
"download": "Скачать выбранный"
},
"selectAll": "Выбрать все",
"selectShare": "Выбрать общую папку {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Предпросмотр",
"download": "Скачать"
"download": "Скачать",
"copyToMyFiles": "Скопируйте в мои файлы",
"copying": "Копирование ..."
},
"uploadedBy": "Загружено {name}",
"anonymous": "Аноним",
@@ -1165,7 +1179,9 @@
"downloadError": "Ошибка при загрузке файла",
"editSuccess": "Файл успешно обновлен",
"editError": "Ошибка при обновлении файла",
"previewNotAvailable": "Предпросмотр недоступен"
"previewNotAvailable": "Предпросмотр недоступен",
"copyError": "Ошибка копирования файла в ваши файлы",
"copySuccess": "Файл успешно скопирован в ваши файлы"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "Отмена",
"preview": "Предпросмотр",
"download": "Скачать",
"delete": "Удалить"
"delete": "Удалить",
"copyToMyFiles": "Скопируйте в мои файлы",
"copying": "Копирование ..."
},
"editField": {
"saveChanges": "Сохранить изменения",

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "Güvenlik ayarlarını güncelleme başarısız",
"expirationUpdateError": "Son kullanma ayarlarını güncelleme başarısız",
"securityUpdateSuccess": "Güvenlik ayarları başarıyla güncellendi",
"expirationUpdateSuccess": "Son kullanma ayarları başarıyla güncellendi"
"expirationUpdateSuccess": "Son kullanma ayarları başarıyla güncellendi",
"creatingZip": "Zip dosyası oluşturma ...",
"defaultShareName": "Paylaşmak",
"downloadError": "Paylaşım dosyalarını indiremedi",
"downloadSuccess": "İndir başarıyla başladı",
"multipleSharesZipName": "{Count} _Shares_files.zip",
"noFilesToDownload": "İndirilebilecek dosya yok",
"singleShareZipName": "{Sharename} _files.zip",
"zipDownloadError": "Zip dosyası oluşturulamadı",
"zipDownloadSuccess": "Zip dosyası başarıyla indirildi"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "Bağlantıyı Düzenle",
"copyLink": "Bağlantıyı Kopyala",
"notifyRecipients": "Alıcıları Bilgilendir",
"delete": "Sil"
"delete": "Sil",
"downloadShareFiles": "Tüm dosyaları indirin"
},
"bulkActions": {
"delete": "Sil",
"selected": "{count, plural, =1 {1 paylaşım seçildi} other {# paylaşım seçildi}}"
"selected": "{count, plural, =1 {1 paylaşım seçildi} other {# paylaşım seçildi}}",
"actions": "Eylem",
"download": "Seçili indir"
},
"selectAll": "Tümünü seç",
"selectShare": "Paylaşım {shareName} seç"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "Önizle",
"download": "İndir"
"download": "İndir",
"copyToMyFiles": "Dosyalarımı kopyala",
"copying": "Kopyalama ..."
},
"uploadedBy": "{name} tarafından gönderildi",
"anonymous": "Anonim",
@@ -1165,7 +1179,9 @@
"downloadError": "Dosya indirilirken hata oluştu",
"editSuccess": "Dosya başarıyla güncellendi",
"editError": "Dosya güncellenirken hata oluştu",
"previewNotAvailable": "Önizleme mevcut değil"
"previewNotAvailable": "Önizleme mevcut değil",
"copyError": "Dosyalarınıza dosyayı kopyalama hatası",
"copySuccess": "Dosyalarınıza başarılı bir şekilde kopyalanan dosya"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "İptal",
"preview": "Önizle",
"download": "İndir",
"delete": "Sil"
"delete": "Sil",
"copyToMyFiles": "Dosyalarımı kopyala",
"copying": "Kopyalama ..."
},
"editField": {
"saveChanges": "Değişiklikleri kaydet",

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Script para traduzir automaticamente strings marcadas com [TO_TRANSLATE]
usando Google Translate gratuito.
"""
import json
import time
import re
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
import argparse
import sys
# Mapeamento de códigos de idioma dos arquivos para códigos do Google Translate
LANGUAGE_MAPPING = {
'pt-BR.json': 'pt', # Português (Brasil) -> Português
'es-ES.json': 'es', # Espanhol (Espanha) -> Espanhol
'fr-FR.json': 'fr', # Francês (França) -> Francês
'de-DE.json': 'de', # Alemão -> Alemão
'it-IT.json': 'it', # Italiano -> Italiano
'ru-RU.json': 'ru', # Russo -> Russo
'ja-JP.json': 'ja', # Japonês -> Japonês
'ko-KR.json': 'ko', # Coreano -> Coreano
'zh-CN.json': 'zh-cn', # Chinês (Simplificado) -> Chinês Simplificado
'ar-SA.json': 'ar', # Árabe -> Árabe
'hi-IN.json': 'hi', # Hindi -> Hindi
'nl-NL.json': 'nl', # Holandês -> Holandês
'tr-TR.json': 'tr', # Turco -> Turco
'pl-PL.json': 'pl', # Polonês -> Polonês
}
# Prefixo para identificar strings não traduzidas
TO_TRANSLATE_PREFIX = '[TO_TRANSLATE] '
def load_json_file(file_path: Path) -> Dict[str, Any]:
"""Carrega um arquivo JSON."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"❌ Erro ao carregar {file_path}: {e}")
return {}
def save_json_file(file_path: Path, data: Dict[str, Any], indent: int = 2) -> bool:
"""Salva um arquivo JSON com formatação consistente."""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=indent, separators=(',', ': '))
f.write('\n') # Adiciona nova linha no final
return True
except Exception as e:
print(f"❌ Erro ao salvar {file_path}: {e}")
return False
def get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
"""Obtém um valor aninhado usando uma chave com pontos como separador."""
keys = key_path.split('.')
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return None
return current
def set_nested_value(data: Dict[str, Any], key_path: str, value: Any) -> None:
"""Define um valor aninhado usando uma chave com pontos como separador."""
keys = key_path.split('.')
current = data
# Navega até o penúltimo nível, criando dicionários conforme necessário
for key in keys[:-1]:
if key not in current:
current[key] = {}
elif not isinstance(current[key], dict):
current[key] = {}
current = current[key]
# Define o valor no último nível
current[keys[-1]] = value
def find_untranslated_strings(data: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
"""Encontra todas as strings marcadas com [TO_TRANSLATE] recursivamente."""
untranslated = []
for key, value in data.items():
current_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, str) and value.startswith(TO_TRANSLATE_PREFIX):
# Remove o prefixo para obter o texto original
original_text = value[len(TO_TRANSLATE_PREFIX):].strip()
untranslated.append((current_key, original_text))
elif isinstance(value, dict):
untranslated.extend(find_untranslated_strings(value, current_key))
return untranslated
def install_googletrans():
"""Instala a biblioteca googletrans se não estiver disponível."""
try:
import googletrans
return True
except ImportError:
print("📦 Biblioteca 'googletrans' não encontrada. Tentando instalar...")
import subprocess
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "googletrans==4.0.0rc1"])
print("✅ googletrans instalada com sucesso!")
return True
except subprocess.CalledProcessError:
print("❌ Falha ao instalar googletrans. Instale manualmente com:")
print("pip install googletrans==4.0.0rc1")
return False
def translate_text(text: str, target_language: str, max_retries: int = 3) -> Optional[str]:
"""Traduz um texto usando Google Translate gratuito."""
try:
from googletrans import Translator
translator = Translator()
for attempt in range(max_retries):
try:
# Traduz do inglês para o idioma alvo
result = translator.translate(text, src='en', dest=target_language)
if result and result.text:
return result.text.strip()
except Exception as e:
if attempt < max_retries - 1:
print(f" ⚠️ Tentativa {attempt + 1} falhou: {str(e)[:50]}... Reentando em 2s...")
time.sleep(2)
else:
print(f" ❌ Falha após {max_retries} tentativas: {str(e)[:50]}...")
return None
except ImportError:
print("❌ Biblioteca googletrans não disponível")
return None
def translate_file(file_path: Path, target_language: str, dry_run: bool = False,
delay_between_requests: float = 1.0) -> Tuple[int, int, int]:
"""
Traduz todas as strings [TO_TRANSLATE] em um arquivo.
Retorna: (total_found, successful_translations, failed_translations)
"""
print(f"🔍 Processando: {file_path.name}")
# Carrega o arquivo
data = load_json_file(file_path)
if not data:
return 0, 0, 0
# Encontra strings não traduzidas
untranslated_strings = find_untranslated_strings(data)
if not untranslated_strings:
print(f" ✅ Nenhuma string para traduzir")
return 0, 0, 0
print(f" 📝 Encontradas {len(untranslated_strings)} strings para traduzir")
if dry_run:
print(f" 🔍 [DRY RUN] Strings que seriam traduzidas:")
for key, text in untranslated_strings[:3]:
print(f" - {key}: \"{text[:50]}{'...' if len(text) > 50 else ''}\"")
if len(untranslated_strings) > 3:
print(f" ... e mais {len(untranslated_strings) - 3}")
return len(untranslated_strings), 0, 0
# Traduz cada string
successful = 0
failed = 0
updated_data = data.copy()
for i, (key_path, original_text) in enumerate(untranslated_strings, 1):
print(f" 📍 ({i}/{len(untranslated_strings)}) Traduzindo: {key_path}")
# Traduz o texto
translated_text = translate_text(original_text, target_language)
if translated_text and translated_text != original_text:
# Atualiza no dicionário
set_nested_value(updated_data, key_path, translated_text)
successful += 1
print(f"\"{original_text[:30]}...\"\"{translated_text[:30]}...\"")
else:
failed += 1
print(f" ❌ Falha na tradução")
# Delay entre requisições para evitar rate limiting
if i < len(untranslated_strings): # Não espera após a última
time.sleep(delay_between_requests)
# Salva o arquivo atualizado
if successful > 0:
if save_json_file(file_path, updated_data):
print(f" 💾 Arquivo salvo com {successful} traduções")
else:
print(f" ❌ Erro ao salvar arquivo")
failed += successful # Conta como falha se não conseguiu salvar
successful = 0
return len(untranslated_strings), successful, failed
def translate_all_files(messages_dir: Path, delay_between_requests: float = 1.0,
dry_run: bool = False, skip_languages: List[str] = None) -> None:
"""Traduz todos os arquivos de idioma que têm strings [TO_TRANSLATE]."""
if not install_googletrans():
return
skip_languages = skip_languages or []
# Encontra arquivos JSON de idioma
language_files = []
for file_name, lang_code in LANGUAGE_MAPPING.items():
file_path = messages_dir / file_name
if file_path.exists() and file_name not in skip_languages:
language_files.append((file_path, lang_code))
if not language_files:
print("❌ Nenhum arquivo de idioma encontrado")
return
print(f"🌍 Traduzindo {len(language_files)} idiomas...")
print(f"⏱️ Delay entre requisições: {delay_between_requests}s")
if dry_run:
print("🔍 MODO DRY RUN - Nenhuma alteração será feita")
print("-" * 60)
total_found = 0
total_successful = 0
total_failed = 0
for i, (file_path, lang_code) in enumerate(language_files, 1):
print(f"\n[{i}/{len(language_files)}] 🌐 Idioma: {lang_code.upper()}")
found, successful, failed = translate_file(
file_path, lang_code, dry_run, delay_between_requests
)
total_found += found
total_successful += successful
total_failed += failed
# Pausa entre arquivos (exceto o último)
if i < len(language_files) and not dry_run:
print(f" ⏸️ Pausando {delay_between_requests * 2}s antes do próximo idioma...")
time.sleep(delay_between_requests * 2)
# Sumário final
print("\n" + "=" * 60)
print("📊 SUMÁRIO FINAL")
print("=" * 60)
if dry_run:
print(f"🔍 MODO DRY RUN:")
print(f"{total_found} strings seriam traduzidas")
else:
print(f"✅ Traduções realizadas:")
print(f"{total_successful} sucessos")
print(f"{total_failed} falhas")
print(f"{total_found} total processadas")
if total_successful > 0:
success_rate = (total_successful / total_found) * 100
print(f" • Taxa de sucesso: {success_rate:.1f}%")
print("\n💡 DICAS:")
print("• Execute 'python3 check_translations.py' para verificar o resultado")
print("• Strings que falharam na tradução mantêm o prefixo [TO_TRANSLATE]")
print("• Considere revisar as traduções automáticas para garantir qualidade")
def main():
parser = argparse.ArgumentParser(
description='Traduz automaticamente strings marcadas com [TO_TRANSLATE]'
)
parser.add_argument(
'--messages-dir',
type=Path,
default=Path(__file__).parent,
help='Diretório contendo os arquivos de mensagem (padrão: diretório atual)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Apenas mostra o que seria traduzido, sem fazer alterações'
)
parser.add_argument(
'--delay',
type=float,
default=1.0,
help='Delay em segundos entre requisições de tradução (padrão: 1.0)'
)
parser.add_argument(
'--skip-languages',
nargs='*',
default=[],
help='Lista de idiomas para pular (ex: pt-BR.json fr-FR.json)'
)
args = parser.parse_args()
if not args.messages_dir.exists():
print(f"❌ Diretório não encontrado: {args.messages_dir}")
return 1
print(f"📁 Diretório: {args.messages_dir}")
print(f"🔍 Dry run: {args.dry_run}")
print(f"⏱️ Delay: {args.delay}s")
if args.skip_languages:
print(f"⏭️ Ignorando: {', '.join(args.skip_languages)}")
print("-" * 60)
translate_all_files(
messages_dir=args.messages_dir,
delay_between_requests=args.delay,
dry_run=args.dry_run,
skip_languages=args.skip_languages
)
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -656,7 +656,16 @@
"securityUpdateError": "更新安全设置失败",
"expirationUpdateError": "更新过期设置失败",
"securityUpdateSuccess": "安全设置更新成功",
"expirationUpdateSuccess": "过期设置更新成功"
"expirationUpdateSuccess": "过期设置更新成功",
"creatingZip": "创建zip文件...",
"defaultShareName": "分享",
"downloadError": "无法下载共享文件",
"downloadSuccess": "下载成功开始",
"multipleSharesZipName": "{count} _shares_files.zip",
"noFilesToDownload": "无需下载文件",
"singleShareZipName": "{sharename} _files.zip",
"zipDownloadError": "无法创建zip文件",
"zipDownloadSuccess": "zip文件成功下载了"
},
"shares": {
"errors": {
@@ -719,11 +728,14 @@
"editLink": "编辑链接",
"copyLink": "复制链接",
"notifyRecipients": "通知收件人",
"delete": "删除"
"delete": "删除",
"downloadShareFiles": "下载所有文件"
},
"bulkActions": {
"delete": "删除",
"selected": "{count, plural, =1 {已选择1个共享} other {已选择#个共享}}"
"selected": "{count, plural, =1 {已选择1个共享} other {已选择#个共享}}",
"actions": "动作",
"download": "选择下载"
},
"selectAll": "全选",
"selectShare": "选择共享 {shareName}"
@@ -1157,7 +1169,9 @@
},
"actions": {
"preview": "预览",
"download": "下载"
"download": "下载",
"copyToMyFiles": "复制到我的文件",
"copying": "复制..."
},
"uploadedBy": "由 {name} 上传",
"anonymous": "匿名",
@@ -1165,7 +1179,9 @@
"downloadError": "下载文件时出错",
"editSuccess": "文件更新成功",
"editError": "更新文件时出错",
"previewNotAvailable": "预览不可用"
"previewNotAvailable": "预览不可用",
"copyError": "错误将文件复制到您的文件",
"copySuccess": "文件已成功复制到您的文件"
}
},
"form": {
@@ -1347,7 +1363,9 @@
"cancel": "取消",
"preview": "预览",
"download": "下载",
"delete": "删除"
"delete": "删除",
"copyToMyFiles": "复制到我的文件",
"copying": "复制..."
},
"editField": {
"saveChanges": "保存更改",

View File

@@ -20,7 +20,13 @@
"lint": "eslint \"src/**/*.+(ts|tsx)\"",
"lint:fix": "eslint \"src/**/*.+(ts|tsx)\" --fix",
"format": "prettier . --write",
"format:check": "prettier . --check"
"format:check": "prettier . --check",
"translations": "python3 scripts/run_translations.py all",
"translations:check": "python3 scripts/run_translations.py check",
"translations:sync": "python3 scripts/run_translations.py sync",
"translations:translate": "python3 scripts/run_translations.py translate",
"translations:help": "python3 scripts/run_translations.py help",
"translations:dry-run": "python3 scripts/run_translations.py all --dry-run"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",

View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
Script to check translation status and identify strings that need translation.
"""
import json
from pathlib import Path
from typing import Dict, Any, List, Tuple
import argparse
def load_json_file(file_path: Path) -> Dict[str, Any]:
"""Load a JSON file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading {file_path}: {e}")
return {}
def get_all_string_values(data: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
"""Extract all strings from nested JSON with their keys."""
strings = []
for key, value in data.items():
current_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, str):
strings.append((current_key, value))
elif isinstance(value, dict):
strings.extend(get_all_string_values(value, current_key))
return strings
def check_untranslated_strings(file_path: Path) -> Tuple[int, int, List[str]]:
"""Check for untranslated strings in a file."""
data = load_json_file(file_path)
if not data:
return 0, 0, []
all_strings = get_all_string_values(data)
untranslated = []
for key, value in all_strings:
if value.startswith('[TO_TRANSLATE]'):
untranslated.append(key)
return len(all_strings), len(untranslated), untranslated
def compare_languages(reference_file: Path, target_file: Path) -> Dict[str, Any]:
"""Compare two language files."""
reference_data = load_json_file(reference_file)
target_data = load_json_file(target_file)
if not reference_data or not target_data:
return {}
reference_strings = dict(get_all_string_values(reference_data))
target_strings = dict(get_all_string_values(target_data))
# Find common keys
common_keys = set(reference_strings.keys()) & set(target_strings.keys())
# Check identical strings (possibly untranslated)
identical_strings = []
for key in common_keys:
if reference_strings[key] == target_strings[key] and len(reference_strings[key]) > 3:
identical_strings.append(key)
return {
'total_reference': len(reference_strings),
'total_target': len(target_strings),
'common_keys': len(common_keys),
'identical_strings': identical_strings
}
def generate_translation_report(messages_dir: Path, reference_file: str = 'en-US.json'):
"""Generate complete translation report."""
reference_path = messages_dir / reference_file
if not reference_path.exists():
print(f"Reference file not found: {reference_path}")
return
# Load reference data
reference_data = load_json_file(reference_path)
reference_strings = dict(get_all_string_values(reference_data))
total_reference_strings = len(reference_strings)
print(f"📊 TRANSLATION REPORT")
print(f"Reference: {reference_file} ({total_reference_strings} strings)")
print("=" * 80)
# Find all JSON files
json_files = [f for f in messages_dir.glob('*.json') if f.name != reference_file]
if not json_files:
print("No translation files found")
return
reports = []
for json_file in sorted(json_files):
total_strings, untranslated_count, untranslated_keys = check_untranslated_strings(json_file)
comparison = compare_languages(reference_path, json_file)
# Calculate percentages
completion_percentage = (total_strings / total_reference_strings) * 100 if total_reference_strings > 0 else 0
untranslated_percentage = (untranslated_count / total_strings) * 100 if total_strings > 0 else 0
reports.append({
'file': json_file.name,
'total_strings': total_strings,
'untranslated_count': untranslated_count,
'untranslated_keys': untranslated_keys,
'completion_percentage': completion_percentage,
'untranslated_percentage': untranslated_percentage,
'identical_strings': comparison.get('identical_strings', [])
})
# Sort by completion percentage
reports.sort(key=lambda x: x['completion_percentage'], reverse=True)
print(f"{'LANGUAGE':<15} {'COMPLETENESS':<12} {'STRINGS':<15} {'UNTRANSLATED':<15} {'POSSIBLE MATCHES'}")
print("-" * 80)
for report in reports:
language = report['file'].replace('.json', '')
completion = f"{report['completion_percentage']:.1f}%"
strings_info = f"{report['total_strings']}/{total_reference_strings}"
untranslated_info = f"{report['untranslated_count']} ({report['untranslated_percentage']:.1f}%)"
identical_count = len(report['identical_strings'])
# Choose icon based on completeness
if report['completion_percentage'] >= 100:
icon = "" if report['untranslated_count'] == 0 else "⚠️"
elif report['completion_percentage'] >= 90:
icon = "🟡"
else:
icon = "🔴"
print(f"{icon} {language:<13} {completion:<12} {strings_info:<15} {untranslated_info:<15} {identical_count}")
print("\n" + "=" * 80)
# Show details of problematic files
problematic_files = [r for r in reports if r['untranslated_count'] > 0 or r['completion_percentage'] < 100]
if problematic_files:
print("📋 DETAILS OF FILES THAT NEED ATTENTION:")
print()
for report in problematic_files:
language = report['file'].replace('.json', '')
print(f"🔍 {language.upper()}:")
if report['completion_percentage'] < 100:
missing_count = total_reference_strings - report['total_strings']
print(f" • Missing {missing_count} strings ({100 - report['completion_percentage']:.1f}%)")
if report['untranslated_count'] > 0:
print(f"{report['untranslated_count']} strings marked as [TO_TRANSLATE]")
if report['untranslated_count'] <= 10:
print(" • Untranslated keys:")
for key in report['untranslated_keys']:
print(f" - {key}")
else:
print(" • First 10 untranslated keys:")
for key in report['untranslated_keys'][:10]:
print(f" - {key}")
print(f" ... and {report['untranslated_count'] - 10} more")
if report['identical_strings']:
identical_count = len(report['identical_strings'])
print(f"{identical_count} strings identical to English (possibly untranslated)")
if identical_count <= 5:
for key in report['identical_strings']:
value = reference_strings.get(key, '')[:50]
print(f" - {key}: \"{value}...\"")
else:
for key in report['identical_strings'][:5]:
value = reference_strings.get(key, '')[:50]
print(f" - {key}: \"{value}...\"")
print(f" ... and {identical_count - 5} more")
print()
else:
print("🎉 All translations are complete!")
print("=" * 80)
print("💡 TIPS:")
print("• Use 'python3 sync_translations.py --dry-run' to see what would be added")
print("• Use 'python3 sync_translations.py' to synchronize all translations")
print("• Strings marked with [TO_TRANSLATE] need manual translation")
print("• Strings identical to English may need translation")
def main():
parser = argparse.ArgumentParser(
description='Check translation status and identify strings that need translation'
)
parser.add_argument(
'--messages-dir',
type=Path,
default=Path(__file__).parent.parent / 'messages',
help='Directory containing message files (default: ../messages)'
)
parser.add_argument(
'--reference',
default='en-US.json',
help='Reference file (default: en-US.json)'
)
args = parser.parse_args()
if not args.messages_dir.exists():
print(f"Directory not found: {args.messages_dir}")
return 1
generate_translation_report(args.messages_dir, args.reference)
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Main script to run all Palmr translation operations.
Makes it easy to run scripts without remembering specific names.
"""
import sys
import subprocess
from pathlib import Path
import argparse
def run_command(script_name: str, args: list) -> int:
"""Execute a script with the provided arguments."""
script_path = Path(__file__).parent / script_name
cmd = [sys.executable, str(script_path)] + args
return subprocess.run(cmd).returncode
def main():
parser = argparse.ArgumentParser(
description='Main script to manage Palmr translations',
epilog='Examples:\n'
' python3 run_translations.py check\n'
' python3 run_translations.py sync --dry-run\n'
' python3 run_translations.py translate --delay 2.0\n'
' python3 run_translations.py all # Complete workflow\n',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'command',
choices=['check', 'sync', 'translate', 'all', 'help'],
help='Command to execute:\n'
'check - Check translation status\n'
'sync - Synchronize missing keys\n'
'translate - Automatically translate strings\n'
'all - Run complete workflow\n'
'help - Show detailed help'
)
# Capture remaining arguments to pass to scripts
args, remaining_args = parser.parse_known_args()
if args.command == 'help':
print("🌍 PALMR TRANSLATION MANAGER")
print("=" * 50)
print()
print("📋 AVAILABLE COMMANDS:")
print()
print("🔍 check - Check translation status")
print(" python3 run_translations.py check")
print(" python3 run_translations.py check --reference pt-BR.json")
print()
print("🔄 sync - Synchronize missing keys")
print(" python3 run_translations.py sync")
print(" python3 run_translations.py sync --dry-run")
print(" python3 run_translations.py sync --no-mark-untranslated")
print()
print("🌐 translate - Automatically translate")
print(" python3 run_translations.py translate")
print(" python3 run_translations.py translate --dry-run")
print(" python3 run_translations.py translate --delay 2.0")
print(" python3 run_translations.py translate --skip-languages pt-BR.json")
print()
print("⚡ all - Complete workflow (sync + translate)")
print(" python3 run_translations.py all")
print(" python3 run_translations.py all --dry-run")
print()
print("📁 STRUCTURE:")
print(" apps/web/scripts/ - Management scripts")
print(" apps/web/messages/ - Translation files")
print()
print("💡 TIPS:")
print("• Use --dry-run on any command to test")
print("• Use --help on any command for specific options")
print("• Read https://docs.palmr.dev/docs/3.0-beta/translation-management for complete documentation")
return 0
elif args.command == 'check':
print("🔍 Checking translation status...")
return run_command('check_translations.py', remaining_args)
elif args.command == 'sync':
print("🔄 Synchronizing translation keys...")
return run_command('sync_translations.py', remaining_args)
elif args.command == 'translate':
print("🌐 Automatically translating strings...")
return run_command('translate_missing.py', remaining_args)
elif args.command == 'all':
print("⚡ Running complete translation workflow...")
print()
# Determine if it's dry-run based on arguments
is_dry_run = '--dry-run' in remaining_args
# 1. Initial check
print("1⃣ Checking initial status...")
result = run_command('check_translations.py', remaining_args)
if result != 0:
print("❌ Error in initial check")
return result
print("\n" + "="*50)
# 2. Sync
print("2⃣ Synchronizing missing keys...")
result = run_command('sync_translations.py', remaining_args)
if result != 0:
print("❌ Error in synchronization")
return result
if not is_dry_run:
print("\n" + "="*50)
# 3. Translate
print("3⃣ Automatically translating strings...")
result = run_command('translate_missing.py', remaining_args)
if result != 0:
print("❌ Error in translation")
return result
print("\n" + "="*50)
# 4. Final check
print("4⃣ Final check...")
result = run_command('check_translations.py', remaining_args)
if result != 0:
print("❌ Error in final check")
return result
print("\n🎉 Complete workflow executed successfully!")
if is_dry_run:
print("💡 Run without --dry-run to apply changes")
return 0
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""
Script to synchronize translations using en-US.json as reference.
Adds missing keys to other language files.
"""
import json
import os
from pathlib import Path
from typing import Dict, Any, Set, List
import argparse
def load_json_file(file_path: Path) -> Dict[str, Any]:
"""Load a JSON file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading {file_path}: {e}")
return {}
def save_json_file(file_path: Path, data: Dict[str, Any], indent: int = 2) -> bool:
"""Save a JSON file with consistent formatting."""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=indent, separators=(',', ': '))
f.write('\n') # Add newline at the end
return True
except Exception as e:
print(f"Error saving {file_path}: {e}")
return False
def get_all_keys(data: Dict[str, Any], prefix: str = '') -> Set[str]:
"""Extract all keys from nested JSON recursively."""
keys = set()
for key, value in data.items():
current_key = f"{prefix}.{key}" if prefix else key
keys.add(current_key)
if isinstance(value, dict):
keys.update(get_all_keys(value, current_key))
return keys
def get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
"""Get a nested value using a key with dots as separator."""
keys = key_path.split('.')
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return None
return current
def set_nested_value(data: Dict[str, Any], key_path: str, value: Any) -> None:
"""Set a nested value using a key with dots as separator."""
keys = key_path.split('.')
current = data
# Navigate to the second-to-last level, creating dictionaries as needed
for key in keys[:-1]:
if key not in current:
current[key] = {}
elif not isinstance(current[key], dict):
current[key] = {}
current = current[key]
# Set the value at the last level
current[keys[-1]] = value
def find_missing_keys(reference_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]:
"""Find keys that are in reference but not in target."""
reference_keys = get_all_keys(reference_data)
target_keys = get_all_keys(target_data)
missing_keys = reference_keys - target_keys
return sorted(list(missing_keys))
def add_missing_keys(reference_data: Dict[str, Any], target_data: Dict[str, Any],
missing_keys: List[str], mark_as_untranslated: bool = True) -> Dict[str, Any]:
"""Add missing keys to target_data using reference values."""
updated_data = target_data.copy()
for key_path in missing_keys:
reference_value = get_nested_value(reference_data, key_path)
if reference_value is not None:
# If marking as untranslated, add prefix
if mark_as_untranslated and isinstance(reference_value, str):
translated_value = f"[TO_TRANSLATE] {reference_value}"
else:
translated_value = reference_value
set_nested_value(updated_data, key_path, translated_value)
return updated_data
def sync_translations(messages_dir: Path, reference_file: str = 'en-US.json',
mark_as_untranslated: bool = True, dry_run: bool = False) -> None:
"""Synchronize all translations using a reference file."""
# Load reference file
reference_path = messages_dir / reference_file
if not reference_path.exists():
print(f"Reference file not found: {reference_path}")
return
print(f"Loading reference file: {reference_file}")
reference_data = load_json_file(reference_path)
if not reference_data:
print("Error loading reference file")
return
# Find all JSON files in the folder
json_files = [f for f in messages_dir.glob('*.json') if f.name != reference_file]
if not json_files:
print("No translation files found")
return
total_keys_reference = len(get_all_keys(reference_data))
print(f"Reference file contains {total_keys_reference} keys")
print(f"Processing {len(json_files)} translation files...\n")
summary = []
for json_file in sorted(json_files):
print(f"Processing: {json_file.name}")
# Load translation file
translation_data = load_json_file(json_file)
if not translation_data:
print(f" ❌ Error loading {json_file.name}")
continue
# Find missing keys
missing_keys = find_missing_keys(reference_data, translation_data)
current_keys = len(get_all_keys(translation_data))
if not missing_keys:
print(f" ✅ Complete ({current_keys}/{total_keys_reference} keys)")
summary.append({
'file': json_file.name,
'status': 'complete',
'missing': 0,
'total': current_keys
})
continue
print(f" 🔍 Found {len(missing_keys)} missing keys")
if dry_run:
print(f" 📝 [DRY RUN] Keys that would be added:")
for key in missing_keys[:5]: # Show only first 5
print(f" - {key}")
if len(missing_keys) > 5:
print(f" ... and {len(missing_keys) - 5} more")
else:
# Add missing keys
updated_data = add_missing_keys(reference_data, translation_data,
missing_keys, mark_as_untranslated)
# Save updated file
if save_json_file(json_file, updated_data):
print(f" ✅ Updated successfully ({current_keys + len(missing_keys)}/{total_keys_reference} keys)")
summary.append({
'file': json_file.name,
'status': 'updated',
'missing': len(missing_keys),
'total': current_keys + len(missing_keys)
})
else:
print(f" ❌ Error saving {json_file.name}")
summary.append({
'file': json_file.name,
'status': 'error',
'missing': len(missing_keys),
'total': current_keys
})
print()
# Show summary
print("=" * 60)
print("SUMMARY")
print("=" * 60)
if dry_run:
print("🔍 DRY RUN MODE - No changes were made\n")
for item in summary:
status_icon = {
'complete': '',
'updated': '🔄',
'error': ''
}.get(item['status'], '')
print(f"{status_icon} {item['file']:<15} - {item['total']}/{total_keys_reference} keys", end='')
if item['missing'] > 0:
print(f" (+{item['missing']} added)" if item['status'] == 'updated' else f" ({item['missing']} missing)")
else:
print()
def main():
parser = argparse.ArgumentParser(
description='Synchronize translations using en-US.json as reference'
)
parser.add_argument(
'--messages-dir',
type=Path,
default=Path(__file__).parent.parent / 'messages',
help='Directory containing message files (default: ../messages)'
)
parser.add_argument(
'--reference',
default='en-US.json',
help='Reference file (default: en-US.json)'
)
parser.add_argument(
'--no-mark-untranslated',
action='store_true',
help='Don\'t mark added keys as [TO_TRANSLATE]'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Only show what would be changed without making modifications'
)
args = parser.parse_args()
if not args.messages_dir.exists():
print(f"Directory not found: {args.messages_dir}")
return 1
print(f"Directory: {args.messages_dir}")
print(f"Reference: {args.reference}")
print(f"Mark untranslated: {not args.no_mark_untranslated}")
print(f"Dry run: {args.dry_run}")
print("-" * 60)
sync_translations(
messages_dir=args.messages_dir,
reference_file=args.reference,
mark_as_untranslated=not args.no_mark_untranslated,
dry_run=args.dry_run
)
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Script to automatically translate strings marked with [TO_TRANSLATE]
using free Google Translate.
"""
import json
import time
import re
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
import argparse
import sys
# Language code mapping from file names to Google Translate codes
LANGUAGE_MAPPING = {
'pt-BR.json': 'pt', # Portuguese (Brazil) -> Portuguese
'es-ES.json': 'es', # Spanish (Spain) -> Spanish
'fr-FR.json': 'fr', # French (France) -> French
'de-DE.json': 'de', # German -> German
'it-IT.json': 'it', # Italian -> Italian
'ru-RU.json': 'ru', # Russian -> Russian
'ja-JP.json': 'ja', # Japanese -> Japanese
'ko-KR.json': 'ko', # Korean -> Korean
'zh-CN.json': 'zh-cn', # Chinese (Simplified) -> Simplified Chinese
'ar-SA.json': 'ar', # Arabic -> Arabic
'hi-IN.json': 'hi', # Hindi -> Hindi
'nl-NL.json': 'nl', # Dutch -> Dutch
'tr-TR.json': 'tr', # Turkish -> Turkish
'pl-PL.json': 'pl', # Polish -> Polish
}
# Prefix to identify untranslated strings
TO_TRANSLATE_PREFIX = '[TO_TRANSLATE] '
def load_json_file(file_path: Path) -> Dict[str, Any]:
"""Load a JSON file."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"❌ Error loading {file_path}: {e}")
return {}
def save_json_file(file_path: Path, data: Dict[str, Any], indent: int = 2) -> bool:
"""Save a JSON file with consistent formatting."""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=indent, separators=(',', ': '))
f.write('\n') # Add newline at the end
return True
except Exception as e:
print(f"❌ Error saving {file_path}: {e}")
return False
def get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
"""Get a nested value using a key with dots as separator."""
keys = key_path.split('.')
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return None
return current
def set_nested_value(data: Dict[str, Any], key_path: str, value: Any) -> None:
"""Set a nested value using a key with dots as separator."""
keys = key_path.split('.')
current = data
# Navigate to the second-to-last level, creating dictionaries as needed
for key in keys[:-1]:
if key not in current:
current[key] = {}
elif not isinstance(current[key], dict):
current[key] = {}
current = current[key]
# Set the value at the last level
current[keys[-1]] = value
def find_untranslated_strings(data: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
"""Find all strings marked with [TO_TRANSLATE] recursively."""
untranslated = []
for key, value in data.items():
current_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, str) and value.startswith(TO_TRANSLATE_PREFIX):
# Remove prefix to get original text
original_text = value[len(TO_TRANSLATE_PREFIX):].strip()
untranslated.append((current_key, original_text))
elif isinstance(value, dict):
untranslated.extend(find_untranslated_strings(value, current_key))
return untranslated
def install_googletrans():
"""Install the googletrans library if not available."""
try:
import googletrans
return True
except ImportError:
print("📦 'googletrans' library not found. Attempting to install...")
import subprocess
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "googletrans==4.0.0rc1"])
print("✅ googletrans installed successfully!")
return True
except subprocess.CalledProcessError:
print("❌ Failed to install googletrans. Install manually with:")
print("pip install googletrans==4.0.0rc1")
return False
def translate_text(text: str, target_language: str, max_retries: int = 3) -> Optional[str]:
"""Translate text using free Google Translate."""
try:
from googletrans import Translator
translator = Translator()
for attempt in range(max_retries):
try:
# Translate from English to target language
result = translator.translate(text, src='en', dest=target_language)
if result and result.text:
return result.text.strip()
except Exception as e:
if attempt < max_retries - 1:
print(f" ⚠️ Attempt {attempt + 1} failed: {str(e)[:50]}... Retrying in 2s...")
time.sleep(2)
else:
print(f" ❌ Failed after {max_retries} attempts: {str(e)[:50]}...")
return None
except ImportError:
print("❌ googletrans library not available")
return None
def translate_file(file_path: Path, target_language: str, dry_run: bool = False,
delay_between_requests: float = 1.0) -> Tuple[int, int, int]:
"""
Translate all [TO_TRANSLATE] strings in a file.
Returns: (total_found, successful_translations, failed_translations)
"""
print(f"🔍 Processing: {file_path.name}")
# Load file
data = load_json_file(file_path)
if not data:
return 0, 0, 0
# Find untranslated strings
untranslated_strings = find_untranslated_strings(data)
if not untranslated_strings:
print(f" ✅ No strings to translate")
return 0, 0, 0
print(f" 📝 Found {len(untranslated_strings)} strings to translate")
if dry_run:
print(f" 🔍 [DRY RUN] Strings that would be translated:")
for key, text in untranslated_strings[:3]:
print(f" - {key}: \"{text[:50]}{'...' if len(text) > 50 else ''}\"")
if len(untranslated_strings) > 3:
print(f" ... and {len(untranslated_strings) - 3} more")
return len(untranslated_strings), 0, 0
# Translate each string
successful = 0
failed = 0
updated_data = data.copy()
for i, (key_path, original_text) in enumerate(untranslated_strings, 1):
print(f" 📍 ({i}/{len(untranslated_strings)}) Translating: {key_path}")
# Translate text
translated_text = translate_text(original_text, target_language)
if translated_text and translated_text != original_text:
# Update in dictionary
set_nested_value(updated_data, key_path, translated_text)
successful += 1
print(f"\"{original_text[:30]}...\"\"{translated_text[:30]}...\"")
else:
failed += 1
print(f" ❌ Translation failed")
# Delay between requests to avoid rate limiting
if i < len(untranslated_strings): # Don't wait after the last one
time.sleep(delay_between_requests)
# Save updated file
if successful > 0:
if save_json_file(file_path, updated_data):
print(f" 💾 File saved with {successful} translations")
else:
print(f" ❌ Error saving file")
failed += successful # Count as failure if couldn't save
successful = 0
return len(untranslated_strings), successful, failed
def translate_all_files(messages_dir: Path, delay_between_requests: float = 1.0,
dry_run: bool = False, skip_languages: List[str] = None) -> None:
"""Translate all language files that have [TO_TRANSLATE] strings."""
if not install_googletrans():
return
skip_languages = skip_languages or []
# Find language JSON files
language_files = []
for file_name, lang_code in LANGUAGE_MAPPING.items():
file_path = messages_dir / file_name
if file_path.exists() and file_name not in skip_languages:
language_files.append((file_path, lang_code))
if not language_files:
print("❌ No language files found")
return
print(f"🌍 Translating {len(language_files)} languages...")
print(f"⏱️ Delay between requests: {delay_between_requests}s")
if dry_run:
print("🔍 DRY RUN MODE - No changes will be made")
print("-" * 60)
total_found = 0
total_successful = 0
total_failed = 0
for i, (file_path, lang_code) in enumerate(language_files, 1):
print(f"\n[{i}/{len(language_files)}] 🌐 Language: {lang_code.upper()}")
found, successful, failed = translate_file(
file_path, lang_code, dry_run, delay_between_requests
)
total_found += found
total_successful += successful
total_failed += failed
# Pause between files (except the last one)
if i < len(language_files) and not dry_run:
print(f" ⏸️ Pausing {delay_between_requests * 2}s before next language...")
time.sleep(delay_between_requests * 2)
# Final summary
print("\n" + "=" * 60)
print("📊 FINAL SUMMARY")
print("=" * 60)
if dry_run:
print(f"🔍 DRY RUN MODE:")
print(f"{total_found} strings would be translated")
else:
print(f"✅ Translations performed:")
print(f"{total_successful} successes")
print(f"{total_failed} failures")
print(f"{total_found} total processed")
if total_successful > 0:
success_rate = (total_successful / total_found) * 100
print(f" • Success rate: {success_rate:.1f}%")
print("\n💡 TIPS:")
print("• Run 'python3 check_translations.py' to verify results")
print("• Strings that failed translation keep the [TO_TRANSLATE] prefix")
print("• Consider reviewing automatic translations to ensure quality")
def main():
parser = argparse.ArgumentParser(
description='Automatically translate strings marked with [TO_TRANSLATE]'
)
parser.add_argument(
'--messages-dir',
type=Path,
default=Path(__file__).parent.parent / 'messages',
help='Directory containing message files (default: ../messages)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Only show what would be translated without making changes'
)
parser.add_argument(
'--delay',
type=float,
default=1.0,
help='Delay in seconds between translation requests (default: 1.0)'
)
parser.add_argument(
'--skip-languages',
nargs='*',
default=[],
help='List of languages to skip (ex: pt-BR.json fr-FR.json)'
)
args = parser.parse_args()
if not args.messages_dir.exists():
print(f"❌ Directory not found: {args.messages_dir}")
return 1
print(f"📁 Directory: {args.messages_dir}")
print(f"🔍 Dry run: {args.dry_run}")
print(f"⏱️ Delay: {args.delay}s")
if args.skip_languages:
print(f"⏭️ Skipping: {', '.join(args.skip_languages)}")
print("-" * 60)
translate_all_files(
messages_dir=args.messages_dir,
delay_between_requests=args.delay,
dry_run=args.dry_run,
skip_languages=args.skip_languages
)
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -9,6 +9,8 @@ export function SharesTableContainer({ shares, onCopyLink, onCreateShare, shareM
onCopyLink={onCopyLink}
onDelete={shareManager.setShareToDelete}
onBulkDelete={shareManager.handleBulkDelete}
onBulkDownload={shareManager.handleBulkDownload}
onDownloadShareFiles={shareManager.handleDownloadShareFiles}
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}

View File

@@ -15,26 +15,6 @@ export interface SharesTableContainerProps {
shareManager: any;
}
export interface ShareManager {
shareToDelete: ListUserShares200SharesItem | null;
shareToEdit: ListUserShares200SharesItem | null;
shareToManageFiles: ListUserShares200SharesItem | null;
shareToManageRecipients: ListUserShares200SharesItem | null;
setShareToDelete: (share: ListUserShares200SharesItem | null) => void;
setShareToEdit: (share: ListUserShares200SharesItem | null) => void;
setShareToManageFiles: (share: ListUserShares200SharesItem | null) => void;
setShareToManageRecipients: (share: ListUserShares200SharesItem | null) => void;
setShareToViewDetails: (share: ListUserShares200SharesItem | null) => void;
setShareToGenerateLink: (share: ListUserShares200SharesItem | null) => void;
handleDelete: (shareId: string) => Promise<void>;
handleEdit: (shareId: string, data: any) => Promise<void>;
handleManageFiles: (shareId: string, files: any[]) => Promise<void>;
handleManageRecipients: (shareId: string, recipients: any[]) => Promise<void>;
handleGenerateLink: (shareId: string, alias: string) => Promise<void>;
handleNotifyRecipients: (share: ListUserShares200SharesItem) => Promise<void>;
}
export interface SharesModalsProps {
isCreateModalOpen: boolean;
onCloseCreateModal: () => void;

View File

@@ -51,6 +51,8 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
onCopyLink={onCopyLink}
onDelete={shareManager.setShareToDelete}
onBulkDelete={shareManager.handleBulkDelete}
onBulkDownload={shareManager.handleBulkDownload}
onDownloadShareFiles={shareManager.handleDownloadShareFiles}
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}

View File

@@ -60,11 +60,11 @@ export function BulkDownloadModal({ isOpen, onClose, onDownload, fileCount }: Bu
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={handleClose}>
<IconX className="h-4 w-4 mr-2" />
<IconX className="h-4 w-4" />
{t("common.cancel")}
</Button>
<Button onClick={handleSubmit} disabled={!zipName.trim()}>
<IconDownload className="h-4 w-4 mr-2" />
<IconDownload className="h-4 w-4" />
{t("bulkDownload.download")}
</Button>
</DialogFooter>

View File

@@ -4,6 +4,7 @@ import {
IconChevronDown,
IconCopy,
IconDotsVertical,
IconDownload,
IconEdit,
IconEye,
IconFolder,
@@ -45,7 +46,9 @@ export interface SharesTableProps {
onGenerateLink: (share: any) => void;
onCopyLink: (share: any) => void;
onNotifyRecipients: (share: any) => void;
onDownloadShareFiles?: (share: any) => void;
onBulkDelete?: (shares: any[]) => void;
onBulkDownload?: (shares: any[]) => void;
setClearSelectionCallback?: (callback: () => void) => void;
}
@@ -63,7 +66,9 @@ export function SharesTable({
onGenerateLink,
onCopyLink,
onNotifyRecipients,
onDownloadShareFiles,
onBulkDelete,
onBulkDownload,
setClearSelectionCallback,
}: SharesTableProps) {
const t = useTranslations();
@@ -180,7 +185,17 @@ export function SharesTable({
}
};
const showBulkActions = selectedShares.size > 0 && onBulkDelete;
const handleBulkDownload = () => {
const selectedShareObjects = getSelectedShares();
if (selectedShareObjects.length === 0) return;
if (onBulkDownload) {
onBulkDownload(selectedShareObjects);
}
};
const showBulkActions = selectedShares.size > 0 && (onBulkDelete || onBulkDownload);
return (
<div className="space-y-4">
@@ -192,10 +207,31 @@ export function SharesTable({
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="destructive" size="sm" className="gap-2" onClick={handleBulkDelete}>
<IconTrash className="h-4 w-4" />
{t("sharesTable.bulkActions.delete")}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" size="sm" className="gap-2">
{t("sharesTable.bulkActions.actions")}
<IconChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
{onBulkDownload && (
<DropdownMenuItem className="cursor-pointer py-2" onClick={handleBulkDownload}>
<IconDownload className="h-4 w-4" />
{t("sharesTable.bulkActions.download")}
</DropdownMenuItem>
)}
{onBulkDelete && (
<DropdownMenuItem
onClick={handleBulkDelete}
className="cursor-pointer py-2 text-destructive focus:text-destructive"
>
<IconTrash className="h-4 w-4" />
{t("sharesTable.bulkActions.delete")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => setSelectedShares(new Set())}>
{t("common.cancel")}
</Button>
@@ -574,6 +610,17 @@ export function SharesTable({
{t("sharesTable.actions.notifyRecipients")}
</DropdownMenuItem>
)}
{onDownloadShareFiles && share.files && share.files.length > 0 && (
<DropdownMenuItem
className="cursor-pointer py-2"
onClick={() => {
onDownloadShareFiles(share);
}}
>
<IconDownload className="h-4 w-4" />
{t("sharesTable.actions.downloadShareFiles")}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => onDelete(share)}
className="cursor-pointer py-2 text-destructive focus:text-destructive"

View File

@@ -9,9 +9,9 @@ import {
addRecipients,
createShareAlias,
deleteShare,
getDownloadUrl,
notifyRecipients,
updateShare,
updateSharePassword,
} from "@/http/endpoints";
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
@@ -36,6 +36,9 @@ export interface ShareManagerHook {
setSharesToDelete: (shares: ListUserShares200SharesItem[] | null) => void;
handleDelete: (shareId: string) => Promise<void>;
handleBulkDelete: (shares: ListUserShares200SharesItem[]) => void;
handleBulkDownload: (shares: ListUserShares200SharesItem[]) => void;
handleDownloadShareFiles: (share: ListUserShares200SharesItem) => Promise<void>;
handleBulkDownloadWithZip: (shares: ListUserShares200SharesItem[], zipName: string) => Promise<void>;
handleDeleteBulk: () => Promise<void>;
handleEdit: (shareId: string, data: any) => Promise<void>;
handleUpdateName: (shareId: string, newName: string) => Promise<void>;
@@ -198,6 +201,114 @@ export function useShareManager(onSuccess: () => void) {
}
};
const handleBulkDownload = (shares: ListUserShares200SharesItem[]) => {
const zipName =
shares.length === 1
? t("shareManager.singleShareZipName", { shareName: shares[0].name || t("shareManager.defaultShareName") })
: t("shareManager.multipleSharesZipName", { count: shares.length });
handleBulkDownloadWithZip(shares, zipName);
};
const handleDownloadShareFiles = async (share: ListUserShares200SharesItem) => {
if (!share.files || share.files.length === 0) {
toast.error(t("shareManager.noFilesToDownload"));
return;
}
if (share.files.length === 1) {
const file = share.files[0];
try {
const encodedObjectName = encodeURIComponent(file.objectName);
const response = await getDownloadUrl(encodedObjectName);
const downloadUrl = response.data.url;
const link = document.createElement("a");
link.href = downloadUrl;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success(t("shareManager.downloadSuccess"));
} catch (error) {
console.error("Download error:", error);
toast.error(t("shareManager.downloadError"));
}
} else {
const zipName = t("shareManager.singleShareZipName", {
shareName: share.name || t("shareManager.defaultShareName"),
});
await handleBulkDownloadWithZip([share], zipName);
}
};
const handleBulkDownloadWithZip = async (shares: ListUserShares200SharesItem[], zipName: string) => {
try {
toast.promise(
(async () => {
const JSZip = (await import("jszip")).default;
const zip = new JSZip();
const allFiles: any[] = [];
shares.forEach((share) => {
if (share.files) {
share.files.forEach((file) => {
allFiles.push({
...file,
shareName: share.name || "Unnamed Share",
});
});
}
});
const downloadPromises = allFiles.map(async (file) => {
try {
const encodedObjectName = encodeURIComponent(file.objectName);
const downloadResponse = await getDownloadUrl(encodedObjectName);
const downloadUrl = downloadResponse.data.url;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`Failed to download ${file.name}`);
}
const blob = await response.blob();
const fileName = shares.length > 1 ? `${file.shareName}/${file.name}` : file.name;
zip.file(fileName, blob);
} catch (error) {
console.error(`Error downloading file ${file.name}:`, error);
throw error;
}
});
await Promise.all(downloadPromises);
const zipBlob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(zipBlob);
const a = document.createElement("a");
a.href = url;
a.download = zipName.endsWith(".zip") ? zipName : `${zipName}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
if (clearSelectionCallback) {
clearSelectionCallback();
}
})(),
{
loading: t("shareManager.creatingZip"),
success: t("shareManager.zipDownloadSuccess"),
error: t("shareManager.zipDownloadError"),
}
);
} catch (error) {
console.error("Error creating ZIP:", error);
}
};
return {
shareToDelete,
shareToEdit,
@@ -229,6 +340,9 @@ export function useShareManager(onSuccess: () => void) {
handleManageRecipients,
handleGenerateLink,
handleNotifyRecipients,
handleBulkDownload,
handleDownloadShareFiles,
handleBulkDownloadWithZip,
setClearSelectionCallback,
};
}