Compare commits

...

14 Commits

Author SHA1 Message Date
Daniel Luiz Alves
828fbd4cfd v3.0.0-beta.12 (#109) 2025-06-25 08:42:28 -03:00
Daniel Luiz Alves
ea68e771a8 feat: enhance file management features and localization (#108) 2025-06-24 14:05:16 -03:00
Daniel Luiz Alves
229f9a3ca0 feat: enhance file management features and localization
- Added bulk download functionality for files in the PublicSharePage, allowing users to download multiple files as a ZIP.
- Introduced new success and error messages for file deletion and password protection updates, improving user feedback.
- Updated translation files to include new keys for bulk download, file deletion, and password protection messages, enhancing localization support.
- Improved UI components to accommodate new features and ensure a seamless user experience.
2025-06-24 13:58:57 -03:00
Daniel Luiz Alves
dd10c17a3a feat: enhance SMTP configuration options in settings (#107) 2025-06-24 11:19:22 -03:00
Daniel Luiz Alves
ab071916b8 feat: enhance SMTP configuration options in settings
- Added new fields for smtpSecure and smtpNoAuth in the email settings, allowing users to specify the connection security method and disable authentication for internal servers.
- Updated the EmailService to handle the new configuration options, improving flexibility in SMTP setup.
- Enhanced the UI components to include the new fields, ensuring proper user interaction and validation.
- Updated translation files to include descriptions and titles for the new SMTP settings, improving localization support.
2025-06-24 11:15:44 -03:00
Daniel Luiz Alves
1ab0504288 chore: add pull request template for improved contribution guidelines
- Introduced a new pull request template to standardize submissions and enhance clarity for contributors.
- The template includes sections for description, related issues, motivation, AI usage, testing, and a checklist to ensure quality and completeness.
- Aims to streamline the review process and improve collaboration within the project.
2025-06-24 01:02:41 -03:00
Daniel Luiz Alves
652f1f47d2 feat: implement file name truncation in modals for better display
- Added a utility function to intelligently truncate file names while preserving extensions.
- Updated ReceivedFilesModal and DeleteConfirmationModal to use the new truncation function, improving the display of long file names.
- Enhanced UI components to ensure proper layout and readability of file names in both modals.
2025-06-24 00:33:54 -03:00
Daniel Luiz Alves
cd9598a6d3 v3.0.0-beta.11 (#104) 2025-06-23 21:38:38 -03:00
Daniel Luiz Alves
017a1debd3 feat: add SMTP connection testing functionality (#103) 2025-06-23 18:28:54 -03:00
Daniel Luiz Alves
b917dbc05f feat: add SMTP connection testing functionality
- Implemented a new endpoint for testing SMTP connections, allowing administrators to validate SMTP settings before saving.
- Added a method in the EmailService to handle SMTP connection testing, utilizing either provided or saved configurations.
- Introduced a new UI component for testing SMTP connections within the settings, enhancing user experience with real-time feedback.
- Updated translation files to include new keys and messages related to SMTP testing across multiple languages.
2025-06-23 18:26:51 -03:00
Daniel Luiz Alves
4fdee6b98b Feat: Add bulk actions for file management in received files modal (#102) 2025-06-23 17:14:42 -03:00
Daniel Luiz Alves
a669a6c048 fix: change Docker build command to load images instead of pushing
- Updated the Docker build command in build-docker.sh to use --load instead of --push, allowing for local image loading during the build process.
- This change facilitates testing and debugging of Docker images before deployment.
2025-06-23 17:13:59 -03:00
Daniel Luiz Alves
264521f1c4 refactor: update translation management system to enhance key management and remove auto-translation
- Revised the translation management system to focus on key management, automating synchronization and validation of internationalization files.
- Removed the auto-translation functionality, emphasizing the need for manual translation by native speakers or professionals.
- Updated documentation to reflect changes in the workflow and best practices for managing translations.
- Adjusted related scripts and commands in package.json to align with the new translation process.
2025-06-23 17:11:25 -03:00
Daniel Luiz Alves
b1cc9dbb21 feat: add bulk actions for file management in received files modal
- Implemented bulk selection functionality for files in the ReceivedFilesModal, allowing users to select multiple files for actions such as download, copy, and delete.
- Added UI elements for bulk actions, including a dropdown menu for selecting actions and confirmation dialogs for deletion.
- Enhanced user experience by providing feedback during bulk operations and clearing selections after successful actions.
- Localized new messages for bulk actions across multiple languages to ensure consistent user feedback.
2025-06-23 17:11:08 -03:00
44 changed files with 2040 additions and 715 deletions

56
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,56 @@
## **🎯 Please make sure you are opening this Pull Request against the `next` branch!**
## 📝 Description
Please provide a clear and concise description of the changes introduced by this pull request.
## 🔗 Related Issue(s)
If this PR fixes or relates to an issue, please link it here (e.g., `Closes #123`).
## 💡 Motivation and Context
Why is this change required? What problem does it solve?
## 🤖 Use of Artificial Intelligence (AI)
The use of AI tools is absolutely welcome and not an issue. For transparency and continuous improvement, please answer the following:
- Did you use any AI tools (such as GitHub Copilot, ChatGPT, etc.) to help develop this PR?
- [ ] No, this PR was developed without the assistance of AI tools.
- [ ] Yes, AI tools assisted in the development of this PR (please specify which ones and how they were used):
- Tool(s) used:
- Brief description of how AI contributed:
- Was this PR generated entirely by an AI tool (i.e., with minimal human intervention)?
- [ ] No
- [ ] Yes (please provide details):
## 🧪 How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Include details about your test environment, and the test cases you ran.
## 📸 Screenshots (if appropriate)
Add any relevant screenshots to help explain your changes.
## 🔄 Types of Changes
Check the relevant option(s) below:
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 📚 Documentation update
## ✅ Checklist
- [ ] My code follows the code style of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added necessary documentation (if appropriate)
- [ ] I have rebased and/or merged on top of the latest `next` branch
---
🙏 Thank you for your contribution!

View File

@@ -6,7 +6,7 @@ icon: Languages
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Callout } from "fumadocs-ui/components/callout";
Palmr includes a comprehensive translation management system that automates synchronization, validation, and translation of the application's internationalization files.
Palmr includes a comprehensive translation key management system that automates synchronization and validation of the application's internationalization files.
## Overview
@@ -14,7 +14,7 @@ The translation management system consists of Python scripts that help maintain
- **Synchronization**: Automatically add missing translation keys
- **Validation**: Check translation status and completeness
- **Auto-translation**: Use Google Translate API for initial translations
- **Key Management**: Manage translation keys structure and consistency
- **Reporting**: Generate detailed translation reports
## Quick Start
@@ -31,10 +31,7 @@ The translation management system consists of Python scripts that help maintain
# Synchronize missing keys
pnpm run translations:sync
# Auto-translate [TO_TRANSLATE] strings
pnpm run translations:translate
# Dry run mode (test without changes)
# Test workflow without changes (dry run)
pnpm run translations:dry-run
# Show help
@@ -52,7 +49,7 @@ The translation management system consists of Python scripts that help maintain
# Individual commands
python3 run_translations.py check
python3 run_translations.py sync
python3 run_translations.py translate
python3 run_translations.py all --dry-run
python3 run_translations.py help
```
@@ -63,14 +60,13 @@ The translation management system consists of Python scripts that help maintain
### Main Commands (npm/pnpm)
| Command | Description |
| --------------------------------- | ------------------------------------------- |
| `pnpm run translations` | Complete workflow: sync + translate + check |
| `pnpm run translations:check` | Check translation status and completeness |
| `pnpm run translations:sync` | Synchronize missing keys from en-US.json |
| `pnpm run translations:translate` | Auto-translate [TO_TRANSLATE] strings |
| `pnpm run translations:dry-run` | Test workflow without making changes |
| `pnpm run translations:help` | Show detailed help and examples |
| Command | Description |
| ------------------------------- | ----------------------------------------- |
| `pnpm run translations` | Complete workflow: sync + check |
| `pnpm run translations:check` | Check translation status and completeness |
| `pnpm run translations:sync` | Synchronize missing keys from en-US.json |
| `pnpm run translations:dry-run` | Test workflow without making changes |
| `pnpm run translations:help` | Show detailed help and examples |
## Workflow
@@ -80,13 +76,13 @@ When you add new text to the application:
1. **Add to English**: Update `apps/web/messages/en-US.json` with your new keys
2. **Sync translations**: Run `pnpm run translations:sync` to add missing keys to all languages
3. **Auto-translate**: Run `pnpm run translations:translate` for automatic translations
4. **Review translations**: **Mandatory step** - Check all auto-generated translations for accuracy
5. **Test in UI**: Verify translations work correctly in the application interface
3. **Manual translation**: Translate strings marked with `[TO_TRANSLATE]` manually
4. **Test in UI**: Verify translations work correctly in the application interface
<Callout type="important">
**Never skip step 4**: Auto-generated translations must be reviewed before
committing to production. They are a starting point, not a final solution.
**Manual translation required**: All strings marked with `[TO_TRANSLATE]` must
be manually translated by native speakers or professional translators for
accuracy.
</Callout>
### 2. Checking Translation Status
@@ -110,7 +106,7 @@ The report shows:
### 3. Manual Translation Process
For critical strings or when automatic translation isn't sufficient:
For all translation strings:
1. **Find untranslated strings**: Look for `[TO_TRANSLATE] Original text` in language files
2. **Replace with translation**: Remove the prefix and add proper translation
@@ -130,13 +126,13 @@ apps/web/
├── run_translations.py # Main wrapper
├── sync_translations.py # Synchronization
├── check_translations.py # Status checking
└── translate_missing.py # Auto-translation
└── clean_translations.py # Cleanup utilities
```
## Prerequisites
- **Python 3.6 or higher** - Required for running the translation scripts
- **No external dependencies** - Scripts use only Python standard libraries (googletrans auto-installs when needed)
- **No external dependencies** - Scripts use only Python standard libraries
- **UTF-8 support** - Ensure your terminal supports UTF-8 for proper display of translations
## Script Details
@@ -149,8 +145,7 @@ The main script provides a unified interface for all translation operations:
- `check` - Check translation status and generate reports
- `sync` - Synchronize missing keys from reference language
- `translate` - Automatically translate marked strings
- `all` - Run complete workflow (sync + translate + check)
- `all` - Run complete workflow (sync + check)
- `help` - Show detailed help with examples
#### How it Works
@@ -197,27 +192,6 @@ Provides comprehensive translation analysis:
- **Quality insights**: Identifies potential translation issues
- **Export friendly**: Output can be redirected to files for reports
### Auto-Translation Script (`translate_missing.py`)
Automated translation using Google Translate API:
#### Process
1. **Dependency check**: Auto-installs `googletrans` if needed
2. **Scan for work**: Finds all `[TO_TRANSLATE]` prefixed strings
3. **Language mapping**: Maps file names to Google Translate language codes
4. **Batch translation**: Processes strings with rate limiting
5. **Update files**: Replaces marked strings with translations
6. **Error handling**: Retries failed translations, reports results
#### Safety Features
- **Rate limiting**: Configurable delay between requests
- **Retry logic**: Multiple attempts for failed translations
- **Dry run mode**: Preview changes without modifications
- **Language skipping**: Exclude specific languages from processing
- **Progress tracking**: Real-time status of translation progress
## Advanced Usage
### Custom Parameters
@@ -240,22 +214,6 @@ python3 scripts/run_translations.py sync --messages-dir /path/to/messages
python3 scripts/run_translations.py sync --dry-run
```
#### Translation Parameters (`translate`)
```bash
# Custom delay between translation requests (avoid rate limiting)
python3 scripts/run_translations.py translate --delay 2.0
# Skip specific languages from translation
python3 scripts/run_translations.py translate --skip-languages pt-BR.json fr-FR.json
# Dry run - see what would be translated
python3 scripts/run_translations.py translate --dry-run
# Specify custom messages directory
python3 scripts/run_translations.py translate --messages-dir /path/to/messages
```
#### Check Parameters (`check`)
```bash
@@ -268,58 +226,46 @@ python3 scripts/run_translations.py check --messages-dir /path/to/messages
### Parameter Reference
| Parameter | Commands | Description |
| ------------------------ | -------------------------- | --------------------------------------------- |
| `--dry-run` | `sync`, `translate`, `all` | Preview changes without modifying files |
| `--messages-dir` | All | Custom directory containing translation files |
| `--reference` | `sync`, `check` | Reference file to use (default: en-US.json) |
| `--no-mark-untranslated` | `sync` | Don't add [TO_TRANSLATE] prefix to new keys |
| `--delay` | `translate` | Delay in seconds between translation requests |
| `--skip-languages` | `translate` | List of language files to skip |
| Parameter | Commands | Description |
| ------------------------ | --------------- | --------------------------------------------- |
| `--dry-run` | `sync`, `all` | Preview changes without modifying files |
| `--messages-dir` | All | Custom directory containing translation files |
| `--reference` | `sync`, `check` | Reference file to use (default: en-US.json) |
| `--no-mark-untranslated` | `sync` | Don't add [TO_TRANSLATE] prefix to new keys |
### Dry Run Mode
Always test changes first:
```bash
# Test complete workflow
# Test complete workflow (recommended)
pnpm run translations:dry-run
# Test individual commands
# Alternative: Direct Python commands
python3 scripts/run_translations.py all --dry-run
python3 scripts/run_translations.py sync --dry-run
python3 scripts/run_translations.py translate --dry-run
```
### Common Use Cases
#### Scenario 1: Careful Translation with Review
#### Scenario 1: Adding Keys Without Marking for Translation
```bash
# 1. Sync new keys without auto-marking for translation
# Sync new keys without auto-marking for translation
python3 scripts/run_translations.py sync --no-mark-untranslated
# 2. Manually mark specific keys that need translation
# Manually mark specific keys that need translation
# Edit files to add [TO_TRANSLATE] prefix where needed
# 3. Translate only marked strings with slower rate
python3 scripts/run_translations.py translate --delay 2.0
```
#### Scenario 2: Skip Already Reviewed Languages
```bash
# Skip languages that were already manually reviewed
python3 scripts/run_translations.py translate --skip-languages pt-BR.json es-ES.json
```
#### Scenario 3: Custom Project Structure
#### Scenario 2: Custom Project Structure
```bash
# Work with translations in different directory
python3 scripts/run_translations.py all --messages-dir ../custom-translations
```
#### Scenario 4: Quality Assurance
#### Scenario 3: Quality Assurance
```bash
# Use different language as reference for comparison
@@ -352,15 +298,15 @@ Translation files use nested JSON structure:
}
```
## Automatic Translation
## Manual Translation
<Callout type="warning">
**Review Required**: Automatic translations are provided for convenience but
**must be reviewed** before production use. They serve as a starting point,
not a final solution.
**Professional translation recommended**: For production applications,
consider using professional translation services or native speakers to ensure
high-quality, culturally appropriate translations.
</Callout>
The system uses Google Translate (free API) to automatically translate strings marked with `[TO_TRANSLATE]`:
The system marks strings that need translation with the `[TO_TRANSLATE]` prefix:
```json
{
@@ -368,7 +314,7 @@ The system uses Google Translate (free API) to automatically translate strings m
}
```
After auto-translation:
After manual translation:
```json
{
@@ -378,14 +324,15 @@ After auto-translation:
### Translation Review Process
1. **Generate**: Use `pnpm run translations:translate` to auto-translate
2. **Review**: Check each translation for:
1. **Identify**: Use `pnpm run translations:check` to find untranslated strings
2. **Translate**: Replace `[TO_TRANSLATE]` strings with proper translations
3. **Review**: Check each translation for:
- **Context accuracy**: Does it make sense in the UI?
- **Technical terms**: Are they correctly translated?
- **Tone consistency**: Matches the application's voice?
- **Cultural appropriateness**: Suitable for target audience?
3. **Test**: Verify translations in the actual application interface
4. **Document**: Note any translation decisions for future reference
4. **Test**: Verify translations in the actual application interface
5. **Document**: Note any translation decisions for future reference
### Common Review Points
@@ -405,15 +352,15 @@ After auto-translation:
### Translation Workflow for Development
1. **English First**: Add all new text to `apps/web/messages/en-US.json`
2. **Auto-generate**: Use scripts to generate translations for other languages
3. **Review Required**: All auto-generated translations must be reviewed before production use
2. **Sync keys**: Use scripts to generate key structure for other languages
3. **Manual translation**: All strings must be manually translated for accuracy
4. **Quality Check**: Run translation validation before merging PRs
### Why English as Parent Language?
- **Consistency**: Ensures all languages have the same keys structure
- **Reference**: English serves as the source of truth for meaning
- **Auto-translation**: Scripts use English as source for automatic translation
- **Key management**: Scripts use English as source for key synchronization
- **Documentation**: Most technical documentation is in English
## Best Practices
@@ -422,8 +369,8 @@ After auto-translation:
1. **Always use English as reference**: Add new keys to `en-US.json` first - never add keys directly to other languages
2. **Use semantic key names**: `dashboard.welcome` instead of `text1`
3. **Test translations**: Run `pnpm run translations:dry-run` before committing
4. **Review auto-translations**: All generated translations must be reviewed before production
3. **Test key sync**: Run `pnpm run translations:dry-run` before committing
4. **Coordinate with translators**: Ensure translation team is aware of new keys
5. **Maintain consistency**: Use existing patterns for similar UI elements
### For Translators
@@ -432,6 +379,7 @@ After auto-translation:
2. **Check identical strings**: May need localization even if identical to English
3. **Use proper formatting**: Maintain HTML tags and placeholders
4. **Test in context**: Verify translations work in the actual UI
5. **Maintain glossary**: Keep consistent terminology across translations
## Troubleshooting
@@ -439,17 +387,9 @@ After auto-translation:
**Python not found**: Ensure Python 3.6+ is installed and in PATH
**googletrans errors**: The system auto-installs dependencies, but you can manually install:
**Permission errors**: Ensure write permissions to message files
```bash
pip install googletrans==4.0.0rc1
```
**Rate limiting**: Increase delay between requests:
```bash
python3 scripts/run_translations.py translate --delay 2.0
```
**Encoding issues**: Ensure your terminal supports UTF-8
### Getting Help
@@ -491,28 +431,6 @@ LANGUAGE COMPLETENESS STRINGS UNTRANSLATED POSSIBLE MATCHES
================================================================================
```
### Auto-Translation Progress
```
🌍 Translating 3 languages...
⏱️ Delay between requests: 1.0s
[1/3] 🌐 Language: PT
🔍 Processing: pt-BR.json
📝 Found 12 strings to translate
📍 (1/12) Translating: dashboard.welcome
✅ "Welcome to Palmr" → "Bem-vindo ao Palmr"
💾 File saved with 12 translations
📊 FINAL SUMMARY
================================================================================
✅ Translations performed:
• 34 successes
• 2 failures
• 36 total processed
• Success rate: 94.4%
```
## Contributing
When contributing translations:
@@ -520,6 +438,6 @@ When contributing translations:
1. **Follow the workflow**: Use the provided scripts for consistency
2. **Test thoroughly**: Run complete checks before submitting
3. **Document changes**: Note any significant translation decisions
4. **Maintain quality**: Review auto-translations for accuracy
4. **Maintain quality**: Ensure manual translations are accurate and appropriate
The translation management system ensures consistency and makes it easy to maintain high-quality localization across all supported languages.

View File

@@ -117,6 +117,18 @@ const defaultConfigs = [
type: "string",
group: "email",
},
{
key: "smtpSecure",
value: "auto",
type: "string",
group: "email",
},
{
key: "smtpNoAuth",
value: "false",
type: "boolean",
group: "email",
},
{
key: "passwordResetTokenExpiration",
value: "3600",

View File

@@ -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();

View File

@@ -126,6 +126,59 @@ 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)"),
smtpSecure: z
.string()
.optional()
.describe("Connection security method ('auto', 'ssl', 'tls', or 'none')"),
smtpNoAuth: z.string().optional().describe("Disable SMTP authentication ('true' or 'false')"),
})
.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",
{

View File

@@ -1,6 +1,16 @@
import { ConfigService } from "../config/service";
import nodemailer from "nodemailer";
interface SmtpConfig {
smtpEnabled: string;
smtpHost: string;
smtpPort: string;
smtpUser: string;
smtpPass: string;
smtpSecure?: string;
smtpNoAuth?: string;
}
export class EmailService {
private configService = new ConfigService();
@@ -10,15 +20,118 @@ export class EmailService {
return null;
}
return nodemailer.createTransport({
const port = Number(await this.configService.getValue("smtpPort"));
const smtpSecure = (await this.configService.getValue("smtpSecure")) || "auto";
const smtpNoAuth = await this.configService.getValue("smtpNoAuth");
let secure = false;
let requireTLS = false;
if (smtpSecure === "ssl") {
secure = true;
} else if (smtpSecure === "tls") {
requireTLS = true;
} else if (smtpSecure === "none") {
secure = false;
requireTLS = false;
} else if (smtpSecure === "auto") {
if (port === 465) {
secure = true;
} else if (port === 587 || port === 25) {
requireTLS = true;
}
}
const transportConfig: any = {
host: await this.configService.getValue("smtpHost"),
port: Number(await this.configService.getValue("smtpPort")),
secure: false,
auth: {
port: port,
secure: secure,
requireTLS: requireTLS,
tls: {
rejectUnauthorized: false,
},
};
if (smtpNoAuth !== "true") {
transportConfig.auth = {
user: await this.configService.getValue("smtpUser"),
pass: await this.configService.getValue("smtpPass"),
};
}
return nodemailer.createTransport(transportConfig);
}
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"),
smtpSecure: (await this.configService.getValue("smtpSecure")) || "auto",
smtpNoAuth: await this.configService.getValue("smtpNoAuth"),
};
}
if (smtpConfig.smtpEnabled !== "true") {
throw new Error("SMTP is not enabled");
}
const port = Number(smtpConfig.smtpPort);
const smtpSecure = smtpConfig.smtpSecure || "auto";
const smtpNoAuth = smtpConfig.smtpNoAuth;
let secure = false;
let requireTLS = false;
if (smtpSecure === "ssl") {
secure = true;
} else if (smtpSecure === "tls") {
requireTLS = true;
} else if (smtpSecure === "none") {
secure = false;
requireTLS = false;
} else if (smtpSecure === "auto") {
if (port === 465) {
secure = true;
} else if (port === 587 || port === 25) {
requireTLS = true;
}
}
const transportConfig: any = {
host: smtpConfig.smtpHost,
port: port,
secure: secure,
requireTLS: requireTLS,
tls: {
rejectUnauthorized: false,
},
});
};
if (smtpNoAuth !== "true") {
transportConfig.auth = {
user: smtpConfig.smtpUser,
pass: smtpConfig.smtpPass,
};
}
const transporter = nodemailer.createTransport(transportConfig);
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) {

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "نطاقات البريد الإلكتروني للمشرف",
"description": "نطاقات البريد الإلكتروني التي تحصل على صلاحيات المشرف تلقائياً"
},
"testSmtp": {
"title": "اختبار اتصال SMTP",
"description": "اختبار ما إذا كان تكوين SMTP صالحًا"
},
"smtpNoAuth": {
"title": "بدون مصادقة",
"description": "قم بتمكين هذا للخوادم الداخلية التي لا تتطلب اسم مستخدم/كلمة مرور (يخفي حقول المصادقة)"
},
"smtpSecure": {
"title": "أمان الاتصال",
"description": "طريقة أمان اتصال SMTP - تلقائي (موصى به)، SSL، STARTTLS، أو بدون (غير آمن)",
"options": {
"auto": "تلقائي (موصى به)",
"ssl": "SSL (منفذ 465)",
"tls": "STARTTLS (منفذ 587)",
"none": "بدون (غير آمن)"
}
}
},
"buttons": {
"save": "احفظ {group}"
"save": "احفظ {group}",
"testSmtp": "اختبار الاتصال",
"testing": "جاري الاختبار..."
},
"errors": {
"loadFailed": "فشل في تحميل الإعدادات",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "لا توجد تغييرات للحفظ",
"updateSuccess": "تم تحديث إعدادات {group} بنجاح"
"updateSuccess": "تم تحديث إعدادات {group} بنجاح",
"smtpTestFailed": "فشل اتصال SMTP: {error}",
"smtpTestGenericError": "فشل اختبار اتصال SMTP. يرجى التحقق من إعداداتك والمحاولة مرة أخرى.",
"smtpTestSuccess": "تم اتصال SMTP بنجاح! تكوين البريد الإلكتروني الخاص بك يعمل بشكل صحيح.",
"smtpMissingAuth": "يرجى ملء اسم المستخدم وكلمة المرور الخاصة بـ SMTP، أو تمكين خيار 'بدون مصادقة'.",
"smtpMissingHostPort": "يرجى ملء مضيف ومنفذ SMTP قبل الاختبار.",
"smtpNotEnabled": "SMTP غير مفعل. يرجى تفعيل SMTP أولاً."
},
"title": "الإعدادات",
"breadcrumb": "الإعدادات",
"pageTitle": "الإعدادات",
"tooltips": {
"oidcScope": "أدخل نطاقاً واضغط Enter للإضافة",
"oidcAdminEmailDomains": "أدخل نطاقاً واضغط Enter للإضافة"
"oidcAdminEmailDomains": "أدخل نطاقاً واضغط Enter للإضافة",
"testSmtp": "يختبر اتصال SMTP بالقيم المدخلة حاليًا في النموذج. لجعل التغييرات دائمة، تذكر حفظ إعداداتك بعد الاختبار.",
"defaultPlaceholder": "أدخل واضغط Enter"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -579,7 +607,8 @@
"title": "لم يتم العثور على المشاركة",
"description": "قد يكون تم حذف هذه المشاركة أو انتهت صلاحيتها."
},
"pageTitle": "المشاركة"
"pageTitle": "المشاركة",
"downloadAll": "تحميل الكل"
},
"shareActions": {
"deleteTitle": "حذف المشاركة",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "خطأ في حذف الملف",
"deleteSuccess": "تم حذف الملف بنجاح"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "تم حذف رابط الاستلام بنجاح!",
"aliasCreated": "تم إنشاء الاسم المستعار بنجاح!",
"activateSuccess": "تم تفعيل رابط الاستلام بنجاح!",
"deactivateSuccess": "تم تعطيل رابط الاستلام بنجاح!"
"deactivateSuccess": "تم تعطيل رابط الاستلام بنجاح!",
"passwordProtectionDisabled": "تم إزالة حماية كلمة المرور بنجاح!",
"passwordProtectionEnabled": "تم تمكين حماية كلمة المرور بنجاح!"
},
"errors": {
"loadFailed": "فشل في تحميل روابط الاستلام",
"createFailed": "فشل في إنشاء رابط الاستلام. حاول مرة أخرى.",
"updateFailed": "فشل في تحديث رابط الاستلام. حاول مرة أخرى.",
"deleteFailed": "فشل في حذف رابط الاستلام. حاول مرة أخرى.",
"aliasCreateFailed": "فشل في إنشاء الاسم المستعار. حاول مرة أخرى."
"aliasCreateFailed": "فشل في إنشاء الاسم المستعار. حاول مرة أخرى.",
"passwordUpdateFailed": "فشل في تحديث حماية كلمة المرور"
},
"delete": {
"title": "حذف رابط الاستلام",
@@ -1379,7 +1429,9 @@
"components": {
"fileRow": {
"addDescription": "إضافة وصف...",
"anonymous": "مجهول"
"anonymous": "مجهول",
"deleteError": "خطأ في حذف الملف",
"deleteSuccess": "تم حذف الملف بنجاح"
},
"fileActions": {
"edit": "تحرير",
@@ -1395,6 +1447,7 @@
"saveChanges": "حفظ التغييرات",
"cancelEdit": "إلغاء التحرير"
}
}
},
"defaultLinkName": "الملفات المستلمة"
}
}
}

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Keine Authentifizierung",
"description": "Aktivieren Sie dies für interne Server, die keinen Benutzernamen/Passwort benötigen (blendet Authentifizierungsfelder aus)"
},
"smtpSecure": {
"title": "Verbindungssicherheit",
"description": "SMTP-Verbindungssicherheitsmethode - Auto (empfohlen), SSL, STARTTLS oder Keine (unsicher)",
"options": {
"auto": "Auto (Empfohlen)",
"ssl": "SSL (Port 465)",
"tls": "STARTTLS (Port 587)",
"none": "Keine (Unsicher)"
}
}
},
"buttons": {
"save": "{group} speichern"
"save": "{group} speichern",
"testSmtp": "Verbindung testen",
"testing": "Teste..."
},
"errors": {
"loadFailed": "Fehler beim Laden der Einstellungen",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Bitte geben Sie SMTP-Benutzername und Passwort ein oder aktivieren Sie die Option 'Keine Authentifizierung'.",
"smtpMissingHostPort": "Bitte geben Sie SMTP-Host und Port vor dem Test ein.",
"smtpNotEnabled": "SMTP ist nicht aktiviert. Bitte aktivieren Sie zuerst SMTP."
},
"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.",
"defaultPlaceholder": "Eingeben und Enter drücken"
},
"redirectUri": {
"placeholder": "https://meineseite.com",
@@ -579,7 +607,8 @@
"title": "Freigabe nicht gefunden",
"description": "Diese Freigabe wurde möglicherweise gelöscht oder ist abgelaufen."
},
"pageTitle": "Freigabe"
"pageTitle": "Freigabe",
"downloadAll": "Alle herunterladen"
},
"shareActions": {
"deleteTitle": "Freigabe Löschen",
@@ -1189,7 +1218,25 @@
"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",
"deleteError": "Fehler beim Löschen der Datei",
"deleteSuccess": "Datei erfolgreich gelöscht"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Empfangslink erfolgreich gelöscht!",
"aliasCreated": "Alias erfolgreich erstellt!",
"activateSuccess": "Empfangslink erfolgreich aktiviert!",
"deactivateSuccess": "Empfangslink erfolgreich deaktiviert!"
"deactivateSuccess": "Empfangslink erfolgreich deaktiviert!",
"passwordProtectionDisabled": "Passwortschutz erfolgreich entfernt!",
"passwordProtectionEnabled": "Passwortschutz erfolgreich aktiviert!"
},
"errors": {
"loadFailed": "Fehler beim Laden der Empfangslinks",
"createFailed": "Fehler beim Erstellen des Empfangslinks. Bitte versuchen Sie es erneut.",
"updateFailed": "Fehler beim Aktualisieren des Empfangslinks. Bitte versuchen Sie es erneut.",
"deleteFailed": "Fehler beim Löschen des Empfangslinks. Bitte versuchen Sie es erneut.",
"aliasCreateFailed": "Fehler beim Erstellen des Alias. Bitte versuchen Sie es erneut."
"aliasCreateFailed": "Fehler beim Erstellen des Alias. Bitte versuchen Sie es erneut.",
"passwordUpdateFailed": "Fehler beim Aktualisieren des Passwortschutzes"
},
"delete": {
"title": "Empfangslink löschen",
@@ -1395,6 +1445,7 @@
"saveChanges": "Änderungen speichern",
"cancelEdit": "Bearbeitung abbrechen"
}
}
},
"defaultLinkName": "Empfangene Dateien"
}
}

View File

@@ -465,6 +465,24 @@
"title": "Sender Email",
"description": "Sender email address"
},
"smtpSecure": {
"title": "Connection Security",
"description": "SMTP connection security method - Auto (recommended), SSL, STARTTLS, or None (insecure)",
"options": {
"auto": "Auto (Recommended)",
"ssl": "SSL (Port 465)",
"tls": "STARTTLS (Port 587)",
"none": "None (Insecure)"
}
},
"smtpNoAuth": {
"title": "No Authentication",
"description": "Enable this for internal servers that don't require username/password (hides auth fields)"
},
"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 +549,9 @@
}
},
"buttons": {
"save": "Save {group}"
"save": "Save {group}",
"testSmtp": "Test Connection",
"testing": "Testing..."
},
"errors": {
"loadFailed": "Failed to load settings",
@@ -539,14 +559,22 @@
},
"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.",
"smtpNotEnabled": "SMTP is not enabled. Please enable SMTP first.",
"smtpMissingHostPort": "Please fill in SMTP Host and Port before testing.",
"smtpMissingAuth": "Please fill in SMTP Username and Password, or enable 'No Authentication' option."
},
"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.",
"defaultPlaceholder": "Enter and press Enter"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -575,6 +603,7 @@
"created": "Created: {date}",
"expires": "Expires: {date}"
},
"downloadAll": "Download All",
"notFound": {
"title": "Share Not Found",
"description": "This share may have been deleted or expired."
@@ -1189,7 +1218,25 @@
"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",
"deleteSuccess": "File deleted successfully",
"deleteError": "Error deleting file",
"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": {
@@ -1272,14 +1319,18 @@
"deleteSuccess": "Receive link deleted successfully!",
"aliasCreated": "Alias created successfully!",
"activateSuccess": "Receive link activated successfully!",
"deactivateSuccess": "Receive link deactivated successfully!"
"deactivateSuccess": "Receive link deactivated successfully!",
"passwordProtectionEnabled": "Password protection enabled successfully!",
"passwordProtectionDisabled": "Password protection removed successfully!"
},
"defaultLinkName": "Received Files",
"errors": {
"loadFailed": "Failed to load receive links",
"createFailed": "Failed to create receive link. Please try again.",
"updateFailed": "Failed to update receive link. Please try again.",
"deleteFailed": "Failed to delete receive link. Please try again.",
"aliasCreateFailed": "Failed to create alias. Please try again."
"aliasCreateFailed": "Failed to create alias. Please try again.",
"passwordUpdateFailed": "Failed to update password protection"
},
"delete": {
"title": "Delete receive link",

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Sin Autenticación",
"description": "Habilitar para servidores internos que no requieren usuario/contraseña (oculta los campos de autenticación)"
},
"smtpSecure": {
"title": "Seguridad de Conexión",
"description": "Método de seguridad de conexión SMTP - Auto (recomendado), SSL, STARTTLS o Ninguno (inseguro)",
"options": {
"auto": "Auto (Recomendado)",
"ssl": "SSL (Puerto 465)",
"tls": "STARTTLS (Puerto 587)",
"none": "Ninguno (Inseguro)"
}
}
},
"buttons": {
"save": "Guardar {group}"
"save": "Guardar {group}",
"testSmtp": "Probar Conexión",
"testing": "Probando..."
},
"errors": {
"loadFailed": "Error al cargar la configuración",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Por favor ingresa el Usuario y Contraseña SMTP, o habilita la opción 'Sin Autenticación'.",
"smtpMissingHostPort": "Por favor ingresa el Host y Puerto SMTP antes de probar.",
"smtpNotEnabled": "SMTP no está habilitado. Por favor habilita SMTP primero."
},
"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.",
"defaultPlaceholder": "Ingrese y presione Enter"
},
"redirectUri": {
"placeholder": "https://misitio.com",
@@ -579,7 +607,8 @@
"title": "Compartición no encontrada",
"description": "Esta compartición puede haber sido eliminada o haber expirado."
},
"pageTitle": "Compartición"
"pageTitle": "Compartición",
"downloadAll": "Descargar Todo"
},
"shareActions": {
"deleteTitle": "Eliminar Compartir",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Error al eliminar el archivo",
"deleteSuccess": "Archivo eliminado correctamente"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "¡Enlace de recepción eliminado con éxito!",
"aliasCreated": "¡Alias creado con éxito!",
"activateSuccess": "¡Enlace de recepción activado con éxito!",
"deactivateSuccess": "¡Enlace de recepción desactivado con éxito!"
"deactivateSuccess": "¡Enlace de recepción desactivado con éxito!",
"passwordProtectionDisabled": "¡Protección por contraseña eliminada con éxito!",
"passwordProtectionEnabled": "¡Protección por contraseña habilitada con éxito!"
},
"errors": {
"loadFailed": "Error al cargar enlaces de recepción",
"createFailed": "Error al crear enlace de recepción. Inténtalo de nuevo.",
"updateFailed": "Error al actualizar enlace de recepción. Inténtalo de nuevo.",
"deleteFailed": "Error al eliminar enlace de recepción. Inténtalo de nuevo.",
"aliasCreateFailed": "Error al crear alias. Inténtalo de nuevo."
"aliasCreateFailed": "Error al crear alias. Inténtalo de nuevo.",
"passwordUpdateFailed": "Error al actualizar la protección por contraseña"
},
"delete": {
"title": "Eliminar enlace de recepción",
@@ -1395,6 +1445,7 @@
"saveChanges": "Guardar cambios",
"cancelEdit": "Cancelar edición"
}
}
},
"defaultLinkName": "Archivos recibidos"
}
}

View File

@@ -531,10 +531,30 @@
"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"
},
"smtpNoAuth": {
"title": "Pas d'Authentification",
"description": "Activez cette option pour les serveurs internes qui ne nécessitent pas de nom d'utilisateur/mot de passe (masque les champs d'authentification)"
},
"smtpSecure": {
"title": "Sécurité de la Connexion",
"description": "Méthode de sécurité de connexion SMTP - Auto (recommandé), SSL, STARTTLS, ou Aucune (non sécurisé)",
"options": {
"auto": "Auto (Recommandé)",
"ssl": "SSL (Port 465)",
"tls": "STARTTLS (Port 587)",
"none": "Aucune (Non sécurisé)"
}
}
},
"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 +562,19 @@
},
"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.",
"smtpMissingAuth": "Veuillez remplir le nom d'utilisateur et le mot de passe SMTP, ou activer l'option 'Pas d'Authentification'.",
"smtpMissingHostPort": "Veuillez remplir l'hôte et le port SMTP avant de tester.",
"smtpNotEnabled": "SMTP n'est pas activé. Veuillez d'abord activer SMTP."
},
"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.",
"defaultPlaceholder": "Entrez et appuyez sur Entrée"
},
"redirectUri": {
"placeholder": "https://monsite.com",
@@ -579,7 +607,8 @@
"title": "Partage Non Trouvé",
"description": "Ce partage a peut-être été supprimé ou a expiré."
},
"pageTitle": "Partage"
"pageTitle": "Partage",
"downloadAll": "Tout Télécharger"
},
"shareActions": {
"deleteTitle": "Supprimer le Partage",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Erreur lors de la suppression du fichier",
"deleteSuccess": "Fichier supprimé avec succès"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Lien de réception supprimé avec succès !",
"aliasCreated": "Alias créé avec succès !",
"activateSuccess": "Lien de réception activé avec succès !",
"deactivateSuccess": "Lien de réception désactivé avec succès !"
"deactivateSuccess": "Lien de réception désactivé avec succès !",
"passwordProtectionDisabled": "Protection par mot de passe supprimée avec succès !",
"passwordProtectionEnabled": "Protection par mot de passe activée avec succès !"
},
"errors": {
"loadFailed": "Échec du chargement des liens de réception",
"createFailed": "Échec de la création du lien de réception. Veuillez réessayer.",
"updateFailed": "Échec de la mise à jour du lien de réception. Veuillez réessayer.",
"deleteFailed": "Échec de la suppression du lien de réception. Veuillez réessayer.",
"aliasCreateFailed": "Échec de la création de l'alias. Veuillez réessayer."
"aliasCreateFailed": "Échec de la création de l'alias. Veuillez réessayer.",
"passwordUpdateFailed": "Échec de la mise à jour de la protection par mot de passe"
},
"delete": {
"title": "Supprimer le lien de réception",
@@ -1395,6 +1445,7 @@
"saveChanges": "Enregistrer les modifications",
"cancelEdit": "Annuler la modification"
}
}
},
"defaultLinkName": "Fichiers reçus"
}
}

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "एडमिन ईमेल डोमेन",
"description": "जिन ईमेल डोमेन को स्वचालित रूप से व्यवस्थापक विशेषाधिकार प्राप्त होंगे"
},
"testSmtp": {
"title": "SMTP कनेक्शन का परीक्षण करें",
"description": "जांचें कि SMTP कॉन्फ़िगरेशन मान्य है"
},
"smtpNoAuth": {
"title": "कोई प्रमाणीकरण नहीं",
"description": "आंतरिक सर्वर के लिए सक्षम करें जिन्हें उपयोगकर्ता नाम/पासवर्ड की आवश्यकता नहीं होती है (प्रमाणीकरण फ़ील्ड छिपाता है)"
},
"smtpSecure": {
"title": "कनेक्शन सुरक्षा",
"description": "SMTP कनेक्शन सुरक्षा विधि - स्वचालित (अनुशंसित), SSL, STARTTLS, या कोई नहीं (असुरक्षित)",
"options": {
"auto": "स्वचालित (अनुशंसित)",
"ssl": "SSL (पोर्ट 465)",
"tls": "STARTTLS (पोर्ट 587)",
"none": "कोई नहीं (असुरक्षित)"
}
}
},
"buttons": {
"save": "{group} सहेजें"
"save": "{group} सहेजें",
"testSmtp": "कनेक्शन का परीक्षण करें",
"testing": "परीक्षण किया जा रहा है..."
},
"errors": {
"loadFailed": "सेटिंग्स लोड करने में विफल",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "सहेजने के लिए कोई परिवर्तन नहीं",
"updateSuccess": "{group} सेटिंग्स सफलतापूर्वक अपडेट हुईं"
"updateSuccess": "{group} सेटिंग्स सफलतापूर्वक अपडेट हुईं",
"smtpTestFailed": "SMTP कनेक्शन विफल: {error}",
"smtpTestGenericError": "SMTP कनेक्शन का परीक्षण करने में विफल। कृपया अपनी सेटिंग्स जांचें और पुनः प्रयास करें।",
"smtpTestSuccess": "SMTP कनेक्शन सफल! आपका ईमेल कॉन्फ़िगरेशन सही ढंग से काम कर रहा है।",
"smtpMissingAuth": "कृपया SMTP उपयोगकर्ता नाम और पासवर्ड भरें, या 'कोई प्रमाणीकरण नहीं' विकल्प सक्षम करें।",
"smtpMissingHostPort": "परीक्षण से पहले कृपया SMTP होस्ट और पोर्ट भरें।",
"smtpNotEnabled": "SMTP सक्षम नहीं है। कृपया पहले SMTP सक्षम करें।"
},
"title": "सेटिंग्स",
"breadcrumb": "सेटिंग्स",
"pageTitle": "सेटिंग्स",
"tooltips": {
"oidcScope": "स्कोप जोड़ने के लिए एक स्कोप दर्ज करें और Enter दबाएं",
"oidcAdminEmailDomains": "डोमेन जोड़ने के लिए एक डोमेन दर्ज करें और Enter दबाएं"
"oidcAdminEmailDomains": "डोमेन जोड़ने के लिए एक डोमेन दर्ज करें और Enter दबाएं",
"testSmtp": "फॉर्म में वर्तमान में दर्ज मानों के साथ SMTP कनेक्शन का परीक्षण करता है। परिवर्तनों को स्थायी बनाने के लिए, परीक्षण के बाद अपनी सेटिंग्स को सहेजना याद रखें।",
"defaultPlaceholder": "दर्ज करें और Enter दबाएं"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -579,7 +607,8 @@
"title": "साझाकरण नहीं मिला",
"description": "यह साझाकरण हटा दिया गया हो सकता है या समाप्त हो चुका है।"
},
"pageTitle": "साझाकरण"
"pageTitle": "साझाकरण",
"downloadAll": "सभी डाउनलोड करें"
},
"shareActions": {
"deleteTitle": "साझाकरण हटाएं",
@@ -1189,7 +1218,25 @@
"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} चुनें",
"deleteError": "फ़ाइल हटाने में त्रुटि",
"deleteSuccess": "फ़ाइल सफलतापूर्वक हटा दी गई"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "प्राप्ति लिंक सफलतापूर्वक हटाया गया!",
"aliasCreated": "उपनाम सफलतापूर्वक बनाया गया!",
"activateSuccess": "प्राप्ति लिंक सफलतापूर्वक सक्रिय किया गया!",
"deactivateSuccess": "प्राप्ति लिंक सफलतापूर्वक निष्क्रिय किया गया!"
"deactivateSuccess": "प्राप्ति लिंक सफलतापूर्वक निष्क्रिय किया गया!",
"passwordProtectionDisabled": "पासवर्ड सुरक्षा सफलतापूर्वक हटा दी गई!",
"passwordProtectionEnabled": "पासवर्ड सुरक्षा सफलतापूर्वक सक्षम की गई!"
},
"errors": {
"loadFailed": "प्राप्ति लिंक लोड करने में विफल",
"createFailed": "प्राप्ति लिंक बनाने में विफल। कृपया पुनः प्रयास करें।",
"updateFailed": "प्राप्ति लिंक अपडेट करने में विफल। कृपया पुनः प्रयास करें।",
"deleteFailed": "प्राप्ति लिंक हटाने में विफल। कृपया पुनः प्रयास करें।",
"aliasCreateFailed": "उपनाम बनाने में विफल। कृपया पुनः प्रयास करें।"
"aliasCreateFailed": "उपनाम बनाने में विफल। कृपया पुनः प्रयास करें।",
"passwordUpdateFailed": "पासवर्ड सुरक्षा अपडेट करने में विफल"
},
"delete": {
"title": "प्राप्ति लिंक हटाएं",
@@ -1395,6 +1445,7 @@
"saveChanges": "परिवर्तन सहेजें",
"cancelEdit": "संपादन रद्द करें"
}
}
},
"defaultLinkName": "प्राप्त फ़ाइलें"
}
}
}

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Nessuna Autenticazione",
"description": "Abilita questa opzione per i server interni che non richiedono nome utente/password (nasconde i campi di autenticazione)"
},
"smtpSecure": {
"title": "Sicurezza Connessione",
"description": "Metodo di sicurezza della connessione SMTP - Auto (raccomandato), SSL, STARTTLS o Nessuno (non sicuro)",
"options": {
"auto": "Auto (Raccomandato)",
"ssl": "SSL (Porta 465)",
"tls": "STARTTLS (Porta 587)",
"none": "Nessuno (Non sicuro)"
}
}
},
"buttons": {
"save": "Salva {group}"
"save": "Salva {group}",
"testSmtp": "Prova Connessione",
"testing": "Verifica in corso..."
},
"errors": {
"loadFailed": "Errore durante il caricamento delle impostazioni",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Inserisci Nome utente e Password SMTP, oppure abilita l'opzione 'Nessuna Autenticazione'.",
"smtpMissingHostPort": "Inserisci Host e Porta SMTP prima di effettuare il test.",
"smtpNotEnabled": "SMTP non è abilitato. Abilitalo prima di procedere."
},
"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.",
"defaultPlaceholder": "Inserisci e premi Invio"
},
"redirectUri": {
"placeholder": "https://miosito.com",
@@ -579,7 +607,8 @@
"title": "Condivisione Non Trovata",
"description": "Questa condivisione potrebbe essere stata eliminata o è scaduta."
},
"pageTitle": "Condivisione"
"pageTitle": "Condivisione",
"downloadAll": "Scarica Tutto"
},
"shareActions": {
"deleteTitle": "Elimina Condivisione",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Errore durante l'eliminazione del file",
"deleteSuccess": "File eliminato con successo"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Link di ricezione eliminato con successo!",
"aliasCreated": "Alias creato con successo!",
"activateSuccess": "Link di ricezione attivato con successo!",
"deactivateSuccess": "Link di ricezione disattivato con successo!"
"deactivateSuccess": "Link di ricezione disattivato con successo!",
"passwordProtectionDisabled": "Protezione con password rimossa con successo!",
"passwordProtectionEnabled": "Protezione con password abilitata con successo!"
},
"errors": {
"loadFailed": "Impossibile caricare i link di ricezione",
"createFailed": "Impossibile creare il link di ricezione. Riprova.",
"updateFailed": "Impossibile aggiornare il link di ricezione. Riprova.",
"deleteFailed": "Impossibile eliminare il link di ricezione. Riprova.",
"aliasCreateFailed": "Impossibile creare l'alias. Riprova."
"aliasCreateFailed": "Impossibile creare l'alias. Riprova.",
"passwordUpdateFailed": "Impossibile aggiornare la protezione con password"
},
"delete": {
"title": "Elimina link di ricezione",
@@ -1395,6 +1445,7 @@
"saveChanges": "Salva modifiche",
"cancelEdit": "Annulla modifica"
}
}
},
"defaultLinkName": "File ricevuti"
}
}

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "管理者メールドメイン",
"description": "自動的に管理者権限が付与されるメールドメイン"
},
"testSmtp": {
"title": "SMTP接続テスト",
"description": "SMTP設定が有効かどうかをテストします"
},
"smtpNoAuth": {
"title": "認証なし",
"description": "ユーザー名/パスワードを必要としない内部サーバー用(認証フィールドを非表示)"
},
"smtpSecure": {
"title": "接続セキュリティ",
"description": "SMTP接続セキュリティ方式 - 自動推奨、SSL、STARTTLS、または なし(安全でない)",
"options": {
"auto": "自動(推奨)",
"ssl": "SSLポート465",
"tls": "STARTTLSポート587",
"none": "なし(安全でない)"
}
}
},
"buttons": {
"save": "{group}を保存"
"save": "{group}を保存",
"testSmtp": "接続テスト",
"testing": "テスト中..."
},
"errors": {
"loadFailed": "設定の読み込みに失敗しました",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "保存する変更はありません",
"updateSuccess": "{group}の設定が正常に更新されました"
"updateSuccess": "{group}の設定が正常に更新されました",
"smtpTestFailed": "SMTP接続に失敗しました: {error}",
"smtpTestGenericError": "SMTP接続のテストに失敗しました。設定を確認して再度お試しください。",
"smtpTestSuccess": "SMTP接続に成功しましたメール設定は正常に動作しています。",
"smtpMissingAuth": "SMTPユーザー名とパスワードを入力するか、「認証なし」オプションを有効にしてください。",
"smtpMissingHostPort": "テストの前にSMTPホストとポートを入力してください。",
"smtpNotEnabled": "SMTPが有効になっていません。最初にSMTPを有効にしてください。"
},
"title": "設定",
"breadcrumb": "設定",
"pageTitle": "設定",
"tooltips": {
"oidcScope": "スコープを入力してEnterキーを押して追加",
"oidcAdminEmailDomains": "ドメインを入力してEnterキーを押して追加"
"oidcAdminEmailDomains": "ドメインを入力してEnterキーを押して追加",
"testSmtp": "フォームに現在入力されている値でSMTP接続をテストします。変更を永続化するには、テスト後に設定を保存することを忘れないでください。",
"defaultPlaceholder": "入力してEnterキーを押してください"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -579,7 +607,8 @@
"title": "共有が見つかりません",
"description": "この共有は削除されたか、期限が切れている可能性があります。"
},
"pageTitle": "共有"
"pageTitle": "共有",
"downloadAll": "すべてダウンロード"
},
"shareActions": {
"deleteTitle": "共有を削除",
@@ -1189,7 +1218,25 @@
"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}を選択",
"deleteError": "ファイルの削除に失敗しました",
"deleteSuccess": "ファイルを正常に削除しました"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "受信リンクを削除しました!",
"aliasCreated": "エイリアスを作成しました!",
"activateSuccess": "受信リンクを有効化しました!",
"deactivateSuccess": "受信リンクを無効化しました!"
"deactivateSuccess": "受信リンクを無効化しました!",
"passwordProtectionDisabled": "パスワード保護を正常に解除しました!",
"passwordProtectionEnabled": "パスワード保護を正常に有効化しました!"
},
"errors": {
"loadFailed": "受信リンクの読み込みに失敗しました",
"createFailed": "受信リンクの作成に失敗しました。もう一度お試しください。",
"updateFailed": "受信リンクの更新に失敗しました。もう一度お試しください。",
"deleteFailed": "受信リンクの削除に失敗しました。もう一度お試しください。",
"aliasCreateFailed": "エイリアスの作成に失敗しました。もう一度お試しください。"
"aliasCreateFailed": "エイリアスの作成に失敗しました。もう一度お試しください。",
"passwordUpdateFailed": "パスワード保護の更新に失敗しました"
},
"delete": {
"title": "受信リンクを削除",
@@ -1395,6 +1445,7 @@
"saveChanges": "変更を保存",
"cancelEdit": "編集をキャンセル"
}
}
},
"defaultLinkName": "受信したファイル"
}
}
}

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "관리자 이메일 도메인",
"description": "자동으로 관리자 권한이 부여되는 이메일 도메인"
},
"testSmtp": {
"title": "SMTP 연결 테스트",
"description": "SMTP 구성이 유효한지 테스트합니다"
},
"smtpNoAuth": {
"title": "인증 없음",
"description": "사용자 이름/비밀번호가 필요하지 않은 내부 서버의 경우 활성화하세요 (인증 필드 숨김)"
},
"smtpSecure": {
"title": "연결 보안",
"description": "SMTP 연결 보안 방식 - 자동 (권장), SSL, STARTTLS 또는 없음 (비보안)",
"options": {
"auto": "자동 (권장)",
"ssl": "SSL (포트 465)",
"tls": "STARTTLS (포트 587)",
"none": "없음 (비보안)"
}
}
},
"buttons": {
"save": "{group} 저장"
"save": "{group} 저장",
"testSmtp": "연결 테스트",
"testing": "테스트 중..."
},
"errors": {
"loadFailed": "설정을 불러오는데 실패했습니다",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "저장할 변경 사항이 없습니다",
"updateSuccess": "{group} 설정이 성공적으로 업데이트되었습니다"
"updateSuccess": "{group} 설정이 성공적으로 업데이트되었습니다",
"smtpTestFailed": "SMTP 연결 실패: {error}",
"smtpTestGenericError": "SMTP 연결 테스트에 실패했습니다. 설정을 확인하고 다시 시도해주세요.",
"smtpTestSuccess": "SMTP 연결 성공! 이메일 구성이 올바르게 작동합니다.",
"smtpMissingAuth": "SMTP 사용자 이름과 비밀번호를 입력하거나 '인증 없음' 옵션을 활성화하세요.",
"smtpMissingHostPort": "테스트하기 전에 SMTP 호스트와 포트를 입력하세요.",
"smtpNotEnabled": "SMTP가 활성화되지 않았습니다. 먼저 SMTP를 활성화하세요."
},
"title": "설정",
"breadcrumb": "설정",
"pageTitle": "설정",
"tooltips": {
"oidcScope": "스코프를 입력하고 Enter 키를 눌러 추가",
"oidcAdminEmailDomains": "도메인을 입력하고 Enter 키를 눌러 추가"
"oidcAdminEmailDomains": "도메인을 입력하고 Enter 키를 눌러 추가",
"testSmtp": "현재 입력된 값으로 SMTP 연결을 테스트합니다. 변경 사항을 영구적으로 적용하려면 테스트 후 설정을 저장하는 것을 잊지 마세요.",
"defaultPlaceholder": "입력하고 Enter 키를 누르세요"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -579,7 +607,8 @@
"title": "공유를 찾을 수 없습니다",
"description": "이 공유는 삭제되었거나 만료되었을 수 있습니다."
},
"pageTitle": "공유"
"pageTitle": "공유",
"downloadAll": "모두 다운로드"
},
"shareActions": {
"deleteTitle": "공유 삭제",
@@ -1189,7 +1218,25 @@
"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} 파일 선택",
"deleteError": "파일 삭제 오류",
"deleteSuccess": "파일이 성공적으로 삭제됨"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "수신 링크가 성공적으로 삭제되었습니다!",
"aliasCreated": "별칭이 성공적으로 생성되었습니다!",
"activateSuccess": "수신 링크가 성공적으로 활성화되었습니다!",
"deactivateSuccess": "수신 링크가 성공적으로 비활성화되었습니다!"
"deactivateSuccess": "수신 링크가 성공적으로 비활성화되었습니다!",
"passwordProtectionDisabled": "비밀번호 보호가 성공적으로 제거되었습니다!",
"passwordProtectionEnabled": "비밀번호 보호가 성공적으로 활성화되었습니다!"
},
"errors": {
"loadFailed": "수신 링크 로드 실패",
"createFailed": "수신 링크 생성 실패. 다시 시도하세요.",
"updateFailed": "수신 링크 업데이트 실패. 다시 시도하세요.",
"deleteFailed": "수신 링크 삭제 실패. 다시 시도하세요.",
"aliasCreateFailed": "별칭 생성 실패. 다시 시도하세요."
"aliasCreateFailed": "별칭 생성 실패. 다시 시도하세요.",
"passwordUpdateFailed": "비밀번호 보호 업데이트 실패"
},
"delete": {
"title": "수신 링크 삭제",
@@ -1395,6 +1445,7 @@
"saveChanges": "변경사항 저장",
"cancelEdit": "편집 취소"
}
}
},
"defaultLinkName": "받은 파일"
}
}
}

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Geen Authenticatie",
"description": "Schakel dit in voor interne servers die geen gebruikersnaam/wachtwoord vereisen (verbergt authenticatievelden)"
},
"smtpSecure": {
"title": "Verbindingsbeveiliging",
"description": "SMTP verbindingsbeveiligingsmethode - Auto (aanbevolen), SSL, STARTTLS, of Geen (onveilig)",
"options": {
"auto": "Auto (Aanbevolen)",
"ssl": "SSL (Poort 465)",
"tls": "STARTTLS (Poort 587)",
"none": "Geen (Onveilig)"
}
}
},
"buttons": {
"save": "{group} Opslaan"
"save": "{group} Opslaan",
"testSmtp": "Test Verbinding",
"testing": "Testen..."
},
"errors": {
"loadFailed": "Fout bij het laden van instellingen",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Vul de SMTP gebruikersnaam en wachtwoord in, of schakel de optie 'Geen Authenticatie' in.",
"smtpMissingHostPort": "Vul de SMTP Host en Poort in voordat u test.",
"smtpNotEnabled": "SMTP is niet ingeschakeld. Schakel eerst SMTP in."
},
"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.",
"defaultPlaceholder": "Voer in en druk op Enter"
},
"redirectUri": {
"placeholder": "https://mijnsite.com",
@@ -579,7 +607,8 @@
"title": "Delen Niet Gevonden",
"description": "Dit delen is mogelijk verwijderd of verlopen."
},
"pageTitle": "Delen"
"pageTitle": "Delen",
"downloadAll": "Alles Downloaden"
},
"shareActions": {
"deleteTitle": "Delen Verwijderen",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Fout bij verwijderen bestand",
"deleteSuccess": "Bestand succesvol verwijderd"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Ontvangstlink succesvol verwijderd!",
"aliasCreated": "Alias succesvol aangemaakt!",
"activateSuccess": "Ontvangstlink succesvol geactiveerd!",
"deactivateSuccess": "Ontvangstlink succesvol gedeactiveerd!"
"deactivateSuccess": "Ontvangstlink succesvol gedeactiveerd!",
"passwordProtectionDisabled": "Wachtwoordbeveiliging succesvol verwijderd!",
"passwordProtectionEnabled": "Wachtwoordbeveiliging succesvol ingeschakeld!"
},
"errors": {
"loadFailed": "Fout bij laden ontvangstlinks",
"createFailed": "Fout bij aanmaken ontvangstlink. Probeer het opnieuw.",
"updateFailed": "Fout bij bijwerken ontvangstlink. Probeer het opnieuw.",
"deleteFailed": "Fout bij verwijderen ontvangstlink. Probeer het opnieuw.",
"aliasCreateFailed": "Fout bij aanmaken alias. Probeer het opnieuw."
"aliasCreateFailed": "Fout bij aanmaken alias. Probeer het opnieuw.",
"passwordUpdateFailed": "Fout bij bijwerken wachtwoordbeveiliging"
},
"delete": {
"title": "Ontvangstlink verwijderen",
@@ -1395,6 +1445,7 @@
"saveChanges": "Wijzigingen opslaan",
"cancelEdit": "Bewerken annuleren"
}
}
},
"defaultLinkName": "Ontvangen bestanden"
}
}
}

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Bez uwierzytelniania",
"description": "Włącz tę opcję dla serwerów wewnętrznych, które nie wymagają nazwy użytkownika/hasła (ukrywa pola uwierzytelniania)"
},
"smtpSecure": {
"title": "Bezpieczeństwo połączenia",
"description": "Metoda zabezpieczenia połączenia SMTP - Auto (zalecane), SSL, STARTTLS lub Brak (niezabezpieczone)",
"options": {
"auto": "Auto (Zalecane)",
"ssl": "SSL (Port 465)",
"tls": "STARTTLS (Port 587)",
"none": "Brak (Niezabezpieczone)"
}
}
},
"buttons": {
"save": "Zapisz {group}"
"save": "Zapisz {group}",
"testSmtp": "Testuj połączenie",
"testing": "Testowanie..."
},
"errors": {
"loadFailed": "Nie udało się załadować ustawień",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Proszę wypełnić nazwę użytkownika i hasło SMTP lub włączyć opcję 'Bez uwierzytelniania'.",
"smtpMissingHostPort": "Proszę wypełnić host i port SMTP przed testowaniem.",
"smtpNotEnabled": "SMTP nie jest włączone. Proszę najpierw włączyć SMTP."
},
"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.",
"defaultPlaceholder": "Wprowadź i naciśnij Enter"
},
"redirectUri": {
"placeholder": "https://mysite.com",
@@ -579,7 +607,8 @@
"title": "Udostępnienie nie znaleziono",
"description": "To udostępnienie mogło zostać usunięte lub wygasło."
},
"pageTitle": "Udostępnij"
"pageTitle": "Udostępnij",
"downloadAll": "[TO_TRANSLATE] Download All"
},
"shareActions": {
"deleteTitle": "Usuń udostępnienie",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Błąd usuwania pliku",
"deleteSuccess": "Plik usunięty pomyślnie"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Link do odbierania usunięty pomyślnie!",
"aliasCreated": "Alias utworzony pomyślnie!",
"activateSuccess": "Link do odbierania aktywowany pomyślnie!",
"deactivateSuccess": "Link do odbierania dezaktywowany pomyślnie!"
"deactivateSuccess": "Link do odbierania dezaktywowany pomyślnie!",
"passwordProtectionDisabled": "Ochrona hasłem została pomyślnie usunięta!",
"passwordProtectionEnabled": "Ochrona hasłem została pomyślnie włączona!"
},
"errors": {
"loadFailed": "Nie udało się załadować linków do odbierania",
"createFailed": "Nie udało się utworzyć linku do odbierania. Spróbuj ponownie.",
"updateFailed": "Nie udało się zaktualizować linku do odbierania. Spróbuj ponownie.",
"deleteFailed": "Nie udało się usunąć linku do odbierania. Spróbuj ponownie.",
"aliasCreateFailed": "Nie udało się utworzyć aliasu. Spróbuj ponownie."
"aliasCreateFailed": "Nie udało się utworzyć aliasu. Spróbuj ponownie.",
"passwordUpdateFailed": "Nie udało się zaktualizować ochrony hasłem"
},
"delete": {
"title": "Usuń link do odbierania",
@@ -1395,6 +1445,7 @@
"saveChanges": "Zapisz zmiany",
"cancelEdit": "Anuluj edycję"
}
}
},
"defaultLinkName": "Otrzymane pliki"
}
}
}

View File

@@ -443,7 +443,9 @@
},
"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.",
"defaultPlaceholder": "Digite e pressione Enter"
},
"redirectUri": {
"placeholder": "https://meusite.com",
@@ -558,10 +560,30 @@
"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"
},
"smtpNoAuth": {
"title": "Sem Autenticação",
"description": "Ative isso para servidores internos que não exigem nome de usuário/senha (oculta campos de autenticação)"
},
"smtpSecure": {
"title": "Segurança da Conexão",
"description": "Método de segurança da conexão SMTP - Auto (recomendado), SSL, STARTTLS ou Nenhum (inseguro)",
"options": {
"auto": "Auto (Recomendado)",
"ssl": "SSL (Porta 465)",
"tls": "STARTTLS (Porta 587)",
"none": "Nenhum (Inseguro)"
}
}
},
"buttons": {
"save": "Salvar {group}"
"save": "Salvar {group}",
"testSmtp": "Testar Conexão",
"testing": "Testando..."
},
"errors": {
"loadFailed": "Falha ao carregar configurações",
@@ -569,7 +591,13 @@
},
"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.",
"smtpMissingAuth": "Por favor, preencha o Nome de Usuário e Senha SMTP, ou ative a opção 'Sem Autenticação'.",
"smtpMissingHostPort": "Por favor, preencha o Host e Porta SMTP antes de testar.",
"smtpNotEnabled": "SMTP não está habilitado. Por favor, habilite o SMTP primeiro."
},
"title": "Configurações",
"breadcrumb": "Configurações",
@@ -601,7 +629,8 @@
"title": "Compartilhamento não Encontrado",
"description": "Este compartilhamento pode ter sido excluído ou expirado."
},
"pageTitle": "Compartilhamento"
"pageTitle": "Compartilhamento",
"downloadAll": "Baixar todos"
},
"shareActions": {
"deleteTitle": "Excluir Compartilhamento",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Erro ao excluir arquivo",
"deleteSuccess": "Arquivo excluído com sucesso"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Link de recebimento excluído com sucesso!",
"aliasCreated": "Alias criado com sucesso!",
"activateSuccess": "Link de recebimento ativado com sucesso!",
"deactivateSuccess": "Link de recebimento desativado com sucesso!"
"deactivateSuccess": "Link de recebimento desativado com sucesso!",
"passwordProtectionDisabled": "Proteção por senha removida com sucesso!",
"passwordProtectionEnabled": "Proteção por senha ativada com sucesso!"
},
"errors": {
"loadFailed": "Falha ao carregar links de recebimento",
"createFailed": "Falha ao criar link de recebimento. Tente novamente.",
"updateFailed": "Falha ao atualizar link de recebimento. Tente novamente.",
"deleteFailed": "Falha ao excluir link de recebimento. Tente novamente.",
"aliasCreateFailed": "Falha ao criar alias. Tente novamente."
"aliasCreateFailed": "Falha ao criar alias. Tente novamente.",
"passwordUpdateFailed": "Falha ao atualizar a proteção por senha"
},
"delete": {
"title": "Excluir link de recebimento",
@@ -1395,6 +1445,7 @@
"saveChanges": "Salvar alterações",
"cancelEdit": "Cancelar edição"
}
}
},
"defaultLinkName": "Arquivos recebidos"
}
}

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "Домены Email Администратора",
"description": "Домены электронной почты, которые автоматически получают права администратора"
},
"testSmtp": {
"title": "Проверить SMTP-соединение",
"description": "Проверить правильность настройки SMTP"
},
"smtpNoAuth": {
"title": "Без аутентификации",
"description": "Включите для внутренних серверов, не требующих имя пользователя/пароль (скрывает поля аутентификации)"
},
"smtpSecure": {
"title": "Безопасность соединения",
"description": "Метод безопасности SMTP-соединения - Авто (рекомендуется), SSL, STARTTLS или Нет (небезопасно)",
"options": {
"auto": "Авто (Рекомендуется)",
"ssl": "SSL (Порт 465)",
"tls": "STARTTLS (Порт 587)",
"none": "Нет (Небезопасно)"
}
}
},
"buttons": {
"save": "Сохранить {group}"
"save": "Сохранить {group}",
"testSmtp": "Проверить соединение",
"testing": "Проверка..."
},
"errors": {
"loadFailed": "Ошибка загрузки настроек",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "Изменений для сохранения нет",
"updateSuccess": "Настройки {group} успешно обновлены"
"updateSuccess": "Настройки {group} успешно обновлены",
"smtpTestFailed": "Ошибка SMTP-соединения: {error}",
"smtpTestGenericError": "Не удалось проверить SMTP-соединение. Пожалуйста, проверьте настройки и попробуйте снова.",
"smtpTestSuccess": "SMTP-соединение успешно установлено! Ваша конфигурация электронной почты работает корректно.",
"smtpMissingAuth": "Пожалуйста, заполните имя пользователя и пароль SMTP или включите опцию 'Без аутентификации'.",
"smtpMissingHostPort": "Пожалуйста, заполните хост и порт SMTP перед тестированием.",
"smtpNotEnabled": "SMTP не включен. Пожалуйста, сначала включите 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-соединение с текущими значениями в форме. Чтобы сохранить изменения, не забудьте сохранить настройки после тестирования.",
"defaultPlaceholder": "Введите и нажмите Enter"
},
"redirectUri": {
"placeholder": "https://meusite.com",
@@ -579,7 +607,8 @@
"title": "Общий доступ не найден",
"description": "Этот общий доступ может быть удален или истек."
},
"pageTitle": "Общий доступ"
"pageTitle": "Общий доступ",
"downloadAll": "Скачать все"
},
"shareActions": {
"deleteTitle": "Удалить Общий Доступ",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "Ошибка при удалении файла",
"deleteSuccess": "Файл успешно удален"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Ссылка для получения успешно удалена!",
"aliasCreated": "Псевдоним успешно создан!",
"activateSuccess": "Ссылка для получения успешно активирована!",
"deactivateSuccess": "Ссылка для получения успешно деактивирована!"
"deactivateSuccess": "Ссылка для получения успешно деактивирована!",
"passwordProtectionDisabled": "Защита паролем успешно отключена!",
"passwordProtectionEnabled": "Защита паролем успешно включена!"
},
"errors": {
"loadFailed": "Не удалось загрузить ссылки для получения",
"createFailed": "Не удалось создать ссылку для получения. Попробуйте снова.",
"updateFailed": "Не удалось обновить ссылку для получения. Попробуйте снова.",
"deleteFailed": "Не удалось удалить ссылку для получения. Попробуйте снова.",
"aliasCreateFailed": "Не удалось создать псевдоним. Попробуйте снова."
"aliasCreateFailed": "Не удалось создать псевдоним. Попробуйте снова.",
"passwordUpdateFailed": "Не удалось обновить защиту паролем"
},
"delete": {
"title": "Удалить ссылку для получения",
@@ -1395,6 +1445,7 @@
"saveChanges": "Сохранить изменения",
"cancelEdit": "Отменить редактирование"
}
}
},
"defaultLinkName": "Полученные файлы"
}
}
}

View File

@@ -528,10 +528,30 @@
"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"
},
"smtpNoAuth": {
"title": "Kimlik Doğrulama Yok",
"description": "Kullanıcı adı/şifre gerektirmeyen dahili sunucular için bunu etkinleştirin (kimlik doğrulama alanlarını gizler)"
},
"smtpSecure": {
"title": "Bağlantı Güvenliği",
"description": "SMTP bağlantı güvenlik yöntemi - Otomatik (önerilen), SSL, STARTTLS veya Hiçbiri (güvensiz)",
"options": {
"auto": "Otomatik (Önerilen)",
"ssl": "SSL (Port 465)",
"tls": "STARTTLS (Port 587)",
"none": "Hiçbiri (Güvensiz)"
}
}
},
"buttons": {
"save": "{group} Kaydet"
"save": "{group} Kaydet",
"testSmtp": "Bağlantıyı Test Et",
"testing": "Test Ediliyor..."
},
"errors": {
"loadFailed": "Ayarlar yüklenemedi",
@@ -539,14 +559,22 @@
},
"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.",
"smtpMissingAuth": "Lütfen SMTP Kullanıcı Adı ve Şifresini doldurun veya 'Kimlik Doğrulama Yok' seçeneğini etkinleştirin.",
"smtpMissingHostPort": "Lütfen test etmeden önce SMTP Ana Bilgisayar ve Bağlantı Noktasını doldurun.",
"smtpNotEnabled": "SMTP etkin değil. Lütfen önce SMTP'yi etkinleştirin."
},
"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.",
"defaultPlaceholder": "Girin ve Enter'a basın"
},
"redirectUri": {
"placeholder": "https://sitem.com",
@@ -579,7 +607,8 @@
"title": "Paylaşım Bulunamadı",
"description": "Bu paylaşım silinmiş veya süresi dolmuş olabilir."
},
"pageTitle": "Paylaşım"
"pageTitle": "Paylaşım",
"downloadAll": "Tümünü İndir"
},
"shareActions": {
"deleteTitle": "Paylaşımı Sil",
@@ -1189,7 +1218,25 @@
"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ç",
"deleteError": "Dosya silinirken hata oluştu",
"deleteSuccess": "Dosya başarıyla silindi"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "Alma bağlantısı başarıyla silindi!",
"aliasCreated": "Takma ad başarıyla oluşturuldu!",
"activateSuccess": "Alma bağlantısı başarıyla etkinleştirildi!",
"deactivateSuccess": "Alma bağlantısı başarıyla devre dışı bırakıldı!"
"deactivateSuccess": "Alma bağlantısı başarıyla devre dışı bırakıldı!",
"passwordProtectionDisabled": "Şifre koruması başarıyla kaldırıldı!",
"passwordProtectionEnabled": "Şifre koruması başarıyla etkinleştirildi!"
},
"errors": {
"loadFailed": "Alma bağlantıları yüklenemedi",
"createFailed": "Alma bağlantısı oluşturulamadı. Lütfen tekrar deneyin.",
"updateFailed": "Alma bağlantısı güncellenemedi. Lütfen tekrar deneyin.",
"deleteFailed": "Alma bağlantısı silinemedi. Lütfen tekrar deneyin.",
"aliasCreateFailed": "Takma ad oluşturulamadı. Lütfen tekrar deneyin."
"aliasCreateFailed": "Takma ad oluşturulamadı. Lütfen tekrar deneyin.",
"passwordUpdateFailed": "Şifre koruması güncellenemedi"
},
"delete": {
"title": "Alma bağlantısını sil",
@@ -1395,6 +1445,7 @@
"saveChanges": "Değişiklikleri kaydet",
"cancelEdit": "Düzenlemeyi iptal et"
}
}
},
"defaultLinkName": "Alınan Dosyalar"
}
}
}

View File

@@ -528,10 +528,30 @@
"oidcAdminEmailDomains": {
"title": "管理员电子邮件域名",
"description": "自动获得管理员权限的电子邮件域名"
},
"testSmtp": {
"title": "测试SMTP连接",
"description": "测试SMTP配置是否有效"
},
"smtpNoAuth": {
"title": "无需认证",
"description": "为不需要用户名/密码的内部服务器启用此选项(隐藏认证字段)"
},
"smtpSecure": {
"title": "连接安全性",
"description": "SMTP连接安全方法 - 自动推荐、SSL、STARTTLS或无不安全",
"options": {
"auto": "自动(推荐)",
"ssl": "SSL端口465",
"tls": "STARTTLS端口587",
"none": "无(不安全)"
}
}
},
"buttons": {
"save": "保存 {group}"
"save": "保存 {group}",
"testSmtp": "测试连接",
"testing": "测试中..."
},
"errors": {
"loadFailed": "加载设置失败",
@@ -539,14 +559,22 @@
},
"messages": {
"noChanges": "没有需要保存的更改",
"updateSuccess": "{group}设置更新成功"
"updateSuccess": "{group}设置更新成功",
"smtpTestFailed": "SMTP连接失败{error}",
"smtpTestGenericError": "SMTP连接测试失败。请检查您的设置并重试。",
"smtpTestSuccess": "SMTP连接成功您的电子邮件配置工作正常。",
"smtpMissingAuth": "请填写SMTP用户名和密码或启用“无需认证”选项。",
"smtpMissingHostPort": "测试前请填写SMTP主机和端口。",
"smtpNotEnabled": "SMTP未启用。请先启用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连接。要使更改永久生效请记得在测试后保存您的设置。",
"defaultPlaceholder": "输入并按回车键"
},
"redirectUri": {
"placeholder": "https://meusite.com",
@@ -579,7 +607,8 @@
"title": "未找到共享",
"description": "该共享可能已被删除或已过期。"
},
"pageTitle": "共享"
"pageTitle": "共享",
"downloadAll": "下载所有"
},
"shareActions": {
"deleteTitle": "删除共享",
@@ -1189,7 +1218,25 @@
"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}",
"deleteError": "删除文件时出错",
"deleteSuccess": "文件已成功删除"
}
},
"form": {
@@ -1272,14 +1319,17 @@
"deleteSuccess": "接收链接删除成功!",
"aliasCreated": "别名创建成功!",
"activateSuccess": "接收链接启用成功!",
"deactivateSuccess": "接收链接停用成功!"
"deactivateSuccess": "接收链接停用成功!",
"passwordProtectionDisabled": "密码保护已成功移除!",
"passwordProtectionEnabled": "密码保护已成功启用!"
},
"errors": {
"loadFailed": "加载接收链接失败",
"createFailed": "创建接收链接失败。请重试。",
"updateFailed": "更新接收链接失败。请重试。",
"deleteFailed": "删除接收链接失败。请重试。",
"aliasCreateFailed": "创建别名失败。请重试。"
"aliasCreateFailed": "创建别名失败。请重试。",
"passwordUpdateFailed": "更新密码保护失败"
},
"delete": {
"title": "删除接收链接",
@@ -1395,6 +1445,7 @@
"saveChanges": "保存更改",
"cancelEdit": "取消编辑"
}
}
},
"defaultLinkName": "已接收文件"
}
}
}

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Main script to run all Palmr translation operations.
Main script to run Palmr translation management operations.
Makes it easy to run scripts without remembering specific names.
"""
@@ -17,25 +17,65 @@ def run_command(script_name: str, args: list) -> int:
return subprocess.run(cmd).returncode
def filter_args_for_script(script_name: str, args: list) -> list:
"""Filter arguments based on what each script accepts."""
# Arguments that check_translations.py accepts
check_args = ['--messages-dir', '--reference']
# Arguments that sync_translations.py accepts
sync_args = ['--messages-dir', '--reference', '--no-mark-untranslated', '--dry-run']
if script_name == 'check_translations.py':
filtered = []
skip_next = False
for i, arg in enumerate(args):
if skip_next:
skip_next = False
continue
if arg in check_args:
filtered.append(arg)
# Add the value for the argument if it exists
if i + 1 < len(args) and not args[i + 1].startswith('--'):
filtered.append(args[i + 1])
skip_next = True
return filtered
elif script_name == 'sync_translations.py':
filtered = []
skip_next = False
for i, arg in enumerate(args):
if skip_next:
skip_next = False
continue
if arg in sync_args:
filtered.append(arg)
# Add the value for the argument if it exists
if i + 1 < len(args) and not args[i + 1].startswith('--'):
filtered.append(args[i + 1])
skip_next = True
return filtered
return args
def main():
parser = argparse.ArgumentParser(
description='Main script to manage Palmr translations',
epilog='Examples:\n'
' python3 run_translations.py check\n'
' python3 run_translations.py sync --dry-run\n'
' python3 run_translations.py translate --delay 2.0\n'
' python3 run_translations.py all # Complete workflow\n',
' python3 run_translations.py all --dry-run\n',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'command',
choices=['check', 'sync', 'translate', 'all', 'help'],
choices=['check', 'sync', 'all', 'help'],
help='Command to execute:\n'
'check - Check translation status\n'
'sync - Synchronize missing keys\n'
'translate - Automatically translate strings\n'
'all - Run complete workflow\n'
'all - Run complete workflow (sync + check)\n'
'help - Show detailed help'
)
@@ -57,13 +97,7 @@ def main():
print(" python3 run_translations.py sync --dry-run")
print(" python3 run_translations.py sync --no-mark-untranslated")
print()
print("🌐 translate - Automatically translate")
print(" python3 run_translations.py translate")
print(" python3 run_translations.py translate --dry-run")
print(" python3 run_translations.py translate --delay 2.0")
print(" python3 run_translations.py translate --skip-languages pt-BR.json")
print()
print("⚡ all - Complete workflow (sync + translate)")
print("⚡ all - Complete workflow (sync + check)")
print(" python3 run_translations.py all")
print(" python3 run_translations.py all --dry-run")
print()
@@ -72,22 +106,21 @@ def main():
print(" apps/web/messages/ - Translation files")
print()
print("💡 TIPS:")
print("• Use --dry-run on any command to test")
print("• Use --dry-run on sync or all commands to test")
print("• Use --help on any command for specific options")
print("Read https://docs.palmr.dev/docs/3.0-beta/translation-management for complete documentation")
print("Manually translate strings marked with [TO_TRANSLATE]")
print("• Read documentation for complete translation guidelines")
return 0
elif args.command == 'check':
print("🔍 Checking translation status...")
return run_command('check_translations.py', remaining_args)
filtered_args = filter_args_for_script('check_translations.py', remaining_args)
return run_command('check_translations.py', filtered_args)
elif args.command == 'sync':
print("🔄 Synchronizing translation keys...")
return run_command('sync_translations.py', remaining_args)
elif args.command == 'translate':
print("🌐 Automatically translating strings...")
return run_command('translate_missing.py', remaining_args)
filtered_args = filter_args_for_script('sync_translations.py', remaining_args)
return run_command('sync_translations.py', filtered_args)
elif args.command == 'all':
print("⚡ Running complete translation workflow...")
@@ -98,7 +131,8 @@ def main():
# 1. Initial check
print("1⃣ Checking initial status...")
result = run_command('check_translations.py', remaining_args)
check_args = filter_args_for_script('check_translations.py', remaining_args)
result = run_command('check_translations.py', check_args)
if result != 0:
print("❌ Error in initial check")
return result
@@ -107,33 +141,27 @@ def main():
# 2. Sync
print("2⃣ Synchronizing missing keys...")
result = run_command('sync_translations.py', remaining_args)
sync_args = filter_args_for_script('sync_translations.py', remaining_args)
result = run_command('sync_translations.py', sync_args)
if result != 0:
print("❌ Error in synchronization")
return result
if not is_dry_run:
print("\n" + "="*50)
# 3. Translate
print("3⃣ Automatically translating strings...")
result = run_command('translate_missing.py', remaining_args)
if result != 0:
print("❌ Error in translation")
return result
print("\n" + "="*50)
# 4. Final check
print("4⃣ Final check...")
result = run_command('check_translations.py', remaining_args)
if result != 0:
print("❌ Error in final check")
return result
print("\n" + "="*50)
# 3. Final check
print("3⃣ Final check...")
check_args = filter_args_for_script('check_translations.py', remaining_args)
result = run_command('check_translations.py', check_args)
if result != 0:
print("❌ Error in final check")
return result
print("\n🎉 Complete workflow executed successfully!")
if is_dry_run:
print("💡 Run without --dry-run to apply changes")
else:
print("💡 Review strings marked with [TO_TRANSLATE] and translate them manually")
return 0

View File

@@ -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())

View File

@@ -73,14 +73,16 @@ export function EditPasswordModal({
});
toast.success(
data.hasPassword ? "Proteção por senha ativada com sucesso!" : "Proteção por senha removida com sucesso!"
data.hasPassword
? t("reverseShares.messages.passwordProtectionEnabled")
: t("reverseShares.messages.passwordProtectionDisabled")
);
onClose();
form.reset();
} catch (error) {
console.error("Failed to update password:", error);
toast.error("Erro ao atualizar proteção por senha");
toast.error(t("reverseShares.errors.passwordUpdateFailed"));
}
};

View File

@@ -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";
@@ -32,6 +47,7 @@ import {
} from "@/http/endpoints/reverse-shares";
import type { ReverseShareFile } from "@/http/endpoints/reverse-shares/types";
import { getFileIcon } from "@/utils/file-icons";
import { truncateFileName } from "@/utils/file-utils";
import { ReverseShare } from "../hooks/use-reverse-shares";
import { ReverseShareFilePreviewModal } from "./reverse-share-file-preview-modal";
@@ -262,6 +278,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 +289,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 +299,7 @@ function FileRow({
inputRef,
hoveredFile,
copyingFile,
isSelected,
onStartEdit,
onSaveEdit,
onCancelEdit,
@@ -291,12 +310,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 +452,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);
@@ -506,10 +543,10 @@ export function ReceivedFilesModal({
await onRefresh();
}
toast.success("Arquivo excluído com sucesso");
toast.success(t("reverseShares.modals.receivedFiles.deleteSuccess"));
} catch (error) {
console.error("Error deleting file:", error);
toast.error("Erro ao excluir arquivo");
toast.error(t("reverseShares.modals.receivedFiles.deleteError"));
}
};
@@ -548,6 +585,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 || t("reverseShares.defaultLinkName")}_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 +781,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 +851,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 +877,7 @@ export function ReceivedFilesModal({
inputRef={inputRef}
hoveredFile={hoveredFile}
copyingFile={copyingFile}
isSelected={selectedFiles.has(file.id)}
onStartEdit={startEdit}
onSaveEdit={saveEdit}
onCancelEdit={cancelEdit}
@@ -620,6 +888,7 @@ export function ReceivedFilesModal({
onDownload={handleDownload}
onDelete={handleDeleteFile}
onCopy={handleCopyFile}
onSelectFile={handleSelectFile}
/>
))}
</TableBody>
@@ -630,6 +899,47 @@ 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);
const displayName = truncateFileName(file.name);
return (
<div key={file.id} className="flex items-center gap-2 p-2 bg-muted/20 rounded text-sm min-w-0">
<FileIcon className={`h-4 w-4 ${color} flex-shrink-0`} />
<span className="flex-1 break-all" title={file.name}>
{displayName}
</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}

View File

@@ -1,22 +1,33 @@
import { IconShare } from "@tabler/icons-react";
import { IconDownload, IconShare } from "@tabler/icons-react";
import { format } from "date-fns";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { ShareDetailsProps } from "../types";
import { ShareFilesTable } from "./files-table";
export function ShareDetails({ share, onDownload }: ShareDetailsProps) {
export function ShareDetails({ share, onDownload, onBulkDownload }: ShareDetailsProps) {
const t = useTranslations();
const hasMultipleFiles = share.files && share.files.length > 1;
return (
<Card>
<CardContent>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<IconShare className="w-6 h-6 text-muted-foreground" />
<h1 className="text-2xl font-semibold">{share.name || t("share.details.untitled")}</h1>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<IconShare className="w-6 h-6 text-muted-foreground" />
<h1 className="text-2xl font-semibold">{share.name || t("share.details.untitled")}</h1>
</div>
{hasMultipleFiles && (
<Button onClick={onBulkDownload} className="flex items-center gap-2">
<IconDownload className="w-4 h-4" />
{t("share.downloadAll")}
</Button>
)}
</div>
{share.description && <p className="text-muted-foreground">{share.description}</p>}
<div className="flex gap-4 text-sm text-muted-foreground">

View File

@@ -1,5 +1,6 @@
import Image from "next/image";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { LanguageSwitcher } from "@/components/general/language-switcher";
import { ModeToggle } from "@/components/general/mode-toggle";
@@ -7,12 +8,13 @@ import { useAppInfo } from "@/contexts/app-info-context";
export function ShareHeader() {
const { appName, appLogo } = useAppInfo();
const t = useTranslations();
return (
<header className="w-full px-6 border-b border-default-200/50 bg-background/70 backdrop-blur-sm">
<div className="mx-auto max-w-5xl sm:p-0 h-16 flex items-center justify-between">
<Link className="flex items-center gap-2" href="/">
{appLogo && <img alt="App Logo" className="h-8 w-8 object-contain rounded" src={appLogo} />}
{appLogo && <img alt={t("logo.labels.appLogo")} className="h-8 w-8 object-contain rounded" src={appLogo} />}
<p className="font-bold text-2xl text-foreground">{appName}</p>
</Link>
<div className="flex items-center gap-2">

View File

@@ -69,6 +69,62 @@ export function usePublicShare() {
}
};
const handleBulkDownload = async () => {
if (!share || !share.files || share.files.length === 0) {
toast.error(t("shareManager.noFilesToDownload"));
return;
}
try {
toast.promise(
(async () => {
const JSZip = (await import("jszip")).default;
const zip = new JSZip();
const downloadPromises = share.files.map(async (file) => {
try {
const encodedObjectName = encodeURIComponent(file.objectName);
const downloadResponse = await getDownloadUrl(encodedObjectName);
const downloadUrl = downloadResponse.data.url;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`Failed to download ${file.name}`);
}
const blob = await response.blob();
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 = `${share.name || t("shareManager.defaultShareName")}.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);
})(),
{
loading: t("shareManager.creatingZip"),
success: t("shareManager.zipDownloadSuccess"),
error: t("shareManager.zipDownloadError"),
}
);
} catch (error) {
console.error("Error creating ZIP:", error);
}
};
const downloadFile = async (url: string, fileName: string) => {
const fileResponse = await fetch(url);
const blob = await fileResponse.blob();
@@ -97,5 +153,6 @@ export function usePublicShare() {
setPassword,
handlePasswordSubmit,
handleDownload,
handleBulkDownload,
};
}

View File

@@ -18,6 +18,7 @@ export default function PublicSharePage() {
setPassword,
handlePasswordSubmit,
handleDownload,
handleBulkDownload,
} = usePublicShare();
if (isLoading) {
@@ -31,7 +32,7 @@ export default function PublicSharePage() {
<main className="flex-1 container mx-auto px-6 py-8">
<div className="max-w-5xl mx-auto space-y-6">
{!isPasswordModalOpen && !share && <ShareNotFound />}
{share && <ShareDetails share={share} onDownload={handleDownload} />}
{share && <ShareDetails share={share} onDownload={handleDownload} onBulkDownload={handleBulkDownload} />}
</div>
</main>

View File

@@ -24,4 +24,5 @@ export interface PasswordModalProps {
export interface ShareDetailsProps {
share: GetShareByAlias200Share;
onDownload: (objectName: string, fileName: string) => Promise<void>;
onBulkDownload?: () => Promise<void>;
}

View 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;
}

View File

@@ -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">
@@ -48,40 +51,83 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
<div className="flex flex-col gap-4">
{configs
.filter((config) => !isFieldHidden(config.key))
.map((config) => (
<div key={config.key} className="space-y-2 mb-3">
<SettingsInput
config={config}
error={form.formState.errors.configs?.[config.key]}
register={form.register}
setValue={form.setValue}
smtpEnabled={form.watch("configs.smtpEnabled")}
oidcEnabled={form.watch("configs.oidcEnabled")}
watch={form.watch}
/>
<p className="text-xs text-muted-foreground ml-1">
{t(`settings.fields.${config.key}.description`, {
defaultValue:
FIELD_DESCRIPTIONS[config.key as keyof typeof FIELD_DESCRIPTIONS] ||
config.description ||
t("settings.fields.noDescription"),
})}
</p>
</div>
))}
</div>
<div className="flex justify-end mt-4">
<Button
variant="default"
disabled={form.formState.isSubmitting}
className="flex items-center gap-2"
type="submit"
>
{!form.formState.isSubmitting && <IconDeviceFloppy className="h-4 w-4" />}
{t("settings.buttons.save", {
group: t(`settings.groups.${group}.title`, { defaultValue: metadata.title }),
.map((config) => {
const smtpEnabled = form.watch("configs.smtpEnabled");
const smtpNoAuth = form.watch("configs.smtpNoAuth");
const isSmtpAuthField = config.key === "smtpUser" || config.key === "smtpPass";
const smtpFields = [
"smtpHost",
"smtpPort",
"smtpUser",
"smtpPass",
"smtpSecure",
"smtpNoAuth",
"smtpFromName",
"smtpFromEmail",
];
if (smtpEnabled !== "true" && smtpFields.includes(config.key)) {
return null;
}
if (isSmtpAuthField && smtpNoAuth === "true") {
return null;
}
return (
<div key={config.key} className="space-y-2 mb-3">
<SettingsInput
config={config}
error={form.formState.errors.configs?.[config.key]}
register={form.register}
setValue={form.setValue}
smtpEnabled={form.watch("configs.smtpEnabled")}
oidcEnabled={form.watch("configs.oidcEnabled")}
watch={form.watch}
/>
<p className="text-xs text-muted-foreground ml-1">
{t(`settings.fields.${config.key}.description`, {
defaultValue:
FIELD_DESCRIPTIONS[config.key as keyof typeof FIELD_DESCRIPTIONS] ||
config.description ||
t("settings.fields.noDescription"),
})}
</p>
</div>
);
})}
</Button>
</div>
<div className="flex justify-between items-center mt-4">
<div className="flex">
{isEmailGroup && form.watch("configs.smtpEnabled") === "true" && (
<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") || "",
smtpSecure: form.getValues("configs.smtpSecure") || "auto",
smtpNoAuth: form.getValues("configs.smtpNoAuth") || "false",
})}
/>
)}
</div>
<div className="flex">
<Button
variant="default"
disabled={form.formState.isSubmitting}
className="flex items-center gap-2"
type="submit"
>
{!form.formState.isSubmitting && <IconDeviceFloppy className="h-4 w-4" />}
{t("settings.buttons.save", {
group: t(`settings.groups.${group}.title`, { defaultValue: metadata.title }),
})}
</Button>
</div>
</div>
</CardContent>
</Card>

View File

@@ -2,8 +2,10 @@ import { IconInfoCircle } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { UseFormRegister, UseFormSetValue, UseFormWatch } from "react-hook-form";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Switch } from "@/components/ui/switch";
import { TagsInput } from "@/components/ui/tags-input";
import { createFieldTitles } from "../constants";
import { Config } from "../types";
@@ -129,7 +131,7 @@ export function SettingsInput({
? "openid profile email"
: config.key === "oidcAdminEmailDomains"
? "admin.com company.org"
: "Digite e pressione Enter"
: t("settings.tooltips.defaultPlaceholder", { defaultValue: "Enter and press Enter" })
}
/>
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
@@ -137,6 +139,73 @@ export function SettingsInput({
);
}
if (config.key === "smtpEnabled") {
const currentValue = watch(`configs.${config.key}`) === "true";
return (
<div className="space-y-2">
<div className="flex items-center space-x-3">
<Switch
id={`configs.${config.key}`}
checked={currentValue}
onCheckedChange={(checked) => {
setValue(`configs.${config.key}`, checked ? "true" : "false", { shouldDirty: true });
}}
disabled={isDisabled}
/>
<label htmlFor={`configs.${config.key}`} className="text-sm font-semibold cursor-pointer">
{friendlyLabel}
</label>
</div>
{config.description && <p className="text-xs text-muted-foreground ml-11">{config.description}</p>}
{error && <p className="text-danger text-xs mt-1 ml-11">{error.message}</p>}
</div>
);
}
if (config.key === "smtpSecure") {
return (
<div className="space-y-2">
<label className="block text-sm font-semibold">{friendlyLabel}</label>
<select
{...register(`configs.${config.key}`)}
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm"
disabled={isDisabled}
>
<option value="auto">{t("settings.fields.smtpSecure.options.auto")}</option>
<option value="ssl">{t("settings.fields.smtpSecure.options.ssl")}</option>
<option value="tls">{t("settings.fields.smtpSecure.options.tls")}</option>
<option value="none">{t("settings.fields.smtpSecure.options.none")}</option>
</select>
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
</div>
);
}
if (config.key === "smtpNoAuth") {
const currentValue = watch(`configs.${config.key}`) === "true";
return (
<div className="space-y-2">
<div className="flex items-center space-x-3">
<Checkbox
id={`configs.${config.key}`}
checked={currentValue}
onCheckedChange={(checked) => {
setValue(`configs.${config.key}`, checked ? "true" : "false", { shouldDirty: true });
}}
disabled={isDisabled}
/>
<label htmlFor={`configs.${config.key}`} className="text-sm font-semibold cursor-pointer">
{friendlyLabel}
</label>
</div>
{config.description && <p className="text-xs text-muted-foreground ml-7">{config.description}</p>}
{error && <p className="text-danger text-xs mt-1 ml-7">{error.message}</p>}
</div>
);
}
switch (config.type) {
case "boolean":
return (

View File

@@ -0,0 +1,94 @@
"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;
smtpSecure: string;
smtpNoAuth: 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(t("settings.messages.smtpNotEnabled"));
return;
}
if (!formValues.smtpHost || !formValues.smtpPort) {
toast.error(t("settings.messages.smtpMissingHostPort"));
return;
}
if (formValues.smtpNoAuth !== "true" && (!formValues.smtpUser || !formValues.smtpPass)) {
toast.error(t("settings.messages.smtpMissingAuth"));
return;
}
setIsLoading(true);
try {
const response = await testSmtpConnection({
smtpConfig: {
smtpEnabled: formValues.smtpEnabled,
smtpHost: formValues.smtpHost,
smtpPort: formValues.smtpPort,
smtpUser: formValues.smtpUser,
smtpPass: formValues.smtpPass,
smtpSecure: formValues.smtpSecure,
smtpNoAuth: formValues.smtpNoAuth,
},
});
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 || t("common.unexpectedError");
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>
);
}

View File

@@ -46,6 +46,8 @@ export const createFieldDescriptions = (t: ReturnType<typeof createTranslator>)
smtpPass: t("settings.fields.smtpPass.description"),
smtpFromName: t("settings.fields.smtpFromName.description"),
smtpFromEmail: t("settings.fields.smtpFromEmail.description"),
smtpSecure: t("settings.fields.smtpSecure.description"),
smtpNoAuth: t("settings.fields.smtpNoAuth.description"),
// OIDC settings (nomes corretos do seed)
oidcEnabled: t("settings.fields.oidcEnabled.description"),
@@ -85,6 +87,8 @@ export const createFieldTitles = (t: ReturnType<typeof createTranslator>) => ({
smtpPass: t("settings.fields.smtpPass.title"),
smtpFromName: t("settings.fields.smtpFromName.title"),
smtpFromEmail: t("settings.fields.smtpFromEmail.title"),
smtpSecure: t("settings.fields.smtpSecure.title"),
smtpNoAuth: t("settings.fields.smtpNoAuth.title"),
// OIDC settings
oidcEnabled: t("settings.fields.oidcEnabled.title"),

View File

@@ -77,8 +77,26 @@ export function useSettings() {
}
if (group === "email") {
if (a.key === "smtpEnabled") return -1;
if (b.key === "smtpEnabled") return 1;
const smtpOrder = [
"smtpEnabled",
"smtpHost",
"smtpPort",
"smtpSecure",
"smtpNoAuth",
"smtpUser",
"smtpPass",
"smtpFromName",
"smtpFromEmail",
];
const aIndex = smtpOrder.indexOf(a.key);
const bIndex = smtpOrder.indexOf(b.key);
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
}
if (group === "oidc") {

View File

@@ -42,7 +42,7 @@ export function FileSelector({ shareId, selectedFiles, onSave, onEditFile }: Fil
setShareFiles(allFiles.filter((file) => selectedFiles.includes(file.id)));
setAvailableFiles(allFiles.filter((file) => !selectedFiles.includes(file.id)));
} catch (error) {
toast.error("Failed to load files");
toast.error(t("files.loadError"));
}
};
@@ -79,7 +79,7 @@ export function FileSelector({ shareId, selectedFiles, onSave, onEditFile }: Fil
await onSave(shareFiles.map((f) => f.id));
} catch (error) {
toast.error("Failed to update files");
toast.error(t("shareManager.filesUpdateError"));
} finally {
setIsLoading(false);
}

View File

@@ -6,6 +6,8 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getFileIcon } from "@/utils/file-icons";
import { truncateFileName } from "@/utils/file-utils";
interface DeleteConfirmationModalProps {
isOpen: boolean;
@@ -54,11 +56,18 @@ export function DeleteConfirmationModal({
</p>
<ScrollArea className="h-32 w-full rounded-md border p-2">
<div className="space-y-1">
{files.map((fileName, index) => (
<div key={index} className="text-sm text-muted-foreground truncate">
{fileName}
</div>
))}
{files.map((fileName, index) => {
const { icon: FileIcon, color } = getFileIcon(fileName);
const displayName = truncateFileName(fileName);
return (
<div key={index} className="flex items-center gap-2 p-2 bg-muted/20 rounded text-sm min-w-0">
<FileIcon className={`h-4 w-4 ${color} flex-shrink-0`} />
<span className="flex-1 break-all" title={fileName}>
{displayName}
</span>
</div>
);
})}
</div>
</ScrollArea>
</div>

View File

@@ -195,9 +195,9 @@ export function useFileManager(onRefresh: () => Promise<void>, clearSelection?:
}
})(),
{
loading: "Criando arquivo ZIP...",
success: "Arquivo ZIP baixado com sucesso!",
error: "Erro ao criar arquivo ZIP",
loading: t("shareManager.creatingZip"),
success: t("shareManager.zipDownloadSuccess"),
error: t("shareManager.zipDownloadError"),
}
);
} catch (error) {

View File

@@ -255,7 +255,7 @@ export function useShareManager(onSuccess: () => void) {
share.files.forEach((file) => {
allFiles.push({
...file,
shareName: share.name || "Unnamed Share",
shareName: share.name || t("shareManager.defaultShareName"),
});
});
}

View File

@@ -80,3 +80,24 @@ 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;
smtpSecure: string;
smtpNoAuth: string;
};
}
export const testSmtpConnection = (
body?: TestSmtpConnectionBody,
options?: AxiosRequestConfig
): Promise<{ data: TestSmtpConnectionResult }> => {
return apiInstance.post(`/api/app/test-smtp`, body || {}, options);
};

View File

@@ -6,3 +6,28 @@ export function generateSafeFileName(originalName: string): string {
return `${safeId}.${extension}`;
}
/**
* Intelligently truncates a filename while preserving the extension when possible
* @param fileName - Filename to truncate
* @param maxLength - Maximum length of the name (default: 40)
* @returns Truncated filename
*/
export function truncateFileName(fileName: string, maxLength: number = 40): string {
if (fileName.length <= maxLength) return fileName;
const lastDotIndex = fileName.lastIndexOf(".");
if (lastDotIndex > 0 && lastDotIndex > fileName.length - 10) {
const name = fileName.substring(0, lastDotIndex);
const extension = fileName.substring(lastDotIndex);
const availableLength = maxLength - extension.length - 3;
if (availableLength > 0) {
return `${name.substring(0, availableLength)}...${extension}`;
}
}
const halfLength = Math.floor((maxLength - 3) / 2);
return `${fileName.substring(0, halfLength)}...${fileName.substring(fileName.length - halfLength)}`;
}

View File

@@ -23,7 +23,7 @@ docker buildx build \
--no-cache \
-t kyantech/palmr:latest \
-t kyantech/palmr:$TAG \
--load \
--push \
.
if [ $? -eq 0 ]; then