mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-04 05:53:23 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			v3.0.0-bet
			...
			v3.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cd9598a6d3 | ||
| 
						 | 
					017a1debd3 | ||
| 
						 | 
					b917dbc05f | ||
| 
						 | 
					4fdee6b98b | ||
| 
						 | 
					a669a6c048 | ||
| 
						 | 
					264521f1c4 | ||
| 
						 | 
					b1cc9dbb21 | 
@@ -6,7 +6,7 @@ icon: Languages
 | 
			
		||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
 | 
			
		||||
import { Callout } from "fumadocs-ui/components/callout";
 | 
			
		||||
 | 
			
		||||
Palmr includes a comprehensive translation management system that automates synchronization, validation, and translation of the application's internationalization files.
 | 
			
		||||
Palmr includes a comprehensive translation key management system that automates synchronization and validation of the application's internationalization files.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +14,7 @@ The translation management system consists of Python scripts that help maintain
 | 
			
		||||
 | 
			
		||||
- **Synchronization**: Automatically add missing translation keys
 | 
			
		||||
- **Validation**: Check translation status and completeness
 | 
			
		||||
- **Auto-translation**: Use Google Translate API for initial translations
 | 
			
		||||
- **Key Management**: Manage translation keys structure and consistency
 | 
			
		||||
- **Reporting**: Generate detailed translation reports
 | 
			
		||||
 | 
			
		||||
## Quick Start
 | 
			
		||||
@@ -31,10 +31,7 @@ The translation management system consists of Python scripts that help maintain
 | 
			
		||||
    # Synchronize missing keys
 | 
			
		||||
    pnpm run translations:sync
 | 
			
		||||
 | 
			
		||||
    # Auto-translate [TO_TRANSLATE] strings
 | 
			
		||||
    pnpm run translations:translate
 | 
			
		||||
 | 
			
		||||
    # Dry run mode (test without changes)
 | 
			
		||||
    # Test workflow without changes (dry run)
 | 
			
		||||
    pnpm run translations:dry-run
 | 
			
		||||
 | 
			
		||||
    # Show help
 | 
			
		||||
@@ -52,7 +49,7 @@ The translation management system consists of Python scripts that help maintain
 | 
			
		||||
    # Individual commands
 | 
			
		||||
    python3 run_translations.py check
 | 
			
		||||
    python3 run_translations.py sync
 | 
			
		||||
    python3 run_translations.py translate
 | 
			
		||||
    python3 run_translations.py all --dry-run
 | 
			
		||||
    python3 run_translations.py help
 | 
			
		||||
    ```
 | 
			
		||||
 | 
			
		||||
@@ -64,11 +61,10 @@ The translation management system consists of Python scripts that help maintain
 | 
			
		||||
### Main Commands (npm/pnpm)
 | 
			
		||||
 | 
			
		||||
| Command                         | Description                               |
 | 
			
		||||
| --------------------------------- | ------------------------------------------- |
 | 
			
		||||
| `pnpm run translations`           | Complete workflow: sync + translate + check |
 | 
			
		||||
| ------------------------------- | ----------------------------------------- |
 | 
			
		||||
| `pnpm run translations`         | Complete workflow: sync + check           |
 | 
			
		||||
| `pnpm run translations:check`   | Check translation status and completeness |
 | 
			
		||||
| `pnpm run translations:sync`    | Synchronize missing keys from en-US.json  |
 | 
			
		||||
| `pnpm run translations: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           |
 | 
			
		||||
 | 
			
		||||
@@ -80,13 +76,13 @@ When you add new text to the application:
 | 
			
		||||
 | 
			
		||||
1. **Add to English**: Update `apps/web/messages/en-US.json` with your new keys
 | 
			
		||||
2. **Sync translations**: Run `pnpm run translations:sync` to add missing keys to all languages
 | 
			
		||||
3. **Auto-translate**: Run `pnpm run translations:translate` for automatic translations
 | 
			
		||||
4. **Review translations**: **Mandatory step** - Check all auto-generated translations for accuracy
 | 
			
		||||
5. **Test in UI**: Verify translations work correctly in the application interface
 | 
			
		||||
3. **Manual translation**: Translate strings marked with `[TO_TRANSLATE]` manually
 | 
			
		||||
4. **Test in UI**: Verify translations work correctly in the application interface
 | 
			
		||||
 | 
			
		||||
<Callout type="important">
 | 
			
		||||
  **Never skip step 4**: Auto-generated translations must be reviewed before
 | 
			
		||||
  committing to production. They are a starting point, not a final solution.
 | 
			
		||||
  **Manual translation required**: All strings marked with `[TO_TRANSLATE]` must
 | 
			
		||||
  be manually translated by native speakers or professional translators for
 | 
			
		||||
  accuracy.
 | 
			
		||||
</Callout>
 | 
			
		||||
 | 
			
		||||
### 2. Checking Translation Status
 | 
			
		||||
@@ -110,7 +106,7 @@ The report shows:
 | 
			
		||||
 | 
			
		||||
### 3. Manual Translation Process
 | 
			
		||||
 | 
			
		||||
For critical strings or when automatic translation isn't sufficient:
 | 
			
		||||
For all translation strings:
 | 
			
		||||
 | 
			
		||||
1. **Find untranslated strings**: Look for `[TO_TRANSLATE] Original text` in language files
 | 
			
		||||
2. **Replace with translation**: Remove the prefix and add proper translation
 | 
			
		||||
@@ -130,13 +126,13 @@ apps/web/
 | 
			
		||||
    ├── run_translations.py     # Main wrapper
 | 
			
		||||
    ├── sync_translations.py    # Synchronization
 | 
			
		||||
    ├── check_translations.py   # Status checking
 | 
			
		||||
    └── translate_missing.py    # Auto-translation
 | 
			
		||||
    └── clean_translations.py   # Cleanup utilities
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Prerequisites
 | 
			
		||||
 | 
			
		||||
- **Python 3.6 or higher** - Required for running the translation scripts
 | 
			
		||||
- **No external dependencies** - Scripts use only Python standard libraries (googletrans auto-installs when needed)
 | 
			
		||||
- **No external dependencies** - Scripts use only Python standard libraries
 | 
			
		||||
- **UTF-8 support** - Ensure your terminal supports UTF-8 for proper display of translations
 | 
			
		||||
 | 
			
		||||
## Script Details
 | 
			
		||||
@@ -149,8 +145,7 @@ The main script provides a unified interface for all translation operations:
 | 
			
		||||
 | 
			
		||||
- `check` - Check translation status and generate reports
 | 
			
		||||
- `sync` - Synchronize missing keys from reference language
 | 
			
		||||
- `translate` - Automatically translate marked strings
 | 
			
		||||
- `all` - Run complete workflow (sync + translate + check)
 | 
			
		||||
- `all` - Run complete workflow (sync + check)
 | 
			
		||||
- `help` - Show detailed help with examples
 | 
			
		||||
 | 
			
		||||
#### How it Works
 | 
			
		||||
@@ -197,27 +192,6 @@ Provides comprehensive translation analysis:
 | 
			
		||||
- **Quality insights**: Identifies potential translation issues
 | 
			
		||||
- **Export friendly**: Output can be redirected to files for reports
 | 
			
		||||
 | 
			
		||||
### Auto-Translation Script (`translate_missing.py`)
 | 
			
		||||
 | 
			
		||||
Automated translation using Google Translate API:
 | 
			
		||||
 | 
			
		||||
#### Process
 | 
			
		||||
 | 
			
		||||
1. **Dependency check**: Auto-installs `googletrans` if needed
 | 
			
		||||
2. **Scan for work**: Finds all `[TO_TRANSLATE]` prefixed strings
 | 
			
		||||
3. **Language mapping**: Maps file names to Google Translate language codes
 | 
			
		||||
4. **Batch translation**: Processes strings with rate limiting
 | 
			
		||||
5. **Update files**: Replaces marked strings with translations
 | 
			
		||||
6. **Error handling**: Retries failed translations, reports results
 | 
			
		||||
 | 
			
		||||
#### Safety Features
 | 
			
		||||
 | 
			
		||||
- **Rate limiting**: Configurable delay between requests
 | 
			
		||||
- **Retry logic**: Multiple attempts for failed translations
 | 
			
		||||
- **Dry run mode**: Preview changes without modifications
 | 
			
		||||
- **Language skipping**: Exclude specific languages from processing
 | 
			
		||||
- **Progress tracking**: Real-time status of translation progress
 | 
			
		||||
 | 
			
		||||
## Advanced Usage
 | 
			
		||||
 | 
			
		||||
### Custom Parameters
 | 
			
		||||
@@ -240,22 +214,6 @@ python3 scripts/run_translations.py sync --messages-dir /path/to/messages
 | 
			
		||||
python3 scripts/run_translations.py sync --dry-run
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Translation Parameters (`translate`)
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Custom delay between translation requests (avoid rate limiting)
 | 
			
		||||
python3 scripts/run_translations.py translate --delay 2.0
 | 
			
		||||
 | 
			
		||||
# Skip specific languages from translation
 | 
			
		||||
python3 scripts/run_translations.py translate --skip-languages pt-BR.json fr-FR.json
 | 
			
		||||
 | 
			
		||||
# Dry run - see what would be translated
 | 
			
		||||
python3 scripts/run_translations.py translate --dry-run
 | 
			
		||||
 | 
			
		||||
# Specify custom messages directory
 | 
			
		||||
python3 scripts/run_translations.py translate --messages-dir /path/to/messages
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Check Parameters (`check`)
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
@@ -269,57 +227,45 @@ 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       |
 | 
			
		||||
| ------------------------ | --------------- | --------------------------------------------- |
 | 
			
		||||
| `--dry-run`              | `sync`, `all`   | Preview changes without modifying files       |
 | 
			
		||||
| `--messages-dir`         | All             | Custom directory containing translation files |
 | 
			
		||||
| `--reference`            | `sync`, `check` | Reference file to use (default: en-US.json)   |
 | 
			
		||||
| `--no-mark-untranslated` | `sync`          | Don't add [TO_TRANSLATE] prefix to new keys   |
 | 
			
		||||
| `--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
 | 
			
		||||
# Test complete workflow (recommended)
 | 
			
		||||
pnpm run translations:dry-run
 | 
			
		||||
 | 
			
		||||
# Test individual commands
 | 
			
		||||
# Alternative: Direct Python commands
 | 
			
		||||
python3 scripts/run_translations.py all --dry-run
 | 
			
		||||
python3 scripts/run_translations.py sync --dry-run
 | 
			
		||||
python3 scripts/run_translations.py translate --dry-run
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Common Use Cases
 | 
			
		||||
 | 
			
		||||
#### Scenario 1: Careful Translation with Review
 | 
			
		||||
#### Scenario 1: Adding Keys Without Marking for Translation
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# 1. Sync new keys without auto-marking for translation
 | 
			
		||||
# Sync new keys without auto-marking for translation
 | 
			
		||||
python3 scripts/run_translations.py sync --no-mark-untranslated
 | 
			
		||||
 | 
			
		||||
# 2. Manually mark specific keys that need translation
 | 
			
		||||
# Manually mark specific keys that need translation
 | 
			
		||||
# Edit files to add [TO_TRANSLATE] prefix where needed
 | 
			
		||||
 | 
			
		||||
# 3. Translate only marked strings with slower rate
 | 
			
		||||
python3 scripts/run_translations.py translate --delay 2.0
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Scenario 2: Skip Already Reviewed Languages
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Skip languages that were already manually reviewed
 | 
			
		||||
python3 scripts/run_translations.py translate --skip-languages pt-BR.json es-ES.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Scenario 3: Custom Project Structure
 | 
			
		||||
#### Scenario 2: Custom Project Structure
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Work with translations in different directory
 | 
			
		||||
python3 scripts/run_translations.py all --messages-dir ../custom-translations
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Scenario 4: Quality Assurance
 | 
			
		||||
#### Scenario 3: Quality Assurance
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Use different language as reference for comparison
 | 
			
		||||
@@ -352,15 +298,15 @@ Translation files use nested JSON structure:
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Automatic Translation
 | 
			
		||||
## Manual Translation
 | 
			
		||||
 | 
			
		||||
<Callout type="warning">
 | 
			
		||||
  **Review Required**: Automatic translations are provided for convenience but
 | 
			
		||||
  **must be reviewed** before production use. They serve as a starting point,
 | 
			
		||||
  not a final solution.
 | 
			
		||||
  **Professional translation recommended**: For production applications,
 | 
			
		||||
  consider using professional translation services or native speakers to ensure
 | 
			
		||||
  high-quality, culturally appropriate translations.
 | 
			
		||||
</Callout>
 | 
			
		||||
 | 
			
		||||
The system uses Google Translate (free API) to automatically translate strings marked with `[TO_TRANSLATE]`:
 | 
			
		||||
The system marks strings that need translation with the `[TO_TRANSLATE]` prefix:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
@@ -368,7 +314,7 @@ The system uses Google Translate (free API) to automatically translate strings m
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
After auto-translation:
 | 
			
		||||
After manual translation:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
@@ -378,14 +324,15 @@ After auto-translation:
 | 
			
		||||
 | 
			
		||||
### Translation Review Process
 | 
			
		||||
 | 
			
		||||
1. **Generate**: Use `pnpm run translations:translate` to auto-translate
 | 
			
		||||
2. **Review**: Check each translation for:
 | 
			
		||||
1. **Identify**: Use `pnpm run translations:check` to find untranslated strings
 | 
			
		||||
2. **Translate**: Replace `[TO_TRANSLATE]` strings with proper translations
 | 
			
		||||
3. **Review**: Check each translation for:
 | 
			
		||||
   - **Context accuracy**: Does it make sense in the UI?
 | 
			
		||||
   - **Technical terms**: Are they correctly translated?
 | 
			
		||||
   - **Tone consistency**: Matches the application's voice?
 | 
			
		||||
   - **Cultural appropriateness**: Suitable for target audience?
 | 
			
		||||
3. **Test**: Verify translations in the actual application interface
 | 
			
		||||
4. **Document**: Note any translation decisions for future reference
 | 
			
		||||
4. **Test**: Verify translations in the actual application interface
 | 
			
		||||
5. **Document**: Note any translation decisions for future reference
 | 
			
		||||
 | 
			
		||||
### Common Review Points
 | 
			
		||||
 | 
			
		||||
@@ -405,15 +352,15 @@ After auto-translation:
 | 
			
		||||
### Translation Workflow for Development
 | 
			
		||||
 | 
			
		||||
1. **English First**: Add all new text to `apps/web/messages/en-US.json`
 | 
			
		||||
2. **Auto-generate**: Use scripts to generate translations for other languages
 | 
			
		||||
3. **Review Required**: All auto-generated translations must be reviewed before production use
 | 
			
		||||
2. **Sync keys**: Use scripts to generate key structure for other languages
 | 
			
		||||
3. **Manual translation**: All strings must be manually translated for accuracy
 | 
			
		||||
4. **Quality Check**: Run translation validation before merging PRs
 | 
			
		||||
 | 
			
		||||
### Why English as Parent Language?
 | 
			
		||||
 | 
			
		||||
- **Consistency**: Ensures all languages have the same keys structure
 | 
			
		||||
- **Reference**: English serves as the source of truth for meaning
 | 
			
		||||
- **Auto-translation**: Scripts use English as source for automatic translation
 | 
			
		||||
- **Key management**: Scripts use English as source for key synchronization
 | 
			
		||||
- **Documentation**: Most technical documentation is in English
 | 
			
		||||
 | 
			
		||||
## Best Practices
 | 
			
		||||
@@ -422,8 +369,8 @@ After auto-translation:
 | 
			
		||||
 | 
			
		||||
1. **Always use English as reference**: Add new keys to `en-US.json` first - never add keys directly to other languages
 | 
			
		||||
2. **Use semantic key names**: `dashboard.welcome` instead of `text1`
 | 
			
		||||
3. **Test translations**: Run `pnpm run translations:dry-run` before committing
 | 
			
		||||
4. **Review auto-translations**: All generated translations must be reviewed before production
 | 
			
		||||
3. **Test key sync**: Run `pnpm run translations:dry-run` before committing
 | 
			
		||||
4. **Coordinate with translators**: Ensure translation team is aware of new keys
 | 
			
		||||
5. **Maintain consistency**: Use existing patterns for similar UI elements
 | 
			
		||||
 | 
			
		||||
### For Translators
 | 
			
		||||
@@ -432,6 +379,7 @@ After auto-translation:
 | 
			
		||||
2. **Check identical strings**: May need localization even if identical to English
 | 
			
		||||
3. **Use proper formatting**: Maintain HTML tags and placeholders
 | 
			
		||||
4. **Test in context**: Verify translations work in the actual UI
 | 
			
		||||
5. **Maintain glossary**: Keep consistent terminology across translations
 | 
			
		||||
 | 
			
		||||
## Troubleshooting
 | 
			
		||||
 | 
			
		||||
@@ -439,17 +387,9 @@ After auto-translation:
 | 
			
		||||
 | 
			
		||||
**Python not found**: Ensure Python 3.6+ is installed and in PATH
 | 
			
		||||
 | 
			
		||||
**googletrans errors**: The system auto-installs dependencies, but you can manually install:
 | 
			
		||||
**Permission errors**: Ensure write permissions to message files
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
pip install googletrans==4.0.0rc1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Rate limiting**: Increase delay between requests:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
python3 scripts/run_translations.py translate --delay 2.0
 | 
			
		||||
```
 | 
			
		||||
**Encoding issues**: Ensure your terminal supports UTF-8
 | 
			
		||||
 | 
			
		||||
### Getting Help
 | 
			
		||||
 | 
			
		||||
@@ -491,28 +431,6 @@ LANGUAGE        COMPLETENESS STRINGS         UNTRANSLATED    POSSIBLE MATCHES
 | 
			
		||||
================================================================================
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Auto-Translation Progress
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
🌍 Translating 3 languages...
 | 
			
		||||
⏱️  Delay between requests: 1.0s
 | 
			
		||||
 | 
			
		||||
[1/3] 🌐 Language: PT
 | 
			
		||||
🔍 Processing: pt-BR.json
 | 
			
		||||
  📝 Found 12 strings to translate
 | 
			
		||||
  📍 (1/12) Translating: dashboard.welcome
 | 
			
		||||
    ✅ "Welcome to Palmr" → "Bem-vindo ao Palmr"
 | 
			
		||||
  💾 File saved with 12 translations
 | 
			
		||||
 | 
			
		||||
📊 FINAL SUMMARY
 | 
			
		||||
================================================================================
 | 
			
		||||
✅ Translations performed:
 | 
			
		||||
   • 34 successes
 | 
			
		||||
   • 2 failures
 | 
			
		||||
   • 36 total processed
 | 
			
		||||
   • Success rate: 94.4%
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Contributing
 | 
			
		||||
 | 
			
		||||
When contributing translations:
 | 
			
		||||
@@ -520,6 +438,6 @@ When contributing translations:
 | 
			
		||||
1. **Follow the workflow**: Use the provided scripts for consistency
 | 
			
		||||
2. **Test thoroughly**: Run complete checks before submitting
 | 
			
		||||
3. **Document changes**: Note any significant translation decisions
 | 
			
		||||
4. **Maintain quality**: Review auto-translations for accuracy
 | 
			
		||||
4. **Maintain quality**: Ensure manual translations are accurate and appropriate
 | 
			
		||||
 | 
			
		||||
The translation management system ensures consistency and makes it easy to maintain high-quality localization across all supported languages.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { EmailService } from "../email/service";
 | 
			
		||||
import { LogoService } from "./logo.service";
 | 
			
		||||
import { AppService } from "./service";
 | 
			
		||||
import { FastifyReply, FastifyRequest } from "fastify";
 | 
			
		||||
@@ -26,6 +27,7 @@ if (!fs.existsSync(uploadsDir)) {
 | 
			
		||||
export class AppController {
 | 
			
		||||
  private appService = new AppService();
 | 
			
		||||
  private logoService = new LogoService();
 | 
			
		||||
  private emailService = new EmailService();
 | 
			
		||||
 | 
			
		||||
  async getAppInfo(request: FastifyRequest, reply: FastifyReply) {
 | 
			
		||||
    try {
 | 
			
		||||
@@ -70,6 +72,24 @@ export class AppController {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async testSmtpConnection(request: FastifyRequest, reply: FastifyReply) {
 | 
			
		||||
    try {
 | 
			
		||||
      await request.jwtVerify();
 | 
			
		||||
 | 
			
		||||
      if (!(request as any).user?.isAdmin) {
 | 
			
		||||
        return reply.status(403).send({ error: "Access restricted to administrators" });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const body = request.body as any;
 | 
			
		||||
      const smtpConfig = body.smtpConfig || undefined;
 | 
			
		||||
 | 
			
		||||
      const result = await this.emailService.testConnection(smtpConfig);
 | 
			
		||||
      return reply.send(result);
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      return reply.status(400).send({ error: error.message });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async uploadLogo(request: FastifyRequest, reply: FastifyReply) {
 | 
			
		||||
    try {
 | 
			
		||||
      const file = await request.file();
 | 
			
		||||
 
 | 
			
		||||
@@ -126,6 +126,54 @@ export async function appRoutes(app: FastifyInstance) {
 | 
			
		||||
    appController.bulkUpdateConfigs.bind(appController)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  app.post(
 | 
			
		||||
    "/app/test-smtp",
 | 
			
		||||
    {
 | 
			
		||||
      preValidation: adminPreValidation,
 | 
			
		||||
      schema: {
 | 
			
		||||
        tags: ["App"],
 | 
			
		||||
        operationId: "testSmtpConnection",
 | 
			
		||||
        summary: "Test SMTP connection with provided or saved configuration",
 | 
			
		||||
        description:
 | 
			
		||||
          "Validates SMTP connectivity using either provided configuration parameters or the currently saved settings. This endpoint allows testing SMTP settings before saving them permanently. Requires admin privileges.",
 | 
			
		||||
        body: z
 | 
			
		||||
          .object({
 | 
			
		||||
            smtpConfig: z
 | 
			
		||||
              .object({
 | 
			
		||||
                smtpEnabled: z.string().describe("Whether SMTP is enabled ('true' or 'false')"),
 | 
			
		||||
                smtpHost: z.string().describe("SMTP server hostname or IP address (e.g., 'smtp.gmail.com')"),
 | 
			
		||||
                smtpPort: z
 | 
			
		||||
                  .union([z.string(), z.number()])
 | 
			
		||||
                  .transform(String)
 | 
			
		||||
                  .describe("SMTP server port (typically 587 for TLS, 25 for non-secure)"),
 | 
			
		||||
                smtpUser: z.string().describe("Username for SMTP authentication (e.g., email address)"),
 | 
			
		||||
                smtpPass: z.string().describe("Password for SMTP authentication (for Gmail, use App Password)"),
 | 
			
		||||
              })
 | 
			
		||||
              .optional()
 | 
			
		||||
              .describe("SMTP configuration to test. If not provided, uses currently saved configuration"),
 | 
			
		||||
          })
 | 
			
		||||
          .optional()
 | 
			
		||||
          .describe("Request body containing SMTP configuration to test. Send empty body to test saved configuration"),
 | 
			
		||||
        response: {
 | 
			
		||||
          200: z.object({
 | 
			
		||||
            success: z.boolean().describe("Whether the SMTP connection test was successful"),
 | 
			
		||||
            message: z.string().describe("Descriptive message about the test result"),
 | 
			
		||||
          }),
 | 
			
		||||
          400: z.object({
 | 
			
		||||
            error: z.string().describe("Error message describing what went wrong with the test"),
 | 
			
		||||
          }),
 | 
			
		||||
          401: z.object({
 | 
			
		||||
            error: z.string().describe("Authentication error - invalid or missing JWT token"),
 | 
			
		||||
          }),
 | 
			
		||||
          403: z.object({
 | 
			
		||||
            error: z.string().describe("Authorization error - user does not have admin privileges"),
 | 
			
		||||
          }),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    appController.testSmtpConnection.bind(appController)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  app.post(
 | 
			
		||||
    "/app/logo",
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
import { ConfigService } from "../config/service";
 | 
			
		||||
import nodemailer from "nodemailer";
 | 
			
		||||
 | 
			
		||||
interface SmtpConfig {
 | 
			
		||||
  smtpEnabled: string;
 | 
			
		||||
  smtpHost: string;
 | 
			
		||||
  smtpPort: string;
 | 
			
		||||
  smtpUser: string;
 | 
			
		||||
  smtpPass: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class EmailService {
 | 
			
		||||
  private configService = new ConfigService();
 | 
			
		||||
 | 
			
		||||
@@ -21,6 +29,45 @@ export class EmailService {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async testConnection(config?: SmtpConfig) {
 | 
			
		||||
    let smtpConfig: SmtpConfig;
 | 
			
		||||
 | 
			
		||||
    if (config) {
 | 
			
		||||
      // Use provided configuration
 | 
			
		||||
      smtpConfig = config;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Fallback to saved configuration
 | 
			
		||||
      smtpConfig = {
 | 
			
		||||
        smtpEnabled: await this.configService.getValue("smtpEnabled"),
 | 
			
		||||
        smtpHost: await this.configService.getValue("smtpHost"),
 | 
			
		||||
        smtpPort: await this.configService.getValue("smtpPort"),
 | 
			
		||||
        smtpUser: await this.configService.getValue("smtpUser"),
 | 
			
		||||
        smtpPass: await this.configService.getValue("smtpPass"),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (smtpConfig.smtpEnabled !== "true") {
 | 
			
		||||
      throw new Error("SMTP is not enabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const transporter = nodemailer.createTransport({
 | 
			
		||||
      host: smtpConfig.smtpHost,
 | 
			
		||||
      port: Number(smtpConfig.smtpPort),
 | 
			
		||||
      secure: false,
 | 
			
		||||
      auth: {
 | 
			
		||||
        user: smtpConfig.smtpUser,
 | 
			
		||||
        pass: smtpConfig.smtpPass,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await transporter.verify();
 | 
			
		||||
      return { success: true, message: "SMTP connection successful" };
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      throw new Error(`SMTP connection failed: ${error.message}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sendPasswordResetEmail(to: string, resetToken: string, origin: string) {
 | 
			
		||||
    const transporter = await this.createTransporter();
 | 
			
		||||
    if (!transporter) {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "نطاقات البريد الإلكتروني للمشرف",
 | 
			
		||||
        "description": "نطاقات البريد الإلكتروني التي تحصل على صلاحيات المشرف تلقائياً"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "اختبار اتصال SMTP",
 | 
			
		||||
        "description": "اختبار ما إذا كان تكوين SMTP صالحًا"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "احفظ {group}"
 | 
			
		||||
      "save": "احفظ {group}",
 | 
			
		||||
      "testSmtp": "اختبار الاتصال",
 | 
			
		||||
      "testing": "جاري الاختبار..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "فشل في تحميل الإعدادات",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "لا توجد تغييرات للحفظ",
 | 
			
		||||
      "updateSuccess": "تم تحديث إعدادات {group} بنجاح"
 | 
			
		||||
      "updateSuccess": "تم تحديث إعدادات {group} بنجاح",
 | 
			
		||||
      "smtpTestFailed": "فشل اتصال SMTP: {error}",
 | 
			
		||||
      "smtpTestGenericError": "فشل اختبار اتصال SMTP. يرجى التحقق من إعداداتك والمحاولة مرة أخرى.",
 | 
			
		||||
      "smtpTestSuccess": "تم اتصال SMTP بنجاح! تكوين البريد الإلكتروني الخاص بك يعمل بشكل صحيح."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "الإعدادات",
 | 
			
		||||
    "breadcrumb": "الإعدادات",
 | 
			
		||||
    "pageTitle": "الإعدادات",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "أدخل نطاقاً واضغط Enter للإضافة",
 | 
			
		||||
      "oidcAdminEmailDomains": "أدخل نطاقاً واضغط Enter للإضافة"
 | 
			
		||||
      "oidcAdminEmailDomains": "أدخل نطاقاً واضغط Enter للإضافة",
 | 
			
		||||
      "testSmtp": "يختبر اتصال SMTP بالقيم المدخلة حاليًا في النموذج. لجعل التغييرات دائمة، تذكر حفظ إعداداتك بعد الاختبار."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "خطأ في تحديث الملف",
 | 
			
		||||
        "previewNotAvailable": "المعاينة غير متوفرة",
 | 
			
		||||
        "copyError": "خطأ نسخ الملف إلى ملفاتك",
 | 
			
		||||
        "copySuccess": "تم نسخ الملف إلى ملفاتك بنجاح"
 | 
			
		||||
        "copySuccess": "تم نسخ الملف إلى ملفاتك بنجاح",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =0 {لا ملفات محددة} =1 {ملف واحد محدد} =2 {ملفان محددان} other {# ملفات محددة}}",
 | 
			
		||||
          "actions": "إجراءات",
 | 
			
		||||
          "download": "تحميل المحدد",
 | 
			
		||||
          "copyToMyFiles": "نسخ المحدد إلى ملفاتي",
 | 
			
		||||
          "delete": "حذف المحدد"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "جارٍ نسخ {count, plural, =1 {ملف واحد} =2 {ملفين} other {# ملفات}} إلى ملفاتك...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {تم نسخ ملف واحد إلى ملفاتك بنجاح} =2 {تم نسخ ملفين إلى ملفاتك بنجاح} other {تم نسخ # ملفات إلى ملفاتك بنجاح}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "حذف {count, plural, =1 {الملف} =2 {الملفين} other {الملفات}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "هل أنت متأكد أنك تريد حذف {count, plural, =1 {هذا الملف} =2 {هذين الملفين} other {هذه الملفات الـ #}}؟ لا يمكن التراجع عن هذا الإجراء.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "حذف الملفات المحددة",
 | 
			
		||||
        "bulkDeleteProgress": "جارٍ حذف {count, plural, =1 {ملف واحد} =2 {ملفين} other {# ملفات}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {تم حذف ملف واحد بنجاح} =2 {تم حذف ملفين بنجاح} other {تم حذف # ملفات بنجاح}}",
 | 
			
		||||
        "selectAll": "تحديد الكل",
 | 
			
		||||
        "selectFile": "تحديد الملف {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Admin-E-Mail-Domains",
 | 
			
		||||
        "description": "E-Mail-Domains, die automatisch Administratorrechte erhalten"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "SMTP-Verbindung testen",
 | 
			
		||||
        "description": "Testen Sie, ob die SMTP-Konfiguration gültig ist"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group} speichern"
 | 
			
		||||
      "save": "{group} speichern",
 | 
			
		||||
      "testSmtp": "Verbindung testen",
 | 
			
		||||
      "testing": "Teste..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Fehler beim Laden der Einstellungen",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Keine Änderungen zum Speichern",
 | 
			
		||||
      "updateSuccess": "{group}-Einstellungen erfolgreich aktualisiert"
 | 
			
		||||
      "updateSuccess": "{group}-Einstellungen erfolgreich aktualisiert",
 | 
			
		||||
      "smtpTestFailed": "SMTP-Verbindung fehlgeschlagen: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP-Verbindungstest fehlgeschlagen. Bitte überprüfen Sie Ihre Einstellungen und versuchen Sie es erneut.",
 | 
			
		||||
      "smtpTestSuccess": "SMTP-Verbindung erfolgreich! Ihre E-Mail-Konfiguration funktioniert korrekt."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Einstellungen",
 | 
			
		||||
    "breadcrumb": "Einstellungen",
 | 
			
		||||
    "pageTitle": "Einstellungen",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Geben Sie einen Scope ein und drücken Sie Enter zum Hinzufügen",
 | 
			
		||||
      "oidcAdminEmailDomains": "Geben Sie eine Domain ein und drücken Sie Enter zum Hinzufügen"
 | 
			
		||||
      "oidcAdminEmailDomains": "Geben Sie eine Domain ein und drücken Sie Enter zum Hinzufügen",
 | 
			
		||||
      "testSmtp": "Testet die SMTP-Verbindung mit den aktuell im Formular eingegebenen Werten. Um die Änderungen dauerhaft zu machen, denken Sie daran, Ihre Einstellungen nach dem Test zu speichern."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://meineseite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Fehler beim Aktualisieren der Datei",
 | 
			
		||||
        "previewNotAvailable": "Vorschau nicht verfügbar",
 | 
			
		||||
        "copyError": "Fehler beim Kopieren der Datei in Ihre Dateien",
 | 
			
		||||
        "copySuccess": "Datei erfolgreich in Ihre Dateien kopiert"
 | 
			
		||||
        "copySuccess": "Datei erfolgreich in Ihre Dateien kopiert",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 Datei ausgewählt} other {# Dateien ausgewählt}}",
 | 
			
		||||
          "actions": "Aktionen",
 | 
			
		||||
          "download": "Ausgewählte herunterladen",
 | 
			
		||||
          "copyToMyFiles": "Ausgewählte in meine Dateien kopieren",
 | 
			
		||||
          "delete": "Ausgewählte löschen"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "{count, plural, =1 {1 Datei wird} other {# Dateien werden}} in Ihre Dateien kopiert...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 Datei wurde erfolgreich in Ihre Dateien kopiert} other {# Dateien wurden erfolgreich in Ihre Dateien kopiert}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {Datei löschen} other {Dateien löschen}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Sind Sie sicher, dass Sie {count, plural, =1 {diese Datei} other {diese # Dateien}} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Ausgewählte Dateien löschen",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1 Datei wird} other {# Dateien werden}} gelöscht...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 Datei wurde erfolgreich gelöscht} other {# Dateien wurden erfolgreich gelöscht}}",
 | 
			
		||||
        "selectAll": "Alle auswählen",
 | 
			
		||||
        "selectFile": "Datei {fileName} auswählen"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -465,6 +465,10 @@
 | 
			
		||||
        "title": "Sender Email",
 | 
			
		||||
        "description": "Sender email address"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Test SMTP Connection",
 | 
			
		||||
        "description": "Test if the SMTP configuration is valid"
 | 
			
		||||
      },
 | 
			
		||||
      "maxLoginAttempts": {
 | 
			
		||||
        "title": "Maximum Login Attempts",
 | 
			
		||||
        "description": "Maximum number of login attempts before blocking"
 | 
			
		||||
@@ -531,7 +535,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Save {group}"
 | 
			
		||||
      "save": "Save {group}",
 | 
			
		||||
      "testSmtp": "Test Connection",
 | 
			
		||||
      "testing": "Testing..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Failed to load settings",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "No changes to save",
 | 
			
		||||
      "updateSuccess": "{group} settings updated successfully"
 | 
			
		||||
      "updateSuccess": "{group} settings updated successfully",
 | 
			
		||||
      "smtpTestSuccess": "SMTP connection successful! Your email configuration is working correctly.",
 | 
			
		||||
      "smtpTestFailed": "SMTP connection failed: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Failed to test SMTP connection. Please check your settings and try again."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Settings",
 | 
			
		||||
    "breadcrumb": "Settings",
 | 
			
		||||
    "pageTitle": "Settings",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Enter a scope and press Enter to add",
 | 
			
		||||
      "oidcAdminEmailDomains": "Enter a domain and press Enter to add"
 | 
			
		||||
      "oidcAdminEmailDomains": "Enter a domain and press Enter to add",
 | 
			
		||||
      "testSmtp": "Tests the SMTP connection with the values currently entered in the form. To make changes permanent, remember to save your settings after testing."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Error updating file",
 | 
			
		||||
        "previewNotAvailable": "Preview not available",
 | 
			
		||||
        "copySuccess": "File copied to your files successfully",
 | 
			
		||||
        "copyError": "Error copying file to your files"
 | 
			
		||||
        "copyError": "Error copying file to your files",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 file copied to your files successfully} other {# files copied to your files successfully}}",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 file deleted successfully} other {# files deleted successfully}}",
 | 
			
		||||
        "bulkCopyProgress": "Copying {count, plural, =1 {1 file} other {# files}} to your files...",
 | 
			
		||||
        "bulkDeleteProgress": "Deleting {count, plural, =1 {1 file} other {# files}}...",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Delete Selected Files",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Are you sure you want to delete {count, plural, =1 {this file} other {these # files}}? This action cannot be undone.",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Delete {count, plural, =1 {File} other {Files}}",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 file selected} other {# files selected}}",
 | 
			
		||||
          "actions": "Actions",
 | 
			
		||||
          "download": "Download Selected",
 | 
			
		||||
          "copyToMyFiles": "Copy Selected to My Files",
 | 
			
		||||
          "delete": "Delete Selected"
 | 
			
		||||
        },
 | 
			
		||||
        "selectAll": "Select all",
 | 
			
		||||
        "selectFile": "Select file {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Dominios de Correo Admin",
 | 
			
		||||
        "description": "Dominios de correo que reciben privilegios de administrador automáticamente"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Probar Conexión SMTP",
 | 
			
		||||
        "description": "Probar si la configuración SMTP es válida"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Guardar {group}"
 | 
			
		||||
      "save": "Guardar {group}",
 | 
			
		||||
      "testSmtp": "Probar Conexión",
 | 
			
		||||
      "testing": "Probando..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Error al cargar la configuración",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "No hay cambios para guardar",
 | 
			
		||||
      "updateSuccess": "Configuración de {group} actualizada exitosamente"
 | 
			
		||||
      "updateSuccess": "Configuración de {group} actualizada exitosamente",
 | 
			
		||||
      "smtpTestSuccess": "¡Conexión SMTP exitosa! Tu configuración de correo funciona correctamente.",
 | 
			
		||||
      "smtpTestFailed": "Falló la conexión SMTP: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Error al probar la conexión SMTP. Por favor revisa tu configuración e inténtalo de nuevo."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Configuración",
 | 
			
		||||
    "breadcrumb": "Configuración",
 | 
			
		||||
    "pageTitle": "Configuración",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Ingrese un ámbito y presione Enter para agregar",
 | 
			
		||||
      "oidcAdminEmailDomains": "Ingrese un dominio y presione Enter para agregar"
 | 
			
		||||
      "oidcAdminEmailDomains": "Ingrese un dominio y presione Enter para agregar",
 | 
			
		||||
      "testSmtp": "Prueba la conexión SMTP con los valores actualmente ingresados en el formulario. Para hacer los cambios permanentes, recuerda guardar la configuración después de probar."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://misitio.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Error al actualizar archivo",
 | 
			
		||||
        "previewNotAvailable": "Vista previa no disponible",
 | 
			
		||||
        "copyError": "Error de copiar el archivo a sus archivos",
 | 
			
		||||
        "copySuccess": "Archivo copiado en sus archivos correctamente"
 | 
			
		||||
        "copySuccess": "Archivo copiado en sus archivos correctamente",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 archivo seleccionado} other {# archivos seleccionados}}",
 | 
			
		||||
          "actions": "Acciones",
 | 
			
		||||
          "download": "Descargar Seleccionados",
 | 
			
		||||
          "copyToMyFiles": "Copiar Seleccionados a Mis Archivos",
 | 
			
		||||
          "delete": "Eliminar Seleccionados"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Copiando {count, plural, =1 {1 archivo} other {# archivos}} a tus archivos...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 archivo copiado a tus archivos correctamente} other {# archivos copiados a tus archivos correctamente}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Eliminar {count, plural, =1 {Archivo} other {Archivos}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "¿Estás seguro de que quieres eliminar {count, plural, =1 {este archivo} other {estos # archivos}}? Esta acción no se puede deshacer.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Eliminar Archivos Seleccionados",
 | 
			
		||||
        "bulkDeleteProgress": "Eliminando {count, plural, =1 {1 archivo} other {# archivos}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 archivo eliminado correctamente} other {# archivos eliminados correctamente}}",
 | 
			
		||||
        "selectAll": "Seleccionar todo",
 | 
			
		||||
        "selectFile": "Seleccionar archivo {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -531,10 +531,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Domaines Email Admin",
 | 
			
		||||
        "description": "Domaines email qui reçoivent automatiquement les privilèges d'administrateur"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "[TO_TRANSLATE] Test SMTP Connection",
 | 
			
		||||
        "description": "[TO_TRANSLATE] Test if the SMTP configuration is valid"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Enregistrer {group}"
 | 
			
		||||
      "save": "Enregistrer {group}",
 | 
			
		||||
      "testSmtp": "Tester la Connexion",
 | 
			
		||||
      "testing": "Test en cours..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Échec du chargement des paramètres",
 | 
			
		||||
@@ -542,11 +548,15 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Aucun changement à enregistrer",
 | 
			
		||||
      "updateSuccess": "Paramètres {group} mis à jour avec succès"
 | 
			
		||||
      "updateSuccess": "Paramètres {group} mis à jour avec succès",
 | 
			
		||||
      "smtpTestFailed": "La connexion SMTP a échoué : {error}",
 | 
			
		||||
      "smtpTestGenericError": "Échec du test de connexion SMTP. Veuillez vérifier vos paramètres et réessayer.",
 | 
			
		||||
      "smtpTestSuccess": "Connexion SMTP réussie ! Votre configuration email fonctionne correctement."
 | 
			
		||||
    },
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Entrez une portée et appuyez sur Entrée pour l'ajouter",
 | 
			
		||||
      "oidcAdminEmailDomains": "Entrez un domaine et appuyez sur Entrée pour l'ajouter"
 | 
			
		||||
      "oidcAdminEmailDomains": "Entrez un domaine et appuyez sur Entrée pour l'ajouter",
 | 
			
		||||
      "testSmtp": "Teste la connexion SMTP avec les valeurs actuellement saisies dans le formulaire. Pour rendre les modifications permanentes, n'oubliez pas d'enregistrer vos paramètres après le test."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://monsite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Erreur lors de la mise à jour du fichier",
 | 
			
		||||
        "previewNotAvailable": "Aperçu non disponible",
 | 
			
		||||
        "copyError": "Erreur de copie du fichier dans vos fichiers",
 | 
			
		||||
        "copySuccess": "Fichier copié dans vos fichiers avec succès"
 | 
			
		||||
        "copySuccess": "Fichier copié dans vos fichiers avec succès",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 fichier sélectionné} other {# fichiers sélectionnés}}",
 | 
			
		||||
          "actions": "Actes",
 | 
			
		||||
          "download": "Télécharger la sélection",
 | 
			
		||||
          "copyToMyFiles": "Copier la sélection dans mes fichiers",
 | 
			
		||||
          "delete": "Supprimer la sélection"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Copie de {count, plural, =1 {1 fichier} other {# fichiers}} dans vos fichiers...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 fichier copié dans vos fichiers avec succès} other {# fichiers copiés dans vos fichiers avec succès}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Supprimer {count, plural, =1 {le fichier} other {les fichiers}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer {count, plural, =1 {ce fichier} other {ces # fichiers}} ? Cette action est irréversible.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Supprimer les fichiers sélectionnés",
 | 
			
		||||
        "bulkDeleteProgress": "Suppression de {count, plural, =1 {1 fichier} other {# fichiers}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 fichier supprimé avec succès} other {# fichiers supprimés avec succès}}",
 | 
			
		||||
        "selectAll": "Tout sélectionner",
 | 
			
		||||
        "selectFile": "Sélectionner le fichier {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "एडमिन ईमेल डोमेन",
 | 
			
		||||
        "description": "जिन ईमेल डोमेन को स्वचालित रूप से व्यवस्थापक विशेषाधिकार प्राप्त होंगे"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "SMTP कनेक्शन का परीक्षण करें",
 | 
			
		||||
        "description": "जांचें कि SMTP कॉन्फ़िगरेशन मान्य है"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group} सहेजें"
 | 
			
		||||
      "save": "{group} सहेजें",
 | 
			
		||||
      "testSmtp": "कनेक्शन का परीक्षण करें",
 | 
			
		||||
      "testing": "परीक्षण किया जा रहा है..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "सेटिंग्स लोड करने में विफल",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "सहेजने के लिए कोई परिवर्तन नहीं",
 | 
			
		||||
      "updateSuccess": "{group} सेटिंग्स सफलतापूर्वक अपडेट हुईं"
 | 
			
		||||
      "updateSuccess": "{group} सेटिंग्स सफलतापूर्वक अपडेट हुईं",
 | 
			
		||||
      "smtpTestFailed": "SMTP कनेक्शन विफल: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP कनेक्शन का परीक्षण करने में विफल। कृपया अपनी सेटिंग्स जांचें और पुनः प्रयास करें।",
 | 
			
		||||
      "smtpTestSuccess": "SMTP कनेक्शन सफल! आपका ईमेल कॉन्फ़िगरेशन सही ढंग से काम कर रहा है।"
 | 
			
		||||
    },
 | 
			
		||||
    "title": "सेटिंग्स",
 | 
			
		||||
    "breadcrumb": "सेटिंग्स",
 | 
			
		||||
    "pageTitle": "सेटिंग्स",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "स्कोप जोड़ने के लिए एक स्कोप दर्ज करें और Enter दबाएं",
 | 
			
		||||
      "oidcAdminEmailDomains": "डोमेन जोड़ने के लिए एक डोमेन दर्ज करें और Enter दबाएं"
 | 
			
		||||
      "oidcAdminEmailDomains": "डोमेन जोड़ने के लिए एक डोमेन दर्ज करें और Enter दबाएं",
 | 
			
		||||
      "testSmtp": "फॉर्म में वर्तमान में दर्ज मानों के साथ SMTP कनेक्शन का परीक्षण करता है। परिवर्तनों को स्थायी बनाने के लिए, परीक्षण के बाद अपनी सेटिंग्स को सहेजना याद रखें।"
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "फ़ाइल अपडेट करने में त्रुटि",
 | 
			
		||||
        "previewNotAvailable": "पूर्वावलोकन उपलब्ध नहीं है",
 | 
			
		||||
        "copyError": "अपनी फ़ाइलों में फ़ाइल की नकल करने में त्रुटि",
 | 
			
		||||
        "copySuccess": "आपकी फ़ाइलों को सफलतापूर्वक कॉपी की गई फ़ाइल"
 | 
			
		||||
        "copySuccess": "आपकी फ़ाइलों को सफलतापूर्वक कॉपी की गई फ़ाइल",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 फ़ाइल चयनित} other {# फ़ाइलें चयनित}}",
 | 
			
		||||
          "actions": "कार्रवाइयां",
 | 
			
		||||
          "download": "चयनित डाउनलोड करें",
 | 
			
		||||
          "copyToMyFiles": "चयनित को मेरी फ़ाइलों में कॉपी करें",
 | 
			
		||||
          "delete": "चयनित हटाएं"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "{count, plural, =1 {1 फ़ाइल} other {# फ़ाइलें}} आपकी फ़ाइलों में कॉपी की जा रही हैं...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 फ़ाइल सफलतापूर्वक आपकी फ़ाइलों में कॉपी की गई} other {# फ़ाइलें सफलतापूर्वक आपकी फ़ाइलों में कॉपी की गईं}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {फ़ाइल} other {फ़ाइलें}} हटाएं",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "क्या आप वाकई {count, plural, =1 {इस फ़ाइल} other {इन # फ़ाइलों}} को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "चयनित फ़ाइलें हटाएं",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1 फ़ाइल} other {# फ़ाइलें}} हटाई जा रही हैं...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 फ़ाइल सफलतापूर्वक हटाई गई} other {# फ़ाइलें सफलतापूर्वक हटाई गईं}}",
 | 
			
		||||
        "selectAll": "सभी चुनें",
 | 
			
		||||
        "selectFile": "फ़ाइल {fileName} चुनें"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Domini Email Admin",
 | 
			
		||||
        "description": "Domini email che ricevono automaticamente i privilegi di amministratore"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Test Connessione SMTP",
 | 
			
		||||
        "description": "Verifica se la configurazione SMTP è valida"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Salva {group}"
 | 
			
		||||
      "save": "Salva {group}",
 | 
			
		||||
      "testSmtp": "Prova Connessione",
 | 
			
		||||
      "testing": "Verifica in corso..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Errore durante il caricamento delle impostazioni",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Nessuna modifica da salvare",
 | 
			
		||||
      "updateSuccess": "Impostazioni {group} aggiornate con successo"
 | 
			
		||||
      "updateSuccess": "Impostazioni {group} aggiornate con successo",
 | 
			
		||||
      "smtpTestFailed": "Connessione SMTP fallita: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Impossibile testare la connessione SMTP. Controlla le impostazioni e riprova.",
 | 
			
		||||
      "smtpTestSuccess": "Connessione SMTP riuscita! La configurazione email funziona correttamente."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Impostazioni",
 | 
			
		||||
    "breadcrumb": "Impostazioni",
 | 
			
		||||
    "pageTitle": "Impostazioni",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Inserisci uno scope e premi Invio per aggiungerlo",
 | 
			
		||||
      "oidcAdminEmailDomains": "Inserisci un dominio e premi Invio per aggiungerlo"
 | 
			
		||||
      "oidcAdminEmailDomains": "Inserisci un dominio e premi Invio per aggiungerlo",
 | 
			
		||||
      "testSmtp": "Verifica la connessione SMTP con i valori attualmente inseriti nel modulo. Per rendere permanenti le modifiche, ricordati di salvare le impostazioni dopo il test."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://miosito.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Errore durante l'aggiornamento del file",
 | 
			
		||||
        "previewNotAvailable": "Anteprima non disponibile",
 | 
			
		||||
        "copyError": "Errore di copia del file sui tuoi file",
 | 
			
		||||
        "copySuccess": "File copiato sui tuoi file correttamente"
 | 
			
		||||
        "copySuccess": "File copiato sui tuoi file correttamente",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 file selezionato} other {# file selezionati}}",
 | 
			
		||||
          "actions": "Azioni",
 | 
			
		||||
          "download": "Scarica Selezionati",
 | 
			
		||||
          "copyToMyFiles": "Copia Selezionati sui Miei File",
 | 
			
		||||
          "delete": "Elimina Selezionati"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Copia di {count, plural, =1 {1 file} other {# file}} sui tuoi file in corso...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 file copiato sui tuoi file con successo} other {# file copiati sui tuoi file con successo}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Elimina {count, plural, =1 {File} other {File}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Sei sicuro di voler eliminare {count, plural, =1 {questo file} other {questi # file}}? Questa azione non può essere annullata.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Elimina File Selezionati",
 | 
			
		||||
        "bulkDeleteProgress": "Eliminazione di {count, plural, =1 {1 file} other {# file}} in corso...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 file eliminato con successo} other {# file eliminati con successo}}",
 | 
			
		||||
        "selectAll": "Seleziona tutto",
 | 
			
		||||
        "selectFile": "Seleziona file {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "管理者メールドメイン",
 | 
			
		||||
        "description": "自動的に管理者権限が付与されるメールドメイン"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "SMTP接続テスト",
 | 
			
		||||
        "description": "SMTP設定が有効かどうかをテストします"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group}を保存"
 | 
			
		||||
      "save": "{group}を保存",
 | 
			
		||||
      "testSmtp": "接続テスト",
 | 
			
		||||
      "testing": "テスト中..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "設定の読み込みに失敗しました",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "保存する変更はありません",
 | 
			
		||||
      "updateSuccess": "{group}の設定が正常に更新されました"
 | 
			
		||||
      "updateSuccess": "{group}の設定が正常に更新されました",
 | 
			
		||||
      "smtpTestFailed": "SMTP接続に失敗しました: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP接続のテストに失敗しました。設定を確認して再度お試しください。",
 | 
			
		||||
      "smtpTestSuccess": "SMTP接続に成功しました!メール設定は正常に動作しています。"
 | 
			
		||||
    },
 | 
			
		||||
    "title": "設定",
 | 
			
		||||
    "breadcrumb": "設定",
 | 
			
		||||
    "pageTitle": "設定",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "スコープを入力してEnterキーを押して追加",
 | 
			
		||||
      "oidcAdminEmailDomains": "ドメインを入力してEnterキーを押して追加"
 | 
			
		||||
      "oidcAdminEmailDomains": "ドメインを入力してEnterキーを押して追加",
 | 
			
		||||
      "testSmtp": "フォームに現在入力されている値でSMTP接続をテストします。変更を永続化するには、テスト後に設定を保存することを忘れないでください。"
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "ファイルの更新に失敗しました",
 | 
			
		||||
        "previewNotAvailable": "プレビューは利用できません",
 | 
			
		||||
        "copyError": "ファイルにファイルをコピーするエラー",
 | 
			
		||||
        "copySuccess": "ファイルに正常にコピーされたファイル"
 | 
			
		||||
        "copySuccess": "ファイルに正常にコピーされたファイル",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1ファイルを選択} other {#ファイルを選択}}",
 | 
			
		||||
          "actions": "アクション",
 | 
			
		||||
          "download": "選択したファイルをダウンロード",
 | 
			
		||||
          "copyToMyFiles": "選択したファイルを私のファイルにコピー",
 | 
			
		||||
          "delete": "選択したファイルを削除"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "{count, plural, =1 {1ファイル} other {#ファイル}}を私のファイルにコピー中...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1ファイル} other {#ファイル}}を私のファイルに正常にコピーしました",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {ファイル} other {ファイル}}を削除",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "{count, plural, =1 {このファイル} other {これらの#ファイル}}を削除してもよろしいですか?この操作は取り消せません。",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "選択したファイルを削除",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1ファイル} other {#ファイル}}を削除中...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1ファイル} other {#ファイル}}を正常に削除しました",
 | 
			
		||||
        "selectAll": "すべて選択",
 | 
			
		||||
        "selectFile": "ファイル{fileName}を選択"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "관리자 이메일 도메인",
 | 
			
		||||
        "description": "자동으로 관리자 권한이 부여되는 이메일 도메인"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "SMTP 연결 테스트",
 | 
			
		||||
        "description": "SMTP 구성이 유효한지 테스트합니다"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group} 저장"
 | 
			
		||||
      "save": "{group} 저장",
 | 
			
		||||
      "testSmtp": "연결 테스트",
 | 
			
		||||
      "testing": "테스트 중..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "설정을 불러오는데 실패했습니다",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "저장할 변경 사항이 없습니다",
 | 
			
		||||
      "updateSuccess": "{group} 설정이 성공적으로 업데이트되었습니다"
 | 
			
		||||
      "updateSuccess": "{group} 설정이 성공적으로 업데이트되었습니다",
 | 
			
		||||
      "smtpTestFailed": "SMTP 연결 실패: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP 연결 테스트에 실패했습니다. 설정을 확인하고 다시 시도해주세요.",
 | 
			
		||||
      "smtpTestSuccess": "SMTP 연결 성공! 이메일 구성이 올바르게 작동합니다."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "설정",
 | 
			
		||||
    "breadcrumb": "설정",
 | 
			
		||||
    "pageTitle": "설정",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "스코프를 입력하고 Enter 키를 눌러 추가",
 | 
			
		||||
      "oidcAdminEmailDomains": "도메인을 입력하고 Enter 키를 눌러 추가"
 | 
			
		||||
      "oidcAdminEmailDomains": "도메인을 입력하고 Enter 키를 눌러 추가",
 | 
			
		||||
      "testSmtp": "현재 입력된 값으로 SMTP 연결을 테스트합니다. 변경 사항을 영구적으로 적용하려면 테스트 후 설정을 저장하는 것을 잊지 마세요."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "파일 업데이트 오류",
 | 
			
		||||
        "previewNotAvailable": "미리보기 불가",
 | 
			
		||||
        "copyError": "파일에 파일을 복사합니다",
 | 
			
		||||
        "copySuccess": "파일을 파일에 성공적으로 복사했습니다"
 | 
			
		||||
        "copySuccess": "파일을 파일에 성공적으로 복사했습니다",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1개의 파일 선택됨} other {#개의 파일 선택됨}}",
 | 
			
		||||
          "actions": "작업",
 | 
			
		||||
          "download": "선택한 항목 다운로드",
 | 
			
		||||
          "copyToMyFiles": "선택한 항목을 내 파일로 복사",
 | 
			
		||||
          "delete": "선택한 항목 삭제"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "{count, plural, =1 {1개의 파일} other {#개의 파일}}을(를) 내 파일로 복사하는 중...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1개의 파일이} other {#개의 파일이}} 내 파일로 성공적으로 복사됨",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {파일} other {파일}} 삭제",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "{count, plural, =1 {이 파일을} other {이 #개의 파일을}} 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "선택한 파일 삭제",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1개의 파일} other {#개의 파일}} 삭제 중...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1개의 파일이} other {#개의 파일이}} 성공적으로 삭제됨",
 | 
			
		||||
        "selectAll": "모두 선택",
 | 
			
		||||
        "selectFile": "{fileName} 파일 선택"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Admin E-mail Domeinen",
 | 
			
		||||
        "description": "E-mail domeinen die automatisch admin rechten krijgen"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Test SMTP Verbinding",
 | 
			
		||||
        "description": "Test of de SMTP configuratie geldig is"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group} Opslaan"
 | 
			
		||||
      "save": "{group} Opslaan",
 | 
			
		||||
      "testSmtp": "Test Verbinding",
 | 
			
		||||
      "testing": "Testen..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Fout bij het laden van instellingen",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Geen wijzigingen om op te slaan",
 | 
			
		||||
      "updateSuccess": "{group} instellingen succesvol bijgewerkt"
 | 
			
		||||
      "updateSuccess": "{group} instellingen succesvol bijgewerkt",
 | 
			
		||||
      "smtpTestFailed": "SMTP verbinding mislukt: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP verbinding testen mislukt. Controleer uw instellingen en probeer het opnieuw.",
 | 
			
		||||
      "smtpTestSuccess": "SMTP verbinding succesvol! Uw e-mailconfiguratie werkt correct."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Instellingen",
 | 
			
		||||
    "breadcrumb": "Instellingen",
 | 
			
		||||
    "pageTitle": "Instellingen",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Voer een scope in en druk op Enter om toe te voegen",
 | 
			
		||||
      "oidcAdminEmailDomains": "Voer een domein in en druk op Enter om toe te voegen"
 | 
			
		||||
      "oidcAdminEmailDomains": "Voer een domein in en druk op Enter om toe te voegen",
 | 
			
		||||
      "testSmtp": "Test de SMTP verbinding met de waarden die momenteel in het formulier zijn ingevoerd. Om wijzigingen permanent te maken, vergeet niet om uw instellingen op te slaan na het testen."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mijnsite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Fout bij bijwerken bestand",
 | 
			
		||||
        "previewNotAvailable": "Voorvertoning niet beschikbaar",
 | 
			
		||||
        "copyError": "Fout bij het kopiëren van bestand naar uw bestanden",
 | 
			
		||||
        "copySuccess": "Bestand gekopieerd naar uw bestanden succesvol"
 | 
			
		||||
        "copySuccess": "Bestand gekopieerd naar uw bestanden succesvol",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 bestand geselecteerd} other {# bestanden geselecteerd}}",
 | 
			
		||||
          "actions": "Acties",
 | 
			
		||||
          "download": "Geselecteerde Downloaden",
 | 
			
		||||
          "copyToMyFiles": "Geselecteerde Kopiëren naar Mijn Bestanden",
 | 
			
		||||
          "delete": "Geselecteerde Verwijderen"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "{count, plural, =1 {1 bestand} other {# bestanden}} kopiëren naar uw bestanden...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 bestand succesvol gekopieerd naar uw bestanden} other {# bestanden succesvol gekopieerd naar uw bestanden}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {Bestand} other {Bestanden}} Verwijderen",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Weet u zeker dat u {count, plural, =1 {dit bestand} other {deze # bestanden}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Geselecteerde Bestanden Verwijderen",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1 bestand} other {# bestanden}} verwijderen...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 bestand succesvol verwijderd} other {# bestanden succesvol verwijderd}}",
 | 
			
		||||
        "selectAll": "Alles selecteren",
 | 
			
		||||
        "selectFile": "Selecteer bestand {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Domeny e-mail administratora",
 | 
			
		||||
        "description": "Domeny e-mail, które automatycznie otrzymują uprawnienia administratora"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Test połączenia SMTP",
 | 
			
		||||
        "description": "Sprawdź, czy konfiguracja SMTP jest prawidłowa"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Zapisz {group}"
 | 
			
		||||
      "save": "Zapisz {group}",
 | 
			
		||||
      "testSmtp": "Testuj połączenie",
 | 
			
		||||
      "testing": "Testowanie..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Nie udało się załadować ustawień",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Brak zmian do zapisania",
 | 
			
		||||
      "updateSuccess": "Ustawienia {group} zaktualizowane pomyślnie"
 | 
			
		||||
      "updateSuccess": "Ustawienia {group} zaktualizowane pomyślnie",
 | 
			
		||||
      "smtpTestFailed": "Połączenie SMTP nie powiodło się: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Nie udało się przetestować połączenia SMTP. Sprawdź ustawienia i spróbuj ponownie.",
 | 
			
		||||
      "smtpTestSuccess": "Połączenie SMTP udane! Twoja konfiguracja poczty e-mail działa poprawnie."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Ustawienia",
 | 
			
		||||
    "breadcrumb": "Ustawienia",
 | 
			
		||||
    "pageTitle": "Ustawienia",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Wprowadź zakres i naciśnij Enter, aby dodać",
 | 
			
		||||
      "oidcAdminEmailDomains": "Wprowadź domenę i naciśnij Enter, aby dodać"
 | 
			
		||||
      "oidcAdminEmailDomains": "Wprowadź domenę i naciśnij Enter, aby dodać",
 | 
			
		||||
      "testSmtp": "Testuje połączenie SMTP z obecnie wprowadzonymi wartościami w formularzu. Aby wprowadzić trwałe zmiany, pamiętaj o zapisaniu ustawień po testowaniu."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://mysite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Błąd aktualizacji pliku",
 | 
			
		||||
        "previewNotAvailable": "Podgląd niedostępny",
 | 
			
		||||
        "copyError": "Plik kopiowania błędów do plików",
 | 
			
		||||
        "copySuccess": "Plik skopiowany do plików pomyślnie"
 | 
			
		||||
        "copySuccess": "Plik skopiowany do plików pomyślnie",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 plik wybrany} other {# plików wybranych}}",
 | 
			
		||||
          "actions": "Akcje",
 | 
			
		||||
          "download": "Pobierz wybrane",
 | 
			
		||||
          "copyToMyFiles": "Skopiuj wybrane do Moich plików",
 | 
			
		||||
          "delete": "Usuń wybrane"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Kopiowanie {count, plural, =1 {1 pliku} other {# plików}} do twoich plików...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 plik skopiowany pomyślnie do twoich plików} other {# plików skopiowanych pomyślnie do twoich plików}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Usuń {count, plural, =1 {plik} other {pliki}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Czy na pewno chcesz usunąć {count, plural, =1 {ten plik} other {te # pliki}}? Tej akcji nie można cofnąć.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Usuń wybrane pliki",
 | 
			
		||||
        "bulkDeleteProgress": "Usuwanie {count, plural, =1 {1 pliku} other {# plików}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 plik usunięty pomyślnie} other {# plików usuniętych pomyślnie}}",
 | 
			
		||||
        "selectAll": "Zaznacz wszystko",
 | 
			
		||||
        "selectFile": "Wybierz plik {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -443,7 +443,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Digite um escopo e pressione Enter para adicionar",
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar"
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar",
 | 
			
		||||
      "testSmtp": "Testa a conexão SMTP com os valores atualmente inseridos no formulário. Para tornar as alterações permanentes, lembre-se de salvar suas configurações após o teste."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://meusite.com",
 | 
			
		||||
@@ -558,10 +559,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Domínios de E-mail Admin",
 | 
			
		||||
        "description": "Domínios de e-mail que recebem privilégios de administrador automaticamente"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Testar Conexão SMTP",
 | 
			
		||||
        "description": "Testa se a configuração SMTP é válida"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Salvar {group}"
 | 
			
		||||
      "save": "Salvar {group}",
 | 
			
		||||
      "testSmtp": "Testar Conexão",
 | 
			
		||||
      "testing": "Testando..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Falha ao carregar configurações",
 | 
			
		||||
@@ -569,7 +576,10 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Nenhuma alteração para salvar",
 | 
			
		||||
      "updateSuccess": "Configurações de {group} atualizadas com sucesso"
 | 
			
		||||
      "updateSuccess": "Configurações de {group} atualizadas com sucesso",
 | 
			
		||||
      "smtpTestFailed": "Falha na conexão SMTP: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Falha ao testar conexão SMTP. Por favor, verifique suas configurações e tente novamente.",
 | 
			
		||||
      "smtpTestSuccess": "Conexão SMTP bem-sucedida! Sua configuração de e-mail está funcionando corretamente."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Configurações",
 | 
			
		||||
    "breadcrumb": "Configurações",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Erro ao atualizar arquivo",
 | 
			
		||||
        "previewNotAvailable": "Visualização não disponível",
 | 
			
		||||
        "copySuccess": "Arquivo copiado para seus arquivos com sucesso",
 | 
			
		||||
        "copyError": "Erro ao copiar arquivo para seus arquivos"
 | 
			
		||||
        "copyError": "Erro ao copiar arquivo para seus arquivos",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 arquivo selecionado} other {# arquivos selecionados}}",
 | 
			
		||||
          "actions": "Ações",
 | 
			
		||||
          "download": "Baixar Selecionados",
 | 
			
		||||
          "copyToMyFiles": "Copiar Selecionados para Meus Arquivos",
 | 
			
		||||
          "delete": "Excluir Selecionados"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Copiando {count, plural, =1 {1 arquivo} other {# arquivos}} para seus arquivos...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 arquivo copiado para seus arquivos com sucesso} other {# arquivos copiados para seus arquivos com sucesso}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Excluir {count, plural, =1 {Arquivo} other {Arquivos}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Tem certeza que deseja excluir {count, plural, =1 {este arquivo} other {estes # arquivos}}? Esta ação não pode ser desfeita.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Excluir Arquivos Selecionados",
 | 
			
		||||
        "bulkDeleteProgress": "Excluindo {count, plural, =1 {1 arquivo} other {# arquivos}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 arquivo excluído com sucesso} other {# arquivos excluídos com sucesso}}",
 | 
			
		||||
        "selectAll": "Selecionar todos",
 | 
			
		||||
        "selectFile": "Selecionar arquivo {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Домены Email Администратора",
 | 
			
		||||
        "description": "Домены электронной почты, которые автоматически получают права администратора"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "Проверить SMTP-соединение",
 | 
			
		||||
        "description": "Проверить правильность настройки SMTP"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "Сохранить {group}"
 | 
			
		||||
      "save": "Сохранить {group}",
 | 
			
		||||
      "testSmtp": "Проверить соединение",
 | 
			
		||||
      "testing": "Проверка..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Ошибка загрузки настроек",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Изменений для сохранения нет",
 | 
			
		||||
      "updateSuccess": "Настройки {group} успешно обновлены"
 | 
			
		||||
      "updateSuccess": "Настройки {group} успешно обновлены",
 | 
			
		||||
      "smtpTestFailed": "Ошибка SMTP-соединения: {error}",
 | 
			
		||||
      "smtpTestGenericError": "Не удалось проверить SMTP-соединение. Пожалуйста, проверьте настройки и попробуйте снова.",
 | 
			
		||||
      "smtpTestSuccess": "SMTP-соединение успешно установлено! Ваша конфигурация электронной почты работает корректно."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Настройки",
 | 
			
		||||
    "breadcrumb": "Настройки",
 | 
			
		||||
    "pageTitle": "Настройки",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Digite um escopo e pressione Enter para adicionar",
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar"
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar",
 | 
			
		||||
      "testSmtp": "Проверяет SMTP-соединение с текущими значениями в форме. Чтобы сохранить изменения, не забудьте сохранить настройки после тестирования."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://meusite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Ошибка при обновлении файла",
 | 
			
		||||
        "previewNotAvailable": "Предпросмотр недоступен",
 | 
			
		||||
        "copyError": "Ошибка копирования файла в ваши файлы",
 | 
			
		||||
        "copySuccess": "Файл успешно скопирован в ваши файлы"
 | 
			
		||||
        "copySuccess": "Файл успешно скопирован в ваши файлы",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 файл выбран} other {# файлов выбрано}}",
 | 
			
		||||
          "actions": "Действия",
 | 
			
		||||
          "download": "Скачать выбранные",
 | 
			
		||||
          "copyToMyFiles": "Копировать выбранные в мои файлы",
 | 
			
		||||
          "delete": "Удалить выбранные"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Копирование {count, plural, =1 {1 файла} other {# файлов}} в ваши файлы...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 файл успешно скопирован в ваши файлы} other {# файлов успешно скопировано в ваши файлы}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "Удалить {count, plural, =1 {файл} other {файлы}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "Вы уверены, что хотите удалить {count, plural, =1 {этот файл} other {эти # файлов}}? Это действие нельзя отменить.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Удалить выбранные файлы",
 | 
			
		||||
        "bulkDeleteProgress": "Удаление {count, plural, =1 {1 файла} other {# файлов}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 файл успешно удален} other {# файлов успешно удалено}}",
 | 
			
		||||
        "selectAll": "Выбрать все",
 | 
			
		||||
        "selectFile": "Выбрать файл {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "Yönetici E-posta Alanları",
 | 
			
		||||
        "description": "Otomatik olarak yönetici ayrıcalıkları alan e-posta alanları"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "SMTP Bağlantısını Test Et",
 | 
			
		||||
        "description": "SMTP yapılandırmasının geçerli olup olmadığını test et"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "{group} Kaydet"
 | 
			
		||||
      "save": "{group} Kaydet",
 | 
			
		||||
      "testSmtp": "Bağlantıyı Test Et",
 | 
			
		||||
      "testing": "Test Ediliyor..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "Ayarlar yüklenemedi",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "Kaydedilecek değişiklik yok",
 | 
			
		||||
      "updateSuccess": "{group} ayarları başarıyla güncellendi"
 | 
			
		||||
      "updateSuccess": "{group} ayarları başarıyla güncellendi",
 | 
			
		||||
      "smtpTestFailed": "SMTP bağlantısı başarısız: {error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP bağlantısı test edilemedi. Lütfen ayarlarınızı kontrol edin ve tekrar deneyin.",
 | 
			
		||||
      "smtpTestSuccess": "SMTP bağlantısı başarılı! E-posta yapılandırmanız doğru çalışıyor."
 | 
			
		||||
    },
 | 
			
		||||
    "title": "Ayarlar",
 | 
			
		||||
    "breadcrumb": "Ayarlar",
 | 
			
		||||
    "pageTitle": "Ayarlar",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Bir kapsam girin ve eklemek için Enter'a basın",
 | 
			
		||||
      "oidcAdminEmailDomains": "Bir alan girin ve eklemek için Enter'a basın"
 | 
			
		||||
      "oidcAdminEmailDomains": "Bir alan girin ve eklemek için Enter'a basın",
 | 
			
		||||
      "testSmtp": "Formda şu anda girilen değerlerle SMTP bağlantısını test eder. Değişiklikleri kalıcı yapmak için test ettikten sonra ayarlarınızı kaydetmeyi unutmayın."
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://sitem.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "Dosya güncellenirken hata oluştu",
 | 
			
		||||
        "previewNotAvailable": "Önizleme mevcut değil",
 | 
			
		||||
        "copyError": "Dosyalarınıza dosyayı kopyalama hatası",
 | 
			
		||||
        "copySuccess": "Dosyalarınıza başarılı bir şekilde kopyalanan dosya"
 | 
			
		||||
        "copySuccess": "Dosyalarınıza başarılı bir şekilde kopyalanan dosya",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {1 dosya seçildi} other {# dosya seçildi}}",
 | 
			
		||||
          "actions": "İşlemler",
 | 
			
		||||
          "download": "Seçilenleri İndir",
 | 
			
		||||
          "copyToMyFiles": "Seçilenleri Dosyalarıma Kopyala",
 | 
			
		||||
          "delete": "Seçilenleri Sil"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "Dosyalarınıza {count, plural, =1 {1 dosya} other {# dosya}} kopyalanıyor...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1 dosya dosyalarınıza başarıyla kopyalandı} other {# dosya dosyalarınıza başarıyla kopyalandı}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "{count, plural, =1 {Dosyayı} other {Dosyaları}} Sil",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "{count, plural, =1 {Bu dosyayı} other {Bu # dosyayı}} silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "Seçili Dosyaları Sil",
 | 
			
		||||
        "bulkDeleteProgress": "{count, plural, =1 {1 dosya} other {# dosya}} siliniyor...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1 dosya başarıyla silindi} other {# dosya başarıyla silindi}}",
 | 
			
		||||
        "selectAll": "Tümünü seç",
 | 
			
		||||
        "selectFile": "{fileName} dosyasını seç"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -528,10 +528,16 @@
 | 
			
		||||
      "oidcAdminEmailDomains": {
 | 
			
		||||
        "title": "管理员电子邮件域名",
 | 
			
		||||
        "description": "自动获得管理员权限的电子邮件域名"
 | 
			
		||||
      },
 | 
			
		||||
      "testSmtp": {
 | 
			
		||||
        "title": "测试SMTP连接",
 | 
			
		||||
        "description": "测试SMTP配置是否有效"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "save": "保存 {group}"
 | 
			
		||||
      "save": "保存 {group}",
 | 
			
		||||
      "testSmtp": "测试连接",
 | 
			
		||||
      "testing": "测试中..."
 | 
			
		||||
    },
 | 
			
		||||
    "errors": {
 | 
			
		||||
      "loadFailed": "加载设置失败",
 | 
			
		||||
@@ -539,14 +545,18 @@
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "noChanges": "没有需要保存的更改",
 | 
			
		||||
      "updateSuccess": "{group}设置更新成功"
 | 
			
		||||
      "updateSuccess": "{group}设置更新成功",
 | 
			
		||||
      "smtpTestFailed": "SMTP连接失败:{error}",
 | 
			
		||||
      "smtpTestGenericError": "SMTP连接测试失败。请检查您的设置并重试。",
 | 
			
		||||
      "smtpTestSuccess": "SMTP连接成功!您的电子邮件配置工作正常。"
 | 
			
		||||
    },
 | 
			
		||||
    "title": "设置",
 | 
			
		||||
    "breadcrumb": "设置",
 | 
			
		||||
    "pageTitle": "设置",
 | 
			
		||||
    "tooltips": {
 | 
			
		||||
      "oidcScope": "Digite um escopo e pressione Enter para adicionar",
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar"
 | 
			
		||||
      "oidcAdminEmailDomains": "Digite um domínio e pressione Enter para adicionar",
 | 
			
		||||
      "testSmtp": "使用当前在表单中输入的值测试SMTP连接。要使更改永久生效,请记得在测试后保存您的设置。"
 | 
			
		||||
    },
 | 
			
		||||
    "redirectUri": {
 | 
			
		||||
      "placeholder": "https://meusite.com",
 | 
			
		||||
@@ -1189,7 +1199,23 @@
 | 
			
		||||
        "editError": "更新文件时出错",
 | 
			
		||||
        "previewNotAvailable": "预览不可用",
 | 
			
		||||
        "copyError": "错误将文件复制到您的文件",
 | 
			
		||||
        "copySuccess": "文件已成功复制到您的文件"
 | 
			
		||||
        "copySuccess": "文件已成功复制到您的文件",
 | 
			
		||||
        "bulkActions": {
 | 
			
		||||
          "selected": "{count, plural, =1 {已选择1个文件} other {已选择#个文件}}",
 | 
			
		||||
          "actions": "操作",
 | 
			
		||||
          "download": "下载所选",
 | 
			
		||||
          "copyToMyFiles": "复制所选到我的文件",
 | 
			
		||||
          "delete": "删除所选"
 | 
			
		||||
        },
 | 
			
		||||
        "bulkCopyProgress": "正在将{count, plural, =1 {1个文件} other {#个文件}}复制到您的文件...",
 | 
			
		||||
        "bulkCopySuccess": "{count, plural, =1 {1个文件已成功复制到您的文件} other {#个文件已成功复制到您的文件}}",
 | 
			
		||||
        "bulkDeleteConfirmButton": "删除{count, plural, =1 {文件} other {文件}}",
 | 
			
		||||
        "bulkDeleteConfirmMessage": "您确定要删除{count, plural, =1 {这个文件} other {这些#个文件}}吗?此操作无法撤消。",
 | 
			
		||||
        "bulkDeleteConfirmTitle": "删除所选文件",
 | 
			
		||||
        "bulkDeleteProgress": "正在删除{count, plural, =1 {1个文件} other {#个文件}}...",
 | 
			
		||||
        "bulkDeleteSuccess": "{count, plural, =1 {1个文件已成功删除} other {#个文件已成功删除}}",
 | 
			
		||||
        "selectAll": "全选",
 | 
			
		||||
        "selectFile": "选择文件 {fileName}"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "form": {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,8 @@
 | 
			
		||||
    "translations": "python3 scripts/run_translations.py all",
 | 
			
		||||
    "translations:check": "python3 scripts/run_translations.py check",
 | 
			
		||||
    "translations:sync": "python3 scripts/run_translations.py sync",
 | 
			
		||||
    "translations:translate": "python3 scripts/run_translations.py translate",
 | 
			
		||||
    "translations:help": "python3 scripts/run_translations.py help",
 | 
			
		||||
    "translations:dry-run": "python3 scripts/run_translations.py all --dry-run"
 | 
			
		||||
    "translations:dry-run": "python3 scripts/run_translations.py all --dry-run",
 | 
			
		||||
    "translations:help": "python3 scripts/run_translations.py help"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@hookform/resolvers": "^5.0.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
"""
 | 
			
		||||
Main script to run all Palmr translation operations.
 | 
			
		||||
Main script to run Palmr translation management operations.
 | 
			
		||||
Makes it easy to run scripts without remembering specific names.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
@@ -17,25 +17,65 @@ def run_command(script_name: str, args: list) -> int:
 | 
			
		||||
    return subprocess.run(cmd).returncode
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def filter_args_for_script(script_name: str, args: list) -> list:
 | 
			
		||||
    """Filter arguments based on what each script accepts."""
 | 
			
		||||
    
 | 
			
		||||
    # Arguments that check_translations.py accepts
 | 
			
		||||
    check_args = ['--messages-dir', '--reference']
 | 
			
		||||
    
 | 
			
		||||
    # Arguments that sync_translations.py accepts  
 | 
			
		||||
    sync_args = ['--messages-dir', '--reference', '--no-mark-untranslated', '--dry-run']
 | 
			
		||||
    
 | 
			
		||||
    if script_name == 'check_translations.py':
 | 
			
		||||
        filtered = []
 | 
			
		||||
        skip_next = False
 | 
			
		||||
        for i, arg in enumerate(args):
 | 
			
		||||
            if skip_next:
 | 
			
		||||
                skip_next = False
 | 
			
		||||
                continue
 | 
			
		||||
            if arg in check_args:
 | 
			
		||||
                filtered.append(arg)
 | 
			
		||||
                # Add the value for the argument if it exists
 | 
			
		||||
                if i + 1 < len(args) and not args[i + 1].startswith('--'):
 | 
			
		||||
                    filtered.append(args[i + 1])
 | 
			
		||||
                    skip_next = True
 | 
			
		||||
        return filtered
 | 
			
		||||
    
 | 
			
		||||
    elif script_name == 'sync_translations.py':
 | 
			
		||||
        filtered = []
 | 
			
		||||
        skip_next = False
 | 
			
		||||
        for i, arg in enumerate(args):
 | 
			
		||||
            if skip_next:
 | 
			
		||||
                skip_next = False
 | 
			
		||||
                continue
 | 
			
		||||
            if arg in sync_args:
 | 
			
		||||
                filtered.append(arg)
 | 
			
		||||
                # Add the value for the argument if it exists
 | 
			
		||||
                if i + 1 < len(args) and not args[i + 1].startswith('--'):
 | 
			
		||||
                    filtered.append(args[i + 1])
 | 
			
		||||
                    skip_next = True
 | 
			
		||||
        return filtered
 | 
			
		||||
    
 | 
			
		||||
    return args
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description='Main script to manage Palmr translations',
 | 
			
		||||
        epilog='Examples:\n'
 | 
			
		||||
               '  python3 run_translations.py check\n'
 | 
			
		||||
               '  python3 run_translations.py sync --dry-run\n'
 | 
			
		||||
               '  python3 run_translations.py translate --delay 2.0\n'
 | 
			
		||||
               '  python3 run_translations.py all  # Complete workflow\n',
 | 
			
		||||
               '  python3 run_translations.py all --dry-run\n',
 | 
			
		||||
        formatter_class=argparse.RawDescriptionHelpFormatter
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        'command',
 | 
			
		||||
        choices=['check', 'sync', 'translate', 'all', 'help'],
 | 
			
		||||
        choices=['check', 'sync', 'all', 'help'],
 | 
			
		||||
        help='Command to execute:\n'
 | 
			
		||||
             'check - Check translation status\n'
 | 
			
		||||
             'sync - Synchronize missing keys\n' 
 | 
			
		||||
             'translate - Automatically translate strings\n'
 | 
			
		||||
             'all - Run complete workflow\n'
 | 
			
		||||
             'all - Run complete workflow (sync + check)\n'
 | 
			
		||||
             'help - Show detailed help'
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
@@ -57,13 +97,7 @@ def main():
 | 
			
		||||
        print("   python3 run_translations.py sync --dry-run")
 | 
			
		||||
        print("   python3 run_translations.py sync --no-mark-untranslated")
 | 
			
		||||
        print()
 | 
			
		||||
        print("🌐 translate - Automatically translate")
 | 
			
		||||
        print("   python3 run_translations.py translate")
 | 
			
		||||
        print("   python3 run_translations.py translate --dry-run")
 | 
			
		||||
        print("   python3 run_translations.py translate --delay 2.0")
 | 
			
		||||
        print("   python3 run_translations.py translate --skip-languages pt-BR.json")
 | 
			
		||||
        print()
 | 
			
		||||
        print("⚡ all - Complete workflow (sync + translate)")
 | 
			
		||||
        print("⚡ all - Complete workflow (sync + check)")
 | 
			
		||||
        print("   python3 run_translations.py all")
 | 
			
		||||
        print("   python3 run_translations.py all --dry-run")
 | 
			
		||||
        print()
 | 
			
		||||
@@ -72,22 +106,21 @@ def main():
 | 
			
		||||
        print("   apps/web/messages/   - Translation files")
 | 
			
		||||
        print()
 | 
			
		||||
        print("💡 TIPS:")
 | 
			
		||||
        print("• Use --dry-run on any command to test")
 | 
			
		||||
        print("• Use --dry-run on sync or all commands to test")
 | 
			
		||||
        print("• Use --help on any command for specific options")
 | 
			
		||||
        print("• Read https://docs.palmr.dev/docs/3.0-beta/translation-management for complete documentation")
 | 
			
		||||
        print("• Manually translate strings marked with [TO_TRANSLATE]")
 | 
			
		||||
        print("• Read documentation for complete translation guidelines")
 | 
			
		||||
        return 0
 | 
			
		||||
    
 | 
			
		||||
    elif args.command == 'check':
 | 
			
		||||
        print("🔍 Checking translation status...")
 | 
			
		||||
        return run_command('check_translations.py', remaining_args)
 | 
			
		||||
        filtered_args = filter_args_for_script('check_translations.py', remaining_args)
 | 
			
		||||
        return run_command('check_translations.py', filtered_args)
 | 
			
		||||
    
 | 
			
		||||
    elif args.command == 'sync':
 | 
			
		||||
        print("🔄 Synchronizing translation keys...")
 | 
			
		||||
        return run_command('sync_translations.py', remaining_args)
 | 
			
		||||
    
 | 
			
		||||
    elif args.command == 'translate':
 | 
			
		||||
        print("🌐 Automatically translating strings...")
 | 
			
		||||
        return run_command('translate_missing.py', remaining_args)
 | 
			
		||||
        filtered_args = filter_args_for_script('sync_translations.py', remaining_args)
 | 
			
		||||
        return run_command('sync_translations.py', filtered_args)
 | 
			
		||||
    
 | 
			
		||||
    elif args.command == 'all':
 | 
			
		||||
        print("⚡ Running complete translation workflow...")
 | 
			
		||||
@@ -98,7 +131,8 @@ def main():
 | 
			
		||||
        
 | 
			
		||||
        # 1. Initial check
 | 
			
		||||
        print("1️⃣ Checking initial status...")
 | 
			
		||||
        result = run_command('check_translations.py', remaining_args)
 | 
			
		||||
        check_args = filter_args_for_script('check_translations.py', remaining_args)
 | 
			
		||||
        result = run_command('check_translations.py', check_args)
 | 
			
		||||
        if result != 0:
 | 
			
		||||
            print("❌ Error in initial check")
 | 
			
		||||
            return result
 | 
			
		||||
@@ -107,26 +141,18 @@ def main():
 | 
			
		||||
        
 | 
			
		||||
        # 2. Sync
 | 
			
		||||
        print("2️⃣ Synchronizing missing keys...")
 | 
			
		||||
        result = run_command('sync_translations.py', remaining_args)
 | 
			
		||||
        sync_args = filter_args_for_script('sync_translations.py', remaining_args)
 | 
			
		||||
        result = run_command('sync_translations.py', sync_args)
 | 
			
		||||
        if result != 0:
 | 
			
		||||
            print("❌ Error in synchronization")
 | 
			
		||||
            return result
 | 
			
		||||
        
 | 
			
		||||
        if not is_dry_run:
 | 
			
		||||
        print("\n" + "="*50)
 | 
			
		||||
        
 | 
			
		||||
            # 3. Translate
 | 
			
		||||
            print("3️⃣ Automatically translating strings...")
 | 
			
		||||
            result = run_command('translate_missing.py', remaining_args)
 | 
			
		||||
            if result != 0:
 | 
			
		||||
                print("❌ Error in translation")
 | 
			
		||||
                return result
 | 
			
		||||
            
 | 
			
		||||
            print("\n" + "="*50)
 | 
			
		||||
            
 | 
			
		||||
            # 4. Final check
 | 
			
		||||
            print("4️⃣ Final check...")
 | 
			
		||||
            result = run_command('check_translations.py', remaining_args)
 | 
			
		||||
        # 3. Final check
 | 
			
		||||
        print("3️⃣ Final check...")
 | 
			
		||||
        check_args = filter_args_for_script('check_translations.py', remaining_args)
 | 
			
		||||
        result = run_command('check_translations.py', check_args)
 | 
			
		||||
        if result != 0:
 | 
			
		||||
            print("❌ Error in final check")
 | 
			
		||||
            return result
 | 
			
		||||
@@ -134,6 +160,8 @@ def main():
 | 
			
		||||
        print("\n🎉 Complete workflow executed successfully!")
 | 
			
		||||
        if is_dry_run:
 | 
			
		||||
            print("💡 Run without --dry-run to apply changes")
 | 
			
		||||
        else:
 | 
			
		||||
            print("💡 Review strings marked with [TO_TRANSLATE] and translate them manually")
 | 
			
		||||
        
 | 
			
		||||
        return 0
 | 
			
		||||
    
 | 
			
		||||
 
 | 
			
		||||
@@ -1,342 +0,0 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
"""
 | 
			
		||||
Script to automatically translate strings marked with [TO_TRANSLATE] 
 | 
			
		||||
using free Google Translate.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import time
 | 
			
		||||
import re
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Dict, Any, List, Tuple, Optional
 | 
			
		||||
import argparse
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
# Language code mapping from file names to Google Translate codes
 | 
			
		||||
LANGUAGE_MAPPING = {
 | 
			
		||||
    'pt-BR.json': 'pt',      # Portuguese (Brazil) -> Portuguese
 | 
			
		||||
    'es-ES.json': 'es',      # Spanish (Spain) -> Spanish
 | 
			
		||||
    'fr-FR.json': 'fr',      # French (France) -> French
 | 
			
		||||
    'de-DE.json': 'de',      # German -> German
 | 
			
		||||
    'it-IT.json': 'it',      # Italian -> Italian
 | 
			
		||||
    'ru-RU.json': 'ru',      # Russian -> Russian
 | 
			
		||||
    'ja-JP.json': 'ja',      # Japanese -> Japanese
 | 
			
		||||
    'ko-KR.json': 'ko',      # Korean -> Korean
 | 
			
		||||
    'zh-CN.json': 'zh-cn',   # Chinese (Simplified) -> Simplified Chinese
 | 
			
		||||
    'ar-SA.json': 'ar',      # Arabic -> Arabic
 | 
			
		||||
    'hi-IN.json': 'hi',      # Hindi -> Hindi
 | 
			
		||||
    'nl-NL.json': 'nl',      # Dutch -> Dutch
 | 
			
		||||
    'tr-TR.json': 'tr',      # Turkish -> Turkish
 | 
			
		||||
    'pl-PL.json': 'pl',      # Polish -> Polish
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Prefix to identify untranslated strings
 | 
			
		||||
TO_TRANSLATE_PREFIX = '[TO_TRANSLATE] '
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def load_json_file(file_path: Path) -> Dict[str, Any]:
 | 
			
		||||
    """Load a JSON file."""
 | 
			
		||||
    try:
 | 
			
		||||
        with open(file_path, 'r', encoding='utf-8') as f:
 | 
			
		||||
            return json.load(f)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"❌ Error loading {file_path}: {e}")
 | 
			
		||||
        return {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def save_json_file(file_path: Path, data: Dict[str, Any], indent: int = 2) -> bool:
 | 
			
		||||
    """Save a JSON file with consistent formatting."""
 | 
			
		||||
    try:
 | 
			
		||||
        with open(file_path, 'w', encoding='utf-8') as f:
 | 
			
		||||
            json.dump(data, f, ensure_ascii=False, indent=indent, separators=(',', ': '))
 | 
			
		||||
            f.write('\n')  # Add newline at the end
 | 
			
		||||
        return True
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        print(f"❌ Error saving {file_path}: {e}")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_nested_value(data: Dict[str, Any], key_path: str) -> Any:
 | 
			
		||||
    """Get a nested value using a key with dots as separator."""
 | 
			
		||||
    keys = key_path.split('.')
 | 
			
		||||
    current = data
 | 
			
		||||
    
 | 
			
		||||
    for key in keys:
 | 
			
		||||
        if isinstance(current, dict) and key in current:
 | 
			
		||||
            current = current[key]
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
    
 | 
			
		||||
    return current
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_nested_value(data: Dict[str, Any], key_path: str, value: Any) -> None:
 | 
			
		||||
    """Set a nested value using a key with dots as separator."""
 | 
			
		||||
    keys = key_path.split('.')
 | 
			
		||||
    current = data
 | 
			
		||||
    
 | 
			
		||||
    # Navigate to the second-to-last level, creating dictionaries as needed
 | 
			
		||||
    for key in keys[:-1]:
 | 
			
		||||
        if key not in current:
 | 
			
		||||
            current[key] = {}
 | 
			
		||||
        elif not isinstance(current[key], dict):
 | 
			
		||||
            current[key] = {}
 | 
			
		||||
        current = current[key]
 | 
			
		||||
    
 | 
			
		||||
    # Set the value at the last level
 | 
			
		||||
    current[keys[-1]] = value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_untranslated_strings(data: Dict[str, Any], prefix: str = '') -> List[Tuple[str, str]]:
 | 
			
		||||
    """Find all strings marked with [TO_TRANSLATE] recursively."""
 | 
			
		||||
    untranslated = []
 | 
			
		||||
    
 | 
			
		||||
    for key, value in data.items():
 | 
			
		||||
        current_key = f"{prefix}.{key}" if prefix else key
 | 
			
		||||
        
 | 
			
		||||
        if isinstance(value, str) and value.startswith(TO_TRANSLATE_PREFIX):
 | 
			
		||||
            # Remove prefix to get original text
 | 
			
		||||
            original_text = value[len(TO_TRANSLATE_PREFIX):].strip()
 | 
			
		||||
            untranslated.append((current_key, original_text))
 | 
			
		||||
        elif isinstance(value, dict):
 | 
			
		||||
            untranslated.extend(find_untranslated_strings(value, current_key))
 | 
			
		||||
    
 | 
			
		||||
    return untranslated
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def install_googletrans():
 | 
			
		||||
    """Install the googletrans library if not available."""
 | 
			
		||||
    try:
 | 
			
		||||
        import googletrans
 | 
			
		||||
        return True
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        print("📦 'googletrans' library not found. Attempting to install...")
 | 
			
		||||
        import subprocess
 | 
			
		||||
        try:
 | 
			
		||||
            subprocess.check_call([sys.executable, "-m", "pip", "install", "googletrans==4.0.0rc1"])
 | 
			
		||||
            print("✅ googletrans installed successfully!")
 | 
			
		||||
            return True
 | 
			
		||||
        except subprocess.CalledProcessError:
 | 
			
		||||
            print("❌ Failed to install googletrans. Install manually with:")
 | 
			
		||||
            print("pip install googletrans==4.0.0rc1")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def translate_text(text: str, target_language: str, max_retries: int = 3) -> Optional[str]:
 | 
			
		||||
    """Translate text using free Google Translate."""
 | 
			
		||||
    try:
 | 
			
		||||
        from googletrans import Translator
 | 
			
		||||
        
 | 
			
		||||
        translator = Translator()
 | 
			
		||||
        
 | 
			
		||||
        for attempt in range(max_retries):
 | 
			
		||||
            try:
 | 
			
		||||
                # Translate from English to target language
 | 
			
		||||
                result = translator.translate(text, src='en', dest=target_language)
 | 
			
		||||
                
 | 
			
		||||
                if result and result.text:
 | 
			
		||||
                    return result.text.strip()
 | 
			
		||||
                    
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                if attempt < max_retries - 1:
 | 
			
		||||
                    print(f"  ⚠️  Attempt {attempt + 1} failed: {str(e)[:50]}... Retrying in 2s...")
 | 
			
		||||
                    time.sleep(2)
 | 
			
		||||
                else:
 | 
			
		||||
                    print(f"  ❌ Failed after {max_retries} attempts: {str(e)[:50]}...")
 | 
			
		||||
                    
 | 
			
		||||
        return None
 | 
			
		||||
        
 | 
			
		||||
    except ImportError:
 | 
			
		||||
        print("❌ googletrans library not available")
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def translate_file(file_path: Path, target_language: str, dry_run: bool = False, 
 | 
			
		||||
                  delay_between_requests: float = 1.0) -> Tuple[int, int, int]:
 | 
			
		||||
    """
 | 
			
		||||
    Translate all [TO_TRANSLATE] strings in a file.
 | 
			
		||||
    Returns: (total_found, successful_translations, failed_translations)
 | 
			
		||||
    """
 | 
			
		||||
    print(f"🔍 Processing: {file_path.name}")
 | 
			
		||||
    
 | 
			
		||||
    # Load file
 | 
			
		||||
    data = load_json_file(file_path)
 | 
			
		||||
    if not data:
 | 
			
		||||
        return 0, 0, 0
 | 
			
		||||
    
 | 
			
		||||
    # Find untranslated strings
 | 
			
		||||
    untranslated_strings = find_untranslated_strings(data)
 | 
			
		||||
    
 | 
			
		||||
    if not untranslated_strings:
 | 
			
		||||
        print(f"  ✅ No strings to translate")
 | 
			
		||||
        return 0, 0, 0
 | 
			
		||||
    
 | 
			
		||||
    print(f"  📝 Found {len(untranslated_strings)} strings to translate")
 | 
			
		||||
    
 | 
			
		||||
    if dry_run:
 | 
			
		||||
        print(f"  🔍 [DRY RUN] Strings that would be translated:")
 | 
			
		||||
        for key, text in untranslated_strings[:3]:
 | 
			
		||||
            print(f"    - {key}: \"{text[:50]}{'...' if len(text) > 50 else ''}\"")
 | 
			
		||||
        if len(untranslated_strings) > 3:
 | 
			
		||||
            print(f"    ... and {len(untranslated_strings) - 3} more")
 | 
			
		||||
        return len(untranslated_strings), 0, 0
 | 
			
		||||
    
 | 
			
		||||
    # Translate each string
 | 
			
		||||
    successful = 0
 | 
			
		||||
    failed = 0
 | 
			
		||||
    updated_data = data.copy()
 | 
			
		||||
    
 | 
			
		||||
    for i, (key_path, original_text) in enumerate(untranslated_strings, 1):
 | 
			
		||||
        print(f"  📍 ({i}/{len(untranslated_strings)}) Translating: {key_path}")
 | 
			
		||||
        
 | 
			
		||||
        # Translate text
 | 
			
		||||
        translated_text = translate_text(original_text, target_language)
 | 
			
		||||
        
 | 
			
		||||
        if translated_text and translated_text != original_text:
 | 
			
		||||
            # Update in dictionary
 | 
			
		||||
            set_nested_value(updated_data, key_path, translated_text)
 | 
			
		||||
            successful += 1
 | 
			
		||||
            print(f"    ✅ \"{original_text[:30]}...\" → \"{translated_text[:30]}...\"")
 | 
			
		||||
        else:
 | 
			
		||||
            failed += 1
 | 
			
		||||
            print(f"    ❌ Translation failed")
 | 
			
		||||
        
 | 
			
		||||
        # Delay between requests to avoid rate limiting
 | 
			
		||||
        if i < len(untranslated_strings):  # Don't wait after the last one
 | 
			
		||||
            time.sleep(delay_between_requests)
 | 
			
		||||
    
 | 
			
		||||
    # Save updated file
 | 
			
		||||
    if successful > 0:
 | 
			
		||||
        if save_json_file(file_path, updated_data):
 | 
			
		||||
            print(f"  💾 File saved with {successful} translations")
 | 
			
		||||
        else:
 | 
			
		||||
            print(f"  ❌ Error saving file")
 | 
			
		||||
            failed += successful  # Count as failure if couldn't save
 | 
			
		||||
            successful = 0
 | 
			
		||||
    
 | 
			
		||||
    return len(untranslated_strings), successful, failed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def translate_all_files(messages_dir: Path, delay_between_requests: float = 1.0, 
 | 
			
		||||
                       dry_run: bool = False, skip_languages: List[str] = None) -> None:
 | 
			
		||||
    """Translate all language files that have [TO_TRANSLATE] strings."""
 | 
			
		||||
    
 | 
			
		||||
    if not install_googletrans():
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    skip_languages = skip_languages or []
 | 
			
		||||
    
 | 
			
		||||
    # Find language JSON files
 | 
			
		||||
    language_files = []
 | 
			
		||||
    for file_name, lang_code in LANGUAGE_MAPPING.items():
 | 
			
		||||
        file_path = messages_dir / file_name
 | 
			
		||||
        if file_path.exists() and file_name not in skip_languages:
 | 
			
		||||
            language_files.append((file_path, lang_code))
 | 
			
		||||
    
 | 
			
		||||
    if not language_files:
 | 
			
		||||
        print("❌ No language files found")
 | 
			
		||||
        return
 | 
			
		||||
    
 | 
			
		||||
    print(f"🌍 Translating {len(language_files)} languages...")
 | 
			
		||||
    print(f"⏱️  Delay between requests: {delay_between_requests}s")
 | 
			
		||||
    if dry_run:
 | 
			
		||||
        print("🔍 DRY RUN MODE - No changes will be made")
 | 
			
		||||
    print("-" * 60)
 | 
			
		||||
    
 | 
			
		||||
    total_found = 0
 | 
			
		||||
    total_successful = 0
 | 
			
		||||
    total_failed = 0
 | 
			
		||||
    
 | 
			
		||||
    for i, (file_path, lang_code) in enumerate(language_files, 1):
 | 
			
		||||
        print(f"\n[{i}/{len(language_files)}] 🌐 Language: {lang_code.upper()}")
 | 
			
		||||
        
 | 
			
		||||
        found, successful, failed = translate_file(
 | 
			
		||||
            file_path, lang_code, dry_run, delay_between_requests
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
        total_found += found
 | 
			
		||||
        total_successful += successful
 | 
			
		||||
        total_failed += failed
 | 
			
		||||
        
 | 
			
		||||
        # Pause between files (except the last one)
 | 
			
		||||
        if i < len(language_files) and not dry_run:
 | 
			
		||||
            print(f"  ⏸️  Pausing {delay_between_requests * 2}s before next language...")
 | 
			
		||||
            time.sleep(delay_between_requests * 2)
 | 
			
		||||
    
 | 
			
		||||
    # Final summary
 | 
			
		||||
    print("\n" + "=" * 60)
 | 
			
		||||
    print("📊 FINAL SUMMARY")
 | 
			
		||||
    print("=" * 60)
 | 
			
		||||
    
 | 
			
		||||
    if dry_run:
 | 
			
		||||
        print(f"🔍 DRY RUN MODE:")
 | 
			
		||||
        print(f"   • {total_found} strings would be translated")
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"✅ Translations performed:")
 | 
			
		||||
        print(f"   • {total_successful} successes")
 | 
			
		||||
        print(f"   • {total_failed} failures")
 | 
			
		||||
        print(f"   • {total_found} total processed")
 | 
			
		||||
        
 | 
			
		||||
        if total_successful > 0:
 | 
			
		||||
            success_rate = (total_successful / total_found) * 100
 | 
			
		||||
            print(f"   • Success rate: {success_rate:.1f}%")
 | 
			
		||||
    
 | 
			
		||||
    print("\n💡 TIPS:")
 | 
			
		||||
    print("• Run 'python3 check_translations.py' to verify results")
 | 
			
		||||
    print("• Strings that failed translation keep the [TO_TRANSLATE] prefix")
 | 
			
		||||
    print("• Consider reviewing automatic translations to ensure quality")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description='Automatically translate strings marked with [TO_TRANSLATE]'
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '--messages-dir', 
 | 
			
		||||
        type=Path,
 | 
			
		||||
        default=Path(__file__).parent.parent / 'messages',
 | 
			
		||||
        help='Directory containing message files (default: ../messages)'
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '--dry-run', 
 | 
			
		||||
        action='store_true',
 | 
			
		||||
        help='Only show what would be translated without making changes'
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '--delay', 
 | 
			
		||||
        type=float,
 | 
			
		||||
        default=1.0,
 | 
			
		||||
        help='Delay in seconds between translation requests (default: 1.0)'
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '--skip-languages',
 | 
			
		||||
        nargs='*',
 | 
			
		||||
        default=[],
 | 
			
		||||
        help='List of languages to skip (ex: pt-BR.json fr-FR.json)'
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
    
 | 
			
		||||
    if not args.messages_dir.exists():
 | 
			
		||||
        print(f"❌ Directory not found: {args.messages_dir}")
 | 
			
		||||
        return 1
 | 
			
		||||
    
 | 
			
		||||
    print(f"📁 Directory: {args.messages_dir}")
 | 
			
		||||
    print(f"🔍 Dry run: {args.dry_run}")
 | 
			
		||||
    print(f"⏱️  Delay: {args.delay}s")
 | 
			
		||||
    if args.skip_languages:
 | 
			
		||||
        print(f"⏭️  Skipping: {', '.join(args.skip_languages)}")
 | 
			
		||||
    print("-" * 60)
 | 
			
		||||
    
 | 
			
		||||
    translate_all_files(
 | 
			
		||||
        messages_dir=args.messages_dir,
 | 
			
		||||
        delay_between_requests=args.delay,
 | 
			
		||||
        dry_run=args.dry_run,
 | 
			
		||||
        skip_languages=args.skip_languages
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    exit(main()) 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  IconCheck,
 | 
			
		||||
  IconChevronDown,
 | 
			
		||||
  IconClipboardCopy,
 | 
			
		||||
  IconDownload,
 | 
			
		||||
  IconEdit,
 | 
			
		||||
@@ -19,7 +20,21 @@ import { toast } from "sonner";
 | 
			
		||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
 | 
			
		||||
import { Badge } from "@/components/ui/badge";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
 | 
			
		||||
import { Checkbox } from "@/components/ui/checkbox";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
} from "@/components/ui/dialog";
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
} from "@/components/ui/dropdown-menu";
 | 
			
		||||
import { Input } from "@/components/ui/input";
 | 
			
		||||
import { ScrollArea } from "@/components/ui/scroll-area";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
@@ -262,6 +277,7 @@ interface FileRowProps {
 | 
			
		||||
  inputRef: React.RefObject<HTMLInputElement | null>;
 | 
			
		||||
  hoveredFile: HoverState | null;
 | 
			
		||||
  copyingFile: string | null;
 | 
			
		||||
  isSelected: boolean;
 | 
			
		||||
  onStartEdit: (fileId: string, field: string, currentValue: string) => void;
 | 
			
		||||
  onSaveEdit: () => void;
 | 
			
		||||
  onCancelEdit: () => void;
 | 
			
		||||
@@ -272,6 +288,7 @@ interface FileRowProps {
 | 
			
		||||
  onDownload: (file: ReverseShareFile) => void;
 | 
			
		||||
  onDelete: (file: ReverseShareFile) => void;
 | 
			
		||||
  onCopy: (file: ReverseShareFile) => void;
 | 
			
		||||
  onSelectFile: (fileId: string, checked: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function FileRow({
 | 
			
		||||
@@ -281,6 +298,7 @@ function FileRow({
 | 
			
		||||
  inputRef,
 | 
			
		||||
  hoveredFile,
 | 
			
		||||
  copyingFile,
 | 
			
		||||
  isSelected,
 | 
			
		||||
  onStartEdit,
 | 
			
		||||
  onSaveEdit,
 | 
			
		||||
  onCancelEdit,
 | 
			
		||||
@@ -291,12 +309,20 @@ function FileRow({
 | 
			
		||||
  onDownload,
 | 
			
		||||
  onDelete,
 | 
			
		||||
  onCopy,
 | 
			
		||||
  onSelectFile,
 | 
			
		||||
}: FileRowProps) {
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
  const { icon: FileIcon, color } = getFileIcon(file.name);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TableRow key={file.id}>
 | 
			
		||||
      <TableCell>
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          checked={isSelected}
 | 
			
		||||
          onCheckedChange={(checked: boolean) => onSelectFile(file.id, checked)}
 | 
			
		||||
          aria-label={t("reverseShares.modals.receivedFiles.selectFile", { fileName: file.name })}
 | 
			
		||||
        />
 | 
			
		||||
      </TableCell>
 | 
			
		||||
      <TableCell>
 | 
			
		||||
        <div className="flex items-center gap-3">
 | 
			
		||||
          <FileIcon className={`h-8 w-8 ${color} flex-shrink-0`} />
 | 
			
		||||
@@ -425,9 +451,19 @@ export function ReceivedFilesModal({
 | 
			
		||||
  const [previewFile, setPreviewFile] = useState<ReverseShareFile | null>(null);
 | 
			
		||||
  const [hoveredFile, setHoveredFile] = useState<HoverState | null>(null);
 | 
			
		||||
  const [copyingFile, setCopyingFile] = useState<string | null>(null);
 | 
			
		||||
  const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
 | 
			
		||||
  const [bulkCopying, setBulkCopying] = useState(false);
 | 
			
		||||
  const [bulkDeleting, setBulkDeleting] = useState(false);
 | 
			
		||||
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
 | 
			
		||||
  const [filesToDeleteBulk, setFilesToDeleteBulk] = useState<ReverseShareFile[]>([]);
 | 
			
		||||
 | 
			
		||||
  const { editingFile, editValue, setEditValue, inputRef, startEdit, cancelEdit } = useFileEdit();
 | 
			
		||||
 | 
			
		||||
  // Clear selections when files change
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setSelectedFiles(new Set());
 | 
			
		||||
  }, [reverseShare?.files]);
 | 
			
		||||
 | 
			
		||||
  const getTotalSize = () => {
 | 
			
		||||
    if (!reverseShare?.files) return "0 B";
 | 
			
		||||
    const totalBytes = reverseShare.files.reduce((acc, file) => acc + parseInt(file.size), 0);
 | 
			
		||||
@@ -548,6 +584,176 @@ export function ReceivedFilesModal({
 | 
			
		||||
 | 
			
		||||
  const files = reverseShare.files || [];
 | 
			
		||||
 | 
			
		||||
  const handleSelectAll = (checked: boolean) => {
 | 
			
		||||
    if (checked) {
 | 
			
		||||
      setSelectedFiles(new Set(files.map((file) => file.id)));
 | 
			
		||||
    } else {
 | 
			
		||||
      setSelectedFiles(new Set());
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSelectFile = (fileId: string, checked: boolean) => {
 | 
			
		||||
    const newSelected = new Set(selectedFiles);
 | 
			
		||||
    if (checked) {
 | 
			
		||||
      newSelected.add(fileId);
 | 
			
		||||
    } else {
 | 
			
		||||
      newSelected.delete(fileId);
 | 
			
		||||
    }
 | 
			
		||||
    setSelectedFiles(newSelected);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getSelectedFileObjects = () => {
 | 
			
		||||
    return files.filter((file) => selectedFiles.has(file.id));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const isAllSelected = files.length > 0 && selectedFiles.size === files.length;
 | 
			
		||||
 | 
			
		||||
  const handleBulkDownload = async () => {
 | 
			
		||||
    const selectedFileObjects = getSelectedFileObjects();
 | 
			
		||||
    if (selectedFileObjects.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      toast.promise(
 | 
			
		||||
        (async () => {
 | 
			
		||||
          const JSZip = (await import("jszip")).default;
 | 
			
		||||
          const zip = new JSZip();
 | 
			
		||||
 | 
			
		||||
          const downloadPromises = selectedFileObjects.map(async (file) => {
 | 
			
		||||
            try {
 | 
			
		||||
              const response = await downloadReverseShareFile(file.id);
 | 
			
		||||
              const downloadUrl = response.data.url;
 | 
			
		||||
              const fileResponse = await fetch(downloadUrl);
 | 
			
		||||
 | 
			
		||||
              if (!fileResponse.ok) {
 | 
			
		||||
                throw new Error(`Failed to download ${file.name}`);
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              const blob = await fileResponse.blob();
 | 
			
		||||
              zip.file(file.name, blob);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              console.error(`Error downloading file ${file.name}:`, error);
 | 
			
		||||
              throw error;
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await Promise.all(downloadPromises);
 | 
			
		||||
 | 
			
		||||
          const zipBlob = await zip.generateAsync({ type: "blob" });
 | 
			
		||||
          const zipName = `${reverseShare.name || "received_files"}_files.zip`;
 | 
			
		||||
 | 
			
		||||
          const url = URL.createObjectURL(zipBlob);
 | 
			
		||||
          const a = document.createElement("a");
 | 
			
		||||
          a.href = url;
 | 
			
		||||
          a.download = zipName;
 | 
			
		||||
          document.body.appendChild(a);
 | 
			
		||||
          a.click();
 | 
			
		||||
          document.body.removeChild(a);
 | 
			
		||||
          URL.revokeObjectURL(url);
 | 
			
		||||
 | 
			
		||||
          // Clear selections after successful download
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
        })(),
 | 
			
		||||
        {
 | 
			
		||||
          loading: t("shareManager.creatingZip"),
 | 
			
		||||
          success: t("shareManager.zipDownloadSuccess"),
 | 
			
		||||
          error: t("shareManager.zipDownloadError"),
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error("Error creating ZIP:", error);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleBulkCopyToMyFiles = async () => {
 | 
			
		||||
    const selectedFileObjects = getSelectedFileObjects();
 | 
			
		||||
    if (selectedFileObjects.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    toast.promise(
 | 
			
		||||
      (async () => {
 | 
			
		||||
        setBulkCopying(true);
 | 
			
		||||
        try {
 | 
			
		||||
          const copyPromises = selectedFileObjects.map(async (file) => {
 | 
			
		||||
            try {
 | 
			
		||||
              await copyReverseShareFileToUserFiles(file.id);
 | 
			
		||||
            } catch (error: any) {
 | 
			
		||||
              console.error(`Error copying file ${file.name}:`, error);
 | 
			
		||||
              throw new Error(`Failed to copy ${file.name}: ${error.response?.data?.error || error.message}`);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await Promise.all(copyPromises);
 | 
			
		||||
 | 
			
		||||
          // Clear selections after successful copy
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
        } finally {
 | 
			
		||||
          setBulkCopying(false);
 | 
			
		||||
        }
 | 
			
		||||
      })(),
 | 
			
		||||
      {
 | 
			
		||||
        loading: t("reverseShares.modals.receivedFiles.bulkCopyProgress", { count: selectedFileObjects.length }),
 | 
			
		||||
        success: t("reverseShares.modals.receivedFiles.bulkCopySuccess", { count: selectedFileObjects.length }),
 | 
			
		||||
        error: (error: any) => {
 | 
			
		||||
          if (error.message.includes("File size exceeds") || error.message.includes("Insufficient storage")) {
 | 
			
		||||
            return error.message;
 | 
			
		||||
          } else {
 | 
			
		||||
            return t("reverseShares.modals.receivedFiles.copyError");
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleBulkDelete = () => {
 | 
			
		||||
    const selectedFileObjects = getSelectedFileObjects();
 | 
			
		||||
    if (selectedFileObjects.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    setFilesToDeleteBulk(selectedFileObjects);
 | 
			
		||||
    setShowDeleteConfirm(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const confirmBulkDelete = async () => {
 | 
			
		||||
    if (filesToDeleteBulk.length === 0) return;
 | 
			
		||||
 | 
			
		||||
    setShowDeleteConfirm(false);
 | 
			
		||||
 | 
			
		||||
    toast.promise(
 | 
			
		||||
      (async () => {
 | 
			
		||||
        setBulkDeleting(true);
 | 
			
		||||
        try {
 | 
			
		||||
          const deletePromises = filesToDeleteBulk.map(async (file) => {
 | 
			
		||||
            try {
 | 
			
		||||
              await deleteReverseShareFile(file.id);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              console.error(`Error deleting file ${file.name}:`, error);
 | 
			
		||||
              throw new Error(`Failed to delete ${file.name}`);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await Promise.all(deletePromises);
 | 
			
		||||
 | 
			
		||||
          // Clear selections and refresh data
 | 
			
		||||
          setSelectedFiles(new Set());
 | 
			
		||||
          setFilesToDeleteBulk([]);
 | 
			
		||||
          if (onRefresh) {
 | 
			
		||||
            await onRefresh();
 | 
			
		||||
          }
 | 
			
		||||
          if (refreshReverseShare) {
 | 
			
		||||
            await refreshReverseShare(reverseShare.id);
 | 
			
		||||
          }
 | 
			
		||||
        } finally {
 | 
			
		||||
          setBulkDeleting(false);
 | 
			
		||||
        }
 | 
			
		||||
      })(),
 | 
			
		||||
      {
 | 
			
		||||
        loading: t("reverseShares.modals.receivedFiles.bulkDeleteProgress", { count: filesToDeleteBulk.length }),
 | 
			
		||||
        success: t("reverseShares.modals.receivedFiles.bulkDeleteSuccess", { count: filesToDeleteBulk.length }),
 | 
			
		||||
        error: "Error deleting selected files",
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const showBulkActions = selectedFiles.size > 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Dialog open={isOpen} onOpenChange={onClose}>
 | 
			
		||||
@@ -574,6 +780,59 @@ export function ReceivedFilesModal({
 | 
			
		||||
 | 
			
		||||
            <Separator />
 | 
			
		||||
 | 
			
		||||
            {showBulkActions && (
 | 
			
		||||
              <div className="flex items-center justify-between p-4 bg-muted/30 border rounded-lg">
 | 
			
		||||
                <div className="flex items-center gap-3">
 | 
			
		||||
                  <span className="text-sm font-medium text-foreground">
 | 
			
		||||
                    {t("reverseShares.modals.receivedFiles.bulkActions.selected", { count: selectedFiles.size })}
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex items-center gap-2">
 | 
			
		||||
                  <DropdownMenu>
 | 
			
		||||
                    <DropdownMenuTrigger asChild>
 | 
			
		||||
                      <Button variant="default" size="sm" className="gap-2">
 | 
			
		||||
                        {t("reverseShares.modals.receivedFiles.bulkActions.actions")}
 | 
			
		||||
                        <IconChevronDown className="h-4 w-4" />
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </DropdownMenuTrigger>
 | 
			
		||||
                    <DropdownMenuContent align="end" className="w-[200px]">
 | 
			
		||||
                      <DropdownMenuItem className="cursor-pointer py-2" onClick={handleBulkDownload}>
 | 
			
		||||
                        <IconDownload className="h-4 w-4" />
 | 
			
		||||
                        {t("reverseShares.modals.receivedFiles.bulkActions.download")}
 | 
			
		||||
                      </DropdownMenuItem>
 | 
			
		||||
                      <DropdownMenuItem
 | 
			
		||||
                        className="cursor-pointer py-2"
 | 
			
		||||
                        onClick={handleBulkCopyToMyFiles}
 | 
			
		||||
                        disabled={bulkCopying}
 | 
			
		||||
                      >
 | 
			
		||||
                        {bulkCopying ? (
 | 
			
		||||
                          <div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-600 border-t-transparent"></div>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                          <IconClipboardCopy className="h-4 w-4" />
 | 
			
		||||
                        )}
 | 
			
		||||
                        {t("reverseShares.modals.receivedFiles.bulkActions.copyToMyFiles")}
 | 
			
		||||
                      </DropdownMenuItem>
 | 
			
		||||
                      <DropdownMenuItem
 | 
			
		||||
                        className="cursor-pointer py-2 text-destructive focus:text-destructive"
 | 
			
		||||
                        onClick={handleBulkDelete}
 | 
			
		||||
                        disabled={bulkDeleting}
 | 
			
		||||
                      >
 | 
			
		||||
                        {bulkDeleting ? (
 | 
			
		||||
                          <div className="animate-spin rounded-full h-4 w-4 border-2 border-red-600 border-t-transparent"></div>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                          <IconTrash className="h-4 w-4" />
 | 
			
		||||
                        )}
 | 
			
		||||
                        {t("reverseShares.modals.receivedFiles.bulkActions.delete")}
 | 
			
		||||
                      </DropdownMenuItem>
 | 
			
		||||
                    </DropdownMenuContent>
 | 
			
		||||
                  </DropdownMenu>
 | 
			
		||||
                  <Button variant="outline" size="sm" onClick={() => setSelectedFiles(new Set())}>
 | 
			
		||||
                    {t("common.cancel")}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {files.length === 0 ? (
 | 
			
		||||
              <div className="flex flex-col items-center justify-center flex-1 gap-4 py-12">
 | 
			
		||||
                <div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center">
 | 
			
		||||
@@ -591,6 +850,13 @@ export function ReceivedFilesModal({
 | 
			
		||||
                <Table>
 | 
			
		||||
                  <TableHeader>
 | 
			
		||||
                    <TableRow>
 | 
			
		||||
                      <TableHead className="w-[50px]">
 | 
			
		||||
                        <Checkbox
 | 
			
		||||
                          checked={isAllSelected}
 | 
			
		||||
                          onCheckedChange={handleSelectAll}
 | 
			
		||||
                          aria-label={t("reverseShares.modals.receivedFiles.selectAll")}
 | 
			
		||||
                        />
 | 
			
		||||
                      </TableHead>
 | 
			
		||||
                      <TableHead>{t("reverseShares.modals.receivedFiles.columns.file")}</TableHead>
 | 
			
		||||
                      <TableHead>{t("reverseShares.modals.receivedFiles.columns.size")}</TableHead>
 | 
			
		||||
                      <TableHead>{t("reverseShares.modals.receivedFiles.columns.sender")}</TableHead>
 | 
			
		||||
@@ -610,6 +876,7 @@ export function ReceivedFilesModal({
 | 
			
		||||
                        inputRef={inputRef}
 | 
			
		||||
                        hoveredFile={hoveredFile}
 | 
			
		||||
                        copyingFile={copyingFile}
 | 
			
		||||
                        isSelected={selectedFiles.has(file.id)}
 | 
			
		||||
                        onStartEdit={startEdit}
 | 
			
		||||
                        onSaveEdit={saveEdit}
 | 
			
		||||
                        onCancelEdit={cancelEdit}
 | 
			
		||||
@@ -620,6 +887,7 @@ export function ReceivedFilesModal({
 | 
			
		||||
                        onDownload={handleDownload}
 | 
			
		||||
                        onDelete={handleDeleteFile}
 | 
			
		||||
                        onCopy={handleCopyFile}
 | 
			
		||||
                        onSelectFile={handleSelectFile}
 | 
			
		||||
                      />
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </TableBody>
 | 
			
		||||
@@ -630,6 +898,46 @@ export function ReceivedFilesModal({
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
 | 
			
		||||
      {/* Delete Confirmation Modal */}
 | 
			
		||||
      <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
 | 
			
		||||
        <DialogContent className="sm:max-w-md">
 | 
			
		||||
          <DialogHeader>
 | 
			
		||||
            <DialogTitle>{t("reverseShares.modals.receivedFiles.bulkDeleteConfirmTitle")}</DialogTitle>
 | 
			
		||||
            <DialogDescription>
 | 
			
		||||
              {t("reverseShares.modals.receivedFiles.bulkDeleteConfirmMessage", { count: filesToDeleteBulk.length })}
 | 
			
		||||
            </DialogDescription>
 | 
			
		||||
          </DialogHeader>
 | 
			
		||||
 | 
			
		||||
          <div className="max-h-48 overflow-y-auto border rounded-lg p-2">
 | 
			
		||||
            <div className="space-y-1">
 | 
			
		||||
              {filesToDeleteBulk.map((file) => {
 | 
			
		||||
                const { icon: FileIcon, color } = getFileIcon(file.name);
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={file.id} className="flex items-center gap-2 p-2 bg-muted/20 rounded text-sm">
 | 
			
		||||
                    <FileIcon className={`h-4 w-4 ${color} flex-shrink-0`} />
 | 
			
		||||
                    <span className="truncate" title={file.name}>
 | 
			
		||||
                      {file.name}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <DialogFooter className="flex gap-2">
 | 
			
		||||
            <Button variant="outline" onClick={() => setShowDeleteConfirm(false)}>
 | 
			
		||||
              {t("common.cancel")}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button variant="destructive" onClick={confirmBulkDelete} disabled={bulkDeleting}>
 | 
			
		||||
              {bulkDeleting ? (
 | 
			
		||||
                <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2" />
 | 
			
		||||
              ) : null}
 | 
			
		||||
              {t("reverseShares.modals.receivedFiles.bulkDeleteConfirmButton", { count: filesToDeleteBulk.length })}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
 | 
			
		||||
      {previewFile && (
 | 
			
		||||
        <ReverseShareFilePreviewModal
 | 
			
		||||
          isOpen={!!previewFile}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								apps/web/src/app/api/(proxy)/app/test-smtp/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/web/src/app/api/(proxy)/app/test-smtp/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
 | 
			
		||||
export async function POST(req: NextRequest) {
 | 
			
		||||
  const cookieHeader = req.headers.get("cookie");
 | 
			
		||||
  const body = await req.text();
 | 
			
		||||
 | 
			
		||||
  const apiRes = await fetch(`${process.env.API_BASE_URL}/app/test-smtp`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: {
 | 
			
		||||
      cookie: cookieHeader || "",
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
    body: body || "{}",
 | 
			
		||||
    redirect: "manual",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const resBody = await apiRes.text();
 | 
			
		||||
  const res = new NextResponse(resBody, {
 | 
			
		||||
    status: apiRes.status,
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const setCookie = apiRes.headers.getSetCookie?.() || [];
 | 
			
		||||
  if (setCookie.length > 0) {
 | 
			
		||||
    res.headers.set("Set-Cookie", setCookie.join(","));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return res;
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ import { Separator } from "@/components/ui/separator";
 | 
			
		||||
import { createFieldDescriptions, createGroupMetadata } from "../constants";
 | 
			
		||||
import { SettingsGroupProps } from "../types";
 | 
			
		||||
import { isFieldHidden, SettingsInput } from "./settings-input";
 | 
			
		||||
import { SmtpTestButton } from "./smtp-test-button";
 | 
			
		||||
 | 
			
		||||
export function SettingsGroup({ group, configs, form, isCollapsed, onToggleCollapse, onSubmit }: SettingsGroupProps) {
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
@@ -19,6 +20,8 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
 | 
			
		||||
    description: t("settings.groups.defaultDescription"),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const isEmailGroup = group === "email";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form onSubmit={form.handleSubmit(onSubmit)}>
 | 
			
		||||
      <Card className="p-6 gap-0">
 | 
			
		||||
@@ -70,7 +73,20 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
 | 
			
		||||
                </div>
 | 
			
		||||
              ))}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="flex justify-end mt-4">
 | 
			
		||||
          <div className="flex justify-between items-center mt-4">
 | 
			
		||||
            {isEmailGroup && (
 | 
			
		||||
              <SmtpTestButton
 | 
			
		||||
                smtpEnabled={form.watch("configs.smtpEnabled") || "false"}
 | 
			
		||||
                getFormValues={() => ({
 | 
			
		||||
                  smtpEnabled: form.getValues("configs.smtpEnabled") || "false",
 | 
			
		||||
                  smtpHost: form.getValues("configs.smtpHost") || "",
 | 
			
		||||
                  smtpPort: form.getValues("configs.smtpPort") || "",
 | 
			
		||||
                  smtpUser: form.getValues("configs.smtpUser") || "",
 | 
			
		||||
                  smtpPass: form.getValues("configs.smtpPass") || "",
 | 
			
		||||
                })}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <div className={`flex ${isEmailGroup ? "ml-auto" : ""}`}>
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="default"
 | 
			
		||||
                disabled={form.formState.isSubmitting}
 | 
			
		||||
@@ -83,6 +99,7 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
 | 
			
		||||
                })}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </CardContent>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </form>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								apps/web/src/app/settings/components/smtp-test-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								apps/web/src/app/settings/components/smtp-test-button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { IconFlask, IconInfoCircle, IconLoader } from "@tabler/icons-react";
 | 
			
		||||
import { useTranslations } from "next-intl";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { testSmtpConnection } from "@/http/endpoints/app";
 | 
			
		||||
 | 
			
		||||
interface SmtpTestButtonProps {
 | 
			
		||||
  smtpEnabled: string;
 | 
			
		||||
  getFormValues: () => {
 | 
			
		||||
    smtpEnabled: string;
 | 
			
		||||
    smtpHost: string;
 | 
			
		||||
    smtpPort: string;
 | 
			
		||||
    smtpUser: string;
 | 
			
		||||
    smtpPass: string;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function SmtpTestButton({ smtpEnabled, getFormValues }: SmtpTestButtonProps) {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
 | 
			
		||||
  const handleTestConnection = async () => {
 | 
			
		||||
    const formValues = getFormValues();
 | 
			
		||||
 | 
			
		||||
    if (formValues.smtpEnabled !== "true") {
 | 
			
		||||
      toast.error("SMTP is not enabled. Please enable SMTP first.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check if required fields are filled
 | 
			
		||||
    if (!formValues.smtpHost || !formValues.smtpPort || !formValues.smtpUser || !formValues.smtpPass) {
 | 
			
		||||
      toast.error("Please fill in all SMTP configuration fields before testing.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await testSmtpConnection({
 | 
			
		||||
        smtpConfig: {
 | 
			
		||||
          smtpEnabled: formValues.smtpEnabled,
 | 
			
		||||
          smtpHost: formValues.smtpHost,
 | 
			
		||||
          smtpPort: formValues.smtpPort,
 | 
			
		||||
          smtpUser: formValues.smtpUser,
 | 
			
		||||
          smtpPass: formValues.smtpPass,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.data.success) {
 | 
			
		||||
        toast.success(t("settings.messages.smtpTestSuccess"));
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(t("settings.messages.smtpTestGenericError"));
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      const errorMessage = error?.response?.data?.error || error?.message || "Unknown error";
 | 
			
		||||
      toast.error(t("settings.messages.smtpTestFailed", { error: errorMessage }));
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex items-center gap-2">
 | 
			
		||||
      <Button
 | 
			
		||||
        type="button"
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        onClick={handleTestConnection}
 | 
			
		||||
        disabled={isLoading || smtpEnabled !== "true"}
 | 
			
		||||
        className="flex items-center gap-2"
 | 
			
		||||
      >
 | 
			
		||||
        {isLoading ? <IconLoader className="h-4 w-4 animate-spin" /> : <IconFlask className="h-4 w-4" />}
 | 
			
		||||
        {isLoading ? t("settings.buttons.testing") : t("settings.buttons.testSmtp")}
 | 
			
		||||
      </Button>
 | 
			
		||||
      <div className="relative group">
 | 
			
		||||
        <IconInfoCircle className="h-4 w-4 text-muted-foreground hover:text-foreground cursor-help transition-colors" />
 | 
			
		||||
        <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 px-3 py-2 bg-popover text-popover-foreground text-xs rounded-md border shadow-md opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 w-72 text-wrap">
 | 
			
		||||
          {t("settings.tooltips.testSmtp")}
 | 
			
		||||
          <div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-popover"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -80,3 +80,22 @@ export const checkUploadAllowed = <TData = CheckUploadAllowedResult>(
 | 
			
		||||
    params: { ...params, ...options?.params },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TestSmtpConnectionResult = { success: boolean; message: string };
 | 
			
		||||
 | 
			
		||||
export interface TestSmtpConnectionBody {
 | 
			
		||||
  smtpConfig?: {
 | 
			
		||||
    smtpEnabled: string;
 | 
			
		||||
    smtpHost: string;
 | 
			
		||||
    smtpPort: string;
 | 
			
		||||
    smtpUser: string;
 | 
			
		||||
    smtpPass: string;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const testSmtpConnection = (
 | 
			
		||||
  body?: TestSmtpConnectionBody,
 | 
			
		||||
  options?: AxiosRequestConfig
 | 
			
		||||
): Promise<{ data: TestSmtpConnectionResult }> => {
 | 
			
		||||
  return apiInstance.post(`/api/app/test-smtp`, body || {}, options);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user