mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
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:
@@ -22,6 +22,7 @@
|
||||
"architecture",
|
||||
"github-architecture",
|
||||
"api",
|
||||
"translation-management",
|
||||
"contribute",
|
||||
"open-an-issue",
|
||||
"---Sponsor this project---",
|
||||
|
525
apps/docs/content/docs/3.0-beta/translation-management.mdx
Normal file
525
apps/docs/content/docs/3.0-beta/translation-management.mdx
Normal 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.
|
@@ -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": "حفظ التغييرات",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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}}"
|
||||
},
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": "परिवर्तन सहेजें",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": "変更を保存",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": "변경사항 저장",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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": "Сохранить изменения",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
342
apps/web/messages/translate_missing.py
Normal file
342
apps/web/messages/translate_missing.py
Normal 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())
|
@@ -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": "保存更改",
|
||||
@@ -1355,4 +1373,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
231
apps/web/scripts/check_translations.py
Executable file
231
apps/web/scripts/check_translations.py
Executable 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())
|
144
apps/web/scripts/run_translations.py
Executable file
144
apps/web/scripts/run_translations.py
Executable 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())
|
267
apps/web/scripts/sync_translations.py
Executable file
267
apps/web/scripts/sync_translations.py
Executable 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())
|
342
apps/web/scripts/translate_missing.py
Executable file
342
apps/web/scripts/translate_missing.py
Executable 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())
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user