mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
Feat: Add bulk actions for file management in received files modal (#102)
This commit is contained in:
@@ -6,7 +6,7 @@ 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.
|
||||
Palmr includes a comprehensive translation key management system that automates synchronization and validation of the application's internationalization files.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -14,7 +14,7 @@ The translation management system consists of Python scripts that help maintain
|
||||
|
||||
- **Synchronization**: Automatically add missing translation keys
|
||||
- **Validation**: Check translation status and completeness
|
||||
- **Auto-translation**: Use Google Translate API for initial translations
|
||||
- **Key Management**: Manage translation keys structure and consistency
|
||||
- **Reporting**: Generate detailed translation reports
|
||||
|
||||
## Quick Start
|
||||
@@ -31,10 +31,7 @@ The translation management system consists of Python scripts that help maintain
|
||||
# Synchronize missing keys
|
||||
pnpm run translations:sync
|
||||
|
||||
# Auto-translate [TO_TRANSLATE] strings
|
||||
pnpm run translations:translate
|
||||
|
||||
# Dry run mode (test without changes)
|
||||
# Test workflow without changes (dry run)
|
||||
pnpm run translations:dry-run
|
||||
|
||||
# Show help
|
||||
@@ -52,7 +49,7 @@ The translation management system consists of Python scripts that help maintain
|
||||
# Individual commands
|
||||
python3 run_translations.py check
|
||||
python3 run_translations.py sync
|
||||
python3 run_translations.py translate
|
||||
python3 run_translations.py all --dry-run
|
||||
python3 run_translations.py help
|
||||
```
|
||||
|
||||
@@ -63,14 +60,13 @@ The translation management system consists of Python scripts that help maintain
|
||||
|
||||
### 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 |
|
||||
| Command | Description |
|
||||
| ------------------------------- | ----------------------------------------- |
|
||||
| `pnpm run translations` | Complete workflow: sync + check |
|
||||
| `pnpm run translations:check` | Check translation status and completeness |
|
||||
| `pnpm run translations:sync` | Synchronize missing keys from en-US.json |
|
||||
| `pnpm run translations:dry-run` | Test workflow without making changes |
|
||||
| `pnpm run translations:help` | Show detailed help and examples |
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -80,13 +76,13 @@ 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
|
||||
3. **Manual translation**: Translate strings marked with `[TO_TRANSLATE]` manually
|
||||
4. **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.
|
||||
**Manual translation required**: All strings marked with `[TO_TRANSLATE]` must
|
||||
be manually translated by native speakers or professional translators for
|
||||
accuracy.
|
||||
</Callout>
|
||||
|
||||
### 2. Checking Translation Status
|
||||
@@ -110,7 +106,7 @@ The report shows:
|
||||
|
||||
### 3. Manual Translation Process
|
||||
|
||||
For critical strings or when automatic translation isn't sufficient:
|
||||
For all translation strings:
|
||||
|
||||
1. **Find untranslated strings**: Look for `[TO_TRANSLATE] Original text` in language files
|
||||
2. **Replace with translation**: Remove the prefix and add proper translation
|
||||
@@ -130,13 +126,13 @@ apps/web/
|
||||
├── run_translations.py # Main wrapper
|
||||
├── sync_translations.py # Synchronization
|
||||
├── check_translations.py # Status checking
|
||||
└── translate_missing.py # Auto-translation
|
||||
└── clean_translations.py # Cleanup utilities
|
||||
```
|
||||
|
||||
## 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)
|
||||
- **No external dependencies** - Scripts use only Python standard libraries
|
||||
- **UTF-8 support** - Ensure your terminal supports UTF-8 for proper display of translations
|
||||
|
||||
## Script Details
|
||||
@@ -149,8 +145,7 @@ The main script provides a unified interface for all translation operations:
|
||||
|
||||
- `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)
|
||||
- `all` - Run complete workflow (sync + check)
|
||||
- `help` - Show detailed help with examples
|
||||
|
||||
#### How it Works
|
||||
@@ -197,27 +192,6 @@ Provides comprehensive translation analysis:
|
||||
- **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
|
||||
@@ -240,22 +214,6 @@ python3 scripts/run_translations.py sync --messages-dir /path/to/messages
|
||||
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
|
||||
@@ -268,58 +226,46 @@ 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 |
|
||||
| Parameter | Commands | Description |
|
||||
| ------------------------ | --------------- | --------------------------------------------- |
|
||||
| `--dry-run` | `sync`, `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 |
|
||||
|
||||
### Dry Run Mode
|
||||
|
||||
Always test changes first:
|
||||
|
||||
```bash
|
||||
# Test complete workflow
|
||||
# Test complete workflow (recommended)
|
||||
pnpm run translations:dry-run
|
||||
|
||||
# Test individual commands
|
||||
# Alternative: Direct Python commands
|
||||
python3 scripts/run_translations.py all --dry-run
|
||||
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
|
||||
#### Scenario 1: Adding Keys Without Marking for Translation
|
||||
|
||||
```bash
|
||||
# 1. Sync new keys without auto-marking for translation
|
||||
# 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
|
||||
# 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
|
||||
#### Scenario 2: Custom Project Structure
|
||||
|
||||
```bash
|
||||
# Work with translations in different directory
|
||||
python3 scripts/run_translations.py all --messages-dir ../custom-translations
|
||||
```
|
||||
|
||||
#### Scenario 4: Quality Assurance
|
||||
#### Scenario 3: Quality Assurance
|
||||
|
||||
```bash
|
||||
# Use different language as reference for comparison
|
||||
@@ -352,15 +298,15 @@ Translation files use nested JSON structure:
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic Translation
|
||||
## Manual 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.
|
||||
**Professional translation recommended**: For production applications,
|
||||
consider using professional translation services or native speakers to ensure
|
||||
high-quality, culturally appropriate translations.
|
||||
</Callout>
|
||||
|
||||
The system uses Google Translate (free API) to automatically translate strings marked with `[TO_TRANSLATE]`:
|
||||
The system marks strings that need translation with the `[TO_TRANSLATE]` prefix:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -368,7 +314,7 @@ The system uses Google Translate (free API) to automatically translate strings m
|
||||
}
|
||||
```
|
||||
|
||||
After auto-translation:
|
||||
After manual translation:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -378,14 +324,15 @@ After auto-translation:
|
||||
|
||||
### Translation Review Process
|
||||
|
||||
1. **Generate**: Use `pnpm run translations:translate` to auto-translate
|
||||
2. **Review**: Check each translation for:
|
||||
1. **Identify**: Use `pnpm run translations:check` to find untranslated strings
|
||||
2. **Translate**: Replace `[TO_TRANSLATE]` strings with proper translations
|
||||
3. **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
|
||||
4. **Test**: Verify translations in the actual application interface
|
||||
5. **Document**: Note any translation decisions for future reference
|
||||
|
||||
### Common Review Points
|
||||
|
||||
@@ -405,15 +352,15 @@ After auto-translation:
|
||||
### 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
|
||||
2. **Sync keys**: Use scripts to generate key structure for other languages
|
||||
3. **Manual translation**: All strings must be manually translated for accuracy
|
||||
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
|
||||
- **Key management**: Scripts use English as source for key synchronization
|
||||
- **Documentation**: Most technical documentation is in English
|
||||
|
||||
## Best Practices
|
||||
@@ -422,8 +369,8 @@ After auto-translation:
|
||||
|
||||
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
|
||||
3. **Test key sync**: Run `pnpm run translations:dry-run` before committing
|
||||
4. **Coordinate with translators**: Ensure translation team is aware of new keys
|
||||
5. **Maintain consistency**: Use existing patterns for similar UI elements
|
||||
|
||||
### For Translators
|
||||
@@ -432,6 +379,7 @@ After auto-translation:
|
||||
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
|
||||
5. **Maintain glossary**: Keep consistent terminology across translations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -439,17 +387,9 @@ After auto-translation:
|
||||
|
||||
**Python not found**: Ensure Python 3.6+ is installed and in PATH
|
||||
|
||||
**googletrans errors**: The system auto-installs dependencies, but you can manually install:
|
||||
**Permission errors**: Ensure write permissions to message files
|
||||
|
||||
```bash
|
||||
pip install googletrans==4.0.0rc1
|
||||
```
|
||||
|
||||
**Rate limiting**: Increase delay between requests:
|
||||
|
||||
```bash
|
||||
python3 scripts/run_translations.py translate --delay 2.0
|
||||
```
|
||||
**Encoding issues**: Ensure your terminal supports UTF-8
|
||||
|
||||
### Getting Help
|
||||
|
||||
@@ -491,28 +431,6 @@ LANGUAGE COMPLETENESS STRINGS UNTRANSLATED POSSIBLE MATCHES
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### 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:
|
||||
@@ -520,6 +438,6 @@ 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
|
||||
4. **Maintain quality**: Ensure manual translations are accurate and appropriate
|
||||
|
||||
The translation management system ensures consistency and makes it easy to maintain high-quality localization across all supported languages.
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "خطأ في تحديث الملف",
|
||||
"previewNotAvailable": "المعاينة غير متوفرة",
|
||||
"copyError": "خطأ نسخ الملف إلى ملفاتك",
|
||||
"copySuccess": "تم نسخ الملف إلى ملفاتك بنجاح"
|
||||
"copySuccess": "تم نسخ الملف إلى ملفاتك بنجاح",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =0 {لا ملفات محددة} =1 {ملف واحد محدد} =2 {ملفان محددان} other {# ملفات محددة}}",
|
||||
"actions": "إجراءات",
|
||||
"download": "تحميل المحدد",
|
||||
"copyToMyFiles": "نسخ المحدد إلى ملفاتي",
|
||||
"delete": "حذف المحدد"
|
||||
},
|
||||
"bulkCopyProgress": "جارٍ نسخ {count, plural, =1 {ملف واحد} =2 {ملفين} other {# ملفات}} إلى ملفاتك...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {تم نسخ ملف واحد إلى ملفاتك بنجاح} =2 {تم نسخ ملفين إلى ملفاتك بنجاح} other {تم نسخ # ملفات إلى ملفاتك بنجاح}}",
|
||||
"bulkDeleteConfirmButton": "حذف {count, plural, =1 {الملف} =2 {الملفين} other {الملفات}}",
|
||||
"bulkDeleteConfirmMessage": "هل أنت متأكد أنك تريد حذف {count, plural, =1 {هذا الملف} =2 {هذين الملفين} other {هذه الملفات الـ #}}؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"bulkDeleteConfirmTitle": "حذف الملفات المحددة",
|
||||
"bulkDeleteProgress": "جارٍ حذف {count, plural, =1 {ملف واحد} =2 {ملفين} other {# ملفات}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {تم حذف ملف واحد بنجاح} =2 {تم حذف ملفين بنجاح} other {تم حذف # ملفات بنجاح}}",
|
||||
"selectAll": "تحديد الكل",
|
||||
"selectFile": "تحديد الملف {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Fehler beim Aktualisieren der Datei",
|
||||
"previewNotAvailable": "Vorschau nicht verfügbar",
|
||||
"copyError": "Fehler beim Kopieren der Datei in Ihre Dateien",
|
||||
"copySuccess": "Datei erfolgreich in Ihre Dateien kopiert"
|
||||
"copySuccess": "Datei erfolgreich in Ihre Dateien kopiert",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 Datei ausgewählt} other {# Dateien ausgewählt}}",
|
||||
"actions": "Aktionen",
|
||||
"download": "Ausgewählte herunterladen",
|
||||
"copyToMyFiles": "Ausgewählte in meine Dateien kopieren",
|
||||
"delete": "Ausgewählte löschen"
|
||||
},
|
||||
"bulkCopyProgress": "{count, plural, =1 {1 Datei wird} other {# Dateien werden}} in Ihre Dateien kopiert...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 Datei wurde erfolgreich in Ihre Dateien kopiert} other {# Dateien wurden erfolgreich in Ihre Dateien kopiert}}",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {Datei löschen} other {Dateien löschen}}",
|
||||
"bulkDeleteConfirmMessage": "Sind Sie sicher, dass Sie {count, plural, =1 {diese Datei} other {diese # Dateien}} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"bulkDeleteConfirmTitle": "Ausgewählte Dateien löschen",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1 Datei wird} other {# Dateien werden}} gelöscht...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 Datei wurde erfolgreich gelöscht} other {# Dateien wurden erfolgreich gelöscht}}",
|
||||
"selectAll": "Alle auswählen",
|
||||
"selectFile": "Datei {fileName} auswählen"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Error updating file",
|
||||
"previewNotAvailable": "Preview not available",
|
||||
"copySuccess": "File copied to your files successfully",
|
||||
"copyError": "Error copying file to your files"
|
||||
"copyError": "Error copying file to your files",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 file copied to your files successfully} other {# files copied to your files successfully}}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 file deleted successfully} other {# files deleted successfully}}",
|
||||
"bulkCopyProgress": "Copying {count, plural, =1 {1 file} other {# files}} to your files...",
|
||||
"bulkDeleteProgress": "Deleting {count, plural, =1 {1 file} other {# files}}...",
|
||||
"bulkDeleteConfirmTitle": "Delete Selected Files",
|
||||
"bulkDeleteConfirmMessage": "Are you sure you want to delete {count, plural, =1 {this file} other {these # files}}? This action cannot be undone.",
|
||||
"bulkDeleteConfirmButton": "Delete {count, plural, =1 {File} other {Files}}",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 file selected} other {# files selected}}",
|
||||
"actions": "Actions",
|
||||
"download": "Download Selected",
|
||||
"copyToMyFiles": "Copy Selected to My Files",
|
||||
"delete": "Delete Selected"
|
||||
},
|
||||
"selectAll": "Select all",
|
||||
"selectFile": "Select file {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Error al actualizar archivo",
|
||||
"previewNotAvailable": "Vista previa no disponible",
|
||||
"copyError": "Error de copiar el archivo a sus archivos",
|
||||
"copySuccess": "Archivo copiado en sus archivos correctamente"
|
||||
"copySuccess": "Archivo copiado en sus archivos correctamente",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 archivo seleccionado} other {# archivos seleccionados}}",
|
||||
"actions": "Acciones",
|
||||
"download": "Descargar Seleccionados",
|
||||
"copyToMyFiles": "Copiar Seleccionados a Mis Archivos",
|
||||
"delete": "Eliminar Seleccionados"
|
||||
},
|
||||
"bulkCopyProgress": "Copiando {count, plural, =1 {1 archivo} other {# archivos}} a tus archivos...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 archivo copiado a tus archivos correctamente} other {# archivos copiados a tus archivos correctamente}}",
|
||||
"bulkDeleteConfirmButton": "Eliminar {count, plural, =1 {Archivo} other {Archivos}}",
|
||||
"bulkDeleteConfirmMessage": "¿Estás seguro de que quieres eliminar {count, plural, =1 {este archivo} other {estos # archivos}}? Esta acción no se puede deshacer.",
|
||||
"bulkDeleteConfirmTitle": "Eliminar Archivos Seleccionados",
|
||||
"bulkDeleteProgress": "Eliminando {count, plural, =1 {1 archivo} other {# archivos}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 archivo eliminado correctamente} other {# archivos eliminados correctamente}}",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"selectFile": "Seleccionar archivo {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Erreur lors de la mise à jour du fichier",
|
||||
"previewNotAvailable": "Aperçu non disponible",
|
||||
"copyError": "Erreur de copie du fichier dans vos fichiers",
|
||||
"copySuccess": "Fichier copié dans vos fichiers avec succès"
|
||||
"copySuccess": "Fichier copié dans vos fichiers avec succès",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 fichier sélectionné} other {# fichiers sélectionnés}}",
|
||||
"actions": "Actes",
|
||||
"download": "Télécharger la sélection",
|
||||
"copyToMyFiles": "Copier la sélection dans mes fichiers",
|
||||
"delete": "Supprimer la sélection"
|
||||
},
|
||||
"bulkCopyProgress": "Copie de {count, plural, =1 {1 fichier} other {# fichiers}} dans vos fichiers...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 fichier copié dans vos fichiers avec succès} other {# fichiers copiés dans vos fichiers avec succès}}",
|
||||
"bulkDeleteConfirmButton": "Supprimer {count, plural, =1 {le fichier} other {les fichiers}}",
|
||||
"bulkDeleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer {count, plural, =1 {ce fichier} other {ces # fichiers}} ? Cette action est irréversible.",
|
||||
"bulkDeleteConfirmTitle": "Supprimer les fichiers sélectionnés",
|
||||
"bulkDeleteProgress": "Suppression de {count, plural, =1 {1 fichier} other {# fichiers}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 fichier supprimé avec succès} other {# fichiers supprimés avec succès}}",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"selectFile": "Sélectionner le fichier {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "फ़ाइल अपडेट करने में त्रुटि",
|
||||
"previewNotAvailable": "पूर्वावलोकन उपलब्ध नहीं है",
|
||||
"copyError": "अपनी फ़ाइलों में फ़ाइल की नकल करने में त्रुटि",
|
||||
"copySuccess": "आपकी फ़ाइलों को सफलतापूर्वक कॉपी की गई फ़ाइल"
|
||||
"copySuccess": "आपकी फ़ाइलों को सफलतापूर्वक कॉपी की गई फ़ाइल",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 फ़ाइल चयनित} other {# फ़ाइलें चयनित}}",
|
||||
"actions": "कार्रवाइयां",
|
||||
"download": "चयनित डाउनलोड करें",
|
||||
"copyToMyFiles": "चयनित को मेरी फ़ाइलों में कॉपी करें",
|
||||
"delete": "चयनित हटाएं"
|
||||
},
|
||||
"bulkCopyProgress": "{count, plural, =1 {1 फ़ाइल} other {# फ़ाइलें}} आपकी फ़ाइलों में कॉपी की जा रही हैं...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 फ़ाइल सफलतापूर्वक आपकी फ़ाइलों में कॉपी की गई} other {# फ़ाइलें सफलतापूर्वक आपकी फ़ाइलों में कॉपी की गईं}}",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {फ़ाइल} other {फ़ाइलें}} हटाएं",
|
||||
"bulkDeleteConfirmMessage": "क्या आप वाकई {count, plural, =1 {इस फ़ाइल} other {इन # फ़ाइलों}} को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
|
||||
"bulkDeleteConfirmTitle": "चयनित फ़ाइलें हटाएं",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1 फ़ाइल} other {# फ़ाइलें}} हटाई जा रही हैं...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 फ़ाइल सफलतापूर्वक हटाई गई} other {# फ़ाइलें सफलतापूर्वक हटाई गईं}}",
|
||||
"selectAll": "सभी चुनें",
|
||||
"selectFile": "फ़ाइल {fileName} चुनें"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Errore durante l'aggiornamento del file",
|
||||
"previewNotAvailable": "Anteprima non disponibile",
|
||||
"copyError": "Errore di copia del file sui tuoi file",
|
||||
"copySuccess": "File copiato sui tuoi file correttamente"
|
||||
"copySuccess": "File copiato sui tuoi file correttamente",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 file selezionato} other {# file selezionati}}",
|
||||
"actions": "Azioni",
|
||||
"download": "Scarica Selezionati",
|
||||
"copyToMyFiles": "Copia Selezionati sui Miei File",
|
||||
"delete": "Elimina Selezionati"
|
||||
},
|
||||
"bulkCopyProgress": "Copia di {count, plural, =1 {1 file} other {# file}} sui tuoi file in corso...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 file copiato sui tuoi file con successo} other {# file copiati sui tuoi file con successo}}",
|
||||
"bulkDeleteConfirmButton": "Elimina {count, plural, =1 {File} other {File}}",
|
||||
"bulkDeleteConfirmMessage": "Sei sicuro di voler eliminare {count, plural, =1 {questo file} other {questi # file}}? Questa azione non può essere annullata.",
|
||||
"bulkDeleteConfirmTitle": "Elimina File Selezionati",
|
||||
"bulkDeleteProgress": "Eliminazione di {count, plural, =1 {1 file} other {# file}} in corso...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 file eliminato con successo} other {# file eliminati con successo}}",
|
||||
"selectAll": "Seleziona tutto",
|
||||
"selectFile": "Seleziona file {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "ファイルの更新に失敗しました",
|
||||
"previewNotAvailable": "プレビューは利用できません",
|
||||
"copyError": "ファイルにファイルをコピーするエラー",
|
||||
"copySuccess": "ファイルに正常にコピーされたファイル"
|
||||
"copySuccess": "ファイルに正常にコピーされたファイル",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1ファイルを選択} other {#ファイルを選択}}",
|
||||
"actions": "アクション",
|
||||
"download": "選択したファイルをダウンロード",
|
||||
"copyToMyFiles": "選択したファイルを私のファイルにコピー",
|
||||
"delete": "選択したファイルを削除"
|
||||
},
|
||||
"bulkCopyProgress": "{count, plural, =1 {1ファイル} other {#ファイル}}を私のファイルにコピー中...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1ファイル} other {#ファイル}}を私のファイルに正常にコピーしました",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {ファイル} other {ファイル}}を削除",
|
||||
"bulkDeleteConfirmMessage": "{count, plural, =1 {このファイル} other {これらの#ファイル}}を削除してもよろしいですか?この操作は取り消せません。",
|
||||
"bulkDeleteConfirmTitle": "選択したファイルを削除",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1ファイル} other {#ファイル}}を削除中...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1ファイル} other {#ファイル}}を正常に削除しました",
|
||||
"selectAll": "すべて選択",
|
||||
"selectFile": "ファイル{fileName}を選択"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "파일 업데이트 오류",
|
||||
"previewNotAvailable": "미리보기 불가",
|
||||
"copyError": "파일에 파일을 복사합니다",
|
||||
"copySuccess": "파일을 파일에 성공적으로 복사했습니다"
|
||||
"copySuccess": "파일을 파일에 성공적으로 복사했습니다",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1개의 파일 선택됨} other {#개의 파일 선택됨}}",
|
||||
"actions": "작업",
|
||||
"download": "선택한 항목 다운로드",
|
||||
"copyToMyFiles": "선택한 항목을 내 파일로 복사",
|
||||
"delete": "선택한 항목 삭제"
|
||||
},
|
||||
"bulkCopyProgress": "{count, plural, =1 {1개의 파일} other {#개의 파일}}을(를) 내 파일로 복사하는 중...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1개의 파일이} other {#개의 파일이}} 내 파일로 성공적으로 복사됨",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {파일} other {파일}} 삭제",
|
||||
"bulkDeleteConfirmMessage": "{count, plural, =1 {이 파일을} other {이 #개의 파일을}} 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
|
||||
"bulkDeleteConfirmTitle": "선택한 파일 삭제",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1개의 파일} other {#개의 파일}} 삭제 중...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1개의 파일이} other {#개의 파일이}} 성공적으로 삭제됨",
|
||||
"selectAll": "모두 선택",
|
||||
"selectFile": "{fileName} 파일 선택"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Fout bij bijwerken bestand",
|
||||
"previewNotAvailable": "Voorvertoning niet beschikbaar",
|
||||
"copyError": "Fout bij het kopiëren van bestand naar uw bestanden",
|
||||
"copySuccess": "Bestand gekopieerd naar uw bestanden succesvol"
|
||||
"copySuccess": "Bestand gekopieerd naar uw bestanden succesvol",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 bestand geselecteerd} other {# bestanden geselecteerd}}",
|
||||
"actions": "Acties",
|
||||
"download": "Geselecteerde Downloaden",
|
||||
"copyToMyFiles": "Geselecteerde Kopiëren naar Mijn Bestanden",
|
||||
"delete": "Geselecteerde Verwijderen"
|
||||
},
|
||||
"bulkCopyProgress": "{count, plural, =1 {1 bestand} other {# bestanden}} kopiëren naar uw bestanden...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 bestand succesvol gekopieerd naar uw bestanden} other {# bestanden succesvol gekopieerd naar uw bestanden}}",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {Bestand} other {Bestanden}} Verwijderen",
|
||||
"bulkDeleteConfirmMessage": "Weet u zeker dat u {count, plural, =1 {dit bestand} other {deze # bestanden}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"bulkDeleteConfirmTitle": "Geselecteerde Bestanden Verwijderen",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1 bestand} other {# bestanden}} verwijderen...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 bestand succesvol verwijderd} other {# bestanden succesvol verwijderd}}",
|
||||
"selectAll": "Alles selecteren",
|
||||
"selectFile": "Selecteer bestand {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Błąd aktualizacji pliku",
|
||||
"previewNotAvailable": "Podgląd niedostępny",
|
||||
"copyError": "Plik kopiowania błędów do plików",
|
||||
"copySuccess": "Plik skopiowany do plików pomyślnie"
|
||||
"copySuccess": "Plik skopiowany do plików pomyślnie",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 plik wybrany} other {# plików wybranych}}",
|
||||
"actions": "Akcje",
|
||||
"download": "Pobierz wybrane",
|
||||
"copyToMyFiles": "Skopiuj wybrane do Moich plików",
|
||||
"delete": "Usuń wybrane"
|
||||
},
|
||||
"bulkCopyProgress": "Kopiowanie {count, plural, =1 {1 pliku} other {# plików}} do twoich plików...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 plik skopiowany pomyślnie do twoich plików} other {# plików skopiowanych pomyślnie do twoich plików}}",
|
||||
"bulkDeleteConfirmButton": "Usuń {count, plural, =1 {plik} other {pliki}}",
|
||||
"bulkDeleteConfirmMessage": "Czy na pewno chcesz usunąć {count, plural, =1 {ten plik} other {te # pliki}}? Tej akcji nie można cofnąć.",
|
||||
"bulkDeleteConfirmTitle": "Usuń wybrane pliki",
|
||||
"bulkDeleteProgress": "Usuwanie {count, plural, =1 {1 pliku} other {# plików}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 plik usunięty pomyślnie} other {# plików usuniętych pomyślnie}}",
|
||||
"selectAll": "Zaznacz wszystko",
|
||||
"selectFile": "Wybierz plik {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Erro ao atualizar arquivo",
|
||||
"previewNotAvailable": "Visualização não disponível",
|
||||
"copySuccess": "Arquivo copiado para seus arquivos com sucesso",
|
||||
"copyError": "Erro ao copiar arquivo para seus arquivos"
|
||||
"copyError": "Erro ao copiar arquivo para seus arquivos",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 arquivo selecionado} other {# arquivos selecionados}}",
|
||||
"actions": "Ações",
|
||||
"download": "Baixar Selecionados",
|
||||
"copyToMyFiles": "Copiar Selecionados para Meus Arquivos",
|
||||
"delete": "Excluir Selecionados"
|
||||
},
|
||||
"bulkCopyProgress": "Copiando {count, plural, =1 {1 arquivo} other {# arquivos}} para seus arquivos...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 arquivo copiado para seus arquivos com sucesso} other {# arquivos copiados para seus arquivos com sucesso}}",
|
||||
"bulkDeleteConfirmButton": "Excluir {count, plural, =1 {Arquivo} other {Arquivos}}",
|
||||
"bulkDeleteConfirmMessage": "Tem certeza que deseja excluir {count, plural, =1 {este arquivo} other {estes # arquivos}}? Esta ação não pode ser desfeita.",
|
||||
"bulkDeleteConfirmTitle": "Excluir Arquivos Selecionados",
|
||||
"bulkDeleteProgress": "Excluindo {count, plural, =1 {1 arquivo} other {# arquivos}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 arquivo excluído com sucesso} other {# arquivos excluídos com sucesso}}",
|
||||
"selectAll": "Selecionar todos",
|
||||
"selectFile": "Selecionar arquivo {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Ошибка при обновлении файла",
|
||||
"previewNotAvailable": "Предпросмотр недоступен",
|
||||
"copyError": "Ошибка копирования файла в ваши файлы",
|
||||
"copySuccess": "Файл успешно скопирован в ваши файлы"
|
||||
"copySuccess": "Файл успешно скопирован в ваши файлы",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 файл выбран} other {# файлов выбрано}}",
|
||||
"actions": "Действия",
|
||||
"download": "Скачать выбранные",
|
||||
"copyToMyFiles": "Копировать выбранные в мои файлы",
|
||||
"delete": "Удалить выбранные"
|
||||
},
|
||||
"bulkCopyProgress": "Копирование {count, plural, =1 {1 файла} other {# файлов}} в ваши файлы...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 файл успешно скопирован в ваши файлы} other {# файлов успешно скопировано в ваши файлы}}",
|
||||
"bulkDeleteConfirmButton": "Удалить {count, plural, =1 {файл} other {файлы}}",
|
||||
"bulkDeleteConfirmMessage": "Вы уверены, что хотите удалить {count, plural, =1 {этот файл} other {эти # файлов}}? Это действие нельзя отменить.",
|
||||
"bulkDeleteConfirmTitle": "Удалить выбранные файлы",
|
||||
"bulkDeleteProgress": "Удаление {count, plural, =1 {1 файла} other {# файлов}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 файл успешно удален} other {# файлов успешно удалено}}",
|
||||
"selectAll": "Выбрать все",
|
||||
"selectFile": "Выбрать файл {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "Dosya güncellenirken hata oluştu",
|
||||
"previewNotAvailable": "Önizleme mevcut değil",
|
||||
"copyError": "Dosyalarınıza dosyayı kopyalama hatası",
|
||||
"copySuccess": "Dosyalarınıza başarılı bir şekilde kopyalanan dosya"
|
||||
"copySuccess": "Dosyalarınıza başarılı bir şekilde kopyalanan dosya",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {1 dosya seçildi} other {# dosya seçildi}}",
|
||||
"actions": "İşlemler",
|
||||
"download": "Seçilenleri İndir",
|
||||
"copyToMyFiles": "Seçilenleri Dosyalarıma Kopyala",
|
||||
"delete": "Seçilenleri Sil"
|
||||
},
|
||||
"bulkCopyProgress": "Dosyalarınıza {count, plural, =1 {1 dosya} other {# dosya}} kopyalanıyor...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1 dosya dosyalarınıza başarıyla kopyalandı} other {# dosya dosyalarınıza başarıyla kopyalandı}}",
|
||||
"bulkDeleteConfirmButton": "{count, plural, =1 {Dosyayı} other {Dosyaları}} Sil",
|
||||
"bulkDeleteConfirmMessage": "{count, plural, =1 {Bu dosyayı} other {Bu # dosyayı}} silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"bulkDeleteConfirmTitle": "Seçili Dosyaları Sil",
|
||||
"bulkDeleteProgress": "{count, plural, =1 {1 dosya} other {# dosya}} siliniyor...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 dosya başarıyla silindi} other {# dosya başarıyla silindi}}",
|
||||
"selectAll": "Tümünü seç",
|
||||
"selectFile": "{fileName} dosyasını seç"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1189,7 +1189,23 @@
|
||||
"editError": "更新文件时出错",
|
||||
"previewNotAvailable": "预览不可用",
|
||||
"copyError": "错误将文件复制到您的文件",
|
||||
"copySuccess": "文件已成功复制到您的文件"
|
||||
"copySuccess": "文件已成功复制到您的文件",
|
||||
"bulkActions": {
|
||||
"selected": "{count, plural, =1 {已选择1个文件} other {已选择#个文件}}",
|
||||
"actions": "操作",
|
||||
"download": "下载所选",
|
||||
"copyToMyFiles": "复制所选到我的文件",
|
||||
"delete": "删除所选"
|
||||
},
|
||||
"bulkCopyProgress": "正在将{count, plural, =1 {1个文件} other {#个文件}}复制到您的文件...",
|
||||
"bulkCopySuccess": "{count, plural, =1 {1个文件已成功复制到您的文件} other {#个文件已成功复制到您的文件}}",
|
||||
"bulkDeleteConfirmButton": "删除{count, plural, =1 {文件} other {文件}}",
|
||||
"bulkDeleteConfirmMessage": "您确定要删除{count, plural, =1 {这个文件} other {这些#个文件}}吗?此操作无法撤消。",
|
||||
"bulkDeleteConfirmTitle": "删除所选文件",
|
||||
"bulkDeleteProgress": "正在删除{count, plural, =1 {1个文件} other {#个文件}}...",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1个文件已成功删除} other {#个文件已成功删除}}",
|
||||
"selectAll": "全选",
|
||||
"selectFile": "选择文件 {fileName}"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1397,4 +1413,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,9 +24,8 @@
|
||||
"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"
|
||||
"translations:dry-run": "python3 scripts/run_translations.py all --dry-run",
|
||||
"translations:help": "python3 scripts/run_translations.py help"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main script to run all Palmr translation operations.
|
||||
Main script to run Palmr translation management operations.
|
||||
Makes it easy to run scripts without remembering specific names.
|
||||
"""
|
||||
|
||||
@@ -17,25 +17,65 @@ def run_command(script_name: str, args: list) -> int:
|
||||
return subprocess.run(cmd).returncode
|
||||
|
||||
|
||||
def filter_args_for_script(script_name: str, args: list) -> list:
|
||||
"""Filter arguments based on what each script accepts."""
|
||||
|
||||
# Arguments that check_translations.py accepts
|
||||
check_args = ['--messages-dir', '--reference']
|
||||
|
||||
# Arguments that sync_translations.py accepts
|
||||
sync_args = ['--messages-dir', '--reference', '--no-mark-untranslated', '--dry-run']
|
||||
|
||||
if script_name == 'check_translations.py':
|
||||
filtered = []
|
||||
skip_next = False
|
||||
for i, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in check_args:
|
||||
filtered.append(arg)
|
||||
# Add the value for the argument if it exists
|
||||
if i + 1 < len(args) and not args[i + 1].startswith('--'):
|
||||
filtered.append(args[i + 1])
|
||||
skip_next = True
|
||||
return filtered
|
||||
|
||||
elif script_name == 'sync_translations.py':
|
||||
filtered = []
|
||||
skip_next = False
|
||||
for i, arg in enumerate(args):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in sync_args:
|
||||
filtered.append(arg)
|
||||
# Add the value for the argument if it exists
|
||||
if i + 1 < len(args) and not args[i + 1].startswith('--'):
|
||||
filtered.append(args[i + 1])
|
||||
skip_next = True
|
||||
return filtered
|
||||
|
||||
return args
|
||||
|
||||
|
||||
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',
|
||||
' python3 run_translations.py all --dry-run\n',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'command',
|
||||
choices=['check', 'sync', 'translate', 'all', 'help'],
|
||||
choices=['check', 'sync', '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'
|
||||
'all - Run complete workflow (sync + check)\n'
|
||||
'help - Show detailed help'
|
||||
)
|
||||
|
||||
@@ -57,13 +97,7 @@ def main():
|
||||
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("⚡ all - Complete workflow (sync + check)")
|
||||
print(" python3 run_translations.py all")
|
||||
print(" python3 run_translations.py all --dry-run")
|
||||
print()
|
||||
@@ -72,22 +106,21 @@ def main():
|
||||
print(" apps/web/messages/ - Translation files")
|
||||
print()
|
||||
print("💡 TIPS:")
|
||||
print("• Use --dry-run on any command to test")
|
||||
print("• Use --dry-run on sync or all commands 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")
|
||||
print("• Manually translate strings marked with [TO_TRANSLATE]")
|
||||
print("• Read documentation for complete translation guidelines")
|
||||
return 0
|
||||
|
||||
elif args.command == 'check':
|
||||
print("🔍 Checking translation status...")
|
||||
return run_command('check_translations.py', remaining_args)
|
||||
filtered_args = filter_args_for_script('check_translations.py', remaining_args)
|
||||
return run_command('check_translations.py', filtered_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)
|
||||
filtered_args = filter_args_for_script('sync_translations.py', remaining_args)
|
||||
return run_command('sync_translations.py', filtered_args)
|
||||
|
||||
elif args.command == 'all':
|
||||
print("⚡ Running complete translation workflow...")
|
||||
@@ -98,7 +131,8 @@ def main():
|
||||
|
||||
# 1. Initial check
|
||||
print("1️⃣ Checking initial status...")
|
||||
result = run_command('check_translations.py', remaining_args)
|
||||
check_args = filter_args_for_script('check_translations.py', remaining_args)
|
||||
result = run_command('check_translations.py', check_args)
|
||||
if result != 0:
|
||||
print("❌ Error in initial check")
|
||||
return result
|
||||
@@ -107,33 +141,27 @@ def main():
|
||||
|
||||
# 2. Sync
|
||||
print("2️⃣ Synchronizing missing keys...")
|
||||
result = run_command('sync_translations.py', remaining_args)
|
||||
sync_args = filter_args_for_script('sync_translations.py', remaining_args)
|
||||
result = run_command('sync_translations.py', sync_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" + "="*50)
|
||||
|
||||
# 3. Final check
|
||||
print("3️⃣ Final check...")
|
||||
check_args = filter_args_for_script('check_translations.py', remaining_args)
|
||||
result = run_command('check_translations.py', check_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")
|
||||
else:
|
||||
print("💡 Review strings marked with [TO_TRANSLATE] and translate them manually")
|
||||
|
||||
return 0
|
||||
|
||||
|
@@ -1,342 +0,0 @@
|
||||
#!/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())
|
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
IconClipboardCopy,
|
||||
IconDownload,
|
||||
IconEdit,
|
||||
@@ -19,7 +20,21 @@ import { toast } from "sonner";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -262,6 +277,7 @@ interface FileRowProps {
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
hoveredFile: HoverState | null;
|
||||
copyingFile: string | null;
|
||||
isSelected: boolean;
|
||||
onStartEdit: (fileId: string, field: string, currentValue: string) => void;
|
||||
onSaveEdit: () => void;
|
||||
onCancelEdit: () => void;
|
||||
@@ -272,6 +288,7 @@ interface FileRowProps {
|
||||
onDownload: (file: ReverseShareFile) => void;
|
||||
onDelete: (file: ReverseShareFile) => void;
|
||||
onCopy: (file: ReverseShareFile) => void;
|
||||
onSelectFile: (fileId: string, checked: boolean) => void;
|
||||
}
|
||||
|
||||
function FileRow({
|
||||
@@ -281,6 +298,7 @@ function FileRow({
|
||||
inputRef,
|
||||
hoveredFile,
|
||||
copyingFile,
|
||||
isSelected,
|
||||
onStartEdit,
|
||||
onSaveEdit,
|
||||
onCancelEdit,
|
||||
@@ -291,12 +309,20 @@ function FileRow({
|
||||
onDownload,
|
||||
onDelete,
|
||||
onCopy,
|
||||
onSelectFile,
|
||||
}: FileRowProps) {
|
||||
const t = useTranslations();
|
||||
const { icon: FileIcon, color } = getFileIcon(file.name);
|
||||
|
||||
return (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked: boolean) => onSelectFile(file.id, checked)}
|
||||
aria-label={t("reverseShares.modals.receivedFiles.selectFile", { fileName: file.name })}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileIcon className={`h-8 w-8 ${color} flex-shrink-0`} />
|
||||
@@ -425,9 +451,19 @@ export function ReceivedFilesModal({
|
||||
const [previewFile, setPreviewFile] = useState<ReverseShareFile | null>(null);
|
||||
const [hoveredFile, setHoveredFile] = useState<HoverState | null>(null);
|
||||
const [copyingFile, setCopyingFile] = useState<string | null>(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
||||
const [bulkCopying, setBulkCopying] = useState(false);
|
||||
const [bulkDeleting, setBulkDeleting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [filesToDeleteBulk, setFilesToDeleteBulk] = useState<ReverseShareFile[]>([]);
|
||||
|
||||
const { editingFile, editValue, setEditValue, inputRef, startEdit, cancelEdit } = useFileEdit();
|
||||
|
||||
// Clear selections when files change
|
||||
useEffect(() => {
|
||||
setSelectedFiles(new Set());
|
||||
}, [reverseShare?.files]);
|
||||
|
||||
const getTotalSize = () => {
|
||||
if (!reverseShare?.files) return "0 B";
|
||||
const totalBytes = reverseShare.files.reduce((acc, file) => acc + parseInt(file.size), 0);
|
||||
@@ -548,6 +584,176 @@ export function ReceivedFilesModal({
|
||||
|
||||
const files = reverseShare.files || [];
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedFiles(new Set(files.map((file) => file.id)));
|
||||
} else {
|
||||
setSelectedFiles(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||
const newSelected = new Set(selectedFiles);
|
||||
if (checked) {
|
||||
newSelected.add(fileId);
|
||||
} else {
|
||||
newSelected.delete(fileId);
|
||||
}
|
||||
setSelectedFiles(newSelected);
|
||||
};
|
||||
|
||||
const getSelectedFileObjects = () => {
|
||||
return files.filter((file) => selectedFiles.has(file.id));
|
||||
};
|
||||
|
||||
const isAllSelected = files.length > 0 && selectedFiles.size === files.length;
|
||||
|
||||
const handleBulkDownload = async () => {
|
||||
const selectedFileObjects = getSelectedFileObjects();
|
||||
if (selectedFileObjects.length === 0) return;
|
||||
|
||||
try {
|
||||
toast.promise(
|
||||
(async () => {
|
||||
const JSZip = (await import("jszip")).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
const downloadPromises = selectedFileObjects.map(async (file) => {
|
||||
try {
|
||||
const response = await downloadReverseShareFile(file.id);
|
||||
const downloadUrl = response.data.url;
|
||||
const fileResponse = await fetch(downloadUrl);
|
||||
|
||||
if (!fileResponse.ok) {
|
||||
throw new Error(`Failed to download ${file.name}`);
|
||||
}
|
||||
|
||||
const blob = await fileResponse.blob();
|
||||
zip.file(file.name, 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 zipName = `${reverseShare.name || "received_files"}_files.zip`;
|
||||
|
||||
const url = URL.createObjectURL(zipBlob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Clear selections after successful download
|
||||
setSelectedFiles(new Set());
|
||||
})(),
|
||||
{
|
||||
loading: t("shareManager.creatingZip"),
|
||||
success: t("shareManager.zipDownloadSuccess"),
|
||||
error: t("shareManager.zipDownloadError"),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error creating ZIP:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkCopyToMyFiles = async () => {
|
||||
const selectedFileObjects = getSelectedFileObjects();
|
||||
if (selectedFileObjects.length === 0) return;
|
||||
|
||||
toast.promise(
|
||||
(async () => {
|
||||
setBulkCopying(true);
|
||||
try {
|
||||
const copyPromises = selectedFileObjects.map(async (file) => {
|
||||
try {
|
||||
await copyReverseShareFileToUserFiles(file.id);
|
||||
} catch (error: any) {
|
||||
console.error(`Error copying file ${file.name}:`, error);
|
||||
throw new Error(`Failed to copy ${file.name}: ${error.response?.data?.error || error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(copyPromises);
|
||||
|
||||
// Clear selections after successful copy
|
||||
setSelectedFiles(new Set());
|
||||
} finally {
|
||||
setBulkCopying(false);
|
||||
}
|
||||
})(),
|
||||
{
|
||||
loading: t("reverseShares.modals.receivedFiles.bulkCopyProgress", { count: selectedFileObjects.length }),
|
||||
success: t("reverseShares.modals.receivedFiles.bulkCopySuccess", { count: selectedFileObjects.length }),
|
||||
error: (error: any) => {
|
||||
if (error.message.includes("File size exceeds") || error.message.includes("Insufficient storage")) {
|
||||
return error.message;
|
||||
} else {
|
||||
return t("reverseShares.modals.receivedFiles.copyError");
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
const selectedFileObjects = getSelectedFileObjects();
|
||||
if (selectedFileObjects.length === 0) return;
|
||||
|
||||
setFilesToDeleteBulk(selectedFileObjects);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const confirmBulkDelete = async () => {
|
||||
if (filesToDeleteBulk.length === 0) return;
|
||||
|
||||
setShowDeleteConfirm(false);
|
||||
|
||||
toast.promise(
|
||||
(async () => {
|
||||
setBulkDeleting(true);
|
||||
try {
|
||||
const deletePromises = filesToDeleteBulk.map(async (file) => {
|
||||
try {
|
||||
await deleteReverseShareFile(file.id);
|
||||
} catch (error) {
|
||||
console.error(`Error deleting file ${file.name}:`, error);
|
||||
throw new Error(`Failed to delete ${file.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// Clear selections and refresh data
|
||||
setSelectedFiles(new Set());
|
||||
setFilesToDeleteBulk([]);
|
||||
if (onRefresh) {
|
||||
await onRefresh();
|
||||
}
|
||||
if (refreshReverseShare) {
|
||||
await refreshReverseShare(reverseShare.id);
|
||||
}
|
||||
} finally {
|
||||
setBulkDeleting(false);
|
||||
}
|
||||
})(),
|
||||
{
|
||||
loading: t("reverseShares.modals.receivedFiles.bulkDeleteProgress", { count: filesToDeleteBulk.length }),
|
||||
success: t("reverseShares.modals.receivedFiles.bulkDeleteSuccess", { count: filesToDeleteBulk.length }),
|
||||
error: "Error deleting selected files",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const showBulkActions = selectedFiles.size > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -574,6 +780,59 @@ export function ReceivedFilesModal({
|
||||
|
||||
<Separator />
|
||||
|
||||
{showBulkActions && (
|
||||
<div className="flex items-center justify-between p-4 bg-muted/30 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("reverseShares.modals.receivedFiles.bulkActions.selected", { count: selectedFiles.size })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" size="sm" className="gap-2">
|
||||
{t("reverseShares.modals.receivedFiles.bulkActions.actions")}
|
||||
<IconChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={handleBulkDownload}>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
{t("reverseShares.modals.receivedFiles.bulkActions.download")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer py-2"
|
||||
onClick={handleBulkCopyToMyFiles}
|
||||
disabled={bulkCopying}
|
||||
>
|
||||
{bulkCopying ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-600 border-t-transparent"></div>
|
||||
) : (
|
||||
<IconClipboardCopy className="h-4 w-4" />
|
||||
)}
|
||||
{t("reverseShares.modals.receivedFiles.bulkActions.copyToMyFiles")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer py-2 text-destructive focus:text-destructive"
|
||||
onClick={handleBulkDelete}
|
||||
disabled={bulkDeleting}
|
||||
>
|
||||
{bulkDeleting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-red-600 border-t-transparent"></div>
|
||||
) : (
|
||||
<IconTrash className="h-4 w-4" />
|
||||
)}
|
||||
{t("reverseShares.modals.receivedFiles.bulkActions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="outline" size="sm" onClick={() => setSelectedFiles(new Set())}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{files.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center flex-1 gap-4 py-12">
|
||||
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center">
|
||||
@@ -591,6 +850,13 @@ export function ReceivedFilesModal({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px]">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label={t("reverseShares.modals.receivedFiles.selectAll")}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>{t("reverseShares.modals.receivedFiles.columns.file")}</TableHead>
|
||||
<TableHead>{t("reverseShares.modals.receivedFiles.columns.size")}</TableHead>
|
||||
<TableHead>{t("reverseShares.modals.receivedFiles.columns.sender")}</TableHead>
|
||||
@@ -610,6 +876,7 @@ export function ReceivedFilesModal({
|
||||
inputRef={inputRef}
|
||||
hoveredFile={hoveredFile}
|
||||
copyingFile={copyingFile}
|
||||
isSelected={selectedFiles.has(file.id)}
|
||||
onStartEdit={startEdit}
|
||||
onSaveEdit={saveEdit}
|
||||
onCancelEdit={cancelEdit}
|
||||
@@ -620,6 +887,7 @@ export function ReceivedFilesModal({
|
||||
onDownload={handleDownload}
|
||||
onDelete={handleDeleteFile}
|
||||
onCopy={handleCopyFile}
|
||||
onSelectFile={handleSelectFile}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -630,6 +898,46 @@ export function ReceivedFilesModal({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("reverseShares.modals.receivedFiles.bulkDeleteConfirmTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("reverseShares.modals.receivedFiles.bulkDeleteConfirmMessage", { count: filesToDeleteBulk.length })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="max-h-48 overflow-y-auto border rounded-lg p-2">
|
||||
<div className="space-y-1">
|
||||
{filesToDeleteBulk.map((file) => {
|
||||
const { icon: FileIcon, color } = getFileIcon(file.name);
|
||||
return (
|
||||
<div key={file.id} className="flex items-center gap-2 p-2 bg-muted/20 rounded text-sm">
|
||||
<FileIcon className={`h-4 w-4 ${color} flex-shrink-0`} />
|
||||
<span className="truncate" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={confirmBulkDelete} disabled={bulkDeleting}>
|
||||
{bulkDeleting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2" />
|
||||
) : null}
|
||||
{t("reverseShares.modals.receivedFiles.bulkDeleteConfirmButton", { count: filesToDeleteBulk.length })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{previewFile && (
|
||||
<ReverseShareFilePreviewModal
|
||||
isOpen={!!previewFile}
|
||||
|
Reference in New Issue
Block a user