diff --git a/.env.example b/.env.example index 4a7297614..ebcea20a2 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,22 @@ ADMIN_DAEMON_SOCKET=tcp://127.0.0.1:9065 # Telnet Daemon Configuration # TELNET_BIND_HOST=0.0.0.0 # TELNET_PORT=2323 +# TLS is enabled by default on port 8023 with an auto-generated self-signed cert. +# Set TELNET_TLS=false to disable TLS entirely. +# TELNET_TLS=true +# TELNET_TLS_PORT=8023 +# TELNET_TLS_CERT=/etc/ssl/certs/your-cert.pem +# TELNET_TLS_KEY=/etc/ssl/private/your-key.pem + +# PubTerm Native Door Configuration +# Host and port the PubTerm door connects to via telnet (defaults to BBS localhost telnet port) +# PUBTERM_HOST=127.0.0.1 +# PUBTERM_PORT=2323 +# Path to telnet binary if not on PATH (Linux) +# PUBTERM_TELNET_BIN=/usr/bin/telnet +# Windows: PubTerm uses PuTTY plink in telnet mode (no escape character trap). +# Install PuTTY: winget install PuTTY.PuTTY +# PUBTERM_PLINK_BIN=C:\Program Files\PuTTY\plink.exe # Gemini Capsule Server Configuration (optional — only needed if running the daemon) # GEMINI_BIND_HOST=0.0.0.0 @@ -87,6 +103,16 @@ FILEAREA_RULE_ACTION_LOG=data/logs/filearea_rules.log # File action debugging (writes data/logs/file_action_debug.log) # FILE_ACTION_DEBUG=true +# Echomail message date ordering field used by backend sorting and echomail UI date display. +# received = use date_received (default), written = use date_written +# ECHOMAIL_ORDER_DATE=received + +# i18n missing key logging (QA hardening) +# Set to true to log translation keys that are missing from catalogs. +# I18N_LOG_MISSING_KEYS=false +# Optional explicit log file path. If unset, messages go to PHP error_log. +# I18N_MISSING_KEYS_LOG_FILE=data/logs/i18n_missing_keys.log + # DOS Emulator Selection (defaults to DOSBox) # dosbox = DOSBox (default, reliable, ~180MB RAM, works on Windows and Linux) - RECOMMENDED diff --git a/.github/workflows/i18n-error-keys.yml b/.github/workflows/i18n-error-keys.yml new file mode 100644 index 000000000..0d4762748 --- /dev/null +++ b/.github/workflows/i18n-error-keys.yml @@ -0,0 +1,25 @@ +name: i18n Coverage Checks + +on: + push: + branches: + - '**' + pull_request: + +jobs: + check-i18n: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: i18n error key coverage + run: php scripts/check_i18n_error_keys.php + + - name: i18n hardcoded UI strings + run: php scripts/check_i18n_hardcoded_strings.php diff --git a/.gitignore b/.gitignore index 2b5c64de1..448450c54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ composer.lock config/*.json !config/*.example +config/i18n/overrides/ config/*.json.* config/taglines.txt config/welcome.txt @@ -28,12 +29,17 @@ backups/* *~ .env .env.local +tests/.env.test bbs_ads/* !bbs_ads/claudes1.ans # Windows utility scripts delnul.cmd node_modules/ +tests/playwright/.auth-state.json +tests/playwright/.csrf-token.json +tests/playwright-report/ +test-results/ scripts/dosbox-bridge/package-lock.json # DOS Door files - only track manifests diff --git a/CLAUDE.md b/CLAUDE.md index 73b1168c7..9f368e158 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,10 +26,12 @@ A modern web interface and mailer tool that receives and sends Fidonet message p - **IMPORTANT**: CLI scripts must include `src/functions.php` after autoload to access global functions like `generateTzutc()`: `require_once __DIR__ . '/../src/functions.php';` - Scripts should be made executable with `chmod +x` and marked as executable in git with `git update-index --chmod=+x scripts/filename.php` - templates/ - html templates + - **IMPORTANT**: Template resolution order is `templates/custom/` → `templates/shells//` → `templates/`. The active shell (`web` or `bbs-menu`) has its own `base.twig` at `templates/shells/web/base.twig` and `templates/shells/bbs-menu/base.twig` which take priority over `templates/base.twig`. When adding nav links or modifying shared layout, you must update **both** `templates/base.twig` AND `templates/shells/web/base.twig` (and `bbs-menu` if applicable). - public_html/ - the web site files, static assets - tests/ - test scripts used in debugging and troubleshooting - vendor/ - 3rd party libraries managed by composer and should not be touched by Claude - data/ - runtime data (binkp.json, nodelists.json, logs, inbound/outbound packets) + - telnet/ - the telnet BBS server (separate from the web interface) ## Important Notes - User authentication is simple username and password with long lived cookie @@ -127,9 +129,79 @@ return function($db) { - Avoid duplicating code. Whenever possible centralize methods using a class. - **Git Workflow**: Do NOT stage or commit changes until explicitly instructed. Changes should be tested first before committing to git. - When writing out a proposal document state in the preamble that the proposal is a draft, was generated by AI and may not have been reviewed for accuracy. - - **Service Worker Cache**: When making changes to CSS or JavaScript files, increment the CACHE_NAME version in public_html/sw.js (e.g., 'binkcache-v2' to 'binkcache-v3') to force clients to download fresh copies. The service worker handles all static asset caching to bypass aggressive browser caching on mobile devices. + - **Service Worker Cache**: When making changes to CSS or JavaScript files, or when updating i18n language strings in `config/i18n/`, increment the CACHE_NAME version in public_html/sw.js (e.g., 'binkcache-v2' to 'binkcache-v3') to force clients to download fresh copies. The service worker caches static assets and the i18n catalog (`/api/i18n/catalog`) to bypass aggressive browser caching on mobile devices. - Write phpDoc blocks when possible +## Localization (i18n) Workflow + +The project uses key-based localization for both Twig and JavaScript. Translation catalogs live in: +- `config/i18n//common.php` +- `config/i18n//errors.php` + +Current baseline locales are `en` and `es`. + +### Core Rules +- Never hardcode new user-facing UI text in templates/JS when adding or changing features. +- Add a translation key first, then use it from Twig/JS. +- Keep `en` and `es` in sync for every new key in normal feature work. +- Prefer stable key names by page/feature area, e.g. `ui.settings.*`, `ui.polls.*`, `errors.polls.*`. +- Do not change existing key names unless required (avoid breaking references). + +### Twig Translation +- Use the global Twig function: `t(key, params, namespace)`. +- Default namespace is `common`; pass `'errors'` when needed. +- Example: +```twig +{{ t('ui.settings.title', {}, 'common') }} +{{ t('ui.polls.create.submit', {'cost': poll_cost}, 'common') }} +``` + +### JavaScript Translation +- Use `window.t(key, params, fallback)` (or a local wrapper like `uiT`). +- Use placeholders in strings and pass params object: +```js +window.t('ui.polls.create.submit', { cost: 25 }, 'Create Poll ({cost} credits)') +``` +- `window.i18n` supports lazy namespace loading via: + - `loadI18nNamespaces([...])` + - endpoint: `GET /api/i18n/catalog?ns=common,errors&locale=` +- JS should always include a fallback string for resilience. + +### API Errors and `error_code` +- API responses should use structured errors via `apiError(error_code, message, status, extra)` and return: + - `error_code` (translation key) + - `error` (human fallback) +- Frontend should resolve display text with `window.getApiErrorMessage(payload, fallback)`. +- Do not rely on matching raw error message text in frontend logic. +- For new API errors: + 1. Add/choose `errors.*` key in route code. + 2. Add that key to `config/i18n/en/errors.php` (and `es/errors.php`). + 3. Use `getApiErrorMessage` in UI handling. + +### Locale Resolution / Config +- Locale is resolved server-side through `LocaleResolver`/`Translator`. +- Supported locales are exposed to Twig as `supported_locales`. +- Environment settings used by i18n: + - `I18N_DEFAULT_LOCALE` + - `I18N_SUPPORTED_LOCALES` (optional; auto-discovers locale folders if unset) + - `I18N_LOG_MISSING_KEYS` + - `I18N_MISSING_KEYS_LOG_FILE` + +### Required Validation After i18n Changes +- Run: + - `php scripts/check_i18n_hardcoded_strings.php` + - `php scripts/check_i18n_error_keys.php` +- Goal: + - no new hardcoded string violations + - no missing `errors.*` catalog keys used by `apiError(...)` + +### Practical Checklist for New UI/API Work +1. Add new `ui.*`/`errors.*` keys to `en` and `es`. +2. Replace literals in Twig with `t(...)`. +3. Replace JS literals with `window.t(...)` (or `uiT(...)`) fallbacks. +4. Ensure API errors return `error_code`. +5. Run both i18n check scripts before commit. + ## URL Construction When constructing full URLs for the application (e.g., share links, reset password links, meta tags), **always** use the centralized `Config::getSiteUrl()` method: diff --git a/FAQ.md b/FAQ.md index 837c875ab..08082ad8a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -152,6 +152,15 @@ When sending netmail, check the "Crash" option to attempt direct delivery. 4. Run `php cli/binkp_poll.php --domain=` to poll your uplink 5. Check `data/logs/packets.log` and `data/logs/binkp_poll.log` for errors +### Q: If a packet contains multiple messages and one fails, are other messages affected? +**A:** It depends on the failure type: + +- **Single message exception** (e.g. database error, malformed message data): Only that message is skipped. Processing continues normally for all remaining messages in the packet. +- **Undeliverable netmail** (no matching local user found by address or name): The message is dropped with a detailed log entry (from/to/subject/date/MSGID) and processing continues. The original `.pkt` file is also preserved to `data/undeliverable/` for manual inspection. +- **Echomail from an insecure session** (security rejection): Processing stops immediately — the rest of the packet is abandoned and moved to the error directory. + +In the first two cases the packet is still considered successfully processed even if individual messages were skipped. + --- ## Binkp Server & Polling diff --git a/README.md b/README.md index 8143eeb23..2958b1faf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ binkterm-php was largely written by Anthropic's Claude with prompting by awehtta There are no doubt bugs and omissions in the project as it was written by an AI. "Your Mileage May Vary". This code is released under the terms of a [BSD License](LICENSE.md). -awehttam runs an instance of BinktermPHP over at https://mypoint.lovelybits.org as a point system of the Reverse Polarity BBS, and https://claudes.lovelybits.org - Claude's very own Public Home Point BBS. +awehttam operates a full instance of BinktermPHP over at https://claudes.lovelybits.org - Claude's very own BBS, and a point system @ https://mypoint.lovelybits.org. ## 🤝 Contributors Wanted @@ -20,25 +20,32 @@ We're looking for experienced PHP developers interested in contributing to Binkt ## Table of Contents +- [Contributors Wanted](#-contributors-wanted) - [Screen shots](#screen-shots) - [Features](#features) - [Installation](#installation) -- [Configuration](#configuration) +- [Configuration](#configuration) — see also [docs/CONFIGURATION.md](docs/CONFIGURATION.md) +- [Network Ports](#network-ports) - [Upgrading](#upgrading) - [Database Management](#database-management) - [Command Line Scripts](#command-line-scripts) -- [Telnet Interface](#telnet-interface) +- [Terminal Server](#terminal-server) +- [Terminal Access via Telnet](#terminal-access-via-telnet) +- [Terminal Access via SSH](#terminal-access-via-ssh) - [Operation](#operation) - [Joining LovlyNet Network](#joining-lovlynet-network) - [Troubleshooting](#troubleshooting) - [Customization](#customization) - [Security Considerations](#security-considerations) - [File Areas](#file-areas) - - [File Area Rules](#file-area-rules) +- [File Area Rules](#file-area-rules) +- [Authentication Flow](#authentication-flow) +- [API Specification](#api-specification) - [Native Doors](#native-doors---native-linux--windows-door-programs) - [DOS Doors](#dos-doors---classic-bbs-door-games) - [WebDoors](#webdoors---web-based-door-games) - [Gemini Support](#gemini-support) +- [Frequently Asked Questions](#frequently-asked-questions) - [Developer Guide](#developer-guide) - [Contributing](#contributing) - [License](#license) @@ -104,11 +111,12 @@ Here are some screen shots showing various aspects of the interface with differe - **DOS Door support** - Integration with dosbox-x for running DOS based doors - **File Areas** - Networked and local file areas with optional automation rules (see `docs/FileAreas.md`) - **ANSI Support** - Support for ANSI escape sequences and pipe codes (BBS color codes) in message readers. See [ANSI Support](docs/ANSI_Support.md) and [Pipe Code Support](docs/Pipe_Code_Support.md) for details. -- **Credit System** - Support for credits and rewards +- **Credit System** - Support for credits and rewards ([details](docs/CreditSystem.md)) - **Voting Booth** - Voting Booth supports multiple polls. Users can submit new polls for credits - **Shoutbox** - Shoutbox support - **Nodelist Browsers** - Integrated nodelist updater and browser - **Markup Support** - Echomail and netmail can be composed and rendered using Markdown or StyleCodes formatting on compatible networks +- **Localization** - Full multi-language support across the web interface, admin panel, and API error messages. The active locale is resolved automatically from user preferences, browser settings, or a cookie — no configuration required for users. Sysops can add new languages by dropping catalog files in place with no code changes. Ships with English and Spanish out of the box. ### Native Binkp Protocol Support @@ -128,91 +136,44 @@ Here are some screen shots showing various aspects of the interface with differe - **Echomail Maintenance** - Purge old messages by age or count limits to manage database size ([details](scripts/README_echomail_maintenance.md)) - **Move Messages** - Move messages between echo areas for reorganization and consolidation -### Telnet Interface +### Terminal Server -A basic telnet service is available. +BinktermPHP provides a shared terminal server experience for text-mode access. +After login, Telnet and SSH users get the same core functionality: -- **Classic BBS Experience** - Traditional telnet-based text interface with screen-aware display and ANSI color support -- **Full-Screen Editor** - Write and reply to messages with arrow key navigation, line editing, and message quoting -- **Security Features** - Login rate limiting (3 attempts per connection, 5/minute per IP) and connection logging -- **Multi-Platform** - Works with PuTTY, SyncTERM, and standard telnet clients on Linux/macOS/Windows -- See **[telnet/README.md](telnet/README.md)** for complete documentation, configuration options, and troubleshooting +- **Netmail + Echomail** - Browse, read, compose, and reply in terminal mode +- **File Areas** - Browse file areas and transfer files via ZMODEM +- **Doors, Polls, Shoutbox** - Access enabled interactive features from the menu +- **Full-Screen Editor** - Cursor-aware editing with message quoting and shortcuts +- **Screen-Aware ANSI UI** - Terminal-dimension-aware rendering and ANSI color support -### Credits System - -BinktermPHP includes an integrated credits economy that rewards user participation and allows charging for certain actions. Credits can be used to encourage quality content, manage resource usage, and gamify the BBS experience. - -**Key Features:** -- Configurable credit costs and rewards for various activities -- Daily login bonuses to encourage regular participation -- New user approval bonuses to welcome approved members -- Bonus rewards for longer, higher-quality content -- Transaction history and balance tracking - -**Default Credit Values:** +See **[docs/TerminalServer.md](docs/TerminalServer.md)** for full terminal feature documentation. -| Activity | Amount | Type | Notes | -|-----------------------------------------------|--------|------|-------| -| Daily Login | +25 | Reward | Awarded once per day after 5-minute delay | -| New User Approval | +100 | Bonus | One-time reward when account is approved | -| Netmail Sent | -5 | Cost | Private messages to other users | -| Echomail Posted | +3 | Reward | Public forum posts | -| Echomail Posted (approx. 2 paragraphs) | +6 | Bonus | 2x reward for substantial posts (2+ paragraphs) | -| Crashmail Sent | -10 | Cost | Direct delivery bypassing uplink | -| Poll Creation | -15 | Cost | Creating a new poll in voting booth | - -**Configuration:** - -Credits are configured in `config/bbs.json` under the `credits` section. All values are customizable: - -```json -{ - "credits": { - "enabled": true, - "symbol": "CR", - "daily_amount": 25, - "daily_login_delay_minutes": 5, - "approval_bonus": 100, - "netmail_cost": 1, - "echomail_reward": 5, - "crashmail_cost": 10, - "poll_creation_cost": 15 - } -} -``` +### Terminal Access via Telnet -Settings can also be modified through the web interface at **Admin → BBS Settings → Credits System Configuration**. +The Telnet daemon is one access method for the shared Terminal Server. -**Transaction Types:** -- `payment` - User paid for a service -- `system_reward` - Automatic reward for activity -- `daily_login` - Daily login bonus -- `admin_adjustment` - Manual admin modification -- `npc_transaction` - Transaction with system/game -- `refund` - Credit refund +- **Classic BBS Access** - Traditional telnet-based terminal connection +- **Multi-Platform** - Works with PuTTY, SyncTERM, ZOC, and standard telnet clients +- **Optional TLS Listener** - Encrypted telnet access available when enabled -**Developer API:** +See **[telnet/README.md](telnet/README.md)** for daemon setup, configuration, and troubleshooting. -Extensions and WebDoors can integrate with the credits system: +### Terminal Access via SSH -```php -// Get user's balance -$balance = UserCredit::getBalance($userId); +The built-in pure-PHP SSH server is another access method for the same Terminal Server. -// Award credits -UserCredit::credit($userId, 10, 'Completed quest', null, UserCredit::TYPE_SYSTEM_REWARD); +- **Encrypted Transport** - SSH-2 encryption for terminal sessions +- **Direct Login Path** - Valid SSH credentials can skip the BBS login menu +- **No External SSH Daemon Required** - Runs from BinktermPHP directly -// Charge credits -UserCredit::debit($userId, 5, 'Used service', null, UserCredit::TYPE_PAYMENT); +See **[docs/SSHServer.md](docs/SSHServer.md)** for daemon setup, configuration, and troubleshooting. -// Get configurable costs/rewards -$cost = UserCredit::getCreditCost('action_name', $defaultValue); -$reward = UserCredit::getRewardAmount('action_name', $defaultValue); -``` +### Credits System -**Disabling Credits:** +BinktermPHP includes an integrated credits economy that rewards user participation and allows charging for certain actions. Credits can be used to encourage quality content, manage resource usage, and gamify the BBS experience. Configuration is done in `config/bbs.json` under the `credits` section, or via **Admin → BBS Settings → Credits System Configuration**. -Set `"enabled": false` in the credits configuration to disable the entire system. When disabled, all credit-related functionality is hidden and no transactions are recorded. +See **[docs/CreditSystem.md](docs/CreditSystem.md)** for default values, configuration options, transaction types, and the developer API. ### Markup Support @@ -288,7 +249,7 @@ Incoming messages are rendered based on the `^AMARKUP` kludge in the message. Ma BinktermPHP can be installed using two methods: Git-based installation, or the installer. ### Requirements -- **PHP 8.1+** with extensions: PDO, PostgreSQL, Sockets, JSON, DOM, Zip, OpenSSL +- **PHP 8.1+** with extensions: PDO, PostgreSQL, Sockets, JSON, DOM, Zip, OpenSSL, GMP - **NodeJS** for DOS Doors support (optional) - **PostgreSQL** - Database server - **Web Server** - Apache, Nginx, or PHP built-in server @@ -478,326 +439,41 @@ update_nodelists can be used if you have URL's to update from. Otherwise nodeli ## Configuration -### Basic System Configuration -`config/binkp.json` is used to configure your system. See `config/binkp.json.example` for a complete reference, including uplink-only options like `send_domain_in_addr`. Settings can be edited through the web interface. - -Note: Be sure to restart BBS services after editing binkp.json. You can use the `scripts/restart_daemons.sh` script for this on Linux. - -```json -{ - "system": { - "name": "My new BinktermPHP system", - "address": "1:123/456.500", - "sysop": "Claude the Coder", - "location": "Over Yonder", - "hostname": "localhost", - "timezone": "UTC" - }, - "binkp": { - "port": 24554, - "timeout": 300, - "max_connections": 10, - "bind_address": "0.0.0.0", - "inbound_path": "data/inbound", - "outbound_path": "data/outbound", - "preserve_processed_packets": false - }, - "uplinks": [ - { - "me": "1:123/456.500", - "networks": [ - "1:*/*", - "2:*/*", - "3:*/*", - "4:*/*" - ], - "address": "1:123/456", - "domain": "fidonet", - "hostname": "ip.or.hostname.of.uplink", - "port": 24554, - "password": "xyzzy", - "pkt_password": "", - "tic_password": "", - "poll_schedule": "*/15 * * * *", - "allow_markup": false, - "send_domain_in_addr": false, - "binkp_zone": "", - "enabled": true, - "compression": false, - "crypt": false, - "default": true - } - ], - "security": { - "allow_insecure_inbound": false, - "insecure_inbound_receive_only": true, - "require_allowlist_for_insecure": false, - "max_insecure_sessions_per_hour": 10, - "allow_plaintext_fallback": true - }, - "crashmail": { - "enabled": true, - "max_attempts": 3, - "retry_interval_minutes": 15, - "use_nodelist_for_routing": true, - "fallback_port": 24554, - "allow_insecure_crash_delivery": true - } -} -``` - -### Configuration Options - -#### System Settings -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Your system's display name | -| `address` | Yes | Your primary FTN address (zone:net/node.point) | -| `sysop` | Yes | System operator name. **Must match the real name on your sysop user account** for netmail addressed to "sysop" to be delivered correctly | -| `location` | No | Geographic location (displayed in system info) | -| `hostname` | Yes | Your internet hostname or IP address | -| `website` | No | Website URL (included in message origin lines) | -| `timezone` | Yes | System timezone ([PHP timezone list](https://www.php.net/manual/en/timezones.php)) | - -**Note**: When the `website` field is configured, it will be included in FidoNet message origin lines: -- Without website: `* Origin: My BBS System (1:234/567)` -- With website: `* Origin: My BBS System (1:234/567)` - -#### Binkp Settings -| Field | Default | Description | -|-------|---------|-------------| -| `port` | 24554 | TCP port for binkp server | -| `timeout` | 300 | Connection timeout in seconds | -| `max_connections` | 10 | Maximum simultaneous connections | -| `bind_address` | 0.0.0.0 | IP address to bind to (0.0.0.0 for all interfaces) | -| `inbound_path` | data/inbound | Directory for incoming packets | -| `outbound_path` | data/outbound | Directory for outgoing packets | -| `preserve_processed_packets` | false | If true, moves processed packets to a `processed/` subdirectory instead of deleting | - -#### Uplink Configuration -Each uplink in the `uplinks` array supports the following fields: - -| Field | Required | Description | -|-------|----------|-------------| -| `me` | Yes | Your FTN address as presented to this uplink | -| `address` | Yes | The uplink's FTN address | -| `hostname` | Yes | Uplink hostname or IP address | -| `port` | Yes | Uplink port (typically 24554) | -| `password` | Yes | Authentication password (shared secret) | -| `domain` | Yes | Network domain (e.g., "fidonet", "fsxnet", "agoranet") | -| `networks` | Yes | Array of address patterns this uplink routes (e.g., `["1:*/*", "2:*/*"]`) | -| `poll_schedule` | No | Cron expression for automated polling (e.g., `"0 */4 * * *"` = every 4 hours) | -| `allow_markup` | No | Enables Markdown and StyleCodes markup support for messages routed through this uplink | -| `send_domain_in_addr` | No | Includes the `@domain` suffix in the ADR address sent to this uplink | -| `enabled` | No | Whether uplink is active (default: true) | -| `default` | No | Whether this is the default uplink for unrouted messages | -| `compression` | No | Enable compression (not yet implemented) | -| `crypt` | No | Enable encryption (not yet implemented) | -| `binkp_zone` | No | DNS zone for crashmail address resolution (e.g. `"binkp.net"`) — see below | - -**Network Patterns**: The `networks` field uses wildcard patterns to define which addresses route through this uplink: -- `1:*/*` - All Zone 1 addresses -- `21:*/*` - All Zone 21 addresses (FSXNet) -- `46:*/*` - All Zone 46 addresses (AgoraNet) - -**Multiple Networks Example**: -```json -{ - "uplinks": [ - { - "me": "1:123/456.500", - "address": "1:123/456", - "domain": "fidonet", - "networks": ["1:*/*", "2:*/*", "3:*/*", "4:*/*"], - "hostname": "fidonet-hub.example.com", - "port": 24554, - "password": "fido_password", - "pkt_password": "", - "tic_password": "", - "poll_schedule": "*/15 * * * *", - "default": true, - "enabled": true - }, - { - "me": "21:1/999", - "address": "21:1/100", - "domain": "fsxnet", - "networks": ["21:*/*"], - "hostname": "fsxnet-hub.example.com", - "port": 24554, - "password": "fsx_password", - "pkt_password": "", - "tic_password": "", - "poll_schedule": "*/15 * * * *", - "enabled": true - } - ] -} -``` - -#### Security Settings -The `security` section controls insecure (passwordless) binkp sessions: - -| Field | Default | Description | -|-------|---------|-------------| -| `allow_insecure_inbound` | false | Allow incoming connections without password authentication | -| `insecure_inbound_receive_only` | true | Insecure sessions can only deliver mail, not pick up | -| `require_allowlist_for_insecure` | false | Only allow insecure sessions from nodes in the allowlist | -| `max_insecure_sessions_per_hour` | 10 | Rate limit for insecure sessions per remote address | -| `allow_plaintext_fallback` | true | Allow plaintext fallback when CRAM-MD5 is available | - -**Security Note**: Insecure sessions should be used with caution. They are typically used for receiving mail from nodes that don't have your password configured. The allowlist (managed via Admin > Insecure Nodes) provides fine-grained control over which nodes can connect without authentication. - -#### Crashmail Settings -The `crashmail` section controls immediate/direct delivery of netmail: +Full configuration reference: **[docs/CONFIGURATION.md](docs/CONFIGURATION.md)** -| Field | Default | Description | -|-------|---------|-------------| -| `enabled` | false | Enable crashmail (direct delivery) functionality | -| `max_attempts` | 3 | Maximum delivery attempts before marking as failed | -| `retry_interval_minutes` | 15 | Minutes to wait between retry attempts | -| `use_nodelist_for_routing` | true | Look up destination in nodelist for hostname/port | -| `fallback_port` | 24554 | Default port if not found in nodelist | -| `allow_insecure_crash_delivery` | false | Allow crashmail delivery without password | +BinktermPHP uses two primary configuration files: -**About Crashmail**: Crashmail bypasses normal hub routing and attempts direct delivery to the destination node. This is useful for urgent messages but requires the destination node to be directly reachable. The system uses nodelist IBN/INA flags to determine the destination's hostname and port. - -**binkp_zone DNS Fallback**: When a destination node cannot be found in the nodelist, crashmail can fall back to DNS resolution using the `binkp_zone` field on the uplink that handles that address range. The hostname is constructed from the FTN address using the standard FidoNet DNS convention: - -``` -f{node}.n{net}.z{zone}.{binkp_zone} -# Examples: -# 1:123/456 → f456.n123.z1.binkp.net -# 2:250/10 → f10.n250.z2.binkp.net -``` +- **`.env`** — database, SMTP, daemon ports, and feature flags. Copy `.env.example` to `.env` and fill in values before first run. +- **`config/binkp.json`** — your FTN system identity, uplinks, binkp daemon, security, and crashmail. Copy `config/binkp.json.example` as a starting point. -If the constructed hostname resolves (A or CNAME record), that host is used for delivery on the configured `fallback_port`. This is compatible with DNS-based binkp address registries such as [binkp.net](https://binkp.net). To enable, set `binkp_zone` on the appropriate uplink: - -```json -{ - "me": "1:123/456.0", - "address": "1:1/23", - "hostname": "uplink.example.com", - "networks": ["1:*/*"], - "binkp_zone": "binkp.net" -} -``` - -The field is optional — if omitted or empty, DNS fallback is disabled and crashmail behaves as before. - -### Nodelist Configuration - -Create `config/nodelists.json` to configure automatic nodelist downloads from website or other URLs. See `config/nodelists.json.example` for a complete reference. - -```json -{ - "sources": [ - { - "name": "FidoNet", - "domain": "fidonet", - "url": "https://example.com/NODELIST.Z|DAY|", - "enabled": true - }, - { - "name": "FSXNet", - "domain": "fsxnet", - "url": "https://bbs.nz/fsxnet/FSXNET.ZIP", - "enabled": true - } - ] -} -``` - -#### Source Configuration -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Display name for the nodelist source | -| `domain` | Yes | Network domain identifier (e.g., "fidonet", "fsxnet") | -| `url` | Yes | Download URL, supports date macros (see below) | -| `enabled` | No | Whether this source is active (default: true) | - -#### URL Macros -URLs support date macros for dynamic nodelist filenames: - -| Macro | Description | Example | -|-------|-------------|---------| -| `\|DAY\|` | Day of year (1-366) | 23 | -| `\|YEAR\|` | 4-digit year | 2026 | -| `\|YY\|` | 2-digit year | 26 | -| `\|MONTH\|` | 2-digit month | 01 | -| `\|DATE\|` | 2-digit day of month | 22 | - -### Web Terminal Configuration - -The web terminal web door provides SSH and telnet to various servers through the browser interface. To enable terminal access requires: - -- Enabling terminal access globally through .env (see below) -- Enabling and configuring the webdoor 'terminal' through config/webdoors.json - -This requires both configuration in the `.env` file and a proxy server to handle WebSocket-to-SSH connections. - -#### .env Configuration -Add these settings to your `.env` file: +Most settings can also be changed at runtime through the **Admin web interface** without editing files directly. +After editing any config file, restart services: ```bash -# Web Terminal Configuration -TERMINAL_ENABLED=true -TERMINAL_HOST=your.ssh.server.com -TERMINAL_PORT=22 -TERMINAL_PROXY_HOST=your.proxy.server.com -TERMINAL_PROXY_PORT=443 +bash scripts/restart_daemons.sh ``` -#### Configuration Options -- **TERMINAL_ENABLED**: Set to `true` to enable terminal access, `false` to disable -- **TERMINAL_HOST**: The Priority SSH server hostname/IP that will be displayed by the 'Terminal' webdoor. -- **TERMINAL_PORT**: The Priority SSH server port (typically 22) the port - -- **TERMINAL_PROXY_HOST**: WebSocket proxy server hostname/IP -- **TERMINAL_PROXY_PORT**: WebSocket proxy server port (typically 443 for HTTPS) - -#### Custom Welcome Messages - -You can customize the welcome messages displayed to users in various parts of the system by creating optional text files in the `config/` directory: +See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the complete reference covering all `.env` variables, `binkp.json` fields, nodelists, nodelist URL macros, and welcome text files. -##### Terminal Welcome Message -Create `config/terminal_welcome.txt` to display a custom message on the terminal login page. If this file exists, it replaces the default "SSH Connection to host:port" message. The content supports multiple lines and will be displayed exactly as written. +### Network Ports -Example `config/terminal_welcome.txt`: -```text -Welcome to MyBBS Terminal Gateway! +| Service | Default Port | Protocol | Direction | Configured In | +|---------|-------------|----------|-----------|---------------| +| Web interface (Apache/Caddy/Nginx) | `80`, `443` | HTTP/HTTPS | Inbound | Web server / reverse proxy | +| BinkP daemon | `24554` | TCP | In + Out | `config/binkp.json` → `binkp.port` | +| Telnet daemon (plain) | `2323` | TCP | Inbound | `.env` `TELNET_PORT` | +| Telnet daemon (TLS) | `8023` | TCP/TLS | Inbound | `.env` `TELNET_TLS_PORT` | +| SSH daemon | `2022` | SSH-2/TCP | Inbound | `.env` `SSH_PORT` | +| Gemini capsule daemon | `1965` | Gemini/TLS | Inbound | `.env` `GEMINI_PORT` | +| DOS door WebSocket bridge | `6001` | WebSocket | Inbound | `.env` `DOSDOOR_WS_PORT` | +| DOSBox bridge session range | `5000–5100` | TCP | Internal | Between bridge and emulator | +| Admin daemon (TCP fallback) | `9065` | TCP | localhost | `.env` `ADMIN_DAEMON_SOCKET` | +| PostgreSQL | `5432` | TCP | Internal | `.env` `DB_PORT` | +| MRC relay (remote) | `5000` / `5001` | TCP / TLS | Outbound | `config/mrc.json` | -Connect to our shell server to access: -- Email and messaging systems -- File areas and downloads -- Games and utilities -- Community forums - -Enter your credentials below to connect. -``` - -##### New User Welcome Email Template -Create `config/newuser_welcome.txt` to customize the welcome email sent to newly registered users. This email template is sent automatically after user registration is approved by an administrator and can include instructions, rules, or helpful information for new users. The template supports basic text formatting and will be sent via the configured SMTP server. - -##### General Welcome Message -Create `config/welcome.txt` to display a custom welcome message on the main page or login screen. This can be used for general announcements, system information, or greeting messages for all users. - -#### Proxy Server Requirement - -The web terminal requires a WebSocket-to-SSH proxy server to bridge browser WebSocket connections to SSH servers. You can use a proxy server like [Terminal Gateway](https://github.com/awehttam/terminalgateway) which provides: - -- WebSocket to SSH connection bridging -- Session management and authentication -- Security isolation between web and SSH connections -- Support for multiple concurrent sessions - -#### Security Considerations - -- Users must be authenticated in the web interface to access the terminal -- The terminal is disabled by default (`TERMINAL_ENABLED=false`) -- SSH authentication is handled separately from web authentication -- Consider network security for both the proxy server and target SSH server -- The proxy server should be properly secured and regularly updated +- Expose only the services you actually run. +- Bind internal services (admin daemon, DOSBox bridge, PostgreSQL) to `127.0.0.1`. +- Publish user-facing services through a reverse proxy with TLS. ## Upgrading @@ -832,7 +508,7 @@ Individual versions with specific upgrade documentation: | Version | Date | Highlights | |----------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1.8.5](docs/UPGRADING_1.8.5.md) | Mar 4 2026 | Native doors (PTY), StyleCodes rendering, LSC-001 Draft 2 MARKUP kludge, markup format composer selector, allow_markup uplink config key | +| [1.8.5](docs/UPGRADING_1.8.5.md) | Mar 4 2026 | Native doors (PTY), StyleCodes rendering, LSC-001 Draft 2 MARKUP kludge, markup format composer selector, allow_markup uplink config key | | [1.8.4](docs/UPGRADING_1.8.4.md) | Mar 1 2026 | Username/real name cross-collision check, MRC room list fix, collapsible compose sidebar, echolist new-tab support | | [1.8.3](docs/UPGRADING_1.8.3.md) | Feb 27 2026 | Appearance system & shells, Gemini Capsule Hosting, Gemini echo area exposure, Markdown compose editor, netmail file attachments, file share links, friendly share URLs, address book crashmail preference, crashmail DNS fallback & immediate delivery, scrollable message reader, echomail bulk mark-as-read, MRC Chat WebDoor | | [1.8.2](docs/UPGRADING_1.8.2.md) | Feb 23 2026 | Gemini Browser WebDoor, CSRF protection, telnet anti-bot, security fixes | @@ -1336,6 +1012,9 @@ The recommended approach is to start the core services at boot (systemd or `@reb # Optional: start telnet daemon on boot @reboot /usr/bin/php /path/to/binktest/telnet/telnet_daemon.php --daemon +# Optional: start SSH daemon on boot +@reboot /usr/bin/php /path/to/binktest/ssh/ssh_daemon.php --daemon + # Optional: start Gemini daemon on boot @reboot /usr/bin/php /path/to/binktest/scripts/gemini_daemon.php --daemon @@ -1587,6 +1266,8 @@ File areas are organized collections of downloadable files, similar to echo area Files uploaded or received via TIC are stored under a directory specific to the file area, and the web UI at `/fileareas` lets sysops manage area settings and browse files. This makes it easy to distribute nodelists, archives, and other content across FTN networks while keeping local areas isolated when needed. +BinktermPHP supports optional ClamAV virus scanning for uploaded and TIC-received files, configurable per area. See [docs/AntiVirus.md](docs/AntiVirus.md) for installation and configuration instructions. + ## File Area Rules BinktermPHP supports file area automation rules to run scripts and apply post-processing actions after uploads or TIC imports. Rules are configured in `config/filearea_rules.json` and can be edited in the admin UI at `/admin/filearea-rules`. Each rule matches filenames with a regex, runs a script with macro substitutions, and then performs success/fail actions like delete, move, or notify. Rules can be scoped by area tag and domain and are applied in order (global rules first, then area-specific rules). For full configuration details, see [docs/FileAreas.md](docs/FileAreas.md). @@ -1921,6 +1602,59 @@ For developers working on BinktermPHP or integrating with the system, see the co The Developer Guide is essential reading for anyone contributing code, developing WebDoors, or extending the system. +## Localization (i18n) for Contributors + +BinktermPHP uses key-based localization for Twig templates, JavaScript UI, and API errors. For a full technical reference see [docs/Localization.md](docs/Localization.md). + +### Catalogs and Key Layout + +- Translation files live in: + - `config/i18n/en/common.php` + - `config/i18n/en/errors.php` + - `config/i18n/es/common.php` + - `config/i18n/es/errors.php` +- UI keys should use the `ui.*` prefix (for example `ui.settings.*`). +- API error keys should use the `errors.*` prefix. + +### Twig Usage + +- Use the Twig `t()` helper instead of hardcoded literals: +```twig +{{ t('ui.settings.title', {}, 'common') }} +{{ t('ui.polls.create.submit', {'cost': poll_cost}, 'common') }} +``` + +### JavaScript Usage + +- Use `window.t(key, params, fallback)` (or a local `uiT` wrapper). +- Always provide a fallback string for resilience. +- Example: +```js +window.t('ui.polls.create.submit', { cost: 25 }, 'Create Poll ({cost} credits)'); +``` + +JavaScript catalogs are loaded on demand from: +- `GET /api/i18n/catalog?ns=common,errors&locale=` + +### API Errors (`error_code`) + +- API responses should include both: + - `error_code` (translation key) + - `error` (human fallback text) +- Routes should emit errors through `apiError(errorCode, message, status, extra)`. +- Frontend should resolve display text through `window.getApiErrorMessage(payload, fallback)`. + +This keeps UI text translatable and avoids coupling frontend logic to raw server English strings. + +### Required Validation After i18n Changes + +Run both checks before committing: + +```bash +php scripts/check_i18n_hardcoded_strings.php +php scripts/check_i18n_error_keys.php +``` + ## Contributing We welcome contributions to BinktermPHP! Before contributing, please review: diff --git a/composer.json b/composer.json index c1cfb8dd7..307d6bace 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,11 @@ "BinktermPHP\\": "src/" } }, + "scripts": { + "check:i18n-error-keys": "php scripts/check_i18n_error_keys.php", + "check:i18n-hardcoded": "php scripts/check_i18n_hardcoded_strings.php", + "update:i18n-hardcoded-allowlist": "php scripts/check_i18n_hardcoded_strings.php --update-allowlist" + }, "require-dev": { "phpunit/phpunit": "^10.0" } diff --git a/config/bbs.json.example b/config/bbs.json.example index 4d62c16ab..8cb80ca04 100644 --- a/config/bbs.json.example +++ b/config/bbs.json.example @@ -5,7 +5,8 @@ "advertising": true, "voting_booth": true, "chat": true, - "file_areas": true + "file_areas": true, + "guest_doors_page": false }, "default_echo_interface": "echolist", "max_cross_post_areas": 5, diff --git a/config/i18n/en/common.php b/config/i18n/en/common.php new file mode 100644 index 000000000..8f726ca30 --- /dev/null +++ b/config/i18n/en/common.php @@ -0,0 +1,2663 @@ + 'Soon', + 'time.in_hours' => 'In {count} hour{suffix}', + 'time.tomorrow' => 'Tomorrow', + 'time.in_days' => 'In {count} days', + 'time.just_now' => 'Just now', + 'time.minutes_ago' => '{count} minutes ago', + 'time.hours_ago' => '{count} hour{suffix} ago', + 'time.yesterday' => 'Yesterday', + 'time.days_ago' => '{count} days ago', + 'time.suffix_plural' => 's', + 'time.suffix_singular' => '', + + // Shared/common UI defaults. + 'errors.failed_load_messages' => 'Failed to load messages', + 'messages.none_found' => 'No messages found', + 'messages.no_subject' => '(No Subject)', + 'ui.common.success' => 'Success', + 'ui.common.error' => 'Error', + 'ui.common.unknown_error' => 'Unknown error', + 'ui.common.saving' => 'Saving...', + 'ui.common.copy_failed_manual' => 'Copy to clipboard failed. Please copy manually.', + 'ui.common.copy_not_supported_manual' => 'Copy to clipboard not supported. Please copy manually.', + 'ui.common.loading' => 'Loading...', + 'ui.common.loading_messages' => 'Loading messages...', + 'ui.common.loading_message' => 'Loading message...', + 'ui.common.search' => 'Search', + 'ui.common.search_messages' => 'Search Messages', + 'ui.common.import' => 'Import', + 'ui.common.search_placeholder' => 'Search...', + 'ui.common.search_messages_placeholder' => 'Search messages...', + 'ui.common.all' => 'All', + 'ui.common.all_messages' => 'All Messages', + 'ui.common.unread' => 'Unread', + 'ui.common.read' => 'Read', + 'ui.common.sent' => 'Sent', + 'ui.common.drafts' => 'Drafts', + 'ui.common.has_attachment' => 'Has attachment', + 'ui.common.select' => 'Select', + 'ui.common.selected' => 'Selected', + 'ui.common.total' => 'Total', + 'ui.common.recent' => 'Recent', + 'ui.common.statistics' => 'Statistics', + 'ui.common.manage' => 'Manage', + 'ui.common.mark_as_read' => 'Mark as Read', + 'ui.common.message' => 'Message', + 'ui.common.delete' => 'Delete', + 'ui.common.confirm_delete' => 'Confirm Delete', + 'ui.common.edit' => 'Edit', + 'ui.common.close' => 'Close', + 'ui.common.refresh' => 'Refresh', + 'ui.common.hostname' => 'Hostname', + 'ui.common.port' => 'Port', + 'ui.common.password' => 'Password', + 'ui.common.reply' => 'Reply', + 'ui.common.username' => 'Username', + 'ui.common.real_name' => 'Real Name', + 'ui.common.email' => 'Email', + 'ui.common.email_address' => 'Email Address', + 'ui.common.location' => 'Location', + 'ui.common.address' => 'Address', + 'ui.common.domain' => 'Domain', + 'ui.common.ip_address' => 'IP Address', + 'ui.common.status' => 'Status', + 'ui.common.from' => 'From', + 'ui.common.actions' => 'Actions', + 'ui.common.view' => 'View', + 'ui.common.active' => 'Active', + 'ui.common.inactive' => 'Inactive', + 'ui.common.user' => 'User', + 'ui.common.admin' => 'Admin', + 'ui.common.created' => 'Created', + 'ui.common.last_login' => 'Last Login', + 'ui.common.today' => 'Today', + 'ui.common.one_day_ago' => '1 day ago', + 'ui.common.days_ago' => '{count} days ago', + 'ui.common.enable' => 'Enable', + 'ui.common.disable' => 'Disable', + 'ui.common.previous' => 'Previous', + 'ui.common.next' => 'Next', + 'ui.common.go_back' => 'Go Back', + 'ui.common.previous_message' => 'Previous message', + 'ui.common.next_message' => 'Next message', + 'ui.common.toggle_fullscreen' => 'Toggle fullscreen', + 'ui.common.cancel' => 'Cancel', + 'ui.common.clear_search' => 'Clear Search', + 'ui.common.save' => 'Save', + 'ui.common.save_changes' => 'Save Changes', + 'ui.common.saved' => 'Saved!', + 'ui.common.saved_short' => 'Saved', + 'ui.common.error_click_retry' => 'Error - click to retry', + 'ui.common.share' => 'Share', + 'ui.common.share_url' => 'Share URL:', + 'ui.common.copy' => 'Copy', + 'ui.common.optional' => 'optional', + 'ui.common.never' => 'Never', + 'ui.common.unknown' => 'Unknown', + 'ui.common.not_configured' => 'Not configured', + 'ui.common.name' => 'Name', + 'ui.common.description' => 'Description', + 'ui.common.label' => 'Label', + 'ui.common.url' => 'URL', + 'ui.common.new_tab' => 'New Tab', + 'ui.common.key' => 'Key', + 'ui.common.id' => 'ID', + 'ui.common.preview' => 'Preview', + 'ui.common.yes' => 'Yes', + 'ui.common.no' => 'No', + 'ui.common.not_applicable' => 'n/a', + 'ui.common.sort.newest_first' => 'Newest First', + 'ui.common.sort.oldest_first' => 'Oldest First', + 'ui.common.sort.by_subject' => 'By Subject', + 'ui.common.sort.by_author' => 'By Author', + 'ui.common.threading.show_threaded' => 'Show Threaded', + 'ui.common.threading.show_flat' => 'Show Flat', + 'ui.common.from_label' => 'From:', + 'ui.common.to_label' => 'To:', + 'ui.common.date_label' => 'Date:', + 'ui.common.subject_label' => 'Subject:', + 'ui.common.subject_label_short' => 'Subject', + 'ui.common.area_label' => 'Area:', + 'ui.common.sent_prefix' => 'Sent:', + 'ui.common.received_prefix' => 'Received:', + 'ui.common.written_prefix' => 'Written:', + 'ui.common.shared' => 'Shared', + 'ui.common.remove_from_saved' => 'Remove from saved', + 'ui.common.save_for_later' => 'Save for later', + 'ui.common.delete_message' => 'Delete message', + 'ui.common.delete_draft' => 'Delete draft', + 'ui.common.already_in_address_book' => 'Already in address book', + 'ui.common.save_to_address_book' => 'Save to address book', + 'ui.common.file_attachments_with_count' => 'File Attachments ({count})', + 'ui.common.send_netmail_to' => 'Send netmail to {name}', + 'ui.common.replies_with_count' => '{count} replies', + 'ui.common.unknown_size' => 'Unknown size', + 'ui.common.kludge_lines' => 'Kludge Lines', + 'ui.common.show_kludge_lines' => 'Show Kludge Lines', + 'ui.common.hide_kludge_lines' => 'Hide Kludge Lines', + 'ui.common.no_kludge_lines_found' => 'No kludge lines found', + 'ui.common.time.1_hour' => '1 hour', + 'ui.common.time.24_hours' => '24 hours', + 'ui.common.time.1_week' => '1 week', + 'ui.common.time.30_days' => '30 days', + + // About Page + 'ui.about.title' => 'About', + 'ui.about.about_system' => 'About {system_name}', + 'ui.about.system_name' => 'System Name', + 'ui.about.sysop' => 'Sysop', + 'ui.about.network_addresses' => 'Network Addresses', + 'ui.about.fidonet_address' => 'Fidonet Address', + 'ui.about.software' => 'Software', + 'ui.about.links' => 'Links', + 'ui.about.house_rules' => 'House Rules', + 'ui.footer.powered_by' => 'Powered by', + + // 404 Page + 'ui.error404.title' => 'Page Not Found', + 'ui.error404.description' => "The page you're looking for doesn't exist or has been moved to another location.", + 'ui.error404.requested_prefix' => 'Requested:', + 'ui.error404.dashboard' => 'Dashboard', + 'ui.error404.go_back' => 'Go Back', + 'ui.error404.contact_sysop' => 'If you believe this is an error, please contact the System Operator.', + + // Generic Error Page + 'ui.error.title' => 'Error', + 'ui.error.access_error' => 'Access Error', + 'ui.error.processing_request_failed' => 'An error occurred while processing your request.', + 'ui.error.return_to_dashboard' => 'Return to Dashboard', + 'ui.web.errors.binkp_admin_only' => 'Only administrators can access BinkP functionality.', + 'ui.web.errors.chat_disabled' => 'Sorry, chat is not enabled.', + 'ui.web.errors.user_management_admin_only' => 'Only administrators can access user management.', + 'ui.web.errors.profile_user_not_found' => 'User not found or inactive.', + 'ui.web.errors.echoareas_admin_only' => 'Only administrators can manage echo areas.', + 'ui.web.errors.fileareas_admin_only' => 'Only administrators can manage file areas.', + 'ui.web.errors.files_feature_disabled' => 'File areas are disabled.', + 'ui.web.errors.polls_disabled' => 'Voting booth is disabled.', + 'ui.web.errors.shoutbox_disabled' => 'Shoutbox is disabled.', + 'ui.web.errors.compose_type_invalid' => 'Invalid compose destination.', + 'ui.web.fallback.system_name' => 'BinktermPHP System', + + // Base Layout / Navigation + 'ui.base.update_available' => 'Update Available', + 'ui.base.new_version_ready' => 'A new version is ready', + 'ui.base.reload_now' => 'Reload Now', + 'ui.base.messaging' => 'Messaging', + 'ui.base.netmail' => 'Netmail', + 'ui.base.echomail' => 'Echomail', + 'ui.base.local_chat' => 'Local Chat', + 'ui.base.mrc_chat' => 'MRC Chat', + 'ui.base.doors_games' => 'Doors & Games', + 'ui.base.files' => 'Files', + 'ui.base.nodelist' => 'Nodelist', + 'ui.base.admin' => 'Admin', + 'ui.base.profile' => 'Profile', + 'ui.base.settings' => 'Settings', + 'ui.base.subscriptions' => 'Subscriptions', + 'ui.base.logout' => 'Logout', + 'ui.base.login' => 'Login', + 'ui.base.guest_doors' => 'Guest Doors', + 'ui.base.admin.whos_online' => "Who's Online", + 'ui.base.admin.dashboard' => 'Dashboard', + 'ui.base.admin.binkp_status' => 'Binkp Status', + 'ui.base.admin.manage_users' => 'Manage Users', + 'ui.base.admin.ads' => 'Advertisements', + 'ui.base.admin.area_management' => 'Area Management', + 'ui.base.admin.echo_areas' => 'Echo Areas', + 'ui.base.admin.file_areas' => 'File Areas', + 'ui.base.admin.file_area_rules' => 'File Area Rules', + 'ui.base.admin.subscriptions' => 'Subscriptions', + 'ui.base.admin.auto_feed' => 'Auto Feed', + 'ui.base.admin.chat_rooms' => 'Chat Rooms', + 'ui.base.admin.mrc_settings' => 'MRC Settings', + 'ui.base.admin.polls' => 'Polls', + 'ui.base.admin.shoutbox' => 'Shoutbox', + 'ui.base.admin.doors' => 'Doors', + 'ui.base.admin.doors_dos' => 'DOS Doors', + 'ui.base.admin.doors_native' => 'Native Doors', + 'ui.base.admin.doors_web' => 'Web Doors', + 'ui.base.admin.activity_stats' => 'Activity Statistics', + 'ui.base.admin.economy_viewer' => 'Economy Viewer', + 'ui.base.admin.bbs_settings' => 'BBS Settings', + 'ui.base.admin.appearance' => 'Appearance', + 'ui.base.admin.binkp_configuration' => 'Binkp Configuration', + 'ui.base.admin.template_editor' => 'Template Editor', + 'ui.base.admin.i18n_overrides' => 'Language Overrides', + 'ui.base.admin.help' => 'Help', + 'ui.base.admin.readme' => 'README', + 'ui.base.admin.faq' => 'FAQ', + 'ui.base.admin.upgrade_notes' => 'Upgrade Notes v{version}', + 'ui.base.admin.claudes_bbs' => "Claude's BBS", + 'ui.base.admin.report_issue' => 'Report Issue', + + // Admin Users + 'ui.admin_users.pending_users_error' => 'Pending users error:', + 'ui.admin_users.pending_users_load_failed_prefix' => 'Failed to load pending users: ', + 'ui.admin_users.all_users_error' => 'All users error:', + 'ui.admin_users.users_load_failed_prefix' => 'Failed to load users: ', + 'ui.admin_users.processing' => 'Processing...', + 'ui.admin_users.user_approved_success' => 'User approved successfully!', + 'ui.admin_users.user_rejected_success' => 'User rejected successfully!', + 'ui.admin_users.user_action_failed_prefix' => 'Failed to {action} user: ', + 'ui.admin_users.user_action_failed_short' => 'Failed to {action} user', + 'ui.admin_users.success_prefix' => 'Success: ', + 'ui.admin_users.error_prefix' => 'Error: ', + 'ui.admin_users.user_data_load_failed_prefix' => 'Failed to load user data: ', + 'ui.admin_users.password_min_length' => 'Password must be at least 8 characters long', + 'ui.admin_users.user_updated_success' => 'User updated successfully!', + 'ui.admin_users.user_update_failed_prefix' => 'Failed to update user: ', + 'ui.admin_users.user_toggle_confirm' => 'Are you sure you want to {action} this user?', + 'ui.admin_users.user_toggled_success' => 'User {action}d successfully!', + 'ui.admin_users.required_fields' => 'Username, real name, and password are required', + 'ui.admin_users.username_validation' => 'Username must be 3-20 characters, letters, numbers, and underscores only', + 'ui.admin_users.creating' => 'Creating...', + 'ui.admin_users.user_created_success' => 'User created successfully!', + 'ui.admin_users.user_create_failed_prefix' => 'Failed to create user: ', + 'ui.admin_users.user_create_failed' => 'Failed to create user', + 'ui.admin_users.cleanup_confirm' => 'This will remove all approved registration records and old rejected registrations (30+ days). Are you sure?', + 'ui.admin_users.cleaning' => 'Cleaning...', + 'ui.admin_users.cleanup_success' => 'Cleanup completed! Removed {approved} approved and {rejected} old rejected registrations ({total} total).', + 'ui.admin_users.cleanup_failed_prefix' => 'Failed to cleanup registrations: ', + 'ui.admin_users.send_reminder_confirm' => 'Send account reminder to {username}? This will send a reminder message via netmail and email (if available).', + 'ui.admin_users.reminder_sent_netmail_email_suffix' => ' (sent via netmail and email)', + 'ui.admin_users.reminder_sent_netmail_only_suffix' => ' (sent via netmail only)', + 'ui.admin_users.send_reminder_failed_prefix' => 'Failed to send reminder: ', + 'ui.admin_users.send_reminder_failed' => 'Failed to send reminder', + 'ui.admin_users.no_reminders_needed' => 'No users currently need account reminders. All users have logged in!', + 'ui.admin_users.reminders_found_header' => "Found {count} user(s) who haven't logged in yet:\n\n", + 'ui.admin_users.reminder_user_line' => '- {username} ({real_name}) - created {created_date}' . "\n", + 'ui.admin_users.no_real_name' => 'No real name', + 'ui.admin_users.reminders_found_footer' => "\nYou can send individual reminders using the \"Remind\" buttons in the user list below.", + 'ui.admin_users.reminders_load_failed_prefix' => 'Failed to load users needing reminders: ', + 'ui.admin_users.page_title' => 'User Management', + 'ui.admin_users.heading' => 'User Management', + 'ui.admin_users.create_new_user' => 'Create New User', + 'ui.admin_users.need_reminders' => 'Need Reminders', + 'ui.admin_users.cleanup' => 'Cleanup', + 'ui.admin_users.pending_registrations' => 'Pending Registrations', + 'ui.admin_users.loading_pending_registrations' => 'Loading pending registrations...', + 'ui.admin_users.all_users' => 'All Users', + 'ui.admin_users.search_users_placeholder' => 'Search users...', + 'ui.admin_users.per_page_25' => '25 per page', + 'ui.admin_users.per_page_50' => '50 per page', + 'ui.admin_users.per_page_100' => '100 per page', + 'ui.admin_users.loading_users' => 'Loading users...', + 'ui.admin_users.approve_user' => 'Approve User', + 'ui.admin_users.admin_notes' => 'Admin Notes', + 'ui.admin_users.optional_notes_placeholder' => 'Optional notes about this decision...', + 'ui.admin_users.approve' => 'Approve', + 'ui.admin_users.reject' => 'Reject', + 'ui.admin_users.create_user' => 'Create User', + 'ui.admin_users.edit_user' => 'Edit User', + 'ui.admin_users.account_is_active' => 'Account is active', + 'ui.admin_users.inactive_cannot_login' => 'Inactive users cannot log in', + 'ui.admin_users.admin_privileges' => 'Administrator privileges', + 'ui.admin_users.admin_privileges_help' => 'Admins can manage users and system settings', + 'ui.admin_users.email_optional_help' => 'Optional - for account recovery and notifications', + 'ui.admin_users.min_8_characters' => 'Minimum 8 characters', + 'ui.admin_users.username_cannot_change' => 'Username cannot be changed', + 'ui.admin_users.real_name_cannot_change' => 'Real name cannot be changed', + 'ui.admin_users.new_password' => 'New Password', + 'ui.admin_users.new_password_help' => 'Leave blank to keep current password. Minimum 8 characters if changing.', + 'ui.admin_users.no_pending_registrations' => 'No pending registrations', + 'ui.admin_users.requested' => 'Requested', + 'ui.admin_users.last_reminded' => 'Last Reminded', + 'ui.admin_users.not_provided' => 'Not provided', + 'ui.admin_users.view_profile' => 'View Profile', + 'ui.admin_users.send_account_reminder' => 'Send account reminder', + 'ui.admin_users.remind' => 'Remind', + 'ui.admin_users.referred_by' => 'Referred By:', + 'ui.admin_users.basic_information' => 'Basic Information', + 'ui.admin_users.request_details' => 'Request Details', + 'ui.admin_users.reason_for_joining' => 'Reason for joining:', + 'ui.admin_users.no_reason_provided' => 'No reason provided', + 'ui.admin_users.user_agent' => 'User Agent:', + 'ui.admin_users.not_available' => 'Not available', + 'ui.admin_users.user_details' => 'User Details', + 'ui.admin_users.approve_user_registration' => 'Approve User Registration', + 'ui.admin_users.reject_user_registration' => 'Reject User Registration', + 'ui.admin_users.users_pagination_aria' => 'Users pagination', + 'ui.admin_users.showing_users_range' => 'Showing {start}-{end} of {total} users', + + // Admin Appearance + 'ui.admin.appearance.load_failed_prefix' => 'Failed to load appearance config: ', + 'ui.admin.appearance.save_failed' => 'Save failed', + 'ui.admin.appearance.reset_menu_confirm' => 'Reset menu items to default?', + 'ui.admin.appearance.shell_saved_reload' => 'Saved! Reload the page to see the new shell.', + 'ui.admin.appearance.upload_failed_prefix' => 'Upload failed: ', + 'ui.admin.appearance.delete_shell_art_confirm' => 'Delete {name}?', + 'ui.admin.appearance.delete_failed_prefix' => 'Delete failed: ', + 'ui.admin.appearance.error_prefix' => 'Error: ', + 'ui.admin.appearance.request_failed' => 'Request failed', + 'ui.admin.appearance.page_title' => 'Appearance & Content - Admin', + 'ui.admin.appearance.heading' => 'Appearance & Content', + 'ui.admin.appearance.tab_branding' => 'Branding', + 'ui.admin.appearance.tab_content' => 'Content', + 'ui.admin.appearance.tab_navigation' => 'Navigation', + 'ui.admin.appearance.tab_seo' => 'SEO & Public', + 'ui.admin.appearance.tab_shell' => 'Shell', + 'ui.admin.appearance.tab_message_reader' => 'Message Reader', + 'ui.admin.appearance.invalid_hex_color' => 'Invalid hex color', + 'ui.admin.appearance.no_content' => 'No content', + 'ui.admin.appearance.preview_failed' => 'Preview failed', + 'ui.admin.appearance.branding.title' => 'Branding', + 'ui.admin.appearance.branding.accent_color' => 'Accent Color', + 'ui.admin.appearance.branding.pick_accent_color' => 'Pick accent color', + 'ui.admin.appearance.branding.reset' => 'Reset', + 'ui.admin.appearance.branding.accent_help' => 'Override the primary navigation and button color site-wide. Leave blank for default.', + 'ui.admin.appearance.branding.default_theme' => 'Default Theme', + 'ui.admin.appearance.branding.system_default' => '(System default)', + 'ui.admin.appearance.branding.default_theme_help' => 'Theme applied to all users by default.', + 'ui.admin.appearance.branding.lock_theme' => 'Lock theme (prevent users from overriding)', + 'ui.admin.appearance.branding.logo_url' => 'Logo URL', + 'ui.admin.appearance.branding.logo_url_help' => 'URL to a logo image shown in the navbar. Leave blank to use the system name text.', + 'ui.admin.appearance.branding.footer_text' => 'Footer Text', + 'ui.admin.appearance.branding.footer_placeholder' => 'Leave blank for default node/sysop line', + 'ui.admin.appearance.branding.footer_help' => 'Custom text for the footer. Leave blank for the default system information line.', + 'ui.admin.appearance.branding.save' => 'Save Branding', + 'ui.admin.appearance.content.system_news_title' => 'System News (MOTD)', + 'ui.admin.appearance.content.system_news_placeholder' => 'Enter system news in Markdown format...', + 'ui.admin.appearance.content.system_news_help' => 'Supports Markdown. Shown on the dashboard. Leave blank to show the built-in default or custom systemnews.twig.', + 'ui.admin.appearance.content.house_rules_title' => 'House Rules', + 'ui.admin.appearance.content.house_rules_placeholder' => 'Enter house rules in Markdown format...', + 'ui.admin.appearance.content.house_rules_help' => 'Supports Markdown. Displayed in the House Rules modal linked from the footer.', + 'ui.admin.appearance.content.site_announcement_title' => 'Site Announcement', + 'ui.admin.appearance.content.show_announcement_banner' => 'Show announcement banner', + 'ui.admin.appearance.content.announcement_text' => 'Announcement Text', + 'ui.admin.appearance.content.type' => 'Type', + 'ui.admin.appearance.content.type_info' => 'Info (blue)', + 'ui.admin.appearance.content.type_success' => 'Success (green)', + 'ui.admin.appearance.content.type_warning' => 'Warning (yellow)', + 'ui.admin.appearance.content.type_danger' => 'Danger (red)', + 'ui.admin.appearance.content.type_primary' => 'Primary', + 'ui.admin.appearance.content.expires_at_optional' => 'Expires At (optional)', + 'ui.admin.appearance.content.expires_help' => 'Leave blank to never expire.', + 'ui.admin.appearance.content.dismissible' => 'Dismissible', + 'ui.admin.appearance.content.save' => 'Save Content', + 'ui.admin.appearance.navigation.title' => 'Custom Navigation Links', + 'ui.admin.appearance.navigation.add_link' => 'Add Link', + 'ui.admin.appearance.navigation.help' => 'Up to 3 links appear inline in the navbar; 4 or more are grouped into a "Links" dropdown.', + 'ui.admin.appearance.navigation.url_placeholder' => '/page or https://...', + 'ui.admin.appearance.navigation.save' => 'Save Navigation', + 'ui.admin.appearance.seo.title' => 'SEO & Public Pages', + 'ui.admin.appearance.seo.enable_about_page' => 'Enable /about public page', + 'ui.admin.appearance.seo.about_page_help' => 'When enabled, a publicly accessible /about page shows your system info and house rules. Disabled returns 404.', + 'ui.admin.appearance.seo.site_description' => 'Site Description', + 'ui.admin.appearance.seo.max_300_chars' => '(max 300 chars)', + 'ui.admin.appearance.seo.description_help' => 'Used in meta description and OG tags for search engines and link previews.', + 'ui.admin.appearance.seo.og_image_url' => 'OG Image URL', + 'ui.admin.appearance.seo.og_image_placeholder' => 'https://example.com/preview.png', + 'ui.admin.appearance.seo.og_image_help' => 'Image shown when your site is shared on social media. Should be at least 1200x630px.', + 'ui.admin.appearance.seo.save' => 'Save SEO Settings', + 'ui.admin.appearance.shell.title' => 'Interface Shell', + 'ui.admin.appearance.shell.default_interface_style' => 'Default Interface Style', + 'ui.admin.appearance.shell.web_interface' => 'Web Interface', + 'ui.admin.appearance.shell.web_interface_help' => 'Full navbar, dropdown menus, standard web layout', + 'ui.admin.appearance.shell.bbs_menu' => 'BBS Menu', + 'ui.admin.appearance.shell.bbs_menu_help' => 'Minimal header, hotkey navigation, card/text/ANSI menu', + 'ui.admin.appearance.shell.lock_shell' => 'Lock shell (prevent users from overriding in their settings)', + 'ui.admin.appearance.shell.bbs_menu_config' => 'BBS Menu Configuration', + 'ui.admin.appearance.shell.menu_variant' => 'Menu Variant', + 'ui.admin.appearance.shell.variant_cards' => 'Card Grid', + 'ui.admin.appearance.shell.variant_text' => 'Terminal Text', + 'ui.admin.appearance.shell.variant_ansi' => 'ANSI Art', + 'ui.admin.appearance.shell.ansi_art_file' => 'ANSI Art File', + 'ui.admin.appearance.shell.none_selected' => '(none selected)', + 'ui.admin.appearance.shell.ansi_file_help' => 'Select an uploaded .ans file to display above the menu.', + 'ui.admin.appearance.shell.upload_ans' => 'Upload .ans', + 'ui.admin.appearance.shell.max_512_kb' => 'Max 512 KB.', + 'ui.admin.appearance.shell.delete_selected' => 'Delete Selected', + 'ui.admin.appearance.shell.menu_items' => 'Menu Items', + 'ui.admin.appearance.shell.menu_items_help' => 'Define the hotkeys and destinations for the main menu.', + 'ui.admin.appearance.shell.icon' => 'Icon', + 'ui.admin.appearance.shell.icon_help' => '(Font Awesome name)', + 'ui.admin.appearance.shell.icon_placeholder' => 'envelope', + 'ui.admin.appearance.shell.add_item' => 'Add Item', + 'ui.admin.appearance.shell.reset_to_defaults' => 'Reset to Defaults', + 'ui.admin.appearance.shell.save' => 'Save Shell Settings', + 'ui.admin.appearance.shell.could_not_load_file_list' => 'Could not load file list.', + 'ui.admin.appearance.shell.uploading' => 'Uploading...', + 'ui.admin.appearance.shell.uploaded_prefix' => 'Uploaded: ', + 'ui.admin.appearance.shell.deleted_prefix' => 'Deleted: ', + 'ui.admin.appearance.default_menu.messages' => 'Messages', + 'ui.admin.appearance.default_menu.netmail' => 'Netmail', + 'ui.admin.appearance.default_menu.files' => 'Files', + 'ui.admin.appearance.default_menu.games_doors' => 'Games & Doors', + 'ui.admin.appearance.default_menu.settings' => 'Settings', + 'ui.admin.appearance.message_reader.title' => 'Message Reader', + 'ui.admin.appearance.message_reader.layout' => 'Layout', + 'ui.admin.appearance.message_reader.scrollable_body' => 'Scrollable message body', + 'ui.admin.appearance.message_reader.scrollable_body_help' => 'When enabled, the message header (From, To, Subject, Date) remains fixed at the top while the message body scrolls independently. When disabled, the entire modal scrolls together (default behavior).', + 'ui.admin.appearance.message_reader.save' => 'Save Message Reader Settings', + + // Admin Binkp Config + 'ui.admin.binkp_config.load_failed' => 'Failed to load config', + 'ui.admin.binkp_config.save_failed' => 'Failed to save config', + 'ui.admin.binkp_config.reload_confirm' => 'Reload binkp daemon configuration? This will send SIGHUP to the daemon.', + 'ui.admin.binkp_config.reload_failed' => 'Failed to reload config', + 'ui.admin.binkp_config.remove_uplink_confirm' => 'Remove this uplink?', + 'ui.admin.binkp_config.page_title' => 'Binkp Configuration', + 'ui.admin.binkp_config.heading' => 'Binkp Configuration', + 'ui.admin.binkp_config.restart_notice' => 'After saving, restart the daemons to apply changes.', + 'ui.admin.binkp_config.save_configuration' => 'Save Configuration', + 'ui.admin.binkp_config.reload_config' => 'Reload Config', + 'ui.admin.binkp_config.reload_title' => 'Reload configuration without restarting daemon', + 'ui.admin.binkp_config.config_not_loaded' => 'Config not loaded.', + 'ui.admin.binkp_config.configuration_saved' => 'Configuration saved.', + 'ui.admin.binkp_config.reloading' => 'Reloading configuration...', + 'ui.admin.binkp_config.reloaded_success' => 'Configuration reloaded successfully. Daemon has picked up changes.', + 'ui.admin.binkp_config.current' => 'current', + 'ui.admin.binkp_config.system.title' => 'System', + 'ui.admin.binkp_config.system.system_name' => 'System Name', + 'ui.admin.binkp_config.system.system_address' => 'System Address', + 'ui.admin.binkp_config.system.sysop_name' => 'Sysop Name', + 'ui.admin.binkp_config.system.select_admin_user' => 'Select an admin user.', + 'ui.admin.binkp_config.system.location' => 'Location', + 'ui.admin.binkp_config.system.timezone' => 'Timezone', + 'ui.admin.binkp_config.system.origin_line' => 'Origin Line', + 'ui.admin.binkp_config.binkp.title' => 'Binkp', + 'ui.admin.binkp_config.binkp.timeout_seconds' => 'Timeout (seconds)', + 'ui.admin.binkp_config.binkp.max_connections' => 'Max Connections', + 'ui.admin.binkp_config.binkp.bind_address' => 'Bind Address', + 'ui.admin.binkp_config.binkp.preserve_processed_packets' => 'Preserve Processed Packets', + 'ui.admin.binkp_config.security.title' => 'Security', + 'ui.admin.binkp_config.security.allow_insecure_inbound' => 'Allow Insecure Inbound', + 'ui.admin.binkp_config.security.insecure_receive_only' => 'Insecure Receive Only', + 'ui.admin.binkp_config.security.require_allowlist_for_insecure' => 'Require Allowlist For Insecure', + 'ui.admin.binkp_config.security.max_insecure_sessions_per_hour' => 'Max Insecure Sessions / Hour', + 'ui.admin.binkp_config.security.allow_plaintext_fallback' => 'Allow Plaintext Fallback', + 'ui.admin.binkp_config.crashmail.title' => 'Crashmail', + 'ui.admin.binkp_config.crashmail.enable_crashmail' => 'Enable Crashmail', + 'ui.admin.binkp_config.crashmail.max_attempts' => 'Max Attempts', + 'ui.admin.binkp_config.crashmail.retry_interval_minutes' => 'Retry Interval (minutes)', + 'ui.admin.binkp_config.crashmail.use_nodelist_for_routing' => 'Use Nodelist For Routing', + 'ui.admin.binkp_config.crashmail.fallback_port' => 'Fallback Port', + 'ui.admin.binkp_config.crashmail.allow_insecure_crash_delivery' => 'Allow Insecure Crash Delivery', + 'ui.admin.binkp_config.uplinks.title' => 'Uplinks', + 'ui.admin.binkp_config.uplinks.add_uplink' => 'Add Uplink', + 'ui.admin.binkp_config.uplinks.edit_uplink' => 'Edit Uplink', + 'ui.admin.binkp_config.uplinks.none_configured' => 'No uplinks configured.', + 'ui.admin.binkp_config.uplinks.real_name' => 'Real Name', + 'ui.admin.binkp_config.uplinks.username' => 'Username', + 'ui.admin.binkp_config.uplinks.table.me' => 'Me', + 'ui.admin.binkp_config.uplinks.table.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.table.domain' => 'Domain', + 'ui.admin.binkp_config.uplinks.table.schedule' => 'Schedule', + 'ui.admin.binkp_config.uplinks.table.markdown' => 'Markdown', + 'ui.admin.binkp_config.uplinks.table.posting_name' => 'Posting Name', + 'ui.admin.binkp_config.uplinks.table.adr_domain' => 'ADR @Domain', + 'ui.admin.binkp_config.uplinks.table.enabled' => 'Enabled', + 'ui.admin.binkp_config.uplinks.table.default' => 'Default', + 'ui.admin.binkp_config.uplinks.table.actions' => 'Actions', + 'ui.admin.binkp_config.uplinks.modal.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.modal.me_address' => 'Me Address', + 'ui.admin.binkp_config.uplinks.modal.uplink_address' => 'Uplink Address', + 'ui.admin.binkp_config.uplinks.modal.domain' => 'Domain', + 'ui.admin.binkp_config.uplinks.modal.binkp_session_password' => 'BinkP Session Password', + 'ui.admin.binkp_config.uplinks.modal.packet_password' => 'Packet Password', + 'ui.admin.binkp_config.uplinks.modal.packet_password_help' => 'FTN .pkt header password (max 8 chars)', + 'ui.admin.binkp_config.uplinks.modal.tic_password' => 'TIC Password', + 'ui.admin.binkp_config.uplinks.modal.tic_password_help' => 'Default TIC file password; overridden per file area', + 'ui.admin.binkp_config.uplinks.modal.poll_schedule' => 'Poll Schedule', + 'ui.admin.binkp_config.uplinks.modal.networks_one_per_line' => 'Networks (one per line)', + 'ui.admin.binkp_config.uplinks.modal.allow_markup' => 'Allow Markup', + 'ui.admin.binkp_config.uplinks.modal.allow_markup_help' => 'Enables Markup support (Markdown, StyleCodes, etc.) on compatible/supporting networks', + 'ui.admin.binkp_config.uplinks.modal.posting_name_policy' => 'Posting Name Policy', + 'ui.admin.binkp_config.uplinks.modal.send_domain' => 'Send @Domain In ADR', + 'ui.admin.binkp_config.uplinks.modal.send_domain_help' => 'Include the @Domain portion when sending the ADR address to this uplink', + 'ui.admin.binkp_config.uplinks.modal.enabled_help' => 'Enables this uplink for normal routing, polling, and outbound delivery', + 'ui.admin.binkp_config.uplinks.modal.compression' => 'Compression', + 'ui.admin.binkp_config.uplinks.modal.compression_help' => 'Requests compressed transfers when the remote system supports them', + 'ui.admin.binkp_config.uplinks.modal.crypt' => 'Crypt', + 'ui.admin.binkp_config.uplinks.modal.crypt_help' => 'Uses CRAM-MD5 style authentication for this uplink when supported', + 'ui.admin.binkp_config.uplinks.modal.default_help' => 'Marks this as the default uplink when no more specific route matches', + 'ui.admin.binkp_config.uplinks.modal.save_uplink' => 'Save Uplink', + 'ui.admin.binkp_config.validation.system_required' => 'System name, address, and sysop are required.', + 'ui.admin.binkp_config.validation.port_range' => 'Binkp port must be between 1 and 65535.', + 'ui.admin.binkp_config.validation.timeout_positive' => 'Binkp timeout must be a positive number.', + 'ui.admin.binkp_config.validation.max_connections_positive' => 'Max connections must be a positive number.', + 'ui.admin.binkp_config.validation.uplink_hostname_required' => 'Uplink hostname is required.', + 'ui.admin.binkp_config.validation.uplink_port_range' => 'Uplink port must be between 1 and 65535.', + + // Admin Ads + 'ui.admin.ads.load_failed_with_status' => 'Failed to load ads ({status})', + 'ui.admin.ads.load_failed' => 'Failed to load ads', + 'ui.admin.ads.select_file_to_upload' => 'Select an ANSI file to upload.', + 'ui.admin.ads.upload_failed_with_status' => 'Upload failed ({status})', + 'ui.admin.ads.uploaded' => 'Advertisement uploaded.', + 'ui.admin.ads.upload_failed' => 'Upload failed', + 'ui.admin.ads.delete_confirm' => 'Delete {name}?', + 'ui.admin.ads.delete_failed_with_status' => 'Delete failed ({status})', + 'ui.admin.ads.deleted' => 'Advertisement deleted.', + 'ui.admin.ads.delete_failed' => 'Delete failed', + 'ui.admin.ads.unexpected_response' => 'Unexpected response ({status})', + 'ui.admin.ads.page_title' => 'Advertisements', + 'ui.admin.ads.heading' => 'Advertisements', + 'ui.admin.ads.info_text_prefix' => 'Upload ANSI ads (`.ans`) to the', + 'ui.admin.ads.info_text_suffix' => 'directory. Ads are displayed randomly on the dashboard.', + 'ui.admin.ads.upload_new' => 'Upload New Advertisement', + 'ui.admin.ads.ansi_file' => 'ANSI File (.ans)', + 'ui.admin.ads.save_as_optional' => 'Save As (optional)', + 'ui.admin.ads.save_as_placeholder' => 'retro-sale.ans', + 'ui.admin.ads.save_as_help' => 'Only letters, numbers, dot, dash, underscore.', + 'ui.admin.ads.upload' => 'Upload', + 'ui.admin.ads.current_advertisements' => 'Current Advertisements', + 'ui.admin.ads.size' => 'Size', + 'ui.admin.ads.updated' => 'Updated', + 'ui.admin.ads.actions' => 'Actions', + 'ui.admin.ads.loading_ads' => 'Loading ads...', + 'ui.admin.ads.none_uploaded' => 'No advertisements uploaded.', + 'ui.admin.ads.view' => 'View', + + // Admin Dashboard + 'ui.admin.dashboard.page_title' => 'Admin Dashboard', + 'ui.admin.dashboard.heading' => 'Admin Dashboard', + 'ui.admin.dashboard.total_users' => 'Total Users', + 'ui.admin.dashboard.active_users' => 'Active Users', + 'ui.admin.dashboard.total_netmail' => 'Total Netmail', + 'ui.admin.dashboard.total_echomail' => 'Total Echomail', + 'ui.admin.dashboard.quick_actions' => 'Quick Actions', + 'ui.admin.dashboard.system_information' => 'System Information', + 'ui.admin.dashboard.admin_users' => 'Admin Users:', + 'ui.admin.dashboard.active_sessions' => 'Active Sessions:', + 'ui.admin.dashboard.system_address' => 'System Address:', + 'ui.admin.dashboard.version' => 'Version:', + 'ui.admin.dashboard.git_branch_commit' => 'Git Branch / Commit:', + 'ui.admin.dashboard.database_version' => 'Database Version:', + 'ui.admin.dashboard.service_status' => 'Service Status', + 'ui.admin.dashboard.service.admin_daemon' => 'Admin Daemon', + 'ui.admin.dashboard.service.binkp_scheduler' => 'Binkp Scheduler', + 'ui.admin.dashboard.service.binkp_server' => 'Binkp Server', + 'ui.admin.dashboard.service.telnetd' => 'Telnet Server', + 'ui.admin.dashboard.running' => 'Running', + 'ui.admin.dashboard.stopped' => 'Stopped', + 'ui.admin.dashboard.pid' => 'PID', + + // Admin Users (legacy users page) + 'ui.admin.users.load_failed' => 'Error loading users', + 'ui.admin.users.load_details_failed' => 'Error loading user details', + 'ui.admin.users.updated_success' => 'User updated successfully', + 'ui.admin.users.created_success' => 'User created successfully', + 'ui.admin.users.save_failed' => 'Error saving user', + 'ui.admin.users.delete_confirm' => 'Are you sure you want to delete this user? This action cannot be undone.', + 'ui.admin.users.deleted_success' => 'User deleted successfully', + 'ui.admin.users.delete_failed' => 'Error deleting user', + 'ui.admin.users.page_title' => 'User Management', + 'ui.admin.users.heading' => 'User Management', + 'ui.admin.users.add_new_user' => 'Add New User', + 'ui.admin.users.search_users_placeholder' => 'Search users...', + 'ui.admin.users.active' => 'Active', + 'ui.admin.users.inactive' => 'Inactive', + 'ui.admin.users.admins' => 'Admins', + 'ui.admin.users.username' => 'Username', + 'ui.admin.users.real_name' => 'Real Name', + 'ui.admin.users.fidonet_address' => 'Fidonet Address', + 'ui.admin.users.status' => 'Status', + 'ui.admin.users.admin' => 'Admin', + 'ui.admin.users.created' => 'Created', + 'ui.admin.users.actions' => 'Actions', + 'ui.admin.users.password_help' => 'Leave blank when editing to keep current password', + 'ui.admin.users.fidonet_address_placeholder' => '1:123/456.789', + 'ui.admin.users.active_user' => 'Active User', + 'ui.admin.users.administrator' => 'Administrator', + 'ui.admin.users.save_user' => 'Save User', + 'ui.admin.users.user_details' => 'User Details', + 'ui.admin.users.view_details' => 'View Details', + 'ui.admin.users.view_profile' => 'View Profile', + 'ui.admin.users.pagination_aria' => 'Users pagination', + 'ui.admin.users.previous' => 'Previous', + 'ui.admin.users.next' => 'Next', + 'ui.admin.users.edit_user' => 'Edit User', + 'ui.admin.users.user_information' => 'User Information', + 'ui.admin.users.last_login' => 'Last Login', + 'ui.admin.users.statistics' => 'Statistics', + 'ui.admin.users.netmail_received' => 'Netmail Received', + 'ui.admin.users.netmail_sent' => 'Netmail Sent', + 'ui.admin.users.echomail_posted' => 'Echomail Posted', + 'ui.admin.users.active_sessions' => 'Active Sessions', + + // Admin Chat Rooms + 'ui.admin.chat_rooms.load_failed' => 'Error loading chat rooms', + 'ui.admin.chat_rooms.not_found' => 'Chat room not found', + 'ui.admin.chat_rooms.load_single_failed' => 'Error loading chat room', + 'ui.admin.chat_rooms.updated_success' => 'Room updated successfully', + 'ui.admin.chat_rooms.created_success' => 'Room created successfully', + 'ui.admin.chat_rooms.save_failed' => 'Error saving room', + 'ui.admin.chat_rooms.delete_confirm' => 'Delete this chat room? This cannot be undone.', + 'ui.admin.chat_rooms.deleted_success' => 'Room deleted successfully', + 'ui.admin.chat_rooms.delete_failed' => 'Error deleting room', + 'ui.admin.chat_rooms.page_title' => 'Chat Rooms', + 'ui.admin.chat_rooms.heading' => 'Chat Rooms', + 'ui.admin.chat_rooms.add_room' => 'Add Room', + 'ui.admin.chat_rooms.status' => 'Status', + 'ui.admin.chat_rooms.created' => 'Created', + 'ui.admin.chat_rooms.actions' => 'Actions', + 'ui.admin.chat_rooms.add_chat_room' => 'Add Chat Room', + 'ui.admin.chat_rooms.edit_chat_room' => 'Edit Chat Room', + 'ui.admin.chat_rooms.active_room' => 'Active room', + 'ui.admin.chat_rooms.save_room' => 'Save Room', + 'ui.admin.chat_rooms.no_description' => '-', + 'ui.admin.chat_rooms.active' => 'Active', + 'ui.admin.chat_rooms.inactive' => 'Inactive', + + // Admin Binkp Sessions + 'ui.admin.binkp_sessions.page_title' => 'Binkp Sessions - Admin', + 'ui.admin.binkp_sessions.heading' => 'Binkp Session Log', + 'ui.admin.binkp_sessions.back_to_dashboard' => 'Back to Dashboard', + 'ui.admin.binkp_sessions.total_24h' => 'Total (24h)', + 'ui.admin.binkp_sessions.secure' => 'Secure', + 'ui.admin.binkp_sessions.insecure' => 'Insecure', + 'ui.admin.binkp_sessions.crash_out' => 'Crash Out', + 'ui.admin.binkp_sessions.successful' => 'Successful', + 'ui.admin.binkp_sessions.failed' => 'Failed', + 'ui.admin.binkp_sessions.filter_sessions' => 'Filter Sessions', + 'ui.admin.binkp_sessions.session_type' => 'Session Type', + 'ui.admin.binkp_sessions.crash_outbound' => 'Crash Outbound', + 'ui.admin.binkp_sessions.success' => 'Success', + 'ui.admin.binkp_sessions.rejected' => 'Rejected', + 'ui.admin.binkp_sessions.remote_address' => 'Remote Address', + 'ui.admin.binkp_sessions.remote_address_placeholder' => '1:123/456', + 'ui.admin.binkp_sessions.apply_filter' => 'Apply Filter', + 'ui.admin.binkp_sessions.recent_sessions' => 'Recent Sessions', + 'ui.admin.binkp_sessions.time' => 'Time', + 'ui.admin.binkp_sessions.ip' => 'IP', + 'ui.admin.binkp_sessions.type' => 'Type', + 'ui.admin.binkp_sessions.direction' => 'Direction', + 'ui.admin.binkp_sessions.msgs_in' => 'Msgs In', + 'ui.admin.binkp_sessions.msgs_out' => 'Msgs Out', + 'ui.admin.binkp_sessions.duration' => 'Duration', + 'ui.admin.binkp_sessions.load_stats_failed' => 'Error loading stats', + 'ui.admin.binkp_sessions.no_sessions_found' => 'No sessions found', + 'ui.admin.binkp_sessions.in' => 'In', + 'ui.admin.binkp_sessions.out' => 'Out', + 'ui.admin.binkp_sessions.load_sessions_failed' => 'Error loading sessions', + 'ui.admin.binkp_sessions.crash' => 'Crash', + 'ui.admin.binkp_sessions.active' => 'Active', + + // Admin Polls + 'ui.admin.polls.load_failed' => 'Error loading polls', + 'ui.admin.polls.not_found' => 'Poll not found', + 'ui.admin.polls.load_single_failed' => 'Error loading poll', + 'ui.admin.polls.min_options_required' => 'At least two options are required.', + 'ui.admin.polls.question_required' => 'Question is required.', + 'ui.admin.polls.provide_min_options' => 'Please provide at least two options.', + 'ui.admin.polls.updated_success' => 'Poll updated successfully', + 'ui.admin.polls.created_success' => 'Poll created successfully', + 'ui.admin.polls.save_failed' => 'Error saving poll', + 'ui.admin.polls.delete_confirm' => 'Delete this poll? This cannot be undone.', + 'ui.admin.polls.deleted_success' => 'Poll deleted successfully', + 'ui.admin.polls.delete_failed' => 'Error deleting poll', + 'ui.admin.polls.page_title' => 'Polls', + 'ui.admin.polls.heading' => 'Polls', + 'ui.admin.polls.add_poll' => 'Add Poll', + 'ui.admin.polls.edit_poll' => 'Edit Poll', + 'ui.admin.polls.question' => 'Question', + 'ui.admin.polls.status' => 'Status', + 'ui.admin.polls.votes' => 'Votes', + 'ui.admin.polls.created_by' => 'Created By', + 'ui.admin.polls.created' => 'Created', + 'ui.admin.polls.actions' => 'Actions', + 'ui.admin.polls.options' => 'Options', + 'ui.admin.polls.add_option' => 'Add Option', + 'ui.admin.polls.active_poll' => 'Active poll', + 'ui.admin.polls.editing_options_resets_votes' => 'Editing options will reset votes for this poll.', + 'ui.admin.polls.save_poll' => 'Save Poll', + 'ui.admin.polls.active' => 'Active', + 'ui.admin.polls.inactive' => 'Inactive', + 'ui.admin.polls.option_text_placeholder' => 'Option text', + + // Admin Template Editor + 'ui.admin.template_editor.load_templates_failed' => 'Failed to load templates.', + 'ui.admin.template_editor.no_templates_found' => 'No templates found.', + 'ui.admin.template_editor.unsaved_changes_confirm' => 'You have unsaved changes. Continue and discard them?', + 'ui.admin.template_editor.load_template_failed' => 'Failed to load template', + 'ui.admin.template_editor.enter_template_path' => 'Enter a template path (e.g., systemnews.twig).', + 'ui.admin.template_editor.save_template_failed' => 'Failed to save template', + 'ui.admin.template_editor.template_saved_success' => 'Template saved successfully.', + 'ui.admin.template_editor.delete_confirm' => 'Delete {template}? This cannot be undone.', + 'ui.admin.template_editor.delete_template_failed' => 'Failed to delete template', + 'ui.admin.template_editor.no_template_selected' => 'No template selected', + 'ui.admin.template_editor.template_deleted_success' => 'Template deleted.', + 'ui.admin.template_editor.install_template_failed' => 'Failed to install template', + 'ui.admin.template_editor.template_installed_success' => 'Template installed from example.', + 'ui.admin.template_editor.page_title_prefix' => 'Admin: Template Editor', + 'ui.admin.template_editor.heading' => 'Template Editor', + 'ui.admin.template_editor.restricted_edits_prefix' => 'Edits are restricted to', + 'ui.admin.template_editor.custom_templates' => 'Custom Templates', + 'ui.admin.template_editor.search_templates_placeholder' => 'Search templates...', + 'ui.admin.template_editor.loading_templates' => 'Loading templates...', + 'ui.admin.template_editor.new_template' => 'New Template', + 'ui.admin.template_editor.path_relative_custom' => 'Path (relative to templates/custom)', + 'ui.admin.template_editor.new_template_path_placeholder' => 'systemnews.twig', + 'ui.admin.template_editor.create' => 'Create', + 'ui.admin.template_editor.editor' => 'Editor', + 'ui.admin.template_editor.install' => 'Install', + 'ui.admin.template_editor.changes_apply_immediately' => 'Changes apply immediately after saving. Make sure your Twig syntax is valid.', + + // Admin Language Overlay Editor + 'ui.admin.i18n_overrides.page_title' => 'Admin: Language Overrides', + 'ui.admin.i18n_overrides.heading' => 'Language Overrides', + 'ui.admin.i18n_overrides.description' => 'Customize individual phrases without editing the base translation files. Overrides are applied on top of the base catalog for the selected locale.', + 'ui.admin.i18n_overrides.locale_label' => 'Locale', + 'ui.admin.i18n_overrides.catalog_label' => 'Catalog', + 'ui.admin.i18n_overrides.load_btn' => 'Load', + 'ui.admin.i18n_overrides.search_placeholder' => 'Filter keys...', + 'ui.admin.i18n_overrides.show_overrides_only' => 'Show overrides only', + 'ui.admin.i18n_overrides.col_key' => 'Key', + 'ui.admin.i18n_overrides.col_base' => 'Base value', + 'ui.admin.i18n_overrides.col_override' => 'Your override', + 'ui.admin.i18n_overrides.save_btn' => 'Save Overrides', + 'ui.admin.i18n_overrides.clear_btn' => 'Clear', + 'ui.admin.i18n_overrides.overridden_badge' => 'Overridden', + 'ui.admin.i18n_overrides.no_keys' => 'No keys found.', + 'ui.admin.i18n_overrides.loading' => 'Loading...', + 'ui.admin.i18n_overrides.saved' => 'Overrides saved.', + 'ui.admin.i18n_overrides.save_failed' => 'Failed to save overrides.', + 'ui.admin.i18n_overrides.load_failed' => 'Failed to load catalog.', + 'ui.admin.i18n_overrides.select_locale_catalog' => 'Select a locale and catalog, then click Load.', + + // Admin Upgrade Notes + 'ui.admin.upgrade_notes.none_for_version' => 'No upgrade notes found for version {version}.', + + // Admin Insecure Nodes + 'ui.admin.insecure_nodes.error_prefix' => 'Error: ', + 'ui.admin.insecure_nodes.add_failed' => 'Failed to add node', + 'ui.admin.insecure_nodes.added_success' => 'Node added successfully', + 'ui.admin.insecure_nodes.update_failed' => 'Failed to update node', + 'ui.admin.insecure_nodes.updated_success' => 'Node updated successfully', + 'ui.admin.insecure_nodes.delete_confirm' => 'Delete {address} from the allowlist?', + 'ui.admin.insecure_nodes.delete_failed' => 'Failed to delete node', + 'ui.admin.insecure_nodes.deleted_success' => 'Node deleted successfully', + 'ui.admin.insecure_nodes.page_title' => 'Insecure Nodes - Admin', + 'ui.admin.insecure_nodes.heading' => 'Insecure Node Allowlist', + 'ui.admin.insecure_nodes.add_node' => 'Add Node', + 'ui.admin.insecure_nodes.back_to_dashboard' => 'Back to Dashboard', + 'ui.admin.insecure_nodes.about_insecure_sessions' => 'About Insecure Sessions:', + 'ui.admin.insecure_nodes.info_text_1' => 'Nodes in this allowlist can connect via binkp without password authentication.', + 'ui.admin.insecure_nodes.info_text_2' => 'Use with caution - insecure sessions are typically receive-only (they can deliver mail to you, but cannot pick up mail).', + 'ui.admin.insecure_nodes.allowed_nodes' => 'Allowed Nodes', + 'ui.admin.insecure_nodes.address' => 'Address', + 'ui.admin.insecure_nodes.receive' => 'Receive', + 'ui.admin.insecure_nodes.send' => 'Send', + 'ui.admin.insecure_nodes.max_msgs' => 'Max Msgs', + 'ui.admin.insecure_nodes.last_session' => 'Last Session', + 'ui.admin.insecure_nodes.active' => 'Active', + 'ui.admin.insecure_nodes.inactive' => 'Inactive', + 'ui.admin.insecure_nodes.actions' => 'Actions', + 'ui.admin.insecure_nodes.add_node_to_allowlist' => 'Add Node to Allowlist', + 'ui.admin.insecure_nodes.ftn_address' => 'FTN Address', + 'ui.admin.insecure_nodes.address_placeholder' => '1:123/456', + 'ui.admin.insecure_nodes.description_placeholder' => 'Node description', + 'ui.admin.insecure_nodes.ftn_address_help' => 'The FidoNet address of the node (e.g., 1:123/456)', + 'ui.admin.insecure_nodes.max_messages_per_session' => 'Max Messages Per Session', + 'ui.admin.insecure_nodes.allow_receive' => 'Allow Receive', + 'ui.admin.insecure_nodes.allow_receive_help' => 'Allow this node to deliver mail to us', + 'ui.admin.insecure_nodes.allow_send_pickup' => 'Allow Send (Pickup)', + 'ui.admin.insecure_nodes.allow_send_help' => 'Allow this node to pick up mail from us (use with caution)', + 'ui.admin.insecure_nodes.edit_node' => 'Edit Node', + 'ui.admin.insecure_nodes.save_changes' => 'Save Changes', + 'ui.admin.insecure_nodes.no_nodes' => 'No nodes in allowlist', + 'ui.admin.insecure_nodes.load_nodes_failed' => 'Error loading nodes', + + // Admin Crashmail Queue + 'ui.admin.crashmail_queue.attempt_delivery_confirm' => 'Attempt delivery for pending crashmail now?', + 'ui.admin.crashmail_queue.delivery_attempt_started' => 'Delivery attempt started.', + 'ui.admin.crashmail_queue.delivery_attempt_failed_prefix' => 'Delivery attempt failed: ', + 'ui.admin.crashmail_queue.error_prefix' => 'Error: ', + 'ui.admin.crashmail_queue.retry_confirm' => 'Retry this crashmail delivery?', + 'ui.admin.crashmail_queue.retry_success' => 'Crashmail retry queued.', + 'ui.admin.crashmail_queue.retry_failed_prefix' => 'Failed to retry: ', + 'ui.admin.crashmail_queue.cancel_confirm' => "Cancel this crashmail? The message will remain in your outbox but won't be crash-delivered.", + 'ui.admin.crashmail_queue.cancel_success' => 'Crashmail cancelled.', + 'ui.admin.crashmail_queue.cancel_failed_prefix' => 'Failed to cancel: ', + 'ui.admin.crashmail_queue.page_title' => 'Crashmail Queue - Admin', + 'ui.admin.crashmail_queue.heading' => 'Crashmail Queue', + 'ui.admin.crashmail_queue.attempt_delivery' => 'Attempt Delivery', + 'ui.admin.crashmail_queue.back_to_dashboard' => 'Back to Dashboard', + 'ui.admin.crashmail_queue.pending' => 'Pending', + 'ui.admin.crashmail_queue.attempting' => 'Attempting', + 'ui.admin.crashmail_queue.sent_24h' => 'Sent (24h)', + 'ui.admin.crashmail_queue.sent' => 'Sent', + 'ui.admin.crashmail_queue.failed' => 'Failed', + 'ui.admin.crashmail_queue.filter_queue' => 'Filter Queue', + 'ui.admin.crashmail_queue.status' => 'Status', + 'ui.admin.crashmail_queue.limit' => 'Limit', + 'ui.admin.crashmail_queue.apply_filter' => 'Apply Filter', + 'ui.admin.crashmail_queue.queue_items' => 'Queue Items', + 'ui.admin.crashmail_queue.to' => 'To', + 'ui.admin.crashmail_queue.subject' => 'Subject', + 'ui.admin.crashmail_queue.destination' => 'Destination', + 'ui.admin.crashmail_queue.attempts' => 'Attempts', + 'ui.admin.crashmail_queue.next_attempt' => 'Next Attempt', + 'ui.admin.crashmail_queue.actions' => 'Actions', + 'ui.admin.crashmail_queue.no_items' => 'No items in queue', + 'ui.admin.crashmail_queue.no_subject' => '(no subject)', + 'ui.admin.crashmail_queue.unresolved' => 'Unresolved', + 'ui.admin.crashmail_queue.load_stats_failed' => 'Error loading stats', + 'ui.admin.crashmail_queue.load_queue_failed' => 'Error loading queue', + + // Admin File Area Rules + 'ui.admin.filearea_rules.load_failed' => 'Failed to load rules', + 'ui.admin.filearea_rules.active' => 'Active', + 'ui.admin.filearea_rules.missing_config' => 'Missing config', + 'ui.admin.filearea_rules.error' => 'Error', + 'ui.admin.filearea_rules.invalid_json' => 'Rules JSON is invalid. Please fix errors before saving.', + 'ui.admin.filearea_rules.save_failed' => 'Failed to save rules', + 'ui.admin.filearea_rules.saved_success' => 'Rules saved successfully.', + 'ui.admin.filearea_rules.reset_confirm' => 'Replace current rules with the example file?', + 'ui.admin.filearea_rules.loaded_example' => 'Loaded example rules. Save to apply them.', + 'ui.admin.filearea_rules.page_title_prefix' => 'File Area Rules', + 'ui.admin.filearea_rules.heading' => 'File Area Rules', + 'ui.admin.filearea_rules.config_filename' => 'filearea_rules.json', + 'ui.admin.filearea_rules.reset_to_example' => 'Reset to Example', + 'ui.admin.filearea_rules.save_rules' => 'Save Rules', + 'ui.admin.filearea_rules.editing_requires_valid_json' => 'Editing requires valid JSON.', + 'ui.admin.filearea_rules.macros' => 'Macros', + 'ui.admin.filearea_rules.macro_basedir' => 'Application base directory', + 'ui.admin.filearea_rules.macro_filepath' => 'Full path to file', + 'ui.admin.filearea_rules.macro_filename' => 'File name only', + 'ui.admin.filearea_rules.macro_filesize' => 'File size in bytes', + 'ui.admin.filearea_rules.macro_domain' => 'File area domain', + 'ui.admin.filearea_rules.macro_areatag' => 'File area tag', + 'ui.admin.filearea_rules.macro_uploader' => 'Uploader name/address', + 'ui.admin.filearea_rules.macro_ticfile' => 'TIC path (if available)', + 'ui.admin.filearea_rules.macro_tempdir' => 'Temp directory', + 'ui.admin.filearea_rules.tips' => 'Tips', + 'ui.admin.filearea_rules.tip_1' => 'Rules run in order: global rules first, then area-specific rules.', + 'ui.admin.filearea_rules.tip_2' => 'Area rules can be keyed as TAG or TAG@DOMAIN (domain-specific takes precedence).', + 'ui.admin.filearea_rules.tip_3' => 'Use success_action and fail_action with +stop to halt further processing.', + + // Admin DOS Doors Config + 'ui.admin.dosdoors_config.load_config_failed' => 'Failed to load config', + 'ui.admin.dosdoors_config.load_config_error_prefix' => 'Error loading config: ', + 'ui.admin.dosdoors_config.load_doors_failed' => 'Failed to load doors', + 'ui.admin.dosdoors_config.load_doors_error_prefix' => 'Error loading available doors: ', + 'ui.admin.dosdoors_config.invalid_json_toggle' => 'Invalid JSON - cannot toggle door', + 'ui.admin.dosdoors_config.enabled_all_editor' => 'All doors enabled in editor. Click Save to apply.', + 'ui.admin.dosdoors_config.invalid_json_enable_all' => 'Invalid JSON - cannot enable all', + 'ui.admin.dosdoors_config.disabled_all_editor' => 'All doors disabled in editor. Click Save to apply.', + 'ui.admin.dosdoors_config.invalid_json_disable_all' => 'Invalid JSON - cannot disable all', + 'ui.admin.dosdoors_config.json_formatted' => 'JSON formatted successfully', + 'ui.admin.dosdoors_config.cannot_format_prefix' => 'Cannot format - invalid JSON: ', + 'ui.admin.dosdoors_config.empty_config' => 'Empty configuration', + 'ui.admin.dosdoors_config.valid_json' => 'Valid JSON', + 'ui.admin.dosdoors_config.invalid_json_prefix' => 'Invalid JSON: ', + 'ui.admin.dosdoors_config.save_failed' => 'Save failed', + 'ui.admin.dosdoors_config.saved_success' => 'Configuration saved successfully!', + 'ui.admin.dosdoors_config.save_error_prefix' => 'Error saving config: ', + 'ui.admin.dosdoors_config.cannot_save_invalid_json_prefix' => 'Cannot save - invalid JSON: ', + 'ui.admin.dosdoors_config.page_title' => 'DOS Doors Config', + 'ui.admin.dosdoors_config.heading' => 'DOS Doors Configuration', + 'ui.admin.dosdoors_config.info_text_1' => 'DOS doors are classic BBS door games that run in DOSBox. Configuration is stored in', + 'ui.admin.dosdoors_config.info_text_2' => 'Doors are discovered by scanning', + 'ui.admin.dosdoors_config.info_text_3' => 'for', + 'ui.admin.dosdoors_config.info_text_4' => 'manifests.', + 'ui.admin.dosdoors_config.installed_doors' => 'Installed Doors', + 'ui.admin.dosdoors_config.loading_doors' => 'Loading doors...', + 'ui.admin.dosdoors_config.quick_actions' => 'Quick Actions', + 'ui.admin.dosdoors_config.enable_all_doors' => 'Enable All Doors', + 'ui.admin.dosdoors_config.disable_all_doors' => 'Disable All Doors', + 'ui.admin.dosdoors_config.config_filename' => 'dosdoors.json', + 'ui.admin.dosdoors_config.format' => 'Format', + 'ui.admin.dosdoors_config.loading_config' => 'Loading config...', + 'ui.admin.dosdoors_config.json_validation_before_save' => 'JSON validation runs before saving', + 'ui.admin.dosdoors_config.configuration_options' => 'Configuration Options', + 'ui.admin.dosdoors_config.option_enabled_help' => 'Whether the door is available to users', + 'ui.admin.dosdoors_config.option_credit_cost_help' => 'Credits required to play (0 = free)', + 'ui.admin.dosdoors_config.option_max_time_help' => 'Maximum play time per session', + 'ui.admin.dosdoors_config.option_cpu_cycles_help' => 'DOSBox CPU speed (10000 = typical)', + 'ui.admin.dosdoors_config.option_max_concurrent_help' => 'Maximum simultaneous players', + 'ui.admin.dosdoors_config.no_doors_found_prefix' => 'No doors found. Install doors in', + 'ui.admin.dosdoors_config.no_description' => 'No description', + 'ui.admin.dosdoors_config.credits' => 'credits', + 'ui.admin.dosdoors_config.free' => 'Free', + 'ui.admin.dosdoors_config.disable' => 'Disable', + 'ui.admin.dosdoors_config.enable' => 'Enable', + + // Admin Native Doors Config + 'ui.admin.nativedoors_config.load_config_failed' => 'Failed to load config', + 'ui.admin.nativedoors_config.load_config_error_prefix' => 'Error loading config: ', + 'ui.admin.nativedoors_config.load_doors_failed' => 'Failed to load doors', + 'ui.admin.nativedoors_config.load_doors_error_prefix' => 'Error loading available doors: ', + 'ui.admin.nativedoors_config.invalid_json_toggle' => 'Invalid JSON - cannot toggle door', + 'ui.admin.nativedoors_config.enabled_all_editor' => 'All doors enabled in editor. Click Save to apply.', + 'ui.admin.nativedoors_config.invalid_json_enable_all' => 'Invalid JSON - cannot enable all', + 'ui.admin.nativedoors_config.disabled_all_editor' => 'All doors disabled in editor. Click Save to apply.', + 'ui.admin.nativedoors_config.invalid_json_disable_all' => 'Invalid JSON - cannot disable all', + 'ui.admin.nativedoors_config.json_formatted' => 'JSON formatted successfully', + 'ui.admin.nativedoors_config.cannot_format_prefix' => 'Cannot format - invalid JSON: ', + 'ui.admin.nativedoors_config.empty_config' => 'Empty configuration', + 'ui.admin.nativedoors_config.valid_json' => 'Valid JSON', + 'ui.admin.nativedoors_config.invalid_json_prefix' => 'Invalid JSON: ', + 'ui.admin.nativedoors_config.save_failed' => 'Save failed', + 'ui.admin.nativedoors_config.saved_success' => 'Configuration saved successfully!', + 'ui.admin.nativedoors_config.save_error_prefix' => 'Error saving config: ', + 'ui.admin.nativedoors_config.cannot_save_invalid_json_prefix' => 'Cannot save - invalid JSON: ', + 'ui.admin.nativedoors_config.page_title' => 'Native Doors Config', + 'ui.admin.nativedoors_config.heading' => 'Native Doors Configuration', + 'ui.admin.nativedoors_config.info_text_1' => 'Native doors are Linux programs that run directly via PTY (no emulator needed). Configuration is stored in', + 'ui.admin.nativedoors_config.info_text_2' => 'Doors are discovered by scanning', + 'ui.admin.nativedoors_config.info_text_3' => 'for', + 'ui.admin.nativedoors_config.info_text_4' => 'manifests.', + 'ui.admin.nativedoors_config.installed_doors' => 'Installed Doors', + 'ui.admin.nativedoors_config.loading_doors' => 'Loading doors...', + 'ui.admin.nativedoors_config.quick_actions' => 'Quick Actions', + 'ui.admin.nativedoors_config.enable_all_doors' => 'Enable All Doors', + 'ui.admin.nativedoors_config.disable_all_doors' => 'Disable All Doors', + 'ui.admin.nativedoors_config.config_filename' => 'nativedoors.json', + 'ui.admin.nativedoors_config.format' => 'Format', + 'ui.admin.nativedoors_config.loading_config' => 'Loading config...', + 'ui.admin.nativedoors_config.json_validation_before_save' => 'JSON validation runs before saving', + 'ui.admin.nativedoors_config.configuration_options' => 'Configuration Options', + 'ui.admin.nativedoors_config.option_enabled_help' => 'Whether the door is available to users', + 'ui.admin.nativedoors_config.option_credit_cost_help' => 'Credits required to play (0 = free)', + 'ui.admin.nativedoors_config.option_max_time_help' => 'Maximum play time per session', + 'ui.admin.nativedoors_config.option_max_concurrent_help' => 'Maximum simultaneous players', + 'ui.admin.nativedoors_config.no_doors_found_prefix' => 'No doors found. Install doors in', + 'ui.admin.nativedoors_config.no_description' => 'No description', + 'ui.admin.nativedoors_config.credits' => 'credits', + 'ui.admin.nativedoors_config.free' => 'Free', + 'ui.admin.nativedoors_config.disable' => 'Disable', + 'ui.admin.nativedoors_config.enable' => 'Enable', + + // Admin Webdoors Config + 'ui.admin.webdoors_config.load_webdoors_failed' => 'Failed to load webdoors config', + 'ui.admin.webdoors_config.load_failed' => 'Failed to load config', + 'ui.admin.webdoors_config.json_valid' => 'JSON is valid', + 'ui.admin.webdoors_config.json_has_errors' => 'JSON has errors', + 'ui.admin.webdoors_config.invalid_json' => 'Invalid JSON.', + 'ui.admin.webdoors_config.no_doors_defined' => 'No doors defined.', + 'ui.admin.webdoors_config.cannot_format_invalid_json' => 'Cannot format: invalid JSON', + 'ui.admin.webdoors_config.fix_json_before_save' => 'Please fix JSON errors before saving.', + 'ui.admin.webdoors_config.save_failed' => 'Failed to save config', + 'ui.admin.webdoors_config.saved_success' => 'Webdoors config saved.', + 'ui.admin.webdoors_config.activate_failed' => 'Failed to activate webdoors', + 'ui.admin.webdoors_config.activated_success' => 'Webdoors activated.', + 'ui.admin.webdoors_config.page_title' => 'Webdoors Config', + 'ui.admin.webdoors_config.heading' => 'Webdoors Config', + 'ui.admin.webdoors_config.info_text_prefix' => 'Webdoors are enabled by having a', + 'ui.admin.webdoors_config.info_text_suffix' => 'file. This page lets you activate and edit it.', + 'ui.admin.webdoors_config.status' => 'Status', + 'ui.admin.webdoors_config.activate_webdoors' => 'Activate Webdoors', + 'ui.admin.webdoors_config.config_file' => 'Config file:', + 'ui.admin.webdoors_config.config_never_deleted' => 'Once activated, the config file is never deleted.', + 'ui.admin.webdoors_config.json_controls_help' => 'Editing JSON controls how doors appear in the system. Only enabled is known by the UI.', + 'ui.admin.webdoors_config.doors' => 'Doors', + 'ui.admin.webdoors_config.config_filename' => 'webdoors.json', + 'ui.admin.webdoors_config.no_config_loaded' => 'No config loaded.', + 'ui.admin.webdoors_config.format_json' => 'Format JSON', + 'ui.admin.webdoors_config.waiting_for_config' => 'Waiting for config...', + 'ui.admin.webdoors_config.json_validation_before_save' => 'JSON validation runs before saving.', + 'ui.admin.webdoors_config.active_present' => 'Active (webdoors.json present)', + 'ui.admin.webdoors_config.not_active_using_example' => 'Not active (using example)', + 'ui.admin.webdoors_config.enabled_label' => 'enabled', + 'ui.admin.webdoors_config.true' => 'true', + 'ui.admin.webdoors_config.false' => 'false', + 'ui.admin.webdoors_config.not_in_config' => '(not in config)', + + // Admin BBS Settings + 'ui.admin.bbs_settings.load_failed' => 'Failed to load settings', + 'ui.admin.bbs_settings.save_failed' => 'Failed to save settings', + 'ui.admin.bbs_settings.saved_success' => 'BBS features saved successfully.', + 'ui.admin.bbs_settings.load_system_failed' => 'Failed to load system settings', + 'ui.admin.bbs_settings.save_system_failed' => 'Failed to save system settings', + 'ui.admin.bbs_settings.system_saved_success' => 'System settings saved successfully.', + 'ui.admin.bbs_settings.save_credits_failed' => 'Failed to save credits settings', + 'ui.admin.bbs_settings.credits_saved_success' => 'Credits settings saved successfully.', + 'ui.admin.bbs_settings.load_taglines_failed' => 'Failed to load taglines', + 'ui.admin.bbs_settings.save_taglines_failed' => 'Failed to save taglines', + 'ui.admin.bbs_settings.taglines_saved_success' => 'Taglines saved successfully.', + 'ui.admin.bbs_settings.page_title' => 'BBS Settings', + 'ui.admin.bbs_settings.heading' => 'BBS Settings', + 'ui.admin.bbs_settings.current' => 'current', + 'ui.admin.bbs_settings.system.title' => 'System Settings', + 'ui.admin.bbs_settings.system.system_name' => 'System Name', + 'ui.admin.bbs_settings.system.sysop_name' => 'Sysop Name', + 'ui.admin.bbs_settings.system.select_admin_user' => 'Select an admin user.', + 'ui.admin.bbs_settings.system.location' => 'Location', + 'ui.admin.bbs_settings.system.timezone' => 'Timezone', + 'ui.admin.bbs_settings.system.system_address' => 'System Address', + 'ui.admin.bbs_settings.system.origin_line' => 'Origin Line', + 'ui.admin.bbs_settings.system.save' => 'Save System Settings', + 'ui.admin.bbs_settings.features.title' => 'BBS Features', + 'ui.admin.bbs_settings.features.enable_webdoors' => 'Enable Webdoors', + 'ui.admin.bbs_settings.features.inactive' => 'inactive', + 'ui.admin.bbs_settings.features.activate' => 'Activate', + 'ui.admin.bbs_settings.features.enable_shoutbox' => 'Enable Shoutbox', + 'ui.admin.bbs_settings.features.enable_advertising' => 'Enable Advertising', + 'ui.admin.bbs_settings.features.enable_voting_booth' => 'Enable Voting Booth', + 'ui.admin.bbs_settings.features.enable_chat' => 'Enable Chat', + 'ui.admin.bbs_settings.features.enable_file_areas' => 'Enable File Areas', + 'ui.admin.bbs_settings.features.enable_guest_doors_page' => 'Enable Guest Doors Page', + 'ui.admin.bbs_settings.features.guest_doors_page_help' => 'Shows a public /guest-doors page listing anonymous-accessible doors. Also shows a link on the login page.', + 'ui.admin.bbs_settings.features.default_echo_interface' => 'Default Echo Interface', + 'ui.admin.bbs_settings.features.echo_list_forum' => 'Echo List (Forum view)', + 'ui.admin.bbs_settings.features.reader_message_list' => 'Reader (Message list)', + 'ui.admin.bbs_settings.features.default_echo_help' => 'Default interface for viewing echomail. Users can override this in their settings.', + 'ui.admin.bbs_settings.features.max_cross_post_areas' => 'Max Cross-Post Areas', + 'ui.admin.bbs_settings.features.max_cross_post_help' => 'Maximum number of additional areas a user can cross-post to (2-20).', + 'ui.admin.bbs_settings.features.save' => 'Save Settings', + 'ui.admin.bbs_settings.credits.title' => 'Credits System Configuration', + 'ui.admin.bbs_settings.credits.enabled' => 'Credits System Enabled', + 'ui.admin.bbs_settings.credits.currency_symbol' => 'Currency Symbol', + 'ui.admin.bbs_settings.credits.currency_symbol_help' => 'Example: $, USD (max 5 characters). Leave blank for no symbol.', + 'ui.admin.bbs_settings.credits.daily_login_bonus_amount' => 'Daily Login Bonus Amount', + 'ui.admin.bbs_settings.credits.daily_login_bonus_help' => 'Amount of credits awarded to users on daily login.', + 'ui.admin.bbs_settings.credits.daily_login_delay_minutes' => 'Daily Login Delay (minutes)', + 'ui.admin.bbs_settings.credits.daily_login_delay_help' => 'Minutes after login before daily bonus is awarded.', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus' => 'New User Approval Bonus', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus_help' => 'Credits awarded when a pending user is approved.', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus' => 'New User 14-Day Milestone Bonus', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus_help' => 'One-time bonus awarded when user logs in 14+ days after account creation.', + 'ui.admin.bbs_settings.credits.netmail_cost' => 'Netmail Cost', + 'ui.admin.bbs_settings.credits.netmail_cost_help' => 'Credits charged when sending a netmail message.', + 'ui.admin.bbs_settings.credits.echomail_reward' => 'Echomail Reward', + 'ui.admin.bbs_settings.credits.echomail_reward_help' => 'Credits earned when posting an echomail message. Doubled when >= 1200 characters long.', + 'ui.admin.bbs_settings.credits.crashmail_cost' => 'Crashmail Cost', + 'ui.admin.bbs_settings.credits.crashmail_cost_help' => 'Credits charged when sending direct/crashmail delivery.', + 'ui.admin.bbs_settings.credits.poll_creation_cost' => 'Poll Creation Cost', + 'ui.admin.bbs_settings.credits.poll_creation_cost_help' => 'Credits charged when creating a new poll.', + 'ui.admin.bbs_settings.credits.transfer_fee_percentage' => 'Transfer Fee Percentage', + 'ui.admin.bbs_settings.credits.transfer_fee_help' => 'Percentage of credit transfers taken as fee (0.05 = 5%). Distributed to sysops.', + 'ui.admin.bbs_settings.credits.referral_system' => 'Referral System', + 'ui.admin.bbs_settings.credits.enable_referral_system' => 'Enable Referral System', + 'ui.admin.bbs_settings.credits.referral_bonus' => 'Referral Bonus', + 'ui.admin.bbs_settings.credits.referral_bonus_help' => 'Credits awarded when a referred user is approved by admin.', + 'ui.admin.bbs_settings.credits.save' => 'Save Credit Settings', + 'ui.admin.bbs_settings.taglines.title' => 'Taglines', + 'ui.admin.bbs_settings.taglines.one_per_line' => 'Taglines (one per line)', + 'ui.admin.bbs_settings.taglines.placeholder' => 'One tagline per line.', + 'ui.admin.bbs_settings.taglines.help' => 'These appear as selectable taglines when users compose messages.', + 'ui.admin.bbs_settings.taglines.save' => 'Save Taglines', + 'ui.admin.bbs_settings.validation.max_cross_post_areas_range' => 'Max cross-post areas must be an integer between 2 and 20.', + 'ui.admin.bbs_settings.validation.currency_symbol_length' => 'Currency symbol must be 0-5 characters.', + 'ui.admin.bbs_settings.validation.daily_login_amount_non_negative' => 'Daily login amount must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.daily_login_delay_non_negative' => 'Daily login delay must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.approval_bonus_non_negative' => 'Approval bonus must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.netmail_cost_non_negative' => 'Netmail cost must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.echomail_reward_non_negative' => 'Echomail reward must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.crashmail_cost_non_negative' => 'Crashmail cost must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.poll_creation_cost_non_negative' => 'Poll creation cost must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.return_14_days_non_negative' => '14-day return bonus must be a non-negative integer.', + 'ui.admin.bbs_settings.validation.transfer_fee_range' => 'Transfer fee must be between 0 and 1 (0% to 100%).', + 'ui.admin.bbs_settings.validation.referral_bonus_non_negative' => 'Referral bonus must be a non-negative integer.', + + // Admin MRC Settings + 'ui.admin.mrc_settings.page_title' => 'MRC Settings', + 'ui.admin.mrc_settings.heading' => 'MRC Chat Settings', + 'ui.admin.mrc_settings.note_label' => 'Note', + 'ui.admin.mrc_settings.note_text' => 'MRC (Multi Relay Chat) is available as a WebDoor.', + 'ui.admin.mrc_settings.note_enable_text' => 'Configure daemon settings here, then enable the door in the', + 'ui.admin.mrc_settings.webdoors_admin' => 'WebDoors admin', + 'ui.admin.mrc_settings.load_failed' => 'Failed to load MRC settings', + 'ui.admin.mrc_settings.load_error' => 'Error loading MRC settings', + 'ui.admin.mrc_settings.saved_success' => 'MRC settings saved successfully', + 'ui.admin.mrc_settings.restart_required' => 'Settings saved. Restart the daemon to apply changes.', + 'ui.admin.mrc_settings.save_failed' => 'Failed to save MRC settings', + 'ui.admin.mrc_settings.save_error' => 'Error saving MRC settings', + 'ui.admin.mrc_settings.general_settings' => 'General Settings', + 'ui.admin.mrc_settings.enable_daemon' => 'Enable MRC Daemon', + 'ui.admin.mrc_settings.server_connection' => 'Server Connection', + 'ui.admin.mrc_settings.server_host' => 'MRC Server Host', + 'ui.admin.mrc_settings.server_port_non_ssl' => 'Server Port (Non-SSL)', + 'ui.admin.mrc_settings.server_port_ssl' => 'Server Port (SSL)', + 'ui.admin.mrc_settings.use_ssl_connection' => 'Use SSL Connection', + 'ui.admin.mrc_settings.use_ssl_help' => 'Recommended for secure communication', + 'ui.admin.mrc_settings.save_general_settings' => 'Save General Settings', + 'ui.admin.mrc_settings.bbs_identity' => 'BBS Identity', + 'ui.admin.mrc_settings.bbs_name' => 'BBS Name', + 'ui.admin.mrc_settings.bbs_name_help' => 'Maximum 64 characters', + 'ui.admin.mrc_settings.platform_info' => 'Platform Info', + 'ui.admin.mrc_settings.platform_info_help' => 'Software and platform identifier', + 'ui.admin.mrc_settings.sysop_name' => 'Sysop Name', + 'ui.admin.mrc_settings.bbs_information' => 'BBS Information', + 'ui.admin.mrc_settings.website_url' => 'Website URL', + 'ui.admin.mrc_settings.info_optional_help' => 'Optional - for INFO command', + 'ui.admin.mrc_settings.telnet_address' => 'Telnet Address', + 'ui.admin.mrc_settings.bbs_description' => 'BBS Description', + 'ui.admin.mrc_settings.placeholder.server_host' => 'mrc.bottomlessabyss.net', + 'ui.admin.mrc_settings.placeholder.bbs_name' => 'BinktermPHP BBS', + 'ui.admin.mrc_settings.placeholder.platform' => 'BINKTERMPHP/Linux64/1.0.0', + 'ui.admin.mrc_settings.placeholder.sysop' => 'Sysop', + 'ui.admin.mrc_settings.placeholder.website' => 'https://example.com', + 'ui.admin.mrc_settings.placeholder.telnet' => 'telnet://bbs.example.com:23', + 'ui.admin.mrc_settings.placeholder.description' => 'A modern BBS...', + 'ui.admin.mrc_settings.save_identity_settings' => 'Save Identity Settings', + 'ui.admin.mrc_settings.connection_settings' => 'Connection Settings', + 'ui.admin.mrc_settings.auto_reconnect' => 'Auto-Reconnect', + 'ui.admin.mrc_settings.auto_reconnect_help' => 'Automatically reconnect if connection is lost', + 'ui.admin.mrc_settings.reconnect_delay' => 'Reconnect Delay (seconds)', + 'ui.admin.mrc_settings.reconnect_delay_help' => 'Wait time before reconnecting (5-300 seconds)', + 'ui.admin.mrc_settings.ping_interval' => 'Ping Interval (seconds)', + 'ui.admin.mrc_settings.ping_interval_help' => 'Expected PING interval from server (30-300 seconds)', + 'ui.admin.mrc_settings.keepalive_timeout' => 'Keepalive Timeout (seconds)', + 'ui.admin.mrc_settings.keepalive_timeout_help' => 'Disconnect if no PING received (60-600 seconds)', + 'ui.admin.mrc_settings.save_connection_settings' => 'Save Connection Settings', + 'ui.admin.mrc_settings.room_settings' => 'Room Settings', + 'ui.admin.mrc_settings.default_room' => 'Default Room', + 'ui.admin.mrc_settings.default_room_help' => 'Default room for new connections', + 'ui.admin.mrc_settings.auto_join_rooms' => 'Auto-Join Rooms', + 'ui.admin.mrc_settings.auto_join_rooms_help' => 'Comma-separated list of rooms to join automatically', + 'ui.admin.mrc_settings.message_settings' => 'Message Settings', + 'ui.admin.mrc_settings.max_message_length' => 'Max Message Length', + 'ui.admin.mrc_settings.max_message_length_help' => 'Maximum characters per message (80-255)', + 'ui.admin.mrc_settings.history_limit' => 'History Limit', + 'ui.admin.mrc_settings.history_limit_help' => 'Messages to keep per room (100-10000)', + 'ui.admin.mrc_settings.prune_after_days' => 'Prune After (days)', + 'ui.admin.mrc_settings.prune_after_days_help' => 'Delete messages older than this (1-365 days)', + 'ui.admin.mrc_settings.save_room_settings' => 'Save Room Settings', + + // Admin Activity Stats + 'ui.admin.activity_stats.page_title' => 'Activity Statistics', + 'ui.admin.activity_stats.heading' => 'Activity Statistics', + 'ui.admin.activity_stats.period' => 'Period', + 'ui.admin.activity_stats.period_7d' => 'Last 7 Days', + 'ui.admin.activity_stats.period_30d' => 'Last 30 Days', + 'ui.admin.activity_stats.period_90d' => 'Last 90 Days', + 'ui.admin.activity_stats.period_all' => 'All Time', + 'ui.admin.activity_stats.exclude_admins' => 'Exclude admins', + 'ui.admin.activity_stats.dashboard' => 'Dashboard', + 'ui.admin.activity_stats.loading_activity_data' => 'Loading activity data...', + 'ui.admin.activity_stats.tab_overview' => 'Overview', + 'ui.admin.activity_stats.tab_popular_areas' => 'Popular Areas', + 'ui.admin.activity_stats.tab_doors' => 'Doors', + 'ui.admin.activity_stats.tab_file_activity' => 'File Activity', + 'ui.admin.activity_stats.tab_nodelist' => 'Nodelist', + 'ui.admin.activity_stats.tab_top_users' => 'Top Users', + 'ui.admin.activity_stats.tab_hourly' => 'Hourly', + 'ui.admin.activity_stats.activity_by_category' => 'Activity by Category', + 'ui.admin.activity_stats.category' => 'Category', + 'ui.admin.activity_stats.events' => 'Events', + 'ui.admin.activity_stats.daily_activity_last_30' => 'Daily Activity (last 30 days)', + 'ui.admin.activity_stats.date' => 'Date', + 'ui.admin.activity_stats.most_viewed_echoareas' => 'Most Viewed Echoareas', + 'ui.admin.activity_stats.most_active_echoareas' => 'Most Active Echoareas (posts)', + 'ui.admin.activity_stats.area' => 'Area', + 'ui.admin.activity_stats.views' => 'Views', + 'ui.admin.activity_stats.posts' => 'Posts', + 'ui.admin.activity_stats.most_played_webdoors' => 'Most Played WebDoors', + 'ui.admin.activity_stats.most_played_dos_doors' => 'Most Played DOS Doors', + 'ui.admin.activity_stats.game' => 'Game', + 'ui.admin.activity_stats.door' => 'Door', + 'ui.admin.activity_stats.sessions' => 'Sessions', + 'ui.admin.activity_stats.top_downloaded_files' => 'Top Downloaded Files', + 'ui.admin.activity_stats.file' => 'File', + 'ui.admin.activity_stats.downloads' => 'Downloads', + 'ui.admin.activity_stats.most_browsed_file_areas' => 'Most Browsed File Areas', + 'ui.admin.activity_stats.most_searched_nodelist_queries' => 'Most Searched Nodelist Queries', + 'ui.admin.activity_stats.query' => 'Query', + 'ui.admin.activity_stats.searches' => 'Searches', + 'ui.admin.activity_stats.most_viewed_nodes' => 'Most Viewed Nodes', + 'ui.admin.activity_stats.node' => 'Node', + 'ui.admin.activity_stats.most_active_users' => 'Most Active Users', + 'ui.admin.activity_stats.user' => 'User', + 'ui.admin.activity_stats.activity_by_hour_of_day' => 'Activity by Hour of Day', + 'ui.admin.activity_stats.hour' => 'Hour', + 'ui.admin.activity_stats.utc' => 'UTC', + 'ui.admin.activity_stats.no_data_for_period' => 'No data for this period', + 'ui.admin.activity_stats.no_data' => 'No data', + 'ui.admin.activity_stats.failed_load_statistics_prefix' => 'Failed to load statistics. ', + 'ui.admin.activity_stats.echomail' => 'Echomail', + 'ui.admin.activity_stats.netmail' => 'Netmail', + 'ui.admin.activity_stats.files' => 'Files', + 'ui.admin.activity_stats.door_plays' => 'Door Plays', + 'ui.admin.activity_stats.logins' => 'Logins', + 'ui.admin.activity_stats.total' => 'Total', + 'ui.admin.activity_stats.views_sent' => 'Views: {views} - Sent: {sent}', + 'ui.admin.activity_stats.read_sent' => 'Read: {read} - Sent: {sent}', + 'ui.admin.activity_stats.area_views' => 'Area Views', + 'ui.admin.activity_stats.sent' => 'Sent', + 'ui.admin.activity_stats.read' => 'Read', + 'ui.admin.activity_stats.doors' => 'Doors', + 'ui.admin.activity_stats.nodelist' => 'Nodelist', + 'ui.admin.activity_stats.chat' => 'Chat', + 'ui.admin.activity_stats.auth' => 'Auth', + 'ui.admin.activity_stats.anonymous' => '(anon)', + + // Admin Economy Viewer + 'ui.admin.economy.page_title' => 'Economy Viewer', + 'ui.admin.economy.heading' => 'Economy Viewer', + 'ui.admin.economy.period' => 'Period:', + 'ui.admin.economy.period_7d' => 'Last 7 Days', + 'ui.admin.economy.period_30d' => 'Last 30 Days', + 'ui.admin.economy.period_90d' => 'Last 90 Days', + 'ui.admin.economy.period_all' => 'All Time', + 'ui.admin.economy.dashboard' => 'Dashboard', + 'ui.admin.economy.loading_statistics' => 'Loading economy statistics...', + 'ui.admin.economy.period_snapshot' => 'Period Snapshot', + 'ui.admin.economy.transactions' => 'Transactions', + 'ui.admin.economy.active_users' => 'Active Users', + 'ui.admin.economy.credits_earned' => 'Credits Earned', + 'ui.admin.economy.credits_spent' => 'Credits Spent', + 'ui.admin.economy.net_flow' => 'Net Flow', + 'ui.admin.economy.current_distribution' => 'Current Distribution', + 'ui.admin.economy.funded_users' => 'Funded Users', + 'ui.admin.economy.average_balance' => 'Average Balance', + 'ui.admin.economy.median_balance' => 'Median Balance', + 'ui.admin.economy.largest_balance' => 'Largest Balance', + 'ui.admin.economy.richest_user' => 'Richest User', + 'ui.admin.economy.transaction_types' => 'Transaction Types', + 'ui.admin.economy.type' => 'Type', + 'ui.admin.economy.count' => 'Count', + 'ui.admin.economy.net' => 'Net', + 'ui.admin.economy.top_earners' => 'Top Earners', + 'ui.admin.economy.user' => 'User', + 'ui.admin.economy.tx_short' => 'Tx', + 'ui.admin.economy.earned' => 'Earned', + 'ui.admin.economy.spent' => 'Spent', + 'ui.admin.economy.top_spenders' => 'Top Spenders', + 'ui.admin.economy.richest_accounts' => 'Richest Accounts', + 'ui.admin.economy.balance' => 'Balance', + 'ui.admin.economy.recent_transactions' => 'Recent Transactions', + 'ui.admin.economy.amount' => 'Amount', + 'ui.admin.economy.na' => 'n/a', + 'ui.admin.economy.credits_in_circulation' => 'Credits In Circulation', + 'ui.admin.economy.funded_wallets' => 'Funded Wallets', + 'ui.admin.economy.credits_disabled_notice' => 'Credits are currently disabled. Historical balances and ledger data are still shown.', + 'ui.admin.economy.users' => 'users', + 'ui.admin.economy.no_transactions_period' => 'No transactions for this period', + 'ui.admin.economy.no_earning_activity_period' => 'No earning activity for this period', + 'ui.admin.economy.no_spending_activity_period' => 'No spending activity for this period', + 'ui.admin.economy.no_funded_users_yet' => 'No funded users yet', + 'ui.admin.economy.no_description' => 'No description', + 'ui.admin.economy.bal_short' => 'Bal', + 'ui.admin.economy.no_recent_transactions_period' => 'No recent transactions in this period', + 'ui.admin.economy.load_failed' => 'Failed to load economy statistics', + + // File Areas Page + 'ui.fileareas.page_title' => 'File Areas', + 'ui.fileareas.heading' => 'File Areas Management', + 'ui.fileareas.add_file_area' => 'Add File Area', + 'ui.fileareas.edit_file_area' => 'Edit File Area', + 'ui.fileareas.list_title' => 'File Areas', + 'ui.fileareas.loading_file_areas' => 'Loading file areas...', + 'ui.fileareas.load_failed' => 'Failed to load file areas', + 'ui.fileareas.load_details_failed' => 'Failed to load file area details', + 'ui.fileareas.updated_success' => 'File area updated', + 'ui.fileareas.created_success' => 'File area created', + 'ui.fileareas.deleted_success' => 'File area deleted', + 'ui.fileareas.active_areas' => 'Active Areas:', + 'ui.fileareas.total_files' => 'Total Files:', + 'ui.fileareas.total_size' => 'Total Size:', + 'ui.fileareas.settings_title' => 'File Area Settings', + 'ui.fileareas.replace_existing_label' => 'Replace Existing:', + 'ui.fileareas.replace_existing_help' => 'When enabled, new files with the same name replace old ones (useful for NODELIST, etc.)', + 'ui.fileareas.versioning_label' => 'Versioning:', + 'ui.fileareas.versioning_help' => 'When disabled, files with duplicate names get version suffixes (_1, _2, etc.)', + 'ui.fileareas.tag_required' => 'Tag *', + 'ui.fileareas.tag_help' => 'File area tag (e.g., NODELIST, GENERAL_FILES)', + 'ui.fileareas.description_required' => 'Description *', + 'ui.fileareas.description_help' => 'Brief description of the file area', + 'ui.fileareas.network_domain' => 'Network domain', + 'ui.fileareas.max_file_size_mb' => 'Max File Size (MB)', + 'ui.fileareas.maximum_file_size' => 'Maximum file size', + 'ui.fileareas.tic_password_optional' => 'TIC Password (Optional)', + 'ui.fileareas.tic_password_help' => 'File echo password for TIC files (FSC-87 Pw field)', + 'ui.fileareas.allowed_extensions' => 'Allowed Extensions', + 'ui.fileareas.extensions_placeholder_allowed' => 'e.g., zip,txt,pdf', + 'ui.fileareas.allowed_extensions_help' => 'Comma-separated list (leave empty for all)', + 'ui.fileareas.blocked_extensions' => 'Blocked Extensions', + 'ui.fileareas.extensions_placeholder_blocked' => 'e.g., exe,bat,sh', + 'ui.fileareas.blocked_extensions_help' => 'Comma-separated list', + 'ui.fileareas.replace_existing_files' => 'Replace Existing Files', + 'ui.fileareas.replace_existing_files_help' => 'Replace files with same name instead of versioning', + 'ui.fileareas.allow_duplicate_content' => 'Allow Duplicate Content', + 'ui.fileareas.allow_duplicate_content_help' => 'Allow same file content (hash) with different filenames', + 'ui.fileareas.local_only' => 'Local Only', + 'ui.fileareas.local_only_help' => "Don't forward files to uplinks", + 'ui.fileareas.upload_permission' => 'Upload Permission', + 'ui.fileareas.upload_users_can_upload' => 'Users Can Upload', + 'ui.fileareas.upload_admin_only' => 'Admin Only', + 'ui.fileareas.upload_read_only' => 'Read-Only (No Uploads)', + 'ui.fileareas.upload_permission_help' => 'Control who can upload files to this area', + 'ui.fileareas.virus_scanning' => 'Virus Scanning', + 'ui.fileareas.virus_scanning_help' => 'Scan uploaded files for viruses (requires ClamAV)', + 'ui.fileareas.file_area_is_active' => 'File area is active', + 'ui.fileareas.edit_rules' => 'Edit Rules', + 'ui.fileareas.delete_confirm_prefix' => 'Are you sure you want to delete this file area:', + 'ui.fileareas.delete_confirm_warning' => 'This will also delete all files in this area!', + 'ui.fileareas.no_file_areas_found' => 'No file areas found', + 'ui.fileareas.tag' => 'Tag', + 'ui.fileareas.files' => 'Files', + 'ui.fileareas.size' => 'Size', + 'ui.fileareas.local' => 'Local', + 'ui.fileareas.replace' => 'Replace', + + // Admin Auto Feed + 'ui.admin.auto_feed.load_details_failed' => 'Failed to load feed details', + 'ui.admin.auto_feed.operation_failed' => 'Operation failed', + 'ui.admin.auto_feed.deleted_success' => 'Feed deleted successfully', + 'ui.admin.auto_feed.updated_success' => 'Feed updated successfully', + 'ui.admin.auto_feed.created_success' => 'Feed created successfully', + 'ui.admin.auto_feed.delete_failed' => 'Delete failed', + 'ui.admin.auto_feed.check_failed' => 'Check failed', + 'ui.admin.auto_feed.checked_articles_posted' => 'Feed checked: {count} new article(s) posted', + 'ui.admin.auto_feed.page_title' => 'Auto Feed', + 'ui.admin.auto_feed.heading' => 'Auto Feed', + 'ui.admin.auto_feed.statistics' => 'Statistics', + 'ui.admin.auto_feed.total_feeds' => 'Total Feeds', + 'ui.admin.auto_feed.add_feed' => 'Add Feed', + 'ui.admin.auto_feed.edit_feed' => 'Edit Feed', + 'ui.admin.auto_feed.rss_atom_feeds' => 'RSS/Atom Feeds', + 'ui.admin.auto_feed.loading_feeds' => 'Loading feeds...', + 'ui.admin.auto_feed.loading_echo_areas' => 'Loading echo areas...', + 'ui.admin.auto_feed.loading_users' => 'Loading users...', + 'ui.admin.auto_feed.active_label' => 'Active', + 'ui.admin.auto_feed.inactive_label' => 'Inactive', + 'ui.admin.auto_feed.articles_posted' => 'Articles Posted', + 'ui.admin.auto_feed.about_title' => 'About', + 'ui.admin.auto_feed.about_text' => 'Auto Feed monitors RSS/Atom feeds and automatically posts new articles to specified echoareas.', + 'ui.admin.auto_feed.cron_hint_prefix' => 'Run', + 'ui.admin.auto_feed.cron_hint_suffix' => 'via cron to check feeds periodically.', + 'ui.admin.auto_feed.feed_name' => 'Feed Name', + 'ui.admin.auto_feed.feed_name_placeholder' => 'e.g., Tech News, BBC Headlines', + 'ui.admin.auto_feed.feed_name_help' => 'Friendly name for this feed', + 'ui.admin.auto_feed.feed_url_required' => 'Feed URL *', + 'ui.admin.auto_feed.feed_url_placeholder' => 'https://example.com/feed.rss', + 'ui.admin.auto_feed.feed_url_help' => 'RSS or Atom feed URL', + 'ui.admin.auto_feed.echo_area_required' => 'Echo Area *', + 'ui.admin.auto_feed.echo_area' => 'Echo Area', + 'ui.admin.auto_feed.echo_area_help' => 'Echo area to post articles to', + 'ui.admin.auto_feed.post_as_user_required' => 'Post As User *', + 'ui.admin.auto_feed.post_as' => 'Post As', + 'ui.admin.auto_feed.post_as_user_help' => 'User account to post messages as', + 'ui.admin.auto_feed.max_articles' => 'Max Articles', + 'ui.admin.auto_feed.per_check' => 'Per check', + 'ui.admin.auto_feed.confirm_delete_title' => 'Confirm Delete', + 'ui.admin.auto_feed.confirm_delete_text_prefix' => 'Are you sure you want to delete the feed', + 'ui.admin.auto_feed.confirm_delete_warning' => 'This action cannot be undone.', + 'ui.admin.auto_feed.invalid_api_response' => 'Invalid API response', + 'ui.admin.auto_feed.failed_to_load_feeds_prefix' => 'Failed to load feeds:', + 'ui.admin.auto_feed.no_feeds_configured' => 'No feeds configured', + 'ui.admin.auto_feed.never' => 'Never', + 'ui.admin.auto_feed.unnamed_feed' => 'Unnamed Feed', + 'ui.admin.auto_feed.unknown' => 'Unknown', + 'ui.admin.auto_feed.check_now' => 'Check now', + 'ui.admin.auto_feed.checking_feed' => 'Checking feed...', + 'ui.admin.auto_feed.select_echo_area' => 'Select echo area...', + 'ui.admin.auto_feed.name_url' => 'Name/URL', + 'ui.admin.auto_feed.articles' => 'Articles', + 'ui.admin.auto_feed.last_check' => 'Last Check', + 'ui.admin.auto_feed.actions' => 'Actions', + 'ui.admin.auto_feed.user_prefix' => 'User #', + + // Echo Areas Page + 'ui.echoareas.page_title' => 'Echo Areas', + 'ui.echoareas.heading' => 'Echo Areas Management', + 'ui.echoareas.add_echo_area' => 'Add Echo Area', + 'ui.echoareas.edit_echo_area' => 'Edit Echo Area', + 'ui.echoareas.active_echo_areas' => 'Active Echo Areas', + 'ui.echoareas.search_tags_placeholder' => 'Search tags...', + 'ui.echoareas.loading_echo_areas' => 'Loading echo areas...', + 'ui.echoareas.statistics' => 'Statistics', + 'ui.echoareas.active_areas' => 'Active Areas:', + 'ui.echoareas.total_messages' => 'Total Messages:', + 'ui.echoareas.todays_messages' => "Today's Messages:", + 'ui.echoareas.color_legend' => 'Color Legend', + 'ui.echoareas.color_legend_help' => 'Echo areas can be color-coded for easy identification:', + 'ui.echoareas.color_green' => 'Green', + 'ui.echoareas.color_blue' => 'Blue', + 'ui.echoareas.color_red' => 'Red', + 'ui.echoareas.color_yellow' => 'Yellow', + 'ui.echoareas.color_purple' => 'Purple', + 'ui.echoareas.color_orange' => 'Orange', + 'ui.echoareas.color_teal' => 'Teal', + 'ui.echoareas.color_cyan' => 'Cyan', + 'ui.echoareas.color_light_blue' => 'Light Blue', + 'ui.echoareas.color_indigo' => 'Indigo', + 'ui.echoareas.color_pink' => 'Pink', + 'ui.echoareas.color_gray' => 'Gray', + 'ui.echoareas.legend_general_topics' => 'General topics', + 'ui.echoareas.legend_technical_testing' => 'Technical/Testing', + 'ui.echoareas.legend_important_official' => 'Important/Official', + 'ui.echoareas.legend_administrative' => 'Administrative', + 'ui.echoareas.legend_special_interest' => 'Special interest', + 'ui.echoareas.legend_regional' => 'Regional', + 'ui.echoareas.tag_required' => 'Tag *', + 'ui.echoareas.tag_help' => 'Echo area tag (e.g., FIDONET.GEN, LOCAL.TEST)', + 'ui.echoareas.description_required' => 'Description *', + 'ui.echoareas.description_help' => 'Brief description of the echo area', + 'ui.echoareas.uplink_address' => 'Uplink Address', + 'ui.echoareas.uplink_address_help' => 'Override Uplink FidoNet address', + 'ui.echoareas.color' => 'Color', + 'ui.echoareas.selected_color' => 'Selected:', + 'ui.echoareas.network_domain_help' => 'Network Domain (eg: fidonet, fsxnet, etc.)', + 'ui.echoareas.moderator' => 'Moderator', + 'ui.echoareas.moderator_help' => 'Name of the echo area moderator', + 'ui.echoareas.posting_name_policy' => 'Posting Name Policy', + 'ui.echoareas.posting_name_policy_inherit' => 'Use Uplink Default', + 'ui.echoareas.posting_name_policy_real_name' => 'Real Name', + 'ui.echoareas.posting_name_policy_username' => 'Username', + 'ui.echoareas.posting_name_policy_help' => 'Overrides uplink posting name policy for this echo area.', + 'ui.echoareas.local_only' => 'Local Only', + 'ui.echoareas.local_only_help' => 'Messages in local areas are not transmitted to uplinks', + 'ui.echoareas.sysop_access_only' => 'Sysop Access Only', + 'ui.echoareas.sysop_access_only_help' => 'Restrict this echo area to sysop/admin users only', + 'ui.echoareas.public_gemini_access' => 'Public Gemini Access', + 'ui.echoareas.public_gemini_access_help' => 'Expose this echo area as read-only content on the Gemini capsule server', + 'ui.echoareas.delete_confirm_prefix' => 'Are you sure you want to delete the echo area', + 'ui.echoareas.delete_confirm_warning' => 'This action cannot be undone and will affect message routing.', + 'ui.echoareas.none_found' => 'No echo areas found', + 'ui.echoareas.tag' => 'Tag', + 'ui.echoareas.messages' => 'Messages', + 'ui.echoareas.uplink' => 'Uplink', + 'ui.echoareas.local' => 'Local', + 'ui.echoareas.sysop' => 'Sysop', + 'ui.echoareas.local_only_title' => 'Local only - not transmitted to uplinks', + 'ui.echoareas.sysop_access_only_title' => 'Sysop access only', + 'ui.echoareas.mod_prefix' => 'Mod:', + 'ui.echoareas.load_failed' => 'Failed to load echo areas', + 'ui.echoareas.load_details_failed' => 'Failed to load echo area details', + 'ui.echoareas.updated_success' => 'Echo area updated successfully', + 'ui.echoareas.created_success' => 'Echo area created successfully', + 'ui.echoareas.deleted_success' => 'Echo area deleted successfully', + 'ui.echoareas.sync_not_implemented' => 'Sync functionality not yet implemented', + 'ui.echoareas.export_not_implemented' => 'Export functionality not yet implemented', + 'ui.echoareas.validate_not_implemented' => 'Validation functionality not yet implemented', + + // BBS Menu Shell + 'ui.shell.menu' => 'Menu', + 'ui.shell.menu_main_q_title' => 'Main Menu (Q)', + + // Login Page + 'ui.login.title' => 'Login', + 'ui.login.login_to_system' => 'Login to {system_name}', + 'ui.login.username' => 'Username', + 'ui.login.password' => 'Password', + 'ui.login.remember_me_30_days' => 'Remember me (30 days)', + 'ui.login.no_account' => "Don't have an account?", + 'ui.login.request_access' => 'Request Access', + 'ui.login.forgot_password' => 'Forgot your password?', + 'ui.login.reset_password' => 'Reset Password', + 'ui.login.sending' => 'Sending...', + 'ui.login.reminder_sent_netmail_email_suffix' => '(sent via netmail and email)', + 'ui.login.reminder_sent_netmail_only_suffix' => '(sent via netmail)', + 'ui.login.connect_via_telnet' => 'Connect via Telnet', + + // Guest Doors Page + 'ui.guest_doors.page_title' => 'Guest Doors', + 'ui.guest_doors.heading' => 'Guest Access', + 'ui.guest_doors.subtitle' => 'Choose a door below to get started.', + 'ui.guest_doors.no_doors' => 'No guest doors are currently available.', + 'ui.guest_doors.connect' => 'Connect', + 'ui.guest_doors.signin_link' => 'Sign in', + 'ui.guest_doors.register_link' => 'request an account', + + // Register Page + 'ui.register.title' => 'Register', + 'ui.register.create_account' => 'Create Account', + 'ui.register.approval_required' => 'Registration requires approval.', + 'ui.register.approval_required_help' => 'Your account will be reviewed by an administrator before activation.', + 'ui.register.subject_to_rules' => 'All accounts are subject to', + 'ui.register.honeypot_website_leave_blank' => 'Website (leave blank)', + 'ui.register.username' => 'Username', + 'ui.register.username_help' => '3-20 characters, letters, numbers, and underscores only', + 'ui.register.password' => 'Password', + 'ui.register.password_help' => 'Minimum 8 characters. Use a mix of letters, numbers, and symbols for better security.', + 'ui.register.confirm_password' => 'Confirm Password', + 'ui.register.email_address' => 'Email Address', + 'ui.register.email_help' => 'Optional - for account recovery and notifications', + 'ui.register.real_name' => 'Real Name', + 'ui.register.real_name_help' => 'Your actual name (required for FidoNet)', + 'ui.register.location' => 'Location', + 'ui.register.location_placeholder' => 'City, State/Country', + 'ui.register.location_help' => "Your location (optional, shown in who's online)", + 'ui.register.reason_for_joining' => 'Reason for Joining', + 'ui.register.reason_placeholder' => "Tell us why you'd like to join ...", + 'ui.register.submit_registration' => 'Submit Registration', + 'ui.register.already_have_account' => 'Already have an account?', + 'ui.register.sign_in' => 'Sign In', + 'ui.register.password_strength.weak' => 'Weak - Add more variety', + 'ui.register.password_strength.fair' => 'Fair - Could be stronger', + 'ui.register.password_strength.good' => 'Good - Nice password!', + 'ui.register.password_strength.strong' => 'Strong - Excellent!', + 'ui.register.passwords_do_not_match' => 'Passwords do not match.', + 'ui.register.submitting' => 'Submitting...', + 'ui.register.submitted_success' => 'Registration submitted successfully! You will be notified when your account is approved.', + 'ui.register.go_to_login' => 'Go to Login', + + // Forgot Password Page + 'ui.forgot_password.title' => 'Forgot Password', + 'ui.forgot_password.reset_password' => 'Reset Password', + 'ui.forgot_password.instructions' => "Enter your username or email address and we'll send you a link to reset your password.", + 'ui.forgot_password.username_or_email' => 'Username or Email', + 'ui.forgot_password.username_or_email_help' => 'Enter the username or email address associated with your account.', + 'ui.forgot_password.send_reset_link' => 'Send Reset Link', + 'ui.forgot_password.remember_password' => 'Remember your password?', + 'ui.forgot_password.back_to_login' => 'Back to Login', + 'ui.forgot_password.sending' => 'Sending...', + 'ui.forgot_password.check_email_notice' => 'Please check your email inbox (and spam folder) for the reset link.', + 'ui.forgot_password.request_failed' => 'Failed to process request', + 'ui.forgot_password.reset_link_sent_if_exists' => 'If an account with that username or email exists, a password reset link has been sent.', + 'ui.password_reset_email.subject' => 'Password Reset Request - {system_name}', + 'ui.password_reset_email.header' => 'Password Reset Request', + 'ui.password_reset_email.greeting' => 'Hello {name},', + 'ui.password_reset_email.request_received' => 'We received a request to reset your password for your account on {system_name}.', + 'ui.password_reset_email.click_link_below' => 'To reset your password, please click the link below:', + 'ui.password_reset_email.click_button_below' => 'To reset your password, please click the button below:', + 'ui.password_reset_email.button' => 'Reset Your Password', + 'ui.password_reset_email.copy_link' => 'Or copy and paste this link into your browser:', + 'ui.password_reset_email.expires_in_hours' => 'This link will expire in {hours} hours.', + 'ui.password_reset_email.security_notes' => 'Security Notes:', + 'ui.password_reset_email.note_never_share' => 'Never share this link with anyone', + 'ui.password_reset_email.note_one_time' => 'This link can only be used once', + 'ui.password_reset_email.note_request_new' => 'If you need another reset link, request a new one from the login page', + 'ui.password_reset_email.if_not_requested' => 'If you did not request a password reset, you can safely ignore this email.', + 'ui.password_reset_email.password_unchanged_notice' => 'Your password will not be changed unless you click the link above and create a new password.', + 'ui.password_reset_email.best_regards' => 'Best regards,', + 'ui.password_reset_email.footer_automated' => 'This is an automated message from {system_name}', + 'ui.password_reset_email.footer_no_reply' => 'Please do not reply to this email.', + + // Reset Password Page + 'ui.reset_password.title' => 'Reset Password', + 'ui.reset_password.create_new_password' => 'Create New Password', + 'ui.reset_password.validating_token' => 'Validating reset token...', + 'ui.reset_password.invalid_or_expired_token' => 'Invalid or Expired Token', + 'ui.reset_password.invalid_token_help_line1' => 'This password reset link is invalid or has expired.', + 'ui.reset_password.invalid_token_help_line2' => 'Please request a new password reset link.', + 'ui.reset_password.enter_new_password' => 'Please enter your new password below.', + 'ui.reset_password.new_password' => 'New Password', + 'ui.reset_password.password_min_length_help' => 'Password must be at least 8 characters long.', + 'ui.reset_password.confirm_new_password' => 'Confirm New Password', + 'ui.reset_password.passwords_do_not_match' => 'Passwords do not match.', + 'ui.reset_password.request_new_reset_link' => 'Request New Reset Link', + 'ui.reset_password.resetting' => 'Resetting...', + 'ui.reset_password.redirect_notice' => 'You will be redirected to the login page in 3 seconds...', + 'ui.reset_password.success_reset_complete' => 'Password has been reset successfully. You can now log in with your new password.', + + // Profile Page + 'ui.profile.title' => 'Profile', + 'ui.profile.my_profile' => 'My Profile', + 'ui.profile.profile_information' => 'Profile Information', + 'ui.profile.username' => 'Username', + 'ui.profile.username_readonly_help' => 'Username cannot be changed', + 'ui.profile.real_name' => 'Real Name', + 'ui.profile.real_name_help' => 'Your real name as displayed in messages', + 'ui.profile.email_address' => 'Email Address', + 'ui.profile.email_help' => 'Email address (optional, not shown publicly)', + 'ui.profile.location' => 'Location', + 'ui.profile.location_placeholder' => 'City, State/Country', + 'ui.profile.location_help' => "Your location (shown in who's online)", + 'ui.profile.change_password' => 'Change Password', + 'ui.profile.current_password' => 'Current Password', + 'ui.profile.new_password' => 'New Password', + 'ui.profile.password_blank_keep_help' => 'Leave password fields blank to keep current password', + 'ui.profile.account_created' => 'Account created', + 'ui.profile.last_login' => 'Last login', + 'ui.profile.never' => 'Never', + 'ui.profile.update_profile' => 'Update Profile', + 'ui.profile.system_information' => 'System Information', + 'ui.profile.system' => 'System', + 'ui.profile.address' => 'Address', + 'ui.profile.role' => 'Role', + 'ui.profile.role_administrator' => 'Administrator', + 'ui.profile.role_user' => 'User', + 'ui.profile.activity_summary' => 'Activity Summary', + 'ui.profile.credits' => 'Credits', + 'ui.profile.disabled' => 'Disabled', + 'ui.profile.netmail_sent' => 'Netmail Sent', + 'ui.profile.echomail_posted' => 'Echomail Posted', + 'ui.profile.total_messages' => 'Total Messages', + 'ui.profile.credit_system' => 'Credit System', + 'ui.profile.credit_costs_rewards' => 'Credit costs and rewards:', + 'ui.profile.daily_login' => 'Daily Login', + 'ui.profile.crashmail' => 'Crashmail', + 'ui.profile.credit_note' => 'Web doors and other system elements may have additional costs or rewards.', + 'ui.profile.updating_profile' => 'Updating profile...', + 'ui.profile.updated_successfully' => 'Profile updated successfully!', + + // Public User Profile Page + 'ui.user_profile.title_with_username' => "{username}'s Profile", + 'ui.user_profile.edit_my_profile' => 'Edit My Profile', + 'ui.user_profile.user_information' => 'User Information', + 'ui.user_profile.username' => 'Username', + 'ui.user_profile.real_name' => 'Real Name', + 'ui.user_profile.not_specified' => 'Not specified', + 'ui.user_profile.location' => 'Location', + 'ui.user_profile.fidonet_address' => 'FidoNet Address', + 'ui.user_profile.member_since' => 'Member Since', + 'ui.user_profile.last_seen' => 'Last Seen', + 'ui.user_profile.role' => 'Role', + 'ui.user_profile.role_administrator' => 'Administrator', + 'ui.user_profile.role_user' => 'User', + 'ui.user_profile.credits' => 'Credits', + 'ui.user_profile.current_balance' => 'Current Balance', + 'ui.user_profile.send_credits' => 'Send Credits', + 'ui.user_profile.amount' => 'Amount', + 'ui.user_profile.send_credits_limit_fee' => 'Max 200 credits per transaction. {fee_percent}% fee applies.', + 'ui.user_profile.message_optional' => 'Message (optional)', + 'ui.user_profile.message_optional_placeholder' => "What's this for?", + 'ui.user_profile.send_credits_fee_note' => '{fee_percent}% transaction fee distributed to system operators', + 'ui.user_profile.activity_summary' => 'Activity Summary', + 'ui.user_profile.netmail_sent' => 'Netmail Sent', + 'ui.user_profile.echomail_posted' => 'Echomail Posted', + 'ui.user_profile.total_messages' => 'Total Messages', + 'ui.user_profile.additional_info_private' => 'Additional profile information is private', + 'ui.user_profile.transaction_history' => 'Transaction History', + 'ui.user_profile.admin_view' => 'Admin View', + 'ui.user_profile.date' => 'Date', + 'ui.user_profile.type' => 'Type', + 'ui.user_profile.description' => 'Description', + 'ui.user_profile.balance' => 'Balance', + 'ui.user_profile.load_more_transactions' => 'Load More Transactions', + 'ui.user_profile.no_transactions_yet' => 'No transactions yet', + 'ui.user_profile.no_more_transactions' => 'No more transactions', + 'ui.user_profile.load_transactions_failed' => 'Failed to load transactions', + 'ui.user_profile.sending' => 'Sending...', + 'ui.user_profile.send_credits_success' => 'Successfully sent {symbol}{amount} to {username}! (Fee: {symbol}{fee}, They received: {symbol}{received})', + 'ui.user_profile.transaction_type.daily_login' => 'Daily Login', + 'ui.user_profile.transaction_type.system_reward' => 'System Reward', + 'ui.user_profile.transaction_type.payment' => 'Payment', + 'ui.user_profile.transaction_type.admin_adjustment' => 'Admin Adjustment', + 'ui.user_profile.transaction_type.unknown' => 'Unknown', + + // Shared Message Page + 'ui.shared_message.title' => 'Shared Message', + 'ui.shared_message.loading' => 'Loading shared message...', + 'ui.shared_message.login_to_reply' => 'Login to Reply', + 'ui.shared_message.unable_to_load' => 'Unable to Load Message', + 'ui.shared_message.go_to_main_site' => 'Go to Main Site', + 'ui.shared_message.login_required' => 'Login Required', + 'ui.shared_message.login_required_help' => 'This shared message requires you to be logged in to view it.', + 'ui.shared_message.about_this_message' => 'About This Message', + 'ui.shared_message.about_text' => 'This message is from an echomail system. Echomail is part of a worldwide bulletin board network that allows users to send messages and participate in discussion areas (echoareas) across different systems.', + 'ui.shared_message.join_conversation' => 'Want to Join the Conversation?', + 'ui.shared_message.join_conversation_help' => "To view more messages, participate in discussions, and reply to posts like this one, you'll need to register for an account on this system.", + 'ui.shared_message.register_now' => 'Register Now', + 'ui.shared_message.already_have_account' => 'Already Have Account?', + 'ui.shared_message.network' => 'Network', + 'ui.shared_message.powered_by' => 'Powered by', + 'ui.shared_message.shared_on' => 'This message was shared', + 'ui.shared_message.shared_by' => 'Shared by {name}', + 'ui.shared_message.viewed_times' => 'Viewed {count} times', + 'ui.shared_message.local_bulletin_board' => 'Local Bulletin Board', + 'ui.shared_message.from_label' => 'From:', + 'ui.shared_message.to_label' => 'To:', + 'ui.shared_message.date_label' => 'Date:', + 'ui.shared_message.subject_label' => 'Subject:', + 'ui.shared_message.kludge_lines' => 'Kludge Lines', + 'ui.shared_message.show_kludge_lines' => 'Show Kludge Lines', + 'ui.shared_message.load_failed' => 'Failed to load shared message', + 'ui.shared_message.not_available' => 'This shared message is not available. It may have expired or been revoked.', + 'ui.shared_message.load_failed_retry' => 'Failed to load the shared message. Please try again later.', + + // Netmail Page + 'ui.netmail.title' => 'Netmail', + 'ui.netmail.compose' => 'Compose', + 'ui.netmail.messages' => 'Messages', + 'ui.netmail.delete_selected' => 'Delete Selected', + 'ui.netmail.quick_stats' => 'Quick Stats', + + // Address Book + 'ui.address_book.title' => 'Address Book', + 'ui.address_book.search_placeholder' => 'Search addresses...', + 'ui.address_book.entries' => 'entries', + 'ui.address_book.add_entry' => 'Add Address Book Entry', + 'ui.address_book.edit_entry' => 'Edit Address Book Entry', + 'ui.address_book.no_entries_found' => 'No entries found', + 'ui.address_book.unnamed' => 'Unnamed', + 'ui.address_book.no_address' => 'No address', + 'ui.address_book.user_id' => 'User ID', + 'ui.address_book.node_address' => 'Node Address', + 'ui.address_book.email_address' => 'Email Address', + 'ui.address_book.name_placeholder' => 'Enter a descriptive name (e.g., John Smith)', + 'ui.address_book.name_help' => 'Descriptive name for this contact', + 'ui.address_book.user_id_placeholder' => 'Enter the user ID for messaging', + 'ui.address_book.user_id_help' => 'User ID/handle to use when sending messages', + 'ui.address_book.node_address_help' => 'Format: zone:net/node or zone:net/node.point', + 'ui.address_book.email_help' => 'For your reference only - not used for messaging', + 'ui.address_book.description_placeholder' => 'Notes about this contact...', + 'ui.address_book.always_crashmail' => 'Always use crashmail for this Recipient', + 'ui.address_book.always_crashmail_help' => 'Automatically enable crashmail when composing messages to this contact.', + + // Echomail Page + 'ui.echomail.title' => 'Echomail', + 'ui.echomail.post_message' => 'Post Message', + 'ui.echomail.viewing_prefix' => 'Viewing:', + 'ui.echomail.viewing_all' => 'Viewing: All Messages', + 'ui.echomail.echo_list' => 'Echo List', + 'ui.echomail.search_areas_placeholder' => 'Search areas...', + 'ui.echomail.loading_areas' => 'Loading areas...', + 'ui.echomail.recent_messages' => 'Recent Messages', + 'ui.echomail.to_me' => 'To Me', + 'ui.echomail.saved_items' => 'Saved Items', + 'ui.echomail.echo_areas' => 'Echo Areas', + 'ui.echomail.areas' => 'Areas', + 'ui.echomail.manage_subscriptions' => 'Manage Subscriptions', + 'ui.echomail.share_message' => 'Share Message', + 'ui.echomail.share_message_help' => 'Share this echomail message with others via a web link.', + 'ui.echomail.allow_anonymous_access' => 'Allow anonymous access', + 'ui.echomail.allow_anonymous_access_help' => 'Anyone with the link can view without logging in', + 'ui.echomail.link_expires_after' => 'Link expires after:', + 'ui.echomail.never_expires' => 'Never expires', + 'ui.echomail.share_link_created_success' => 'Share link created successfully! You can now copy and share this URL.', + 'ui.echomail.create_share_link' => 'Create Share Link', + 'ui.echomail.get_friendly_url' => 'Get Friendly URL', + 'ui.echomail.revoke_share' => 'Revoke Share', + 'ui.echomail.messages_refreshed' => 'Messages refreshed', + 'ui.echomail.saved_items.removed' => 'Message removed from saved items', + 'ui.echomail.saved_items.saved' => 'Message saved for later', + 'ui.echomail.shares.using_existing' => 'Using existing share link', + 'ui.echomail.shares.created_success' => 'Share link created successfully!', + 'ui.echomail.shares.friendly_url_generated' => 'Friendly URL generated!', + 'ui.echomail.shares.revoked' => 'Share link revoked', + 'ui.echomail.shares.url_copied' => 'Share URL copied to clipboard!', + + // Files Page + 'ui.files.title' => 'File Areas', + 'ui.files.security_notice_label' => 'Security Notice:', + 'ui.files.security_notice_text' => 'You are responsible for ensuring files you download are safe. Use appropriate malware protection software.', + 'ui.files.recent_uploads' => 'Recent Uploads', + 'ui.files.upload_file' => 'Upload File', + 'ui.files.search_placeholder' => 'Search files...', + 'ui.files.loading_recent_uploads' => 'Loading recent uploads...', + 'ui.files.statistics' => 'Statistics', + 'ui.files.total_areas' => 'Total Areas', + 'ui.files.total_files' => 'Total Files', + 'ui.files.total_size' => 'Total Size', + 'ui.files.file_details' => 'File Details', + 'ui.files.filename' => 'Filename', + 'ui.files.size' => 'Size', + 'ui.files.uploaded' => 'Uploaded', + 'ui.files.from' => 'From', + 'ui.files.virus_scan' => 'Virus Scan', + 'ui.files.download' => 'Download', + 'ui.files.share_file' => 'Share File', + 'ui.files.link_expiry' => 'Link Expiry', + 'ui.files.share_link' => 'Share Link', + 'ui.files.revoke_link' => 'Revoke Link', + 'ui.files.create_share_link' => 'Create Share Link', + 'ui.files.select_file_required' => 'Select File *', + 'ui.files.maximum_file_size' => 'Maximum file size', + 'ui.files.short_description_required' => 'Short Description *', + 'ui.files.short_description_help' => 'Brief description shown in file listings.', + 'ui.files.long_description' => 'Long Description', + 'ui.files.long_description_help' => 'Optional extended description (supports plain text).', + 'ui.files.upload' => 'Upload', + 'ui.files.recent_uploads_load_failed' => 'Failed to load recent uploads', + 'ui.files.no_recent_uploads' => 'No files have been uploaded yet', + 'ui.files.area' => 'Area', + 'ui.files.actions' => 'Actions', + 'ui.files.virus_scan_now' => 'Virus Scan', + 'ui.files.scanning' => 'Scanning…', + 'ui.files.scan_failed' => 'Virus scan failed', + 'ui.files.virus_scan_clean' => 'Virus scan: Clean', + 'ui.files.virus_detected_prefix' => 'Virus detected: ', + 'ui.files.virus_scan_error' => 'Virus scan: Error', + 'ui.files.virus_scan_skipped' => 'Virus scan: Skipped', + 'ui.files.shared' => 'Shared', + 'ui.files.file_areas_load_failed' => 'Failed to load file areas', + 'ui.files.no_file_areas' => 'No file areas available', + 'ui.files.search_areas_placeholder' => 'Search areas…', + 'ui.files.files_count_label' => 'files', + 'ui.files.loading_files' => 'Loading files...', + 'ui.files.load_files_failed' => 'Failed to load files', + 'ui.files.no_files_in_area' => 'No files in this area', + 'ui.files.not_scanned' => 'Not scanned', + 'ui.files.clean' => 'Clean', + 'ui.files.infected' => 'Infected', + 'ui.files.scan_error' => 'Scan Error', + 'ui.files.skipped' => 'Skipped', + 'ui.files.no_description' => 'No description', + 'ui.files.load_details_failed' => 'Failed to load file details', + 'ui.files.select_area_first' => 'Please select a file area first', + 'ui.files.select_file_prompt' => 'Please select a file', + 'ui.files.upload_success' => 'File uploaded successfully', + 'ui.files.delete_confirm' => 'Are you sure you want to delete "{filename}"? This action cannot be undone.', + 'ui.files.delete_success' => 'File deleted successfully', + 'ui.files.rename' => 'Rename', + 'ui.files.rename_file' => 'Rename File', + 'ui.files.new_filename' => 'New Filename', + 'ui.files.rename_success' => 'File renamed successfully', + 'ui.files.edit' => 'Edit', + 'ui.files.edit_file' => 'Edit File', + 'ui.files.short_description' => 'Short Description', + 'ui.files.edit_success' => 'File updated successfully', + 'ui.files.previous_file' => 'Previous file', + 'ui.files.next_file' => 'Next file', + 'ui.files.move_to_area' => 'Move to Area', + 'ui.files.active_share_exists' => 'This file already has an active share link.', + 'ui.files.revoke_confirm' => 'Are you sure you want to revoke this share link? Anyone with the link will no longer be able to access it.', + 'ui.files.share_revoked' => 'Share link revoked', + 'ui.files.share_link_copied_clipboard' => 'Share link copied to clipboard', + 'ui.files.share_link_copied' => 'Share link copied', + + // Polls Page + 'ui.polls.title' => 'Polls', + 'ui.polls.create' => 'Create Poll', + 'ui.polls.loading' => 'Loading poll...', + 'ui.polls.description' => 'Browse active polls and vote to reveal results.', + 'ui.polls.previous' => 'Previous poll', + 'ui.polls.next' => 'Next poll', + 'ui.polls.create.title' => 'Create Poll', + 'ui.polls.create.heading' => 'Create New Poll', + 'ui.polls.create.cost_badge' => 'Cost: {cost} credits', + 'ui.polls.create.insufficient_credits' => 'Insufficient Credits', + 'ui.polls.create.insufficient_credits_help' => 'You need {cost} credits to create a poll, but you only have {balance} credits.', + 'ui.polls.create.cost_info_prefix' => 'Creating a poll costs', + 'ui.polls.create.current_balance' => 'Your current balance:', + 'ui.polls.create.credits' => 'credits', + 'ui.polls.create.question_required' => 'Poll Question *', + 'ui.polls.create.question_placeholder' => 'What would you like to ask?', + 'ui.polls.create.characters_minimum' => 'characters (minimum 10)', + 'ui.polls.create.options_required' => 'Poll Options * (2-10)', + 'ui.polls.create.option_placeholder' => 'Enter option', + 'ui.polls.create.add_option' => 'Add Option', + 'ui.polls.create.options_help' => 'Each option must be unique and under 200 characters', + 'ui.polls.create.note_label' => 'Note:', + 'ui.polls.create.note_text' => 'Once created, polls cannot be edited or deleted. Make sure your question and options are correct before submitting.', + 'ui.polls.create.submit' => 'Create Poll ({cost} credits)', + 'ui.polls.create.max_options_allowed' => 'Maximum 10 options allowed', + 'ui.polls.create.question_min_length' => 'Question must be at least 10 characters', + 'ui.polls.create.min_options_required' => 'Please provide at least 2 options', + 'ui.polls.create.options_unique_required' => 'All options must be unique', + 'ui.polls.create.creating' => 'Creating...', + 'ui.polls.create.created_success_spent' => 'Poll created successfully! You spent {spent} credits.', + 'ui.polls.create.error_prefix' => 'Error: ', + + // Shoutbox Page + 'ui.shoutbox.title' => 'Shoutbox', + 'ui.shoutbox.loading' => 'Loading shouts...', + 'ui.shoutbox.load_older' => 'Load older shouts', + 'ui.shoutbox.leave_shout' => 'Leave a shout...', + 'ui.shoutbox.post' => 'Post', + 'ui.shoutbox.max_chars' => '280 characters max.', + 'ui.admin.shoutbox.page_title' => 'Shoutbox', + 'ui.admin.shoutbox.heading' => 'Shoutbox Moderation', + 'ui.admin.shoutbox.user' => 'User', + 'ui.admin.shoutbox.message' => 'Message', + 'ui.admin.shoutbox.status' => 'Status', + 'ui.admin.shoutbox.created' => 'Created', + 'ui.admin.shoutbox.actions' => 'Actions', + 'ui.admin.shoutbox.error_loading_shouts' => 'Error loading shouts', + 'ui.admin.shoutbox.hidden' => 'Hidden', + 'ui.admin.shoutbox.visible' => 'Visible', + 'ui.admin.shoutbox.unhide' => 'Unhide', + 'ui.admin.shoutbox.hide' => 'Hide', + 'ui.admin.shoutbox.hidden_success' => 'Shout hidden successfully', + 'ui.admin.shoutbox.unhidden_success' => 'Shout unhidden successfully', + 'ui.admin.shoutbox.deleted_success' => 'Shout deleted successfully', + 'ui.admin.shoutbox.failed_to_update' => 'Failed to update shout', + 'ui.admin.shoutbox.failed_to_delete' => 'Failed to delete shout', + 'ui.admin.shoutbox.confirm_delete' => 'Delete this shout?', + + // Chat Page + 'ui.chat.page_title' => 'Chat', + 'ui.chat.heading' => 'Chat', + 'ui.chat.rooms' => 'Rooms', + 'ui.chat.direct' => 'Direct', + 'ui.chat.lobby' => 'Lobby', + 'ui.chat.load_older_messages' => 'Load older messages', + 'ui.chat.type_message_placeholder' => 'Type a message...', + 'ui.chat.kick' => 'Kick', + 'ui.chat.ban' => 'Ban', + 'ui.chat.no_one_online' => 'No one online', + 'ui.chat.room' => 'Room', + 'ui.chat.moderation_hint' => 'Right-click or click to moderate', + 'ui.chat.system' => 'System', + 'ui.chat.confirm_kick' => 'Kick this user from the room?', + 'ui.chat.confirm_ban' => 'Ban this user from the room?', + 'ui.chat.send_failed' => 'Failed to send message', + 'ui.chat.moderation_failed' => 'Moderation failed', + + // BinkP Page + 'ui.binkp.page_title' => 'Binkp Status', + 'ui.binkp.heading' => 'Binkp Status', + 'ui.binkp.subheading' => 'Monitor and manage your Binkp TCP/IP connections', + 'ui.binkp.system_status' => 'System Status', + 'ui.binkp.inbound_queue' => 'Inbound Queue', + 'ui.binkp.outbound_queue' => 'Outbound Queue', + 'ui.binkp.uplinks' => 'Uplinks', + 'ui.binkp.status_tab' => 'Status', + 'ui.binkp.uplinks_tab' => 'Uplinks', + 'ui.binkp.queues_tab' => 'Queues', + 'ui.binkp.logs_tab' => 'Logs', + 'ui.binkp.system_information' => 'System Information', + 'ui.binkp.loading_system_information' => 'Loading system information...', + 'ui.binkp.uplink_status' => 'Uplink Status', + 'ui.binkp.loading_uplink_status' => 'Loading uplink status...', + 'ui.binkp.poll_all' => 'Poll All', + 'ui.binkp.configured_uplinks' => 'Configured Uplinks', + 'ui.binkp.uplink' => 'Uplink', + 'ui.binkp.status' => 'Status', + 'ui.binkp.schedule' => 'Schedule', + 'ui.binkp.actions' => 'Actions', + 'ui.binkp.address' => 'Address', + 'ui.binkp.sysop' => 'Sysop', + 'ui.binkp.location' => 'Location', + 'ui.binkp.process' => 'Process', + 'ui.binkp.send' => 'Send', + 'ui.binkp.poll' => 'Poll', + 'ui.binkp.enabled' => 'Enabled', + 'ui.binkp.disabled' => 'Disabled', + 'ui.binkp.online' => 'Online', + 'ui.binkp.offline' => 'Offline', + 'ui.binkp.no_uplinks_configured' => 'No uplinks configured', + 'ui.binkp.error_loading_status_prefix' => 'Error loading status:', + 'ui.binkp.error_loading_uplinks_prefix' => 'Error loading uplinks:', + 'ui.binkp.file_count' => '{count} files', + 'ui.binkp.pending' => 'Pending', + 'ui.binkp.errors' => 'Errors', + 'ui.binkp.stdout' => 'STDOUT', + 'ui.binkp.stderr' => 'STDERR', + 'ui.binkp.http_status_error' => 'HTTP {code}: {text}', + 'ui.binkp.inbound_api_http_error' => 'Inbound API: HTTP {code}', + 'ui.binkp.outbound_api_http_error' => 'Outbound API: HTTP {code}', + 'ui.binkp.zero_bytes' => '0 Bytes', + 'ui.binkp.byte_units.bytes' => 'Bytes', + 'ui.binkp.byte_units.kb' => 'KB', + 'ui.binkp.byte_units.mb' => 'MB', + 'ui.binkp.byte_units.gb' => 'GB', + 'ui.binkp.poll_schedule_cron' => 'Poll Schedule (cron format)', + 'ui.binkp.poll_schedule_placeholder' => '0 */4 * * *', + 'ui.binkp.logs_heading' => 'Binkp Logs', + 'ui.binkp.lines_option' => '{count} lines', + 'ui.binkp.loading_logs' => 'Loading logs...', + 'ui.binkp.add_new_uplink' => 'Add New Uplink', + 'ui.binkp.ftn_address_required' => 'FTN Address *', + 'ui.binkp.ftn_address_placeholder' => '1:123/456', + 'ui.binkp.hostname_required' => 'Hostname *', + 'ui.binkp.hostname_placeholder' => 'bbs.example.com', + 'ui.binkp.default_every_4_hours' => 'Default: every 4 hours', + 'ui.binkp.add_uplink' => 'Add Uplink', + 'ui.binkp.configured_count' => '{count} configured', + 'ui.binkp.message_count' => '{count} msgs', + 'ui.binkp.poll_completed_exit' => 'Poll completed (exit {code})', + 'ui.binkp.poll_failed_prefix' => 'Poll failed: ', + 'ui.binkp.error_polling_uplink_prefix' => 'Error polling uplink: ', + 'ui.binkp.all_uplinks_polled_exit' => 'All uplinks polled (exit {code})', + 'ui.binkp.polling_failed_prefix' => 'Polling failed: ', + 'ui.binkp.error_polling_uplinks_prefix' => 'Error polling uplinks: ', + 'ui.binkp.inbound_processing_completed' => 'Inbound processing completed', + 'ui.binkp.processing_failed_prefix' => 'Processing failed: ', + 'ui.binkp.error_processing_inbound_prefix' => 'Error processing inbound: ', + 'ui.binkp.outbound_processing_completed' => 'Outbound processing completed', + 'ui.binkp.error_processing_outbound_prefix' => 'Error processing outbound: ', + 'ui.binkp.uplink_added_success' => 'Uplink added successfully', + 'ui.binkp.add_uplink_failed_prefix' => 'Failed to add uplink: ', + 'ui.binkp.error_adding_uplink_prefix' => 'Error adding uplink: ', + 'ui.binkp.remove_uplink_confirm' => 'Remove uplink {address}?', + 'ui.binkp.uplink_removed_success' => 'Uplink removed successfully', + 'ui.binkp.remove_uplink_failed_prefix' => 'Failed to remove uplink: ', + 'ui.binkp.error_removing_uplink_prefix' => 'Error removing uplink: ', + 'ui.binkp.packet_processing_failed_prefix' => 'Packet processing failed: ', + 'ui.binkp.packet_processing_completed_exit' => 'Packet processing completed (exit {code})', + 'ui.binkp.error_processing_packets_prefix' => 'Error processing packets: ', + 'ui.binkp.poll_failed' => 'Poll failed', + 'ui.binkp.polling_failed' => 'Polling failed', + 'ui.binkp.processing_failed' => 'Processing failed', + 'ui.binkp.add_uplink_failed' => 'Failed to add uplink', + 'ui.binkp.remove_uplink_failed' => 'Failed to remove uplink', + 'ui.binkp.unknown_error' => 'Unknown error', + + // Echolist Page + 'ui.echolist.search_min_chars' => 'Please enter at least 2 characters to search', + 'ui.echolist.page_title' => 'Echo Areas', + 'ui.echolist.heading' => 'Echo List', + 'ui.echolist.filter_heading' => 'Filter Echo Areas', + 'ui.echolist.show_subscribed_only' => 'Show only subscribed areas', + 'ui.echolist.show_unread_only' => 'Show only areas with unread messages', + 'ui.echolist.area_filter_placeholder' => 'Type to filter by name or description...', + 'ui.echolist.area_filter_help' => 'Filters the list below in real-time', + 'ui.echolist.search_heading' => 'Search Messages', + 'ui.echolist.search_placeholder' => 'Search message content...', + 'ui.echolist.search_help' => 'Search all echomail message content', + 'ui.echolist.loading' => 'Loading echos...', + 'ui.echolist.stats.total_echos' => 'Total echos', + 'ui.echolist.stats.total_messages' => 'Total Messages', + 'ui.echolist.stats.networks' => 'Networks', + 'ui.echolist.stats.recent_messages' => '24h Messages', + 'ui.echolist.local' => 'Local', + 'ui.echolist.unknown' => 'Unknown', + 'ui.echolist.load_failed' => 'Failed to load echo areas', + 'ui.echolist.none_available' => 'No echo areas available.', + 'ui.echolist.new_post' => 'New Post', + 'ui.echolist.new_messages' => 'New Messages', + 'ui.echolist.local_areas' => 'Local Areas', + 'ui.echolist.lovlynet_network' => 'LOVLYNET Network', + 'ui.echolist.network_suffix' => 'Network', + 'ui.echolist.area_count' => 'area', + 'ui.echolist.plural_suffix' => 's', + 'ui.echolist.sysop_badge' => 'Sysop', + 'ui.echolist.no_description' => 'No description', + 'ui.echolist.moderator_prefix' => 'Moderator:', + 'ui.echolist.unread_of_posts' => '{unread} unread of {total} posts', + 'ui.echolist.post_count' => '{total} posts', + 'ui.echolist.by_author' => 'by {author}', + 'ui.echolist.time.never' => 'Never', + 'ui.echolist.time.just_now' => 'Just now', + 'ui.echolist.time.minutes_ago' => '{count}m ago', + 'ui.echolist.time.hours_ago' => '{count}h ago', + 'ui.echolist.time.days_ago' => '{count}d ago', + + // Nodelist Page + 'ui.nodelist.node_details' => 'Node Details', + 'ui.nodelist.back_to_list' => 'Back to List', + 'ui.nodelist.system_information' => 'System Information', + 'ui.nodelist.system_name' => 'System Name', + 'ui.nodelist.sysop' => 'Sysop', + 'ui.nodelist.location' => 'Location', + 'ui.nodelist.phone' => 'Phone', + 'ui.nodelist.baud_rate' => 'Baud Rate', + 'ui.nodelist.not_specified' => 'Not specified', + 'ui.nodelist.unpublished' => 'Unpublished', + 'ui.nodelist.address_components' => 'Address Components', + 'ui.nodelist.zone' => 'Zone', + 'ui.nodelist.net' => 'Net', + 'ui.nodelist.node' => 'Node', + 'ui.nodelist.point' => 'Point', + 'ui.nodelist.full' => 'Full', + 'ui.nodelist.capabilities_flags' => 'Node Capabilities & Flags', + 'ui.nodelist.quick_actions' => 'Quick Actions', + 'ui.nodelist.send_netmail' => 'Send Netmail', + 'ui.nodelist.login_to_send_netmail' => 'Login to Send Netmail', + 'ui.nodelist.file_request' => 'File Request', + 'ui.nodelist.connection_information' => 'Connection Information', + 'ui.nodelist.internet_binkp' => 'Internet (BinkP)', + 'ui.nodelist.internet_telnet' => 'Internet (Telnet)', + 'ui.nodelist.dialup_pots' => 'Dialup (POTS)', + 'ui.nodelist.no_connection_info' => 'No connection information available', + 'ui.nodelist.continuous_mail' => 'Continuous Mail (24/7)', + 'ui.nodelist.mail_on_hold' => 'Mail on Hold', + 'ui.nodelist.system_down' => 'System Down', + 'ui.nodelist.http' => 'HTTP', + 'ui.nodelist.telnet' => 'Telnet', + 'ui.nodelist.ssh' => 'SSH', + 'ui.nodelist.file_request_coming_soon' => 'File request feature coming soon!', + 'ui.nodelist.index.heading' => 'Node List Browser', + 'ui.nodelist.index.import_nodelist' => 'Import Nodelist', + 'ui.nodelist.index.nodes' => 'nodes', + 'ui.nodelist.index.zones' => 'zones', + 'ui.nodelist.index.nets' => 'nets', + 'ui.nodelist.index.points' => 'points', + 'ui.nodelist.index.special' => 'special', + 'ui.nodelist.index.last_imported' => 'Last imported:', + 'ui.nodelist.index.search_placeholder' => 'Node address (2:5034/10), sysop, location, or system name', + 'ui.nodelist.index.all_zones' => 'All Zones', + 'ui.nodelist.index.zone_prefix' => 'Zone', + 'ui.nodelist.index.all_nets' => 'All Nets', + 'ui.nodelist.index.net_prefix' => 'Net', + 'ui.nodelist.index.search_results' => 'Search Results ({count} nodes)', + 'ui.nodelist.index.address' => 'Address', + 'ui.nodelist.index.type' => 'Type', + 'ui.nodelist.index.phone_host' => 'Phone/Host', + 'ui.nodelist.index.speed' => 'Speed', + 'ui.nodelist.index.actions' => 'Actions', + 'ui.nodelist.index.no_nodes_found_search' => 'No nodes found matching your search criteria.', + 'ui.nodelist.index.use_search_form' => 'Use the search form above to find nodes in the nodelist.', + 'ui.nodelist.index.error_fetching_nets' => 'Error fetching nets:', + 'ui.nodelist.import.heading' => 'Import Nodelist', + 'ui.nodelist.import.upload_new_nodelist' => 'Upload New Nodelist', + 'ui.nodelist.import.network_domain' => 'Network Domain', + 'ui.nodelist.import.domain_placeholder' => 'e.g., fidonet, fsxnet, agoranet', + 'ui.nodelist.import.domain_title' => 'Domain should contain only letters, numbers, underscores, and hyphens', + 'ui.nodelist.import.domain_help_1' => 'The FTN network domain this nodelist belongs to (e.g., fidonet, fsxnet).', + 'ui.nodelist.import.domain_help_2' => 'This is used to distinguish nodes from different networks.', + 'ui.nodelist.import.nodelist_file' => 'Nodelist File', + 'ui.nodelist.import.nodelist_help_1' => 'Upload a FidoNet nodelist file (NODELIST.xxx format).', + 'ui.nodelist.import.nodelist_help_2' => 'Supported: Plain text (.txt, .lst, .nodelist) and ZIP compressed (.Zxxx)', + 'ui.nodelist.import.archive_before_import' => 'Archive current nodelist before importing', + 'ui.nodelist.import.archive_help' => 'Recommended: Keep the current nodelist as inactive backup before importing the new one.', + 'ui.nodelist.import.warning' => 'Warning:', + 'ui.nodelist.import.warning_text_1' => 'Importing a new nodelist will replace the current active nodelist data for this domain.', + 'ui.nodelist.import.warning_text_2' => 'This operation cannot be undone. Make sure you have a backup if needed.', + 'ui.nodelist.import.import_nodelist' => 'Import Nodelist', + 'ui.nodelist.import.current_status' => 'Current Nodelist Status', + 'ui.nodelist.import.file' => 'File:', + 'ui.nodelist.import.day_of_year' => 'Day of Year:', + 'ui.nodelist.import.release_date' => 'Release Date:', + 'ui.nodelist.import.total_nodes' => 'Total Nodes:', + 'ui.nodelist.import.imported' => 'Imported:', + 'ui.nodelist.import.no_active_nodelist' => 'No active nodelist found.', + 'ui.nodelist.import.node_statistics' => 'Node Statistics', + 'ui.nodelist.import.total_nodes_label' => 'Total Nodes', + 'ui.nodelist.import.guidelines' => 'Import Guidelines', + 'ui.nodelist.import.guideline_1' => 'Standard FTS-0005 format', + 'ui.nodelist.import.guideline_2' => 'ASCII text with CR/LF endings', + 'ui.nodelist.import.guideline_3' => 'Proper header with CRC', + 'ui.nodelist.import.guideline_4' => 'Zone/Net/Node hierarchy', + 'ui.nodelist.import.guideline_5' => 'ZIP compressed files (.Zxxx)', + 'ui.nodelist.import.guideline_note' => 'For ARC, ARJ, LZH, RAR files, use the command-line import script', + 'ui.nodelist.import.importing' => 'Importing...', + 'ui.nodelist.import.success' => 'Successfully imported {count} nodes from {filename} (Day {day}) for domain @{domain}', + 'ui.dosdoor_player.page_title' => 'DOS Door Player', + 'ui.dosdoor_player.document_title_suffix' => 'DOS Door', + 'ui.dosdoor_player.status_prefix' => 'Status:', + 'ui.dosdoor_player.status_disconnected' => 'Disconnected', + 'ui.dosdoor_player.status_launching' => 'Launching...', + 'ui.dosdoor_player.status_launch_failed' => 'Launch failed', + 'ui.dosdoor_player.status_connecting' => 'Connecting...', + 'ui.dosdoor_player.status_connected' => 'Connected', + 'ui.dosdoor_player.status_connection_error' => 'Connection error', + 'ui.dosdoor_player.status_error' => 'Error', + 'ui.dosdoor_player.end_session' => 'End Session', + 'ui.dosdoor_player.launching_door_line' => 'Launching door game...', + 'ui.dosdoor_player.failed_launch_line' => 'Failed to launch door session.', + 'ui.dosdoor_player.connecting_to_prefix' => 'Connecting to', + 'ui.dosdoor_player.connected_line' => 'Connected!', + 'ui.dosdoor_player.connection_closed_line' => '[Connection closed]', + 'ui.dosdoor_player.connection_error_line' => '[Connection error]', + 'ui.dosdoor_player.failed_to_connect_prefix' => 'Failed to connect:', + 'ui.dosdoor_player.confirm_end_session' => 'Are you sure you want to end this door session?', + 'ui.dosdoor_player.failed_end_session' => 'Failed to end session', + 'ui.dosdoor_player.error_ending_session' => 'Error ending session', + 'ui.dosdoor_player.error_no_door_specified' => 'Error: No door ID specified', + 'ui.dosdoor_player.failed_launch_door' => 'Failed to launch door', + + // Who's Online + 'ui.whos_online.page_title' => "Who's Online", + 'ui.whos_online.heading' => "Who's Online", + 'ui.whos_online.active_last_minutes' => 'Active in last {minutes} minutes', + 'ui.whos_online.online_users' => 'Online Users', + 'ui.whos_online.service' => 'Service', + 'ui.whos_online.activity' => 'Activity', + 'ui.whos_online.idle' => 'Idle', + 'ui.whos_online.no_users_online' => 'No users are online right now.', + + // Doors and Games + 'ui.webdoors.page_title' => 'Doors and Games', + 'ui.webdoors.heading' => 'Doors and Games', + 'ui.webdoors.description' => 'Access classic DOS door games and modern web-based games.', + 'ui.webdoors.top_scores_all_games' => 'Top Scores (All Games)', + 'ui.webdoors.leaderboard_month_navigation' => 'Leaderboard month navigation', + 'ui.webdoors.previous_month' => 'Previous month', + 'ui.webdoors.next_month' => 'Next month', + 'ui.webdoors.current_month' => 'Current month', + 'ui.webdoors.rank' => 'Rank', + 'ui.webdoors.player' => 'Player', + 'ui.webdoors.game' => 'Game', + 'ui.webdoors.board' => 'Board', + 'ui.webdoors.score' => 'Score', + 'ui.webdoors.date' => 'Date', + 'ui.webdoors.no_scores_for_month' => 'No scores recorded for {month}.', + 'ui.webdoors.players_count' => 'Players: {count}', + 'ui.webdoors.by_author' => 'by {author}', + 'ui.webdoors.version_by_author' => 'v{version} by {author}', + 'ui.webdoors.launch' => 'Launch', + 'ui.webdoors.no_games_available' => 'No games available yet. Check back later!', + 'ui.webdoors.errors.system_disabled' => 'Sorry, the game system is not enabled.', + 'ui.webdoors.errors.admin_only' => 'This door is restricted to administrators.', + 'ui.webdoors.errors.requirements_not_met' => 'This game requires features that are not currently enabled on this system.', + 'ui.webdoor_play.page_title_suffix' => 'Doors and Games', + 'ui.webdoor_play.back_to_doors' => 'Back to Doors', + 'ui.webdoor_play.fullscreen' => 'Fullscreen', + + // Shared File Page + 'ui.shared_file.shared_file' => 'Shared File', + 'ui.shared_file.meta_description_from_system' => 'File shared from {system_name}', + 'ui.shared_file.shared_by' => 'Shared by', + 'ui.shared_file.on_date_prefix' => 'on', + 'ui.shared_file.viewed_count' => 'viewed {count} time(s)', + 'ui.shared_file.expires_prefix' => 'expires', + 'ui.shared_file.clean' => 'Clean', + 'ui.shared_file.size' => 'Size:', + 'ui.shared_file.uploaded' => 'Uploaded:', + 'ui.shared_file.area' => 'Area:', + 'ui.shared_file.description' => 'Description:', + 'ui.shared_file.details' => 'Details:', + 'ui.shared_file.download' => 'Download', + 'ui.shared_file.browse_area' => 'Browse {tag} area', + 'ui.shared_file.join_system_to_download' => 'Join {system_name} to Download', + 'ui.shared_file.register_prompt' => 'Create a free account to download this file and access our full file archive.', + 'ui.shared_file.register' => 'Register', + 'ui.shared_file.login' => 'Login', + 'ui.shared_file.about_system' => 'About {system_name}', + 'ui.shared_file.about_system_text' => '{system_name} is a FidoNet-connected BBS with public file areas. Members can upload, download, and share files from a growing archive.', + 'ui.shared_file.powered_by' => 'Powered by BinktermPHP', + 'ui.shared_file.not_available_title' => 'File Not Available', + 'ui.shared_file.not_available_body' => 'This shared file link is not available. It may have expired or been revoked.', + 'ui.shared_file.go_to_main_site' => 'Go to Main Site', + + // Ads + 'ui.ads.advertisement' => 'Advertisement', + 'ui.ads.none_available' => 'No advertisements available.', + + // BBS Menu Shell + 'ui.bbs_menu.main_menu' => 'Main Menu', + 'ui.bbs_menu.press_key_to_navigate' => 'Press key to navigate', + 'ui.bbs_menu.quit_return' => 'Quit/Return', + 'ui.bbs_menu.tap_item_to_navigate' => 'Tap an item to navigate', + 'ui.bbs_menu.press_highlighted_key' => 'Press the highlighted key to navigate', + 'ui.bbs_menu.returns_to_menu' => 'returns to this menu', + 'ui.bbs_menu.tap_card_to_navigate' => 'Tap a card to navigate', + + // Recent Updates Fragment + 'ui.recent_updates.badge_feature' => 'Feature', + 'ui.recent_updates.badge_fix' => 'Fix', + 'ui.recent_updates.badge_improvement' => 'Improvement', + 'ui.recent_updates.badge_update' => 'Update', + + // House Rules Fragment + 'ui.rules.intro' => 'Be excellent to each other and help keep this system welcoming for everyone.', + 'ui.rules.item_1' => 'Keep it civil. No harassment, hate speech, or personal attacks.', + 'ui.rules.item_2' => 'No spam or flooding.', + 'ui.rules.item_3' => 'Respect privacy. Do not share private information without consent.', + 'ui.rules.item_4' => 'Follow Fidonet etiquette and local network policies.', + 'ui.rules.item_5' => 'Sysop decisions are final.', + + // Dashboard + 'ui.dashboard.title' => 'Dashboard', + 'ui.dashboard.unread_netmail' => 'Unread Netmail', + 'ui.dashboard.unread_echomail' => 'Unread Echomail', + 'ui.dashboard.system_news' => 'System News', + 'ui.dashboard.system_information' => 'System Information', + 'ui.dashboard.sysop' => 'Sysop', + 'ui.dashboard.user' => 'User', + 'ui.dashboard.addresses' => 'Addresses', + 'ui.dashboard.my_referral_link' => 'My Referral Link', + 'ui.dashboard.referral_invite_prefix' => 'Invite friends and earn', + 'ui.dashboard.referral_invite_suffix' => 'credits for each successful referral.', + 'ui.dashboard.referrals.label' => 'Referrals', + 'ui.dashboard.referrals.earned' => 'Earned', + 'ui.dashboard.voting_booth' => 'Voting Booth', + 'ui.dashboard.create_poll_title' => 'Create Poll', + 'ui.dashboard.polls.none_active' => 'No active polls right now.', + 'ui.dashboard.polls.load_failed' => 'Failed to load poll.', + 'ui.dashboard.polls.results' => 'Results', + 'ui.dashboard.polls.no_votes' => 'No votes yet.', + 'ui.dashboard.polls.vote_to_see_results' => 'Vote to see results', + 'ui.dashboard.polls.submit_vote' => 'Submit Vote', + 'ui.dashboard.echoareas.none_available' => 'No echo areas available', + 'ui.dashboard.shoutbox.none_yet' => 'No shouts yet. Be the first!', + 'ui.dashboard.shoutbox.load_failed' => 'Failed to load shouts.', + 'ui.dashboard.shoutbox.post_failed' => 'Failed to post shout.', + 'ui.dashboard.referrals.error_prefix' => 'Referral stats error:', + 'ui.dashboard.referrals.recent' => 'Recent Referrals', + 'ui.dashboard.referrals.copy_failed_prefix' => 'Failed to copy:', + 'ui.dashboard.echoareas.header_area' => 'Echo Area', + 'ui.dashboard.echoareas.header_unread_total' => 'Unread Total', + + // Settings + 'ui.settings.title' => 'Settings', + 'ui.settings.display_preferences' => 'Display Preferences', + 'ui.settings.messages_per_page' => 'Messages per Page', + 'ui.settings.messages_per_page_help' => 'Number of messages to display per page', + 'ui.settings.timezone' => 'Timezone', + 'ui.settings.timezone_help' => 'Your local timezone for displaying dates', + 'ui.settings.language' => 'Language', + 'ui.settings.language_help' => 'Preferred language for the interface', + 'ui.settings.date_format' => 'Date Format', + 'ui.settings.date_format_help' => 'Your preferred date and time format', + 'ui.settings.date_format.option.en_us' => 'American (MM/DD/YYYY) - Jan 31, 2026, 10:30 AM', + 'ui.settings.date_format.option.en_gb' => 'British (DD/MM/YYYY) - 31 Jan 2026, 10:30', + 'ui.settings.date_format.option.en_ca' => 'Canadian (YYYY-MM-DD) - 2026-01-31, 10:30 a.m.', + 'ui.settings.date_format.option.de_de' => 'German (DD.MM.YYYY) - 31.01.2026, 10:30', + 'ui.settings.date_format.option.fr_fr' => 'French (DD/MM/YYYY) - 31/01/2026 10:30', + 'ui.settings.date_format.option.es_es' => 'Spanish (DD/MM/YYYY) - 31/1/2026, 10:30', + 'ui.settings.date_format.option.it_it' => 'Italian (DD/MM/YYYY) - 31/01/2026, 10:30', + 'ui.settings.date_format.option.ja_jp' => 'Japanese (YYYY/MM/DD) - 2026/01/31 10:30', + 'ui.settings.date_format.option.zh_cn' => 'Chinese (YYYY/M/D) - 2026/1/31, 10:30', + 'ui.settings.date_format.option.sv_se' => 'Swedish (YYYY-MM-DD) - 2026-01-31 10:30', + 'ui.settings.theme' => 'Theme', + 'ui.settings.theme_help' => 'Choose your preferred color scheme', + 'ui.settings.interface_style' => 'Interface Style', + 'ui.settings.interface_style.web' => 'Web Interface (default navbar & menus)', + 'ui.settings.interface_style.bbs_menu' => 'BBS Menu (hotkey navigation)', + 'ui.settings.interface_style_help' => 'Choose your preferred navigation style.', + 'ui.settings.default_echo_list' => 'Default Echo Area List', + 'ui.settings.system_default' => 'System Default', + 'ui.settings.default_echo_list.reader' => 'Reader (Message List)', + 'ui.settings.default_echo_list.echolist' => 'Echo List (Grouped by Network)', + 'ui.settings.default_echo_list_help' => 'Choose your preferred default view when accessing echo areas from the menu', + 'ui.settings.message_font' => 'Message Font', + 'ui.settings.message_font_help' => 'Font family for displaying netmail and echomail messages', + 'ui.settings.message_font_size' => 'Message Font Size', + 'ui.settings.message_font_size_help' => 'Font size for displaying netmail and echomail messages', + 'ui.settings.message_signature' => 'Message Signature', + 'ui.settings.message_signature_placeholder' => 'Your signature (max 4 lines)', + 'ui.settings.message_signature_help' => 'Appended to outgoing netmail and echomail messages. Max 4 lines.', + 'ui.settings.default_tagline' => 'Default Tagline', + 'ui.settings.no_tagline' => 'No tagline', + 'ui.settings.random_tagline' => 'Random tagline', + 'ui.settings.default_tagline_help' => 'Used as the default tagline when composing messages. Choose "Random tagline" to pick one at random each time.', + 'ui.settings.threading_preferences' => 'Threading Preferences', + 'ui.settings.threaded_view_echomail' => 'Enable threaded view for echomail', + 'ui.settings.threaded_view_echomail_help' => 'Group echomail messages by conversation threads', + 'ui.settings.threaded_view_netmail' => 'Enable threaded view for netmail', + 'ui.settings.threaded_view_netmail_help' => 'Group netmail messages by conversation threads', + 'ui.settings.quote_display' => 'Quote Display', + 'ui.settings.quote_coloring' => 'Color quoted text by depth', + 'ui.settings.quote_coloring_help' => 'Show quoted message lines (starting with >) in different colors based on nesting level', + 'ui.settings.session_security' => 'Session & Security', + 'ui.settings.active_sessions' => 'Active Sessions', + 'ui.settings.active_sessions_help' => 'Manage your active login sessions', + 'ui.settings.view_sessions' => 'View Sessions', + 'ui.settings.security_actions' => 'Security Actions', + 'ui.settings.logout_all_help' => 'Logout from all devices', + 'ui.settings.logout_all' => 'Logout All', + 'ui.settings.save_settings' => 'Save Settings', + 'ui.settings.system_status' => 'System Status', + 'ui.settings.binkp_status' => 'BinkP Status', + 'ui.settings.online' => 'Online', + 'ui.settings.last_poll' => 'Last Poll', + 'ui.settings.messages_today' => 'Messages Today', + 'ui.settings.never' => 'Never', + 'ui.settings.quick_actions' => 'Quick Actions', + 'ui.settings.compose_netmail' => 'Compose Netmail', + 'ui.settings.post_echomail' => 'Post Echomail', + 'ui.settings.help_support' => 'Help & Support', + 'ui.settings.load_failed_console' => 'Failed to load settings', + 'ui.settings.saving' => 'Saving settings...', + 'ui.settings.saved_successfully' => 'Settings saved successfully!', + 'ui.settings.sessions.none_active' => 'No active sessions found.', + 'ui.settings.sessions.loading' => 'Loading sessions...', + 'ui.settings.sessions.load_failed' => 'Failed to load sessions', + 'ui.settings.sessions.current' => 'Current', + 'ui.settings.sessions.created' => 'Created', + 'ui.settings.sessions.expires' => 'Expires', + 'ui.settings.sessions.revoke' => 'Revoke', + 'ui.settings.sessions.revoke_confirm' => 'Are you sure you want to revoke this session?', + 'ui.settings.sessions.revoked_success' => 'Session revoked successfully', + 'ui.settings.sessions.logged_out_all_success' => 'Logged out from all sessions', + 'ui.settings.sessions.logout_all_confirm' => 'Are you sure you want to logout from all devices? You will need to login again.', + 'ui.settings.polling_uplinks' => 'Polling uplinks... (this may take a moment)', + 'ui.settings.poll_complete_prefix' => 'Uplink poll completed: ', + 'ui.settings.poll_complete_exit' => 'Uplink poll completed (exit {code})', + 'ui.settings.poll_failed_prefix' => 'Poll failed: ', + + // Compose / Drafts / Address Book + 'ui.compose.title_prefix' => 'Compose', + 'ui.compose.back_to_prefix' => 'Back to', + 'ui.compose.to_address_label' => 'To Address (leave blank for local)', + 'ui.compose.fidonet_address_help' => 'Fidonet address (e.g., 1:123/456)', + 'ui.compose.to_name_required' => 'To Name *', + 'ui.compose.attach_file' => 'Attach File', + 'ui.compose.attach_file_help_short' => 'requires crashmail; subject will be set to filename', + 'ui.compose.echo_area_required' => 'Echo Area *', + 'ui.compose.select_echo_area' => 'Select echo area...', + 'ui.compose.to_name' => 'To Name', + 'ui.compose.to_name_public_help' => "Leave as 'All' for public message", + 'ui.compose.cross_post_to_other_areas' => 'Cross-post to other areas', + 'ui.compose.cross_post_help_prefix' => 'Select additional areas to post this message to (max', + 'ui.compose.cross_post_help_suffix' => 'Each area receives an independent copy.', + 'ui.compose.stylecodes.inverse_title' => 'Inverse (#inverse#)', + 'ui.compose.stylecodes.active_prefix' => 'StyleCodes active -', + 'ui.compose.markdown.mode' => 'Markdown mode', + 'ui.compose.markdown.toolbar_aria' => 'Markdown formatting', + 'ui.compose.markdown.bold_title' => 'Bold (Ctrl+B)', + 'ui.compose.markdown.italic_title' => 'Italic (Ctrl+I)', + 'ui.compose.markdown.heading_1_title' => 'Heading 1', + 'ui.compose.markdown.heading_2_title' => 'Heading 2', + 'ui.compose.markdown.heading_3_title' => 'Heading 3', + 'ui.compose.markdown.inline_code_title' => 'Inline code', + 'ui.compose.markdown.code_block_title' => 'Code block', + 'ui.compose.markdown.link_title' => 'Link (Ctrl+K)', + 'ui.compose.markdown.unordered_list_title' => 'Unordered list', + 'ui.compose.markdown.ordered_list_title' => 'Ordered list', + 'ui.compose.markdown.blockquote_title' => 'Blockquote', + 'ui.compose.markdown.horizontal_rule_title' => 'Horizontal rule', + 'ui.compose.markdown.edit_mode_title' => 'Edit mode', + 'ui.compose.markdown.preview_rendered_title' => 'Preview rendered output', + 'ui.compose.markdown.quick_reference_title' => 'Markdown quick reference', + 'ui.compose.markdown.write_placeholder' => 'Write your markdown message here...', + 'ui.compose.markdown.click_preview_hint' => 'Click Preview to see the rendered output.', + 'ui.compose.markdown.formatting_active' => 'Markdown formatting active.', + 'ui.compose.markdown.quick_reference' => 'Quick reference', + 'ui.compose.markup_format' => 'Markup Format', + 'ui.compose.plain_text' => 'Plain text', + 'ui.compose.markdown_label' => 'Markdown', + 'ui.compose.stylecodes_label' => 'StyleCodes (GoldEd, SemPoint Rich Text, Synchronet Markup)', + 'ui.compose.markup_format_help' => 'Send with markup formatting. Markdown and StyleCodes are supported on compatible networks.', + 'ui.compose.hide_panel' => 'Hide panel', + 'ui.compose.show_panel' => 'Show panel', + 'ui.compose.netmail_guidelines' => 'Netmail Guidelines:', + 'ui.compose.netmail_guideline_private' => 'Private messages between nodes', + 'ui.compose.netmail_guideline_identity' => 'Posting identity for this destination: Real Name', + 'ui.compose.netmail_guideline_identity_destination_real_name' => 'Posting identity for this destination: Real Name', + 'ui.compose.netmail_guideline_identity_destination_username' => 'Posting identity for this destination: Username/Alias', + 'ui.compose.netmail_guideline_addressing' => 'Use proper Fidonet addressing', + 'ui.compose.netmail_guideline_respectful' => 'Be respectful and professional', + 'ui.compose.netmail_guideline_routed' => 'Messages are routed through the network', + 'ui.compose.netmail_cost' => 'Cost: {cost} {currency}', + 'ui.compose.crashmail.direct_delivery_label' => 'Crashmail (Direct Delivery)', + 'ui.compose.crashmail.help_direct' => 'Attempt immediate direct delivery to the destination node, bypassing hub routing.', + 'ui.compose.crashmail.help_reachable' => 'Requires the destination to be directly reachable.', + 'ui.compose.crashmail.help_cost' => 'Costs {cost} {currency}.', + 'ui.compose.echomail_guidelines' => 'Echomail Guidelines:', + 'ui.compose.echomail_guideline_identity' => 'Posting identity: Select an echo area to see whether this post uses Real Name or Username/Alias', + 'ui.compose.echomail_guideline_identity_area_real_name' => 'Posting identity for this area: Real Name', + 'ui.compose.echomail_guideline_identity_area_username' => 'Posting identity for this area: Username/Alias', + 'ui.compose.echomail_guideline_public' => 'Public forum messages', + 'ui.compose.echomail_guideline_topic' => 'Stay on-topic for the echo area', + 'ui.compose.echomail_guideline_respectful' => 'Be respectful to all participants', + 'ui.compose.echomail_guideline_rules' => 'Follow the echo rules and moderation', + 'ui.compose.tech_note_plain_text_only' => 'Plain text only (no HTML)', + 'ui.compose.tech_note_line_length' => 'Lines should be less than 79 characters', + 'ui.compose.tech_note_origin_line' => 'Origin line added automatically', + 'ui.compose.invalid_fidonet_address_format' => 'Invalid Fidonet address format (e.g., 1:123/456)', + 'ui.compose.send_failed_type' => 'Failed to send {type}', + 'ui.compose.upload_attachment_failed' => 'Failed to upload attachment', + 'ui.compose.reply_attribution' => 'On {date}, {name} wrote:', + 'ui.compose.markdown.help.text_formatting' => 'Text Formatting', + 'ui.compose.markdown.help.syntax' => 'Syntax', + 'ui.compose.markdown.help.result' => 'Result', + 'ui.compose.markdown.help.hyperlink' => 'hyperlink', + 'ui.compose.markdown.help.headings' => 'Headings', + 'ui.compose.markdown.help.lists_blocks' => 'Lists & Blocks', + 'ui.compose.markdown.help.bullet_list' => 'Bullet list', + 'ui.compose.markdown.help.numbered_list' => 'Numbered list', + 'ui.compose.markdown.help.blockquote' => 'Blockquote', + 'ui.compose.markdown.help.horizontal_rule' => 'Horizontal rule', + 'ui.compose.markdown.help.code_block' => 'Code Block', + 'ui.compose.markdown.help.keyboard_shortcuts' => 'Keyboard Shortcuts', + 'ui.compose.markdown.help.shortcut_bold' => 'Bold', + 'ui.compose.markdown.help.shortcut_italic' => 'Italic', + 'ui.compose.markdown.help.shortcut_link' => 'Link', + 'ui.compose.markdown.help.shortcut_indent' => 'Indent (4 spaces)', + 'ui.compose.subject_required' => 'Subject *', + 'ui.compose.message_required' => 'Message *', + 'ui.compose.plain_text_hint' => 'Use plain text. Fidonet standard formatting applies.', + 'ui.compose.message_guidelines' => 'Message Guidelines', + 'ui.compose.technical_notes' => 'Technical Notes', + 'ui.compose.your_info' => 'Your Info', + 'ui.compose.system' => 'System', + 'ui.compose.address' => 'Address', + 'ui.compose.tagline_help' => 'Select a sysop-provided tagline to append below your signature.', + 'ui.compose.save_draft' => 'Save Draft', + 'ui.compose.send_prefix' => 'Send', + 'ui.compose.sending' => 'Sending...', + 'ui.compose.draft.empty_content' => 'Please add some content before saving draft', + 'ui.compose.draft.saved_success' => 'Draft saved successfully', + 'ui.compose.sent_cross_post_suffix' => ' (posted to {count} areas)', + 'ui.compose.address_book.load_failed_short' => 'Failed to load', + 'ui.compose.address_book.select_title' => 'Select from address book', + 'ui.compose.address_book.create_title' => 'Create new address book entry', + 'ui.compose.address_book.entry_added' => 'Entry added successfully', + 'ui.compose.address_book.use_entry_confirm' => 'Use this entry for the current message?', + 'ui.address_book.already_exists' => 'This contact is already in your address book', + 'ui.address_book.load_failed' => 'Failed to load address book', + 'ui.address_book.added_from_netmail' => 'Added from netmail message', + 'ui.address_book.added_from_netmail_replyto_detail' => 'Added from netmail message. Original sender: {original_name} ({original_address}), Reply-to: {replyto_name} ({replyto_address})', + 'ui.address_book.added_from_netmail_sender_detail' => 'Added from netmail message. Sender: {sender_name} ({sender_address})', + 'ui.address_book.check_existing_failed' => 'Failed to check existing contacts', + 'ui.address_book.entry_updated' => 'Entry updated successfully', + 'ui.address_book.entry_deleted' => 'Entry deleted successfully', + 'ui.address_book.sender_added' => '{name} added to address book', + 'ui.address_book.saved_to_address_book' => 'Saved to address book', + 'ui.address_book.delete_confirm' => 'Are you sure you want to delete "{name}" from your address book?', + 'ui.address_book.node_address_placeholder' => '1:234/567', + 'ui.address_book.node_address_title' => 'Enter a valid Fidonet address (e.g., 1:234/567 or 1:234/567.0)', + 'ui.drafts.delete_confirm' => 'Are you sure you want to delete this draft? This cannot be undone.', + 'ui.drafts.deleted_success' => 'Draft deleted successfully', + 'ui.messages.none_selected' => 'No messages selected', + + // Netmail + 'ui.netmail.search.failed' => 'Search failed', + 'ui.netmail.message_deleted_success' => 'Message deleted successfully', + 'ui.netmail.delete_message_confirm' => 'Are you sure you want to delete this message? This action cannot be undone.', + 'ui.netmail.bulk_delete.confirm' => 'Are you sure you want to delete {count} message(s)?', + 'ui.netmail.bulk_delete.success' => 'Deleted {count} message(s)', + 'ui.netmail.address_book.load_entry_failed_prefix' => 'Failed to load entry: ', + 'ui.netmail.bulk_delete.failed' => 'Failed to delete messages', + 'ui.netmail.no_drafts_found' => 'No drafts found', + 'ui.netmail.to' => 'To', + 'ui.netmail.from_to' => 'From/To', + 'ui.netmail.last_updated' => 'Last Updated', + 'ui.netmail.received' => 'Received', + 'ui.netmail.badge_netmail' => 'NETMAIL', + 'ui.netmail.badge_new' => 'NEW', + 'ui.netmail.received_insecure_session_title' => 'Received via insecure session', + 'ui.netmail.received_insecure_badge_title' => 'This message was received via an insecure/unauthenticated binkp session', + 'ui.netmail.received_insecurely' => 'Received Insecurely', + 'ui.netmail.not_authenticated' => 'This message was not authenticated', + + // Echomail + 'ui.echomail.search.failed' => 'Search failed', + 'ui.echomail.view_all_echo_areas' => 'View all echo areas', + 'ui.echomail.search_found_count' => '{count} found', + 'ui.echomail.no_echoareas_available' => 'No echo areas available', + 'ui.echomail.no_drafts_found' => 'No drafts found', + 'ui.echomail.to_echo_area' => 'To / Echo Area', + 'ui.echomail.last_updated' => 'Last Updated', + 'ui.echomail.to_prefix' => 'to:', + 'ui.echomail.bulk_mark_read_failed' => 'Failed to mark messages as read', + 'ui.echomail.bulk_mark_read_success' => 'Marked {count} message(s) as read', + 'ui.echomail.bulk_marking' => 'Marking...', + 'ui.echomail.viewing_all_messages' => 'Viewing: All Messages', + 'ui.echomail.no_area' => 'No area', + 'ui.echomail.save_status.update_failed' => 'Failed to update save status', + 'ui.echomail.bulk_delete.failed' => 'Failed to delete messages', + 'ui.echomail.bulk_delete.success' => 'Deleted {count} message(s)', + 'ui.echomail.bulk_delete.confirm' => 'Are you sure you want to delete {count} selected message(s) for everyone?', + 'ui.echomail.bulk_deleting' => 'Deleting...', + 'ui.echomail.shares.check_failed' => 'Failed to check existing shares', + 'ui.echomail.shares.friendly_url_failed' => 'Failed to generate friendly URL', + 'ui.echomail.shares.revoke_confirm' => 'Are you sure you want to revoke this share link? It will no longer be accessible to others.', + 'ui.echomail.plain_text_mode' => 'Plain text mode', + 'ui.echomail.press_a_to_toggle' => 'press A to toggle', + 'ui.echomail.shortcuts.title' => 'Keyboard Shortcuts', + 'ui.echomail.shortcuts.prev_next' => 'Previous / next message', + 'ui.echomail.shortcuts.toggle_ansi' => 'Toggle ANSI / plain text rendering', + 'ui.echomail.shortcuts.download' => 'Download message', + 'ui.echomail.shortcuts.fullscreen' => 'Toggle fullscreen', + 'ui.echomail.shortcuts.help' => 'Show / hide keyboard shortcuts', + 'ui.echomail.shortcuts.close' => 'Close message', + 'ui.echomail.shortcuts.dismiss' => 'Press ? or H to close this help', + + // Admin subscriptions + 'ui.admin_subscriptions.page_title' => 'Admin: Manage Subscriptions', + 'ui.admin_subscriptions.heading' => 'Subscription Management', + 'ui.admin_subscriptions.breadcrumb_aria' => 'breadcrumb', + 'ui.admin_subscriptions.breadcrumb_current' => 'Subscriptions', + 'ui.admin_subscriptions.total_echoareas' => 'Total Echoareas', + 'ui.admin_subscriptions.default_echoareas' => 'Default Echoareas', + 'ui.admin_subscriptions.total_subscriptions' => 'Total Subscriptions', + 'ui.admin_subscriptions.active_subscribers' => 'Active Subscribers', + 'ui.admin_subscriptions.echoarea_default_subscriptions' => 'Echoarea Default Subscriptions', + 'ui.admin_subscriptions.manage_default_help' => 'Manage which echoareas new users are automatically subscribed to', + 'ui.admin_subscriptions.default' => 'Default', + 'ui.admin_subscriptions.echoarea' => 'Echoarea', + 'ui.admin_subscriptions.subscribers' => 'Subscribers', + 'ui.admin_subscriptions.user_subs' => 'User Subs', + 'ui.admin_subscriptions.auto_subs' => 'Auto Subs', + 'ui.admin_subscriptions.messages' => 'Messages', + 'ui.admin_subscriptions.default_subscription_title' => 'Default subscription', + 'ui.admin_subscriptions.legend' => 'Legend:', + 'ui.admin_subscriptions.legend_default' => 'Default subscription (new users auto-subscribed)', + 'ui.admin_subscriptions.legend_total_badge' => 'Total', + 'ui.admin_subscriptions.legend_total' => 'All active subscribers', + 'ui.admin_subscriptions.legend_user_badge' => 'User', + 'ui.admin_subscriptions.legend_user' => 'User-initiated subscriptions', + 'ui.admin_subscriptions.legend_auto_badge' => 'Auto', + 'ui.admin_subscriptions.legend_auto' => 'Automatic/admin subscriptions', + 'ui.admin_subscriptions.how_it_works' => 'How it works:', + 'ui.admin_subscriptions.how_default_toggle' => 'Toggle "Default" to make echoareas auto-subscribe for new users', + 'ui.admin_subscriptions.how_existing_users' => 'When enabled, all existing users also get auto-subscribed', + 'ui.admin_subscriptions.how_unsubscribe' => 'Users can still unsubscribe from default echoareas', + 'ui.admin_subscriptions.action_enabled' => 'enabled', + 'ui.admin_subscriptions.action_disabled' => 'disabled', + 'ui.admin_subscriptions.update_success' => 'Default subscription {action} successfully! Page will refresh to show updated stats.', + 'ui.admin_subscriptions.default_enabled_success' => 'Default subscription enabled successfully! Page will refresh to show updated stats.', + 'ui.admin_subscriptions.default_disabled_success' => 'Default subscription disabled successfully! Page will refresh to show updated stats.', + 'ui.admin_subscriptions.update_failed' => 'Failed to update default subscription. Please try again.', + 'ui.admin_subscriptions.network_error' => 'Network error. Please try again.', + + // User subscriptions + 'ui.user_subscriptions.page_title' => 'Manage Your Echoarea Subscriptions', + 'ui.user_subscriptions.heading' => 'Manage Your Echoarea Subscriptions', + 'ui.user_subscriptions.heading_help' => 'Subscribe to echoareas to see their messages', + 'ui.user_subscriptions.search_placeholder' => 'Search echoareas...', + 'ui.user_subscriptions.default_subscription_title' => 'Default subscription', + 'ui.user_subscriptions.message_count' => '{count} messages', + 'ui.user_subscriptions.dot_subscribed' => '- Subscribed', + 'ui.user_subscriptions.automatic' => '(automatic)', + 'ui.user_subscriptions.view_messages' => 'View Messages', + 'ui.user_subscriptions.subscription_info' => 'Subscription Info', + 'ui.user_subscriptions.subscribed_label' => 'Subscribed:', + 'ui.user_subscriptions.how_subscriptions_work' => 'How Subscriptions Work', + 'ui.user_subscriptions.how_new_users' => 'New users are automatically subscribed to popular echoareas', + 'ui.user_subscriptions.how_subscribe_unsubscribe' => 'You can subscribe/unsubscribe to any echoarea', + 'ui.user_subscriptions.how_only_subscribed' => 'Only subscribed echoareas appear in your message lists', + 'ui.user_subscriptions.how_star_means_default' => 'indicates default echoareas', + 'ui.user_subscriptions.subscribed_success' => 'Subscribed successfully!', + 'ui.user_subscriptions.unsubscribed_success' => 'Unsubscribed successfully!', + 'ui.user_subscriptions.update_failed' => 'Failed to update subscription. Please try again.', + 'ui.user_subscriptions.network_error' => 'Network error. Please try again.', + + // Echoareas import + 'ui.echoareas_import.page_title' => 'Import Echo Areas', + 'ui.echoareas_import.heading' => 'Import Echo Areas', + 'ui.echoareas_import.back_to_echo_areas' => 'Back to Echo Areas', + 'ui.echoareas_import.processed' => 'Processed:', + 'ui.echoareas_import.created' => 'Created:', + 'ui.echoareas_import.updated' => 'Updated:', + 'ui.echoareas_import.skipped_blank_rows' => 'Skipped blank rows:', + 'ui.echoareas_import.errors' => 'Errors:', + 'ui.echoareas_import.import_errors' => 'Import Errors', + 'ui.echoareas_import.upload_csv' => 'Upload CSV', + 'ui.echoareas_import.csv_file' => 'CSV File', + 'ui.echoareas_import.csv_help' => 'UTF-8 CSV is recommended. Header row is optional.', + 'ui.echoareas_import.import_echo_areas' => 'Import Echo Areas', + 'ui.echoareas_import.csv_format' => 'CSV Format', + 'ui.echoareas_import.each_row_fields' => 'Each row must contain these fields in this order:', + 'ui.echoareas_import.example' => 'Example:', + 'ui.echoareas_import.import_rules' => 'Import Rules', + 'ui.echoareas_import.rule_domain_blank' => 'DOMAIN may be blank. Blank domain imports the area as local-only.', + 'ui.echoareas_import.rule_domain_exists' => 'If DOMAIN is provided, it must already exist in your BinkP network configuration.', + 'ui.echoareas_import.rule_existing_area' => 'If an area already exists with the same ECHOTAG and DOMAIN, its description is updated and the area is reactivated.', + 'ui.echoareas_import.rule_new_areas' => 'New areas are created active with default settings; you can fine-tune them afterward in Echo Areas Management.', + 'ui.echoareas_import.rule_header_optional' => 'Header row ECHOTAG,DESCRIPTION,DOMAIN is accepted but not required.', + 'ui.echoareas_import.importing' => 'Importing...', + 'ui.echoareas_import.error_line_prefix' => 'Line {line}: {message}', + 'ui.echoareas_import.error_open_csv' => 'Unable to open uploaded CSV file.', + 'ui.echoareas_import.error_duplicate_row' => 'Duplicate ECHOTAG/DOMAIN combination within the CSV file.', + 'ui.echoareas_import.error_tag_description_required' => 'ECHOTAG and DESCRIPTION are required.', + 'ui.echoareas_import.error_invalid_tag' => 'Invalid ECHOTAG. Use only letters, numbers, dots, underscores, and hyphens.', + 'ui.echoareas_import.error_invalid_domain' => 'Invalid DOMAIN. Use only letters, numbers, underscores, and hyphens.', + 'ui.echoareas_import.error_unknown_domain' => 'Unknown DOMAIN \'{domain}\'. Add the network domain first in BinkP configuration.', + 'ui.echoareas_import.error_apply_failed' => 'Import failed and no changes were applied.', + 'ui.echoareas_import.error_unexpected' => 'Unexpected import error.', + 'ui.echoareas_import.error_choose_csv' => 'Please choose a CSV file to import.', + 'ui.echoareas_import.error_upload_failed' => 'The upload failed. Please try again.', + 'ui.echoareas_import.error_invalid_upload' => 'Invalid uploaded file.', + + // API success messages + 'ui.api.reminder.sent' => 'Account reminder sent successfully', + 'ui.api.files.uploaded' => 'File uploaded successfully', + 'ui.api.files.deleted' => 'File deleted successfully', + 'ui.api.files.renamed' => 'File renamed successfully', + 'ui.api.messages.sent' => 'Message sent successfully', + 'ui.api.messages.bulk_mark_read' => 'Marked messages as read', + 'ui.api.messages.bulk_deleted' => 'Deleted messages', + 'ui.api.messages.saved' => 'Message saved', + 'ui.api.messages.unsaved' => 'Message unsaved', + 'ui.api.messages.share_revoked' => 'Share link revoked', + 'ui.api.credits.sent' => 'Credits sent successfully', + 'ui.api.debug.delete_endpoint_accessible' => 'Delete endpoint is accessible', + 'ui.api.admin.mrc_restart_initiated' => 'MRC daemon restart initiated', + 'ui.api.admin.binkp_config_reloaded' => 'BinkP configuration reload requested', + 'ui.api.binkp.uplink_added' => 'Uplink added successfully', + 'ui.api.binkp.uplink_updated' => 'Uplink updated successfully', + 'ui.api.binkp.uplink_removed' => 'Uplink removed successfully', + 'ui.api.binkp.file_deleted' => 'File deleted successfully', + 'ui.api.binkp.file_retry_started' => 'File retry initiated successfully', + 'ui.api.binkp.config_updated' => 'Configuration updated successfully', + 'ui.api.binkp.poll_triggered' => 'BinkP poll triggered', + 'ui.api.binkp.poll_all_triggered' => 'BinkP poll-all triggered', + 'ui.api.binkp.process_packets_started' => 'Packet processing started', + 'ui.api.door.session_resumed' => 'Resuming existing session', +]; diff --git a/config/i18n/en/errors.php b/config/i18n/en/errors.php new file mode 100644 index 000000000..8848e10dc --- /dev/null +++ b/config/i18n/en/errors.php @@ -0,0 +1,447 @@ + 'An unexpected error occurred', + + // Auth + 'errors.auth.authentication_required' => 'Authentication required', + 'errors.auth.invalid_csrf_token' => 'Invalid CSRF token', + 'errors.auth.missing_credentials' => 'Username and password required', + 'errors.auth.invalid_credentials' => 'Invalid credentials', + 'errors.auth.invalid_api_key' => 'Invalid API key', + 'errors.auth.gateway_token_missing_fields' => 'userid and token are required', + 'errors.auth.invalid_or_expired_gateway_token' => 'Invalid or expired token', + 'errors.auth.username_or_email_required' => 'Username or email is required', + 'errors.auth.token_required' => 'Token is required', + 'errors.auth.invalid_or_expired_token' => 'Invalid or expired token', + 'errors.auth.token_and_password_required' => 'Token and new password are required', + 'errors.auth.weak_password' => 'Password must be at least 8 characters long', + 'errors.auth.reset_failed' => 'Failed to reset password', + + // Registration + 'errors.register.invalid_submission' => 'Invalid submission', + 'errors.register.too_fast' => 'Please take your time filling out the form.', + 'errors.register.session_expired' => 'Session expired. Please refresh the page and try again.', + 'errors.register.rate_limited' => 'Too many registration attempts. Please try again later.', + 'errors.register.required_fields' => 'Username, password, and real name are required', + 'errors.register.invalid_username_format' => 'Username must be 3-20 characters, letters, numbers, and underscores only', + 'errors.register.restricted_name' => 'This username or real name is not allowed', + 'errors.register.weak_password' => 'Password must be at least 8 characters long', + 'errors.register.user_exists' => 'A user with this username or name already exists. Please try logging in or contact the sysop for assistance.', + 'errors.register.failed' => 'Registration failed. Please try again later.', + + // Reminder + 'errors.reminder.username_required' => 'Username is required', + 'errors.reminder.user_not_found_or_logged_in' => 'User not found or already logged in', + 'errors.reminder.send_failed' => 'Failed to send reminder. Please try again later.', + + // Settings + 'errors.settings.invalid_input' => 'Invalid input', + 'errors.settings.update_failed' => 'Failed to update settings', + 'errors.settings.exception' => 'Failed to update settings', + + // Messages + 'errors.messages.share_create_failed' => 'Failed to create share link', + 'errors.messages.share_lookup_failed' => 'Failed to load share links', + 'errors.messages.share_revoke_failed' => 'Failed to revoke share link', + 'errors.messages.netmail.not_found' => 'Message not found', + 'errors.messages.netmail.delete_failed' => 'Failed to delete message', + 'errors.messages.netmail.bulk_delete.invalid_input' => 'A non-empty message ID list is required', + 'errors.messages.echomail.bulk_read.invalid_input' => 'A non-empty message ID list is required', + 'errors.messages.echomail.bulk_read.failed' => 'Failed to mark messages as read', + 'errors.messages.echomail.bulk_delete.admin_required' => 'Admin privileges are required', + 'errors.messages.echomail.bulk_delete.invalid_input' => 'A non-empty message ID list is required', + 'errors.messages.echomail.bulk_delete.user_not_found' => 'User not found', + 'errors.messages.echomail.stats.subscription_required' => 'Subscription required for this echo area', + 'errors.messages.echomail.not_found' => 'Message not found', + 'errors.messages.netmail.attachment.no_file' => 'No attachment uploaded', + 'errors.messages.netmail.attachment.upload_error' => 'Attachment upload failed', + 'errors.messages.netmail.attachment.too_large' => 'Attachment exceeds maximum allowed size', + 'errors.messages.netmail.attachment.store_failed' => 'Failed to store uploaded attachment', + 'errors.messages.send.invalid_type' => 'Invalid message type', + 'errors.messages.send.failed' => 'Failed to send message', + 'errors.messages.send.exception' => 'Failed to send message', + + // Notify + 'errors.notify.user_id_missing' => 'Unable to resolve user session', + 'errors.notify.invalid_state' => 'Invalid notification state payload', + 'errors.notify.invalid_target' => 'Invalid notification target', + + // Polls + 'errors.polls.option_required' => 'A poll option is required', + 'errors.polls.not_found' => 'Poll not found', + 'errors.polls.invalid_option' => 'Invalid poll option', + 'errors.polls.vote_failed' => 'Failed to record vote', + 'errors.polls.insufficient_credits' => 'Failed to deduct credits. You may have insufficient balance.', + 'errors.polls.question_required' => 'Poll question is required', + 'errors.polls.question_length_invalid' => 'Poll question must be between 10 and 500 characters', + 'errors.polls.options_count_invalid' => 'Poll must include between 2 and 10 options', + 'errors.polls.option_empty' => 'Poll options cannot be empty', + 'errors.polls.option_length_invalid' => 'Poll options must be 200 characters or fewer', + 'errors.polls.options_duplicate' => 'Poll options must be unique', + 'errors.polls.create_failed' => 'Failed to create poll', + + // Shoutbox + 'errors.shoutbox.message_required' => 'Message is required', + 'errors.shoutbox.message_too_long' => 'Message cannot exceed 280 characters', + + // Chat + 'errors.chat.feature_disabled' => 'Chat is disabled', + 'errors.chat.invalid_message_query' => 'Invalid chat message query', + 'errors.chat.invalid_send_target' => 'Invalid chat target', + 'errors.chat.message_length_invalid' => 'Message must be between 1 and 1000 characters', + 'errors.chat.room_not_found' => 'Chat room not found', + 'errors.chat.user_banned' => 'You are banned from this room', + 'errors.chat.recipient_not_found' => 'Recipient not found', + 'errors.chat.send_blocked' => 'Message could not be sent', + 'errors.chat.admin_required' => 'Admin privileges are required', + 'errors.chat.invalid_moderation_request' => 'Invalid moderation request', + 'errors.chat.user_not_found' => 'User not found', + + // Echo Areas + 'errors.echoareas.admin_required' => 'Admin privileges are required', + 'errors.echoareas.not_found' => 'Echo area not found', + 'errors.echoareas.invalid_posting_name_policy' => 'Invalid posting name policy', + 'errors.echoareas.tag_description_required' => 'Tag and description are required', + 'errors.echoareas.invalid_tag_format' => 'Invalid tag format', + 'errors.echoareas.invalid_color_format' => 'Invalid color format', + 'errors.echoareas.create_failed' => 'Failed to create echo area', + 'errors.echoareas.not_found_or_unchanged' => 'Echo area not found or no changes made', + 'errors.echoareas.update_failed' => 'Failed to update echo area', + 'errors.echoareas.delete_blocked_has_messages' => 'Cannot delete echo area with existing messages', + 'errors.echoareas.delete_failed' => 'Failed to delete echo area', + + // File Areas + 'errors.fileareas.not_found' => 'File area not found', + 'errors.fileareas.create_failed' => 'Failed to create file area', + 'errors.fileareas.update_failed' => 'Failed to update file area', + 'errors.fileareas.delete_failed' => 'Failed to delete file area', + + // Files + 'errors.files.feature_disabled' => 'File areas feature is disabled', + 'errors.files.area_id_required' => 'File area ID is required', + 'errors.files.access_denied' => 'Access denied to this file area', + 'errors.files.not_found' => 'File not found', + 'errors.files.share_not_found_or_forbidden' => 'Share link not found or not permitted', + 'errors.files.delete_failed' => 'Failed to delete file', + 'errors.files.scan_forbidden' => 'Admin access required to scan files', + 'errors.files.scan_failed' => 'Virus scan failed', + 'errors.files.rename_filename_required' => 'New filename is required', + 'errors.files.rename_forbidden' => 'You do not have permission to rename this file', + 'errors.files.rename_conflict' => 'A file with that name already exists in this area', + 'errors.files.rename_failed' => 'Failed to rename file', + 'errors.files.short_description_required' => 'Short description is required', + 'errors.files.edit_forbidden' => 'You do not have permission to edit this file', + 'errors.files.edit_failed' => 'Failed to update file', + 'errors.files.move_forbidden' => 'Only administrators can move files between areas', + 'errors.files.move_conflict' => 'A file with that name already exists in the target area', + 'errors.files.move_failed' => 'Failed to move file', + 'errors.files.upload.no_file' => 'No file uploaded', + 'errors.files.upload.area_id_required' => 'File area ID is required', + 'errors.files.upload.short_description_required' => 'Short description is required', + 'errors.files.upload.area_not_found' => 'File area not found', + 'errors.files.upload.read_only' => 'This file area is read-only', + 'errors.files.upload.admin_only' => 'Only administrators can upload files to this area', + 'errors.files.upload.virus_detected' => 'File rejected: virus detected', + 'errors.files.upload.failed' => 'Failed to upload file', + + // Admin Users + 'errors.admin.users.not_found' => 'User not found', + 'errors.admin.users.create_failed' => 'Failed to create user', + 'errors.admin.users.update_failed' => 'Failed to update user', + 'errors.admin.users.delete_failed' => 'Failed to delete user', + + // Admin Polls + 'errors.admin.polls.question_required' => 'Question is required', + 'errors.admin.polls.options_required' => 'At least two options are required', + 'errors.admin.polls.not_found' => 'Poll not found', + 'errors.admin.polls.create_failed' => 'Failed to create poll', + 'errors.admin.polls.update_failed' => 'Failed to update poll', + 'errors.admin.polls.delete_failed' => 'Failed to delete poll', + + // Message Drafts + 'errors.messages.drafts.invalid_input' => 'Invalid draft payload', + 'errors.messages.drafts.user_id_missing' => 'Unable to resolve user session', + 'errors.messages.drafts.save_failed' => 'Failed to save draft', + 'errors.messages.drafts.list_failed' => 'Failed to load drafts', + 'errors.messages.drafts.not_found' => 'Draft not found', + 'errors.messages.drafts.get_failed' => 'Failed to load draft', + 'errors.messages.drafts.delete_failed' => 'Failed to delete draft', + 'errors.messages.netmail.get_failed' => 'Failed to load message', + 'errors.messages.echomail.get_failed' => 'Failed to load message', + + // Message Search / Read State + 'errors.messages.search.query_too_short' => 'Search query must be at least 2 characters', + 'errors.messages.read.user_id_missing' => 'Unable to resolve user session', + 'errors.messages.read.invalid_type' => 'Invalid message type', + 'errors.messages.read.mark_failed' => 'Failed to mark message as read', + 'errors.messages.save.user_id_missing' => 'Unable to resolve user session', + 'errors.messages.save.invalid_type' => 'Invalid message type', + 'errors.messages.save.failed' => 'Failed to save message', + 'errors.messages.unsave.user_id_missing' => 'Unable to resolve user session', + 'errors.messages.unsave.invalid_type' => 'Invalid message type', + 'errors.messages.unsave.failed' => 'Failed to unsave message', + 'errors.messages.unsave.not_saved' => 'Message was not saved or already removed', + + // User Profile / Stats / Transactions + 'errors.user.profile.current_password_incorrect' => 'Current password is incorrect', + 'errors.user.profile.new_password_too_short' => 'New password must be at least 6 characters long', + 'errors.user.profile.update_failed' => 'Failed to update profile', + 'errors.user.stats.admin_required' => 'Admin privileges are required', + 'errors.user.stats.user_not_found' => 'User not found', + 'errors.user.transactions.admin_required' => 'Admin privileges are required', + 'errors.user.transactions.user_not_found' => 'User not found', + 'errors.user.transactions.list_failed' => 'Failed to load transactions', + + // Credits + 'errors.credits.feature_disabled' => 'Credits feature is disabled', + 'errors.credits.send.invalid_amount' => 'Amount must be between 1 and 200', + 'errors.credits.send.self_transfer_forbidden' => 'You cannot send credits to yourself', + 'errors.credits.send.recipient_not_found' => 'Recipient not found', + 'errors.credits.send.insufficient_balance' => 'Insufficient balance', + 'errors.credits.send.debit_failed' => 'Failed to debit sender account', + 'errors.credits.send.credit_failed' => 'Failed to credit recipient account', + 'errors.credits.send.failed' => 'Credit transfer failed', + + // User Sessions / Activity + 'errors.user.sessions.revoke_failed' => 'Failed to revoke session', + 'errors.user.sessions.revoke_all_failed' => 'Failed to revoke sessions', + 'errors.user.activity.session_missing' => 'Active session is required', + + // BinkP + 'errors.binkp.admin_required' => 'Admin access required', + 'errors.binkp.status_failed' => 'Failed to load BinkP status', + 'errors.binkp.poll_failed' => 'Failed to poll BinkP uplink', + 'errors.binkp.poll_all_failed' => 'Failed to poll all BinkP uplinks', + 'errors.binkp.process_packets_failed' => 'Failed to process packets', + 'errors.binkp.uplinks_list_failed' => 'Failed to load BinkP uplinks', + 'errors.binkp.files_inbound_failed' => 'Failed to load inbound BinkP files', + 'errors.binkp.files_outbound_failed' => 'Failed to load outbound BinkP files', + 'errors.binkp.process_outbound_failed' => 'Failed to process outbound queue', + 'errors.binkp.connection_test_failed' => 'Failed to test BinkP connection', + 'errors.binkp.logs.failed' => 'Failed to load BinkP logs', + 'errors.binkp.uplink.address_hostname_required' => 'Address and hostname are required', + 'errors.binkp.uplink.poll_failed' => 'Failed to poll BinkP uplink', + 'errors.binkp.uplink.poll_all_failed' => 'Failed to poll all BinkP uplinks', + 'errors.binkp.uplink.add_failed' => 'Failed to add BinkP uplink', + 'errors.binkp.uplink.update_failed' => 'Failed to update BinkP uplink', + 'errors.binkp.uplink.remove_failed' => 'Failed to remove BinkP uplink', + 'errors.binkp.files.inbound_failed' => 'Failed to load inbound BinkP files', + 'errors.binkp.files.outbound_failed' => 'Failed to load outbound BinkP files', + 'errors.binkp.files.process_inbound_failed' => 'Failed to process inbound BinkP queue', + 'errors.binkp.files.process_outbound_failed' => 'Failed to process outbound BinkP queue', + 'errors.binkp.files.delete_outbound_failed' => 'Failed to delete outbound BinkP file', + 'errors.binkp.files.retry_error_failed' => 'Failed to retry inbound error file', + 'errors.binkp.config.invalid_section' => 'Invalid BinkP configuration section', + 'errors.binkp.config.update_failed' => 'Failed to update BinkP configuration', + + // Admin Pending Users + 'errors.admin.pending_users.admin_required' => 'Admin privileges are required', + 'errors.admin.pending_users.list_failed' => 'Failed to load pending users', + 'errors.admin.pending_users.not_found' => 'Pending user not found', + 'errors.admin.pending_users.get_failed' => 'Failed to load pending user', + 'errors.admin.pending_users.approve_failed' => 'Failed to approve pending user', + 'errors.admin.pending_users.reject_failed' => 'Failed to reject pending user', + + // Admin Users (extended) + 'errors.admin.users.admin_required' => 'Admin privileges are required', + 'errors.admin.users.list_failed' => 'Failed to load users', + 'errors.admin.users.get_failed' => 'Failed to load user', + 'errors.admin.users.real_name_required' => 'Real name is required', + 'errors.admin.users.password_too_short' => 'Password must be at least 8 characters long', + 'errors.admin.users.update_status_failed' => 'Failed to update user status', + 'errors.admin.users.create_required_fields' => 'Username, real name, and password are required', + 'errors.admin.users.invalid_username_format' => 'Invalid username format', + 'errors.admin.users.restricted_name' => 'This username or real name is not allowed', + 'errors.admin.users.username_exists' => 'Username already exists', + 'errors.admin.users.cleanup_failed' => 'Failed to clean up registrations', + 'errors.admin.users.reminder_not_allowed' => 'User is not eligible for reminder', + 'errors.admin.users.reminder_send_failed' => 'Failed to send reminder', + 'errors.admin.users.need_reminders_failed' => 'Failed to load reminder candidates', + + // Admin Debug + 'errors.admin.debug_failed' => 'Failed to load admin debug information', + + // Admin BBS / Appearance / Shell + 'errors.admin.bbs_settings.load_failed' => 'Failed to load BBS settings', + 'errors.admin.bbs_settings.invalid_payload' => 'Invalid configuration payload', + 'errors.admin.bbs_settings.invalid_credits_config' => 'Invalid credits configuration', + 'errors.admin.bbs_settings.save_failed' => 'Failed to save BBS settings', + 'errors.admin.appearance.load_failed' => 'Failed to load appearance settings', + 'errors.admin.appearance.branding.invalid_accent_color' => 'Invalid accent color format', + 'errors.admin.appearance.branding.footer_too_long' => 'Footer text must be 500 characters or less', + 'errors.admin.appearance.branding.save_failed' => 'Failed to save branding settings', + 'errors.admin.appearance.content.save_failed' => 'Failed to save content settings', + 'errors.admin.appearance.navigation.save_failed' => 'Failed to save navigation settings', + 'errors.admin.appearance.seo.save_failed' => 'Failed to save SEO settings', + 'errors.admin.appearance.shell.save_failed' => 'Failed to save shell settings', + 'errors.admin.appearance.message_reader.save_failed' => 'Failed to save message reader settings', + 'errors.admin.appearance.markdown_preview.failed' => 'Failed to render markdown preview', + 'errors.admin.shell_art.list_failed' => 'Failed to list shell art files', + 'errors.admin.shell_art.upload.no_file' => 'No shell art file uploaded', + 'errors.admin.shell_art.upload.upload_error' => 'Shell art upload failed', + 'errors.admin.shell_art.upload.file_too_large' => 'Shell art file exceeds size limit', + 'errors.admin.shell_art.upload.failed' => 'Failed to upload shell art', + 'errors.admin.shell_art.delete.invalid_name' => 'Invalid shell art filename', + 'errors.admin.shell_art.delete.failed' => 'Failed to delete shell art', + 'errors.admin.taglines.load_failed' => 'Failed to load taglines', + 'errors.admin.taglines.save_failed' => 'Failed to save taglines', + 'errors.admin.mrc_settings.load_failed' => 'Failed to load MRC settings', + 'errors.admin.mrc_settings.save_failed' => 'Failed to save MRC settings', + 'errors.admin.mrc_settings.restart_failed' => 'Failed to restart MRC daemon', + 'errors.admin.bbs_system.load_failed' => 'Failed to load system settings', + 'errors.admin.bbs_system.save_failed' => 'Failed to save system settings', + 'errors.admin.binkp_config.load_failed' => 'Failed to load BinkP configuration', + 'errors.admin.binkp_config.save_failed' => 'Failed to save BinkP configuration', + 'errors.admin.binkp_config.reload_failed' => 'Failed to reload BinkP configuration', + 'errors.admin.dosdoors_config.load_failed' => 'Failed to load DOS doors configuration', + 'errors.admin.dosdoors_config.save_failed' => 'Failed to save DOS doors configuration', + 'errors.admin.native_doors.load_failed' => 'Failed to load native doors configuration', + 'errors.admin.native_doors.save_failed' => 'Failed to save native doors configuration', + 'errors.admin.native_doors.sync_failed' => 'Failed to sync native doors', + 'errors.admin.webdoors_config.load_failed' => 'Failed to load webdoors configuration', + 'errors.admin.webdoors_config.save_failed' => 'Failed to save webdoors configuration', + 'errors.admin.webdoors_config.activate_failed' => 'Failed to activate webdoors configuration', + 'errors.admin.filearea_rules.load_failed' => 'Failed to load file area rules', + 'errors.admin.filearea_rules.save_failed' => 'Failed to save file area rules', + 'errors.admin.ads.list_failed' => 'Failed to load advertisements', + 'errors.admin.ads.upload.no_file' => 'No advertisement file uploaded', + 'errors.admin.ads.upload.upload_error' => 'Advertisement upload failed', + 'errors.admin.ads.upload.file_too_large' => 'Advertisement file exceeds size limit', + 'errors.admin.ads.upload.read_failed' => 'Failed to read uploaded advertisement file', + 'errors.admin.ads.upload.failed' => 'Failed to upload advertisement', + 'errors.admin.ads.delete_failed' => 'Failed to delete advertisement', + 'errors.admin.chat_rooms.invalid_name_length' => 'Room name must be 1-64 characters', + 'errors.admin.chat_rooms.create_failed' => 'Failed to create chat room', + 'errors.admin.chat_rooms.not_found' => 'Chat room not found', + 'errors.admin.chat_rooms.lobby_name_locked' => 'Lobby name cannot be changed', + 'errors.admin.chat_rooms.update_failed' => 'Failed to update chat room', + 'errors.admin.chat_rooms.lobby_delete_forbidden' => 'Lobby cannot be deleted', + 'errors.admin.chat_rooms.delete_failed' => 'Failed to delete chat room', + 'errors.admin.shoutbox.hide_failed' => 'Failed to hide shout', + 'errors.admin.shoutbox.unhide_failed' => 'Failed to unhide shout', + 'errors.admin.shoutbox.delete_failed' => 'Failed to delete shout', + 'errors.admin.insecure_nodes.create_failed' => 'Failed to add insecure node', + 'errors.admin.insecure_nodes.update_failed' => 'Failed to update insecure node', + 'errors.admin.insecure_nodes.delete_failed' => 'Failed to delete insecure node', + 'errors.admin.crashmail.retry_failed' => 'Failed to retry crashmail item', + 'errors.admin.crashmail.cancel_failed' => 'Failed to cancel crashmail item', + 'errors.admin.crashmail.poll_failed' => 'Failed to run crashmail poll', + 'errors.admin.custom_templates.list_failed' => 'Failed to list custom templates', + 'errors.admin.custom_templates.get_failed' => 'Failed to load custom template', + 'errors.admin.custom_templates.save_failed' => 'Failed to save custom template', + 'errors.admin.custom_templates.delete_failed' => 'Failed to delete custom template', + 'errors.admin.custom_templates.install_failed' => 'Failed to install custom template', + 'errors.admin.auto_feed.not_found' => 'Feed source not found', + 'errors.admin.auto_feed.required_fields' => 'Feed URL, echo area, and posting user are required', + 'errors.admin.auto_feed.invalid_url' => 'Feed URL is invalid', + 'errors.admin.auto_feed.echoarea_not_found' => 'Echo area not found', + 'errors.admin.auto_feed.user_not_found' => 'Posting user not found', + 'errors.admin.auto_feed.duplicate_source' => 'Feed source already exists', + 'errors.admin.auto_feed.create_failed' => 'Failed to create feed source', + 'errors.admin.auto_feed.update_failed' => 'Failed to update feed source', + 'errors.admin.auto_feed.check_failed' => 'Feed check failed', + 'errors.admin.activity_stats.table_missing' => 'Activity log table is not available', + + // Address Book + 'errors.address_book.list_failed' => 'Failed to load address book entries', + 'errors.address_book.not_found' => 'Entry not found', + 'errors.address_book.get_failed' => 'Failed to load address book entry', + 'errors.address_book.user_not_found' => 'User ID not found in authentication data', + 'errors.address_book.invalid_fidonet_format' => 'Invalid Fidonet address format. Use format like 1:234/567 or 1:234/567.0', + 'errors.address_book.required_fields' => 'Name and Fidonet address are required', + 'errors.address_book.duplicate_entry' => 'Address book entry already exists', + 'errors.address_book.create_failed' => 'Failed to create address book entry', + 'errors.address_book.update_failed' => 'Failed to update address book entry', + 'errors.address_book.delete_failed' => 'Failed to delete address book entry', + 'errors.address_book.search_failed' => 'Failed to search address book entries', + 'errors.address_book.stats_failed' => 'Failed to load address book statistics', + + // Shared Messages + 'errors.messages.shared.lookup_failed' => 'Failed to load shared message', + 'errors.messages.shared.user_shares_failed' => 'Failed to load user shares', + 'errors.messages.shared.access_denied' => 'Message not found or access denied', + 'errors.messages.shared.sharing_disabled' => 'Sharing is disabled for your account', + 'errors.messages.shared.max_active_reached' => 'Maximum number of active shares reached', + 'errors.messages.shared.not_found_or_expired' => 'Share not found or expired', + 'errors.messages.shared.login_required' => 'Login required to access this share', + 'errors.messages.shared.original_not_found' => 'Original message not found', + 'errors.messages.shared.not_found' => 'Share not found', + 'errors.messages.shared.friendly_url_only_echomail' => 'Friendly URLs are only available for echomail shares', + 'errors.messages.shared.slug_generation_failed' => 'Cannot generate share slug for this message', + + // Subscriptions + 'errors.subscriptions.echoarea_id_required' => 'Echoarea ID required', + 'errors.subscriptions.invalid_action' => 'Invalid action', + 'errors.subscriptions.admin_required' => 'Admin access required', + + // Nodelist API + 'errors.nodelist.api.endpoint_not_found' => 'API endpoint not found', + 'errors.nodelist.api.address_required' => 'Address parameter required', + 'errors.nodelist.api.node_not_found' => 'Node not found', + 'errors.nodelist.api.zone_required' => 'Zone parameter required', + 'errors.nodelist.api.internal_error' => 'Failed to process nodelist API request', + 'errors.nodelist.admin_required' => 'Administrator access required', + 'errors.nodelist.import.domain_required' => 'Please specify a network domain', + 'errors.nodelist.import.domain_invalid' => 'Domain should contain only letters, numbers, underscores, and hyphens', + 'errors.nodelist.import.file_required' => 'Please select a valid nodelist file', + 'errors.nodelist.import.invalid_format' => 'Invalid nodelist format', + 'errors.nodelist.import.zip_extension_missing' => 'ZIP extension not available', + 'errors.nodelist.import.zip_open_failed' => 'Could not open ZIP file', + 'errors.nodelist.import.archive_unsupported' => 'Archive format {format} not supported in web interface (use command line)', + 'errors.nodelist.import.archive_nodelist_missing' => 'Could not find nodelist file in archive', + 'errors.nodelist.import.extract_failed' => 'Failed to extract archive', + 'errors.nodelist.import.failed' => 'Failed to import nodelist', + + // Settings / Taglines + 'errors.settings.load_failed' => 'Failed to load user settings', + 'errors.taglines.load_failed' => 'Failed to load taglines', + + // Referrals + 'errors.referrals.code_not_found' => 'Referral code not found', + 'errors.referrals.stats_failed' => 'Failed to load referral statistics', + 'errors.referrals.admin_stats_failed' => 'Failed to load admin referral statistics', + + // WebDoor + 'errors.webdoor.feature_disabled' => 'Game system is not enabled', + 'errors.webdoor.auth_required' => 'Not authenticated', + 'errors.webdoor.invalid_slot' => 'Invalid slot number', + 'errors.webdoor.save_too_large' => 'Save data exceeds maximum size', + 'errors.webdoor.save_not_found' => 'Save not found', + + // Door API + 'errors.door.door_name_required' => 'Door name required', + 'errors.door.admin_only' => 'This door is restricted to administrators', + 'errors.door.insufficient_credits' => 'Insufficient credits', + 'errors.door.insufficient_credits_detail' => 'This door costs {required} credits. You have {balance} credits.', + 'errors.door.capacity_reached' => 'Door is at capacity', + 'errors.door.capacity_reached_detail' => 'This door is currently in use. Only {max_nodes} player(s) allowed at a time. Please try again later.', + 'errors.door.launch_failed' => 'Failed to start door session', + 'errors.door.session_id_required' => 'Session ID required', + 'errors.door.session_unauthorized' => 'Unauthorized', + 'errors.door.session_end_failed' => 'Failed to end session', + 'errors.door.session_get_failed' => 'Failed to get session', + 'errors.door.asset.invalid_type' => 'Invalid asset type', + 'errors.door.asset.door_not_found' => 'Door not found', + 'errors.door.asset.not_defined' => 'Asset not defined in manifest', + 'errors.door.asset.file_not_found' => 'Asset file not found', + 'errors.door.asset.access_denied' => 'Access denied', + + // TIC Processing + 'errors.tic.file_area_create_failed' => 'Failed to create file area from TIC metadata', + 'errors.tic.validation_failed' => 'TIC file validation failed', + 'errors.tic.virus_detected' => 'File rejected: virus detected', + 'errors.tic.processing_failed' => 'TIC processing failed', + + // Virus Scanner + 'errors.virus_scanner.not_available' => 'Virus scanning not available', + 'errors.virus_scanner.file_not_found' => 'File not found for virus scan', + 'errors.virus_scanner.scan_error' => 'Virus scan error', + + // Language overlay editor + 'errors.admin.i18n_overrides.invalid_locale' => 'Invalid or unsupported locale', + 'errors.admin.i18n_overrides.missing_params' => 'Locale and catalog name are required', + 'errors.admin.i18n_overrides.load_failed' => 'Failed to load catalog', + 'errors.admin.i18n_overrides.save_failed' => 'Failed to save overrides', +]; diff --git a/config/i18n/en/terminalserver.php b/config/i18n/en/terminalserver.php new file mode 100644 index 000000000..b5881fcb3 --- /dev/null +++ b/config/i18n/en/terminalserver.php @@ -0,0 +1,240 @@ + 'Too many failed login attempts. Please try again later.', + + // --- Login / register menu (pre-auth) --- + 'ui.terminalserver.server.login_menu.prompt' => 'Would you like to:', + 'ui.terminalserver.server.login_menu.login' => ' (L) Login to existing account', + 'ui.terminalserver.server.login_menu.register' => ' (R) Register new account', + 'ui.terminalserver.server.login_menu.quit' => ' (Q) Quit', + 'ui.terminalserver.server.login_menu.choice' => 'Your choice: ', + 'ui.terminalserver.server.goodbye' => 'Goodbye!', + 'ui.terminalserver.server.press_enter_disconnect' => 'Press Enter to disconnect.', + + // --- Login --- + 'ui.terminalserver.server.login.username_prompt' => 'Username: ', + 'ui.terminalserver.server.login.password_prompt' => 'Password: ', + 'ui.terminalserver.server.login.success' => 'Login successful.', + 'ui.terminalserver.server.login.failed_remaining' => 'Login failed. {remaining} attempt(s) remaining.', + 'ui.terminalserver.server.login.failed_max' => 'Login failed. Maximum attempts exceeded.', + + // --- Registration --- + 'ui.terminalserver.server.registration.title' => '=== New User Registration ===', + 'ui.terminalserver.server.registration.intro' => 'Please provide the following information to create your account.', + 'ui.terminalserver.server.registration.cancel_hint' => '(Type "cancel" at any prompt to abort registration)', + 'ui.terminalserver.server.registration.username' => 'Username (3-20 chars, letters/numbers/underscore): ', + 'ui.terminalserver.server.registration.password' => 'Password (min 8 characters): ', + 'ui.terminalserver.server.registration.confirm' => 'Confirm password: ', + 'ui.terminalserver.server.registration.password_mismatch' => 'Error: Passwords do not match.', + 'ui.terminalserver.server.registration.realname' => 'Real Name: ', + 'ui.terminalserver.server.registration.email' => 'Email (optional): ', + 'ui.terminalserver.server.registration.location' => 'Location (optional): ', + 'ui.terminalserver.server.registration.submitting' => 'Submitting registration...', + 'ui.terminalserver.server.registration.success' => 'Registration successful!', + 'ui.terminalserver.server.registration.pending' => 'Your account has been created and is pending approval.', + 'ui.terminalserver.server.registration.pending_review' => 'You will be notified once an administrator has reviewed your registration.', + + // --- Anti-bot challenge --- + 'ui.terminalserver.server.press_esc' => 'Press ESC twice to continue...', + + // --- Banner (login screen) --- + 'ui.terminalserver.server.banner.title' => 'BinktermPHP Terminal', + 'ui.terminalserver.server.banner.system' => 'System: ', + 'ui.terminalserver.server.banner.location' => 'Location: ', + 'ui.terminalserver.server.banner.origin' => 'Origin: ', + 'ui.terminalserver.server.banner.web' => 'Web: ', + 'ui.terminalserver.server.banner.visit_web' => 'For a good time visit us on the web @ {url}', + 'ui.terminalserver.server.banner.tls' => 'Connected using TLS', + 'ui.terminalserver.server.banner.no_tls' => 'Connected without TLS - use port {port} for an encrypted connection', + 'ui.terminalserver.server.ssh_banner.welcome' => 'Welcome to {system}.', + 'ui.terminalserver.server.ssh_banner.line2' => 'Log in with your account credentials, or enter any username/password', + 'ui.terminalserver.server.ssh_banner.line3' => 'to continue to the main BBS login screen.', + + // --- Main menu --- + 'ui.terminalserver.server.menu.title' => 'Main Menu', + 'ui.terminalserver.server.menu.select_option' => 'Select option:', + 'ui.terminalserver.server.menu.netmail' => 'N) Netmail ({count} messages)', + 'ui.terminalserver.server.menu.echomail' => 'E) Echomail ({count} messages)', + 'ui.terminalserver.server.menu.whos_online' => "W) Who's Online", + 'ui.terminalserver.server.menu.shoutbox' => 'S) Shoutbox', + 'ui.terminalserver.server.menu.polls' => 'P) Polls', + 'ui.terminalserver.server.menu.doors' => 'D) Door Games', + 'ui.terminalserver.server.menu.quit' => 'Q) Quit', + + // --- Farewell --- + 'ui.terminalserver.server.farewell' => 'Thank you for visiting, have a great day!', + 'ui.terminalserver.server.visit_web' => 'Come back and visit us on the web at {url}', + + // --- Who's Online --- + 'ui.terminalserver.server.whos_online.title' => "Who's Online (last {minutes} minutes)", + 'ui.terminalserver.server.whos_online.empty' => 'No users online.', + + // --- Idle timeout --- + 'ui.terminalserver.server.idle.disconnect' => 'Idle timeout - disconnecting...', + 'ui.terminalserver.server.idle.warning_line' => 'Are you still there? (Press Enter to continue)', + 'ui.terminalserver.server.idle.warning_key' => 'Are you still there? (Press any key to continue)', + + // --- Shared UI prompts --- + 'ui.terminalserver.server.press_any_key' => 'Press any key to return...', + 'ui.terminalserver.server.press_continue' => 'Press any key to continue...', + + // --- Message editor (full screen) --- + 'ui.terminalserver.editor.title' => 'MESSAGE EDITOR - FULL SCREEN MODE', + 'ui.terminalserver.editor.shortcuts' => 'Ctrl+K=Help Ctrl+Z=Send Ctrl+C=Cancel', + 'ui.terminalserver.editor.cancelled' => 'Message cancelled.', + 'ui.terminalserver.editor.saved' => 'Message saved and ready to send.', + 'ui.terminalserver.editor.starting_text' => 'Starting with quoted text. Enter your reply below.', + 'ui.terminalserver.editor.instructions' => 'Enter message text. End with a single "." line. Type "/abort" to cancel.', + + // --- Message editor help --- + 'ui.terminalserver.editor.help.title' => 'MESSAGE EDITOR HELP', + 'ui.terminalserver.editor.help.separator' => '-------------------', + 'ui.terminalserver.editor.help.navigate' => 'Arrow Keys = Navigate cursor', + 'ui.terminalserver.editor.help.edit' => 'Backspace/Delete = Edit text', + 'ui.terminalserver.editor.help.help' => 'Ctrl+K = Help', + 'ui.terminalserver.editor.help.start_of_line' => 'Ctrl+A = Start of line', + 'ui.terminalserver.editor.help.end_of_line' => 'Ctrl+E = End of line', + 'ui.terminalserver.editor.help.delete_line' => 'Ctrl+Y = Delete entire line', + 'ui.terminalserver.editor.help.save' => 'Ctrl+Z = Save message and send', + 'ui.terminalserver.editor.help.cancel' => 'Ctrl+C = Cancel and discard message', + + // --- Compose (shared between netmail and echomail) --- + 'ui.terminalserver.compose.to_name' => 'To Name: ', + 'ui.terminalserver.compose.to_address' => 'To Address: ', + 'ui.terminalserver.compose.subject' => 'Subject: ', + 'ui.terminalserver.compose.no_recipient' => 'Recipient name required. Message cancelled.', + 'ui.terminalserver.compose.enter_message' => 'Enter your message below:', + 'ui.terminalserver.compose.select_tagline' => 'Select a tagline:', + 'ui.terminalserver.compose.no_tagline' => ' 0) None', + 'ui.terminalserver.compose.tagline_default' => 'Tagline # [{default}] (Enter for Default): ', + 'ui.terminalserver.compose.tagline_none' => 'Tagline # (Enter for None): ', + 'ui.terminalserver.compose.message_cancelled' => 'Message cancelled (empty).', + + // --- Echomail --- + 'ui.terminalserver.echomail.no_areas' => 'No echoareas available.', + 'ui.terminalserver.echomail.areas_header' => 'Echoareas (page {page}/{total}):', + 'ui.terminalserver.echomail.areas_nav' => 'Enter #, n/p (next/prev), q (quit)', + 'ui.terminalserver.echomail.no_messages' => 'No echomail messages.', + 'ui.terminalserver.echomail.messages_header' => 'Echomail: {area} (page {page}/{total})', + 'ui.terminalserver.echomail.compose_title' => '=== Compose Echomail ===', + 'ui.terminalserver.echomail.area_label' => 'Area: {area}', + 'ui.terminalserver.echomail.posting' => 'Posting echomail...', + 'ui.terminalserver.echomail.post_success' => '✓ Echomail posted successfully!', + 'ui.terminalserver.echomail.post_failed' => '✗ Failed to post echomail: {error}', + + // --- Netmail --- + 'ui.terminalserver.netmail.no_messages' => 'No netmail messages.', + 'ui.terminalserver.netmail.header' => 'Netmail (page {page}/{total}):', + 'ui.terminalserver.netmail.compose_title' => '=== Compose Netmail ===', + 'ui.terminalserver.netmail.sending' => 'Sending netmail...', + 'ui.terminalserver.netmail.send_success' => '✓ Netmail sent successfully!', + 'ui.terminalserver.netmail.send_failed' => '✗ Failed to send netmail: {error}', + 'ui.terminalserver.netmail.attachments_none' => 'No file attachments on this message.', + 'ui.terminalserver.netmail.attachments_header' => 'Attachments:', + 'ui.terminalserver.netmail.attachment_download_prompt' => 'Attachment # to download (Enter to cancel): ', + + // --- Polls --- + 'ui.terminalserver.polls.disabled' => 'Voting booth is disabled.', + 'ui.terminalserver.polls.title' => 'Polls', + 'ui.terminalserver.polls.no_polls' => 'No active polls.', + 'ui.terminalserver.polls.detail_title' => 'Poll Detail', + 'ui.terminalserver.polls.total_votes' => 'Total votes: {count}', + 'ui.terminalserver.polls.enter_poll' => 'Enter poll # or Q to return: ', + 'ui.terminalserver.polls.vote_prompt' => 'Vote with option # or Q to return: ', + 'ui.terminalserver.polls.voted' => 'Vote recorded.', + + // --- Shoutbox --- + 'ui.terminalserver.shoutbox.title' => 'Shoutbox', + 'ui.terminalserver.shoutbox.recent_title' => 'Recent Shoutbox', + 'ui.terminalserver.shoutbox.no_messages' => 'No shoutbox messages.', + 'ui.terminalserver.shoutbox.menu' => '[P]ost [R]efresh [Q]uit: ', + 'ui.terminalserver.shoutbox.new_shout' => 'New shout (blank to cancel): ', + 'ui.terminalserver.shoutbox.posted' => 'Shout posted.', + 'ui.terminalserver.shoutbox.post_failed' => 'Failed to post shout.', + + // --- Main menu: files --- + 'ui.terminalserver.server.menu.files' => 'F) Files', + + // --- File areas --- + 'ui.terminalserver.files.no_areas' => 'No file areas available.', + 'ui.terminalserver.files.areas_header' => 'File Areas (page {page}/{total}):', + 'ui.terminalserver.files.areas_nav' => 'Enter #, n/p (next/prev), q (quit)', + 'ui.terminalserver.files.area_header' => 'Files: {area} (page {page}/{total})', + 'ui.terminalserver.files.no_files' => 'No files in this area.', + 'ui.terminalserver.files.files_nav' => 'D)ownload n/p (next/prev) Q)uit', + 'ui.terminalserver.files.files_nav_upload' => 'D)ownload U)pload n/p (next/prev) Q)uit', + 'ui.terminalserver.files.files_nav_upload_only' => 'U)pload n/p (next/prev) Q)uit', + 'ui.terminalserver.files.files_nav_none' => 'n/p (next/prev) Q)uit', + 'ui.terminalserver.files.transfer_unavailable' => 'ZMODEM disabled: install lrzsz (sz/rz) on the server to enable transfers.', + 'ui.terminalserver.files.invalid_selection' => 'Invalid selection.', + 'ui.terminalserver.files.download_prompt' => 'File # to download (Enter to cancel): ', + 'ui.terminalserver.files.download_error' => 'File not found on server.', + 'ui.terminalserver.files.download_starting' => 'Starting ZMODEM download: {name}', + 'ui.terminalserver.files.download_hint' => 'Start ZMODEM receive in your terminal now...', + 'ui.terminalserver.files.download_done' => 'Transfer complete.', + 'ui.terminalserver.files.download_failed' => 'Transfer failed or was cancelled.', + 'ui.terminalserver.files.upload_title' => '=== Upload File ===', + 'ui.terminalserver.files.upload_area' => 'Area: {area}', + 'ui.terminalserver.files.upload_desc_prompt' => 'Short description (blank to cancel): ', + 'ui.terminalserver.files.upload_cancelled' => 'Upload cancelled.', + 'ui.terminalserver.files.upload_starting' => 'Start ZMODEM send in your terminal now...', + 'ui.terminalserver.files.upload_failed' => 'Transfer failed or was cancelled.', + 'ui.terminalserver.files.upload_done' => 'File uploaded successfully (ID: {id}).', + 'ui.terminalserver.files.upload_error' => 'Upload error: {error}', + 'ui.terminalserver.files.upload_duplicate' => 'This file already exists in this area.', + 'ui.terminalserver.files.upload_readonly' => 'This area is read-only. Uploads are not permitted.', + 'ui.terminalserver.files.upload_admin_only' => 'Only administrators can upload to this area.', + + // --- Main menu: terminal settings --- + 'ui.terminalserver.server.menu.terminal_settings' => 'T) Terminal Settings', + + // --- Terminal settings page --- + 'ui.terminalserver.settings.title' => '=== Terminal Settings ===', + 'ui.terminalserver.settings.charset_label' => 'Character set : {value}', + 'ui.terminalserver.settings.ansi_label' => 'ANSI color : {value}', + 'ui.terminalserver.settings.not_set' => 'Not configured', + 'ui.terminalserver.settings.menu_detect' => 'D) Run detection wizard', + 'ui.terminalserver.settings.menu_charset' => 'C) Change character set manually', + 'ui.terminalserver.settings.menu_ansi' => 'A) Toggle ANSI color', + 'ui.terminalserver.settings.menu_quit' => 'Q) Return to main menu', + 'ui.terminalserver.settings.saved' => 'Settings saved.', + 'ui.terminalserver.settings.save_failed' => 'Warning: could not save settings.', + 'ui.terminalserver.settings.invalid_choice' => 'Invalid choice.', + 'ui.terminalserver.settings.charset_prompt' => 'Select: (U)TF-8, (C)P437, (A)SCII: ', + + // --- Terminal detection wizard --- + 'ui.terminalserver.detect.title' => '=== Terminal Setup ===', + 'ui.terminalserver.detect.intro' => 'BBS will now test your terminal to ensure content displays correctly.', + 'ui.terminalserver.detect.charset_intro' => 'Character set test:', + 'ui.terminalserver.detect.charset_question' => 'Do the above appear as arrows, checkmarks, and accented letters? (Y/N): ', + 'ui.terminalserver.detect.charset_utf8' => 'UTF-8 character set enabled.', + 'ui.terminalserver.detect.charset_cp437_intro' => 'CP437 box-drawing test:', + 'ui.terminalserver.detect.charset_cp437_question' => 'Do the above appear as a box drawn with lines and corners? (Y/N): ', + 'ui.terminalserver.detect.charset_cp437' => 'CP437 (DOS/ANSI) character set enabled.', + 'ui.terminalserver.detect.charset_ascii' => 'ASCII mode enabled.', + 'ui.terminalserver.detect.ansi_intro' => 'Color test:', + 'ui.terminalserver.detect.ansi_question' => 'Do the words above appear in different colors? (Y/N): ', + 'ui.terminalserver.detect.ansi_yes' => 'ANSI color enabled.', + 'ui.terminalserver.detect.ansi_no' => 'ANSI color disabled.', + 'ui.terminalserver.detect.complete' => 'Terminal setup complete. Settings saved.', + 'ui.terminalserver.detect.press_enter' => 'Press Enter to continue...', + + // --- Door games --- + 'ui.terminalserver.doors.no_doors' => 'No doors are currently available.', + 'ui.terminalserver.doors.title' => '=== Door Games ===', + 'ui.terminalserver.doors.enter_choice' => 'Enter number to play, or Q to return: ', + 'ui.terminalserver.doors.invalid' => 'Invalid selection.', + 'ui.terminalserver.doors.launching' => 'Launching {name}...', + 'ui.terminalserver.doors.launch_error' => 'Error: {error}', + 'ui.terminalserver.doors.connecting' => 'Connecting to game server...', + 'ui.terminalserver.doors.connect_failed' => 'Could not connect to game bridge. Is the DOS door bridge running?', + 'ui.terminalserver.doors.connected' => 'Connected! Starting game...', + 'ui.terminalserver.doors.returned' => 'Returned from {name}.', + + 'ui.terminalserver.message.headers_title' => '=== Message Headers ===', + 'ui.terminalserver.message.no_headers' => '(No message headers)', +]; diff --git a/config/i18n/es/common.php b/config/i18n/es/common.php new file mode 100644 index 000000000..13555edaa --- /dev/null +++ b/config/i18n/es/common.php @@ -0,0 +1,2663 @@ + 'Pronto', + 'time.in_hours' => 'En {count} hora{suffix}', + 'time.tomorrow' => 'Manana', + 'time.in_days' => 'En {count} dias', + 'time.just_now' => 'Ahora mismo', + 'time.minutes_ago' => 'Hace {count} minutos', + 'time.hours_ago' => 'Hace {count} hora{suffix}', + 'time.yesterday' => 'Ayer', + 'time.days_ago' => 'Hace {count} dias', + 'time.suffix_plural' => 's', + 'time.suffix_singular' => '', + + // Shared/common UI defaults. + 'errors.failed_load_messages' => 'No se pudieron cargar los mensajes', + 'messages.none_found' => 'No se encontraron mensajes', + 'messages.no_subject' => '(Sin asunto)', + 'ui.common.success' => 'Exito', + 'ui.common.error' => 'Error', + 'ui.common.unknown_error' => 'Error desconocido', + 'ui.common.saving' => 'Guardando...', + 'ui.common.copy_failed_manual' => 'No se pudo copiar al portapapeles. Copie manualmente.', + 'ui.common.copy_not_supported_manual' => 'El portapapeles no es compatible. Copie manualmente.', + 'ui.common.loading' => 'Cargando...', + 'ui.common.loading_messages' => 'Cargando mensajes...', + 'ui.common.loading_message' => 'Cargando mensaje...', + 'ui.common.search' => 'Buscar', + 'ui.common.search_messages' => 'Buscar mensajes', + 'ui.common.import' => 'Importar', + 'ui.common.search_placeholder' => 'Buscar...', + 'ui.common.search_messages_placeholder' => 'Buscar mensajes...', + 'ui.common.all' => 'Todos', + 'ui.common.all_messages' => 'Todos los mensajes', + 'ui.common.unread' => 'No leidos', + 'ui.common.read' => 'Leidos', + 'ui.common.sent' => 'Enviados', + 'ui.common.drafts' => 'Borradores', + 'ui.common.has_attachment' => 'Tiene adjunto', + 'ui.common.select' => 'Seleccionar', + 'ui.common.selected' => 'Seleccionados', + 'ui.common.total' => 'Total', + 'ui.common.recent' => 'Reciente', + 'ui.common.statistics' => 'Estadisticas', + 'ui.common.manage' => 'Gestionar', + 'ui.common.mark_as_read' => 'Marcar como leido', + 'ui.common.message' => 'Mensaje', + 'ui.common.delete' => 'Eliminar', + 'ui.common.confirm_delete' => 'Confirmar eliminacion', + 'ui.common.edit' => 'Editar', + 'ui.common.close' => 'Cerrar', + 'ui.common.refresh' => 'Actualizar', + 'ui.common.hostname' => 'Hostname', + 'ui.common.port' => 'Puerto', + 'ui.common.password' => 'Contrasena', + 'ui.common.reply' => 'Responder', + 'ui.common.username' => 'Usuario', + 'ui.common.real_name' => 'Nombre real', + 'ui.common.email' => 'Correo', + 'ui.common.email_address' => 'Direccion de correo', + 'ui.common.location' => 'Ubicacion', + 'ui.common.address' => 'Direccion', + 'ui.common.domain' => 'Dominio', + 'ui.common.ip_address' => 'Direccion IP', + 'ui.common.status' => 'Estado', + 'ui.common.from' => 'De', + 'ui.common.actions' => 'Acciones', + 'ui.common.view' => 'Ver', + 'ui.common.active' => 'Activo', + 'ui.common.inactive' => 'Inactivo', + 'ui.common.user' => 'Usuario', + 'ui.common.admin' => 'Admin', + 'ui.common.created' => 'Creado', + 'ui.common.last_login' => 'Ultimo acceso', + 'ui.common.today' => 'Hoy', + 'ui.common.one_day_ago' => 'hace 1 dia', + 'ui.common.days_ago' => 'hace {count} dias', + 'ui.common.enable' => 'Habilitar', + 'ui.common.disable' => 'Deshabilitar', + 'ui.common.previous' => 'Anterior', + 'ui.common.next' => 'Siguiente', + 'ui.common.go_back' => 'Volver', + 'ui.common.previous_message' => 'Mensaje anterior', + 'ui.common.next_message' => 'Mensaje siguiente', + 'ui.common.toggle_fullscreen' => 'Alternar pantalla completa', + 'ui.common.cancel' => 'Cancelar', + 'ui.common.clear_search' => 'Limpiar busqueda', + 'ui.common.save' => 'Guardar', + 'ui.common.save_changes' => 'Guardar cambios', + 'ui.common.saved' => 'Guardado!', + 'ui.common.saved_short' => 'Guardado', + 'ui.common.error_click_retry' => 'Error - haga clic para reintentar', + 'ui.common.share' => 'Compartir', + 'ui.common.share_url' => 'URL para compartir:', + 'ui.common.copy' => 'Copiar', + 'ui.common.optional' => 'opcional', + 'ui.common.never' => 'Nunca', + 'ui.common.unknown' => 'Desconocido', + 'ui.common.not_configured' => 'No configurado', + 'ui.common.name' => 'Nombre', + 'ui.common.description' => 'Descripcion', + 'ui.common.label' => 'Etiqueta', + 'ui.common.url' => 'URL', + 'ui.common.new_tab' => 'Nueva pestana', + 'ui.common.key' => 'Tecla', + 'ui.common.id' => 'ID', + 'ui.common.preview' => 'Vista previa', + 'ui.common.yes' => 'Si', + 'ui.common.no' => 'No', + 'ui.common.not_applicable' => 'n/a', + 'ui.common.sort.newest_first' => 'Mas nuevos primero', + 'ui.common.sort.oldest_first' => 'Mas antiguos primero', + 'ui.common.sort.by_subject' => 'Por asunto', + 'ui.common.sort.by_author' => 'Por autor', + 'ui.common.threading.show_threaded' => 'Mostrar hilos', + 'ui.common.threading.show_flat' => 'Mostrar plano', + 'ui.common.from_label' => 'De:', + 'ui.common.to_label' => 'Para:', + 'ui.common.date_label' => 'Fecha:', + 'ui.common.subject_label' => 'Asunto:', + 'ui.common.subject_label_short' => 'Asunto', + 'ui.common.area_label' => 'Area:', + 'ui.common.sent_prefix' => 'Enviado:', + 'ui.common.received_prefix' => 'Recibido:', + 'ui.common.written_prefix' => 'Escrito:', + 'ui.common.shared' => 'Compartido', + 'ui.common.remove_from_saved' => 'Quitar de guardados', + 'ui.common.save_for_later' => 'Guardar para despues', + 'ui.common.delete_message' => 'Eliminar mensaje', + 'ui.common.delete_draft' => 'Eliminar borrador', + 'ui.common.already_in_address_book' => 'Ya esta en la libreta de direcciones', + 'ui.common.save_to_address_book' => 'Guardar en la libreta de direcciones', + 'ui.common.file_attachments_with_count' => 'Archivos adjuntos ({count})', + 'ui.common.send_netmail_to' => 'Enviar netmail a {name}', + 'ui.common.replies_with_count' => '{count} respuestas', + 'ui.common.unknown_size' => 'Tamano desconocido', + 'ui.common.kludge_lines' => 'Lineas Kludge', + 'ui.common.show_kludge_lines' => 'Mostrar lineas Kludge', + 'ui.common.hide_kludge_lines' => 'Ocultar lineas Kludge', + 'ui.common.no_kludge_lines_found' => 'No se encontraron lineas Kludge', + 'ui.common.time.1_hour' => '1 hora', + 'ui.common.time.24_hours' => '24 horas', + 'ui.common.time.1_week' => '1 semana', + 'ui.common.time.30_days' => '30 dias', + + // About Page + 'ui.about.title' => 'Acerca de', + 'ui.about.about_system' => 'Acerca de {system_name}', + 'ui.about.system_name' => 'Nombre del sistema', + 'ui.about.sysop' => 'Sysop', + 'ui.about.network_addresses' => 'Direcciones de red', + 'ui.about.fidonet_address' => 'Direccion Fidonet', + 'ui.about.software' => 'Software', + 'ui.about.links' => 'Enlaces', + 'ui.about.house_rules' => 'Reglas de la casa', + 'ui.footer.powered_by' => 'Desarrollado por', + + // 404 Page + 'ui.error404.title' => 'Pagina no encontrada', + 'ui.error404.description' => 'La pagina que busca no existe o se movio a otra ubicacion.', + 'ui.error404.requested_prefix' => 'Solicitado:', + 'ui.error404.dashboard' => 'Panel', + 'ui.error404.go_back' => 'Volver', + 'ui.error404.contact_sysop' => 'Si cree que esto es un error, contacte al operador del sistema.', + + // Generic Error Page + 'ui.error.title' => 'Error', + 'ui.error.access_error' => 'Error de acceso', + 'ui.error.processing_request_failed' => 'Ocurrio un error mientras se procesaba su solicitud.', + 'ui.error.return_to_dashboard' => 'Volver al panel', + 'ui.web.errors.binkp_admin_only' => 'Solo los administradores pueden acceder a la funcionalidad BinkP.', + 'ui.web.errors.chat_disabled' => 'Lo sentimos, el chat no esta habilitado.', + 'ui.web.errors.user_management_admin_only' => 'Solo los administradores pueden acceder a la gestion de usuarios.', + 'ui.web.errors.profile_user_not_found' => 'Usuario no encontrado o inactivo.', + 'ui.web.errors.echoareas_admin_only' => 'Solo los administradores pueden gestionar las areas de eco.', + 'ui.web.errors.fileareas_admin_only' => 'Solo los administradores pueden gestionar las areas de archivos.', + 'ui.web.errors.files_feature_disabled' => 'Las areas de archivos estan deshabilitadas.', + 'ui.web.errors.polls_disabled' => 'El modulo de votacion esta deshabilitado.', + 'ui.web.errors.shoutbox_disabled' => 'El shoutbox esta deshabilitado.', + 'ui.web.errors.compose_type_invalid' => 'Destino de composicion invalido.', + 'ui.web.fallback.system_name' => 'Sistema BinktermPHP', + + // Base Layout / Navigation + 'ui.base.update_available' => 'Actualizacion disponible', + 'ui.base.new_version_ready' => 'Hay una nueva version lista', + 'ui.base.reload_now' => 'Recargar ahora', + 'ui.base.messaging' => 'Mensajeria', + 'ui.base.netmail' => 'Netmail', + 'ui.base.echomail' => 'Echomail', + 'ui.base.local_chat' => 'Chat local', + 'ui.base.mrc_chat' => 'Chat MRC', + 'ui.base.doors_games' => 'Puertas y juegos', + 'ui.base.files' => 'Archivos', + 'ui.base.nodelist' => 'Nodelist', + 'ui.base.admin' => 'Admin', + 'ui.base.profile' => 'Perfil', + 'ui.base.settings' => 'Configuracion', + 'ui.base.subscriptions' => 'Suscripciones', + 'ui.base.logout' => 'Cerrar sesion', + 'ui.base.login' => 'Iniciar sesion', + 'ui.base.guest_doors' => 'Puertas de invitados', + 'ui.base.admin.whos_online' => 'Quien esta en linea', + 'ui.base.admin.dashboard' => 'Panel', + 'ui.base.admin.binkp_status' => 'Estado de Binkp', + 'ui.base.admin.manage_users' => 'Gestionar usuarios', + 'ui.base.admin.ads' => 'Anuncios', + 'ui.base.admin.area_management' => 'Gestion de areas', + 'ui.base.admin.echo_areas' => 'Areas de eco', + 'ui.base.admin.file_areas' => 'Areas de archivos', + 'ui.base.admin.file_area_rules' => 'Reglas de areas de archivos', + 'ui.base.admin.subscriptions' => 'Suscripciones', + 'ui.base.admin.auto_feed' => 'Auto Feed', + 'ui.base.admin.chat_rooms' => 'Salas de chat', + 'ui.base.admin.mrc_settings' => 'Configuracion de MRC', + 'ui.base.admin.polls' => 'Encuestas', + 'ui.base.admin.shoutbox' => 'Shoutbox', + 'ui.base.admin.doors' => 'Puertas', + 'ui.base.admin.doors_dos' => 'Puertas DOS', + 'ui.base.admin.doors_native' => 'Puertas nativas', + 'ui.base.admin.doors_web' => 'Puertas web', + 'ui.base.admin.activity_stats' => 'Estadisticas de actividad', + 'ui.base.admin.economy_viewer' => 'Visor de economia', + 'ui.base.admin.bbs_settings' => 'Configuracion del BBS', + 'ui.base.admin.appearance' => 'Apariencia', + 'ui.base.admin.binkp_configuration' => 'Configuracion de Binkp', + 'ui.base.admin.template_editor' => 'Editor de plantillas', + 'ui.base.admin.i18n_overrides' => 'Ajustes de idioma', + 'ui.base.admin.help' => 'Ayuda', + 'ui.base.admin.readme' => 'README', + 'ui.base.admin.faq' => 'FAQ', + 'ui.base.admin.upgrade_notes' => 'Notas de actualizacion v{version}', + 'ui.base.admin.claudes_bbs' => 'BBS de Claude', + 'ui.base.admin.report_issue' => 'Reportar problema', + + // Admin Users + 'ui.admin_users.pending_users_error' => 'Error de usuarios pendientes:', + 'ui.admin_users.pending_users_load_failed_prefix' => 'No se pudieron cargar los usuarios pendientes: ', + 'ui.admin_users.all_users_error' => 'Error de todos los usuarios:', + 'ui.admin_users.users_load_failed_prefix' => 'No se pudieron cargar los usuarios: ', + 'ui.admin_users.processing' => 'Procesando...', + 'ui.admin_users.user_approved_success' => 'Usuario aprobado correctamente', + 'ui.admin_users.user_rejected_success' => 'Usuario rechazado correctamente', + 'ui.admin_users.user_action_failed_prefix' => 'No se pudo {action} el usuario: ', + 'ui.admin_users.user_action_failed_short' => 'No se pudo {action} el usuario', + 'ui.admin_users.success_prefix' => 'Exito: ', + 'ui.admin_users.error_prefix' => 'Error: ', + 'ui.admin_users.user_data_load_failed_prefix' => 'No se pudieron cargar los datos del usuario: ', + 'ui.admin_users.password_min_length' => 'La contrasena debe tener al menos 8 caracteres', + 'ui.admin_users.user_updated_success' => 'Usuario actualizado correctamente', + 'ui.admin_users.user_update_failed_prefix' => 'No se pudo actualizar el usuario: ', + 'ui.admin_users.user_toggle_confirm' => 'Esta seguro de que desea {action} este usuario?', + 'ui.admin_users.user_toggled_success' => 'Usuario {action}do correctamente', + 'ui.admin_users.required_fields' => 'Se requieren usuario, nombre real y contrasena', + 'ui.admin_users.username_validation' => 'El usuario debe tener 3-20 caracteres: letras, numeros y guion bajo', + 'ui.admin_users.creating' => 'Creando...', + 'ui.admin_users.user_created_success' => 'Usuario creado correctamente', + 'ui.admin_users.user_create_failed_prefix' => 'No se pudo crear el usuario: ', + 'ui.admin_users.user_create_failed' => 'No se pudo crear el usuario', + 'ui.admin_users.cleanup_confirm' => 'Esto eliminara todos los registros de registro aprobados y los rechazados antiguos (30+ dias). Esta seguro?', + 'ui.admin_users.cleaning' => 'Limpiando...', + 'ui.admin_users.cleanup_success' => 'Limpieza completada. Se eliminaron {approved} aprobados y {rejected} rechazados antiguos ({total} total).', + 'ui.admin_users.cleanup_failed_prefix' => 'No se pudo limpiar los registros: ', + 'ui.admin_users.send_reminder_confirm' => 'Enviar recordatorio de cuenta a {username}? Esto enviara un recordatorio por netmail y correo electronico (si esta disponible).', + 'ui.admin_users.reminder_sent_netmail_email_suffix' => ' (enviado por netmail y correo electronico)', + 'ui.admin_users.reminder_sent_netmail_only_suffix' => ' (enviado solo por netmail)', + 'ui.admin_users.send_reminder_failed_prefix' => 'No se pudo enviar el recordatorio: ', + 'ui.admin_users.send_reminder_failed' => 'No se pudo enviar el recordatorio', + 'ui.admin_users.no_reminders_needed' => 'Ningun usuario necesita recordatorios de cuenta actualmente. Todos iniciaron sesion.', + 'ui.admin_users.reminders_found_header' => "Se encontraron {count} usuario(s) que aun no iniciaron sesion:\n\n", + 'ui.admin_users.reminder_user_line' => '- {username} ({real_name}) - creado {created_date}' . "\n", + 'ui.admin_users.no_real_name' => 'Sin nombre real', + 'ui.admin_users.reminders_found_footer' => "\nPuede enviar recordatorios individuales usando los botones \"Remind\" en la lista de usuarios.", + 'ui.admin_users.reminders_load_failed_prefix' => 'No se pudieron cargar los usuarios que necesitan recordatorios: ', + 'ui.admin_users.page_title' => 'Gestion de usuarios', + 'ui.admin_users.heading' => 'Gestion de usuarios', + 'ui.admin_users.create_new_user' => 'Crear nuevo usuario', + 'ui.admin_users.need_reminders' => 'Necesitan recordatorios', + 'ui.admin_users.cleanup' => 'Limpieza', + 'ui.admin_users.pending_registrations' => 'Registros pendientes', + 'ui.admin_users.loading_pending_registrations' => 'Cargando registros pendientes...', + 'ui.admin_users.all_users' => 'Todos los usuarios', + 'ui.admin_users.search_users_placeholder' => 'Buscar usuarios...', + 'ui.admin_users.per_page_25' => '25 por pagina', + 'ui.admin_users.per_page_50' => '50 por pagina', + 'ui.admin_users.per_page_100' => '100 por pagina', + 'ui.admin_users.loading_users' => 'Cargando usuarios...', + 'ui.admin_users.approve_user' => 'Aprobar usuario', + 'ui.admin_users.admin_notes' => 'Notas de admin', + 'ui.admin_users.optional_notes_placeholder' => 'Notas opcionales sobre esta decision...', + 'ui.admin_users.approve' => 'Aprobar', + 'ui.admin_users.reject' => 'Rechazar', + 'ui.admin_users.create_user' => 'Crear usuario', + 'ui.admin_users.edit_user' => 'Editar usuario', + 'ui.admin_users.account_is_active' => 'La cuenta esta activa', + 'ui.admin_users.inactive_cannot_login' => 'Los usuarios inactivos no pueden iniciar sesion', + 'ui.admin_users.admin_privileges' => 'Privilegios de administrador', + 'ui.admin_users.admin_privileges_help' => 'Los administradores pueden gestionar usuarios y configuracion del sistema', + 'ui.admin_users.email_optional_help' => 'Opcional: para recuperacion de cuenta y notificaciones', + 'ui.admin_users.min_8_characters' => 'Minimo 8 caracteres', + 'ui.admin_users.username_cannot_change' => 'El usuario no se puede cambiar', + 'ui.admin_users.real_name_cannot_change' => 'El nombre real no se puede cambiar', + 'ui.admin_users.new_password' => 'Nueva contrasena', + 'ui.admin_users.new_password_help' => 'Deje en blanco para mantener la contrasena actual. Minimo 8 caracteres si la cambia.', + 'ui.admin_users.no_pending_registrations' => 'No hay registros pendientes', + 'ui.admin_users.requested' => 'Solicitado', + 'ui.admin_users.last_reminded' => 'Ultimo recordatorio', + 'ui.admin_users.not_provided' => 'No proporcionado', + 'ui.admin_users.view_profile' => 'Ver perfil', + 'ui.admin_users.send_account_reminder' => 'Enviar recordatorio de cuenta', + 'ui.admin_users.remind' => 'Recordar', + 'ui.admin_users.referred_by' => 'Referido por:', + 'ui.admin_users.basic_information' => 'Informacion basica', + 'ui.admin_users.request_details' => 'Detalles de la solicitud', + 'ui.admin_users.reason_for_joining' => 'Motivo para unirse:', + 'ui.admin_users.no_reason_provided' => 'No se proporciono motivo', + 'ui.admin_users.user_agent' => 'Agente de usuario:', + 'ui.admin_users.not_available' => 'No disponible', + 'ui.admin_users.user_details' => 'Detalles del usuario', + 'ui.admin_users.approve_user_registration' => 'Aprobar registro de usuario', + 'ui.admin_users.reject_user_registration' => 'Rechazar registro de usuario', + 'ui.admin_users.users_pagination_aria' => 'Paginacion de usuarios', + 'ui.admin_users.showing_users_range' => 'Mostrando {start}-{end} de {total} usuarios', + + // Admin Appearance + 'ui.admin.appearance.load_failed_prefix' => 'No se pudo cargar la configuracion de apariencia: ', + 'ui.admin.appearance.save_failed' => 'No se pudo guardar', + 'ui.admin.appearance.reset_menu_confirm' => 'Restablecer elementos del menu a los valores predeterminados?', + 'ui.admin.appearance.shell_saved_reload' => 'Guardado. Recargue la pagina para ver el nuevo shell.', + 'ui.admin.appearance.upload_failed_prefix' => 'Fallo la carga: ', + 'ui.admin.appearance.delete_shell_art_confirm' => 'Eliminar {name}?', + 'ui.admin.appearance.delete_failed_prefix' => 'Fallo la eliminacion: ', + 'ui.admin.appearance.error_prefix' => 'Error: ', + 'ui.admin.appearance.request_failed' => 'Solicitud fallida', + 'ui.admin.appearance.page_title' => 'Apariencia y contenido - Admin', + 'ui.admin.appearance.heading' => 'Apariencia y contenido', + 'ui.admin.appearance.tab_branding' => 'Marca', + 'ui.admin.appearance.tab_content' => 'Contenido', + 'ui.admin.appearance.tab_navigation' => 'Navegacion', + 'ui.admin.appearance.tab_seo' => 'SEO y publico', + 'ui.admin.appearance.tab_shell' => 'Shell', + 'ui.admin.appearance.tab_message_reader' => 'Lector de mensajes', + 'ui.admin.appearance.invalid_hex_color' => 'Color hex invalido', + 'ui.admin.appearance.no_content' => 'Sin contenido', + 'ui.admin.appearance.preview_failed' => 'Fallo la vista previa', + 'ui.admin.appearance.branding.title' => 'Marca', + 'ui.admin.appearance.branding.accent_color' => 'Color de acento', + 'ui.admin.appearance.branding.pick_accent_color' => 'Elegir color de acento', + 'ui.admin.appearance.branding.reset' => 'Restablecer', + 'ui.admin.appearance.branding.accent_help' => 'Reemplaza el color principal de navegacion y botones en todo el sitio. Deje en blanco para el valor predeterminado.', + 'ui.admin.appearance.branding.default_theme' => 'Tema predeterminado', + 'ui.admin.appearance.branding.system_default' => '(Predeterminado del sistema)', + 'ui.admin.appearance.branding.default_theme_help' => 'Tema aplicado a todos los usuarios de forma predeterminada.', + 'ui.admin.appearance.branding.lock_theme' => 'Bloquear tema (evita que los usuarios lo cambien)', + 'ui.admin.appearance.branding.logo_url' => 'URL del logo', + 'ui.admin.appearance.branding.logo_url_help' => 'URL de una imagen de logo mostrada en la barra de navegacion. Deje en blanco para usar el nombre del sistema.', + 'ui.admin.appearance.branding.footer_text' => 'Texto del pie', + 'ui.admin.appearance.branding.footer_placeholder' => 'Deje en blanco para la linea predeterminada de nodo/sysop', + 'ui.admin.appearance.branding.footer_help' => 'Texto personalizado para el pie. Deje en blanco para la linea de informacion predeterminada del sistema.', + 'ui.admin.appearance.branding.save' => 'Guardar marca', + 'ui.admin.appearance.content.system_news_title' => 'Noticias del sistema (MOTD)', + 'ui.admin.appearance.content.system_news_placeholder' => 'Ingrese noticias del sistema en formato Markdown...', + 'ui.admin.appearance.content.system_news_help' => 'Compatible con Markdown. Se muestra en el panel. Deje en blanco para mostrar el predeterminado o systemnews.twig personalizado.', + 'ui.admin.appearance.content.house_rules_title' => 'Reglas de la casa', + 'ui.admin.appearance.content.house_rules_placeholder' => 'Ingrese reglas de la casa en formato Markdown...', + 'ui.admin.appearance.content.house_rules_help' => 'Compatible con Markdown. Se muestra en el modal de reglas de la casa enlazado desde el pie.', + 'ui.admin.appearance.content.site_announcement_title' => 'Anuncio del sitio', + 'ui.admin.appearance.content.show_announcement_banner' => 'Mostrar banner de anuncio', + 'ui.admin.appearance.content.announcement_text' => 'Texto del anuncio', + 'ui.admin.appearance.content.type' => 'Tipo', + 'ui.admin.appearance.content.type_info' => 'Info (azul)', + 'ui.admin.appearance.content.type_success' => 'Exito (verde)', + 'ui.admin.appearance.content.type_warning' => 'Advertencia (amarillo)', + 'ui.admin.appearance.content.type_danger' => 'Peligro (rojo)', + 'ui.admin.appearance.content.type_primary' => 'Primario', + 'ui.admin.appearance.content.expires_at_optional' => 'Expira en (opcional)', + 'ui.admin.appearance.content.expires_help' => 'Deje en blanco para no expirar nunca.', + 'ui.admin.appearance.content.dismissible' => 'Descartable', + 'ui.admin.appearance.content.save' => 'Guardar contenido', + 'ui.admin.appearance.navigation.title' => 'Enlaces de navegacion personalizados', + 'ui.admin.appearance.navigation.add_link' => 'Agregar enlace', + 'ui.admin.appearance.navigation.help' => 'Hasta 3 enlaces aparecen en linea en la barra de navegacion; 4 o mas se agrupan en un menu "Enlaces".', + 'ui.admin.appearance.navigation.url_placeholder' => '/pagina o https://...', + 'ui.admin.appearance.navigation.save' => 'Guardar navegacion', + 'ui.admin.appearance.seo.title' => 'SEO y paginas publicas', + 'ui.admin.appearance.seo.enable_about_page' => 'Habilitar pagina publica /about', + 'ui.admin.appearance.seo.about_page_help' => 'Cuando esta habilitada, una pagina publica /about muestra la informacion del sistema y reglas de la casa. Deshabilitada devuelve 404.', + 'ui.admin.appearance.seo.site_description' => 'Descripcion del sitio', + 'ui.admin.appearance.seo.max_300_chars' => '(max 300 caracteres)', + 'ui.admin.appearance.seo.description_help' => 'Se usa en la meta descripcion y etiquetas OG para buscadores y vistas previas de enlaces.', + 'ui.admin.appearance.seo.og_image_url' => 'URL de imagen OG', + 'ui.admin.appearance.seo.og_image_placeholder' => 'https://example.com/preview.png', + 'ui.admin.appearance.seo.og_image_help' => 'Imagen mostrada cuando se comparte el sitio en redes sociales. Debe ser al menos 1200x630px.', + 'ui.admin.appearance.seo.save' => 'Guardar ajustes SEO', + 'ui.admin.appearance.shell.title' => 'Shell de interfaz', + 'ui.admin.appearance.shell.default_interface_style' => 'Estilo de interfaz predeterminado', + 'ui.admin.appearance.shell.web_interface' => 'Interfaz web', + 'ui.admin.appearance.shell.web_interface_help' => 'Barra de navegacion completa, menus desplegables, diseno web estandar', + 'ui.admin.appearance.shell.bbs_menu' => 'Menu BBS', + 'ui.admin.appearance.shell.bbs_menu_help' => 'Encabezado minimo, navegacion por teclas rapidas, menu de tarjetas/texto/ANSI', + 'ui.admin.appearance.shell.lock_shell' => 'Bloquear shell (evita que los usuarios lo cambien en su configuracion)', + 'ui.admin.appearance.shell.bbs_menu_config' => 'Configuracion del menu BBS', + 'ui.admin.appearance.shell.menu_variant' => 'Variante de menu', + 'ui.admin.appearance.shell.variant_cards' => 'Cuadricula de tarjetas', + 'ui.admin.appearance.shell.variant_text' => 'Texto terminal', + 'ui.admin.appearance.shell.variant_ansi' => 'Arte ANSI', + 'ui.admin.appearance.shell.ansi_art_file' => 'Archivo de arte ANSI', + 'ui.admin.appearance.shell.none_selected' => '(ninguno seleccionado)', + 'ui.admin.appearance.shell.ansi_file_help' => 'Seleccione un archivo .ans cargado para mostrar sobre el menu.', + 'ui.admin.appearance.shell.upload_ans' => 'Subir .ans', + 'ui.admin.appearance.shell.max_512_kb' => 'Max 512 KB.', + 'ui.admin.appearance.shell.delete_selected' => 'Eliminar seleccionado', + 'ui.admin.appearance.shell.menu_items' => 'Elementos del menu', + 'ui.admin.appearance.shell.menu_items_help' => 'Define las teclas rapidas y destinos del menu principal.', + 'ui.admin.appearance.shell.icon' => 'Icono', + 'ui.admin.appearance.shell.icon_help' => '(nombre de Font Awesome)', + 'ui.admin.appearance.shell.icon_placeholder' => 'envelope', + 'ui.admin.appearance.shell.add_item' => 'Agregar elemento', + 'ui.admin.appearance.shell.reset_to_defaults' => 'Restablecer predeterminados', + 'ui.admin.appearance.shell.save' => 'Guardar ajustes del shell', + 'ui.admin.appearance.shell.could_not_load_file_list' => 'No se pudo cargar la lista de archivos.', + 'ui.admin.appearance.shell.uploading' => 'Subiendo...', + 'ui.admin.appearance.shell.uploaded_prefix' => 'Subido: ', + 'ui.admin.appearance.shell.deleted_prefix' => 'Eliminado: ', + 'ui.admin.appearance.default_menu.messages' => 'Mensajes', + 'ui.admin.appearance.default_menu.netmail' => 'Netmail', + 'ui.admin.appearance.default_menu.files' => 'Archivos', + 'ui.admin.appearance.default_menu.games_doors' => 'Juegos y puertas', + 'ui.admin.appearance.default_menu.settings' => 'Configuracion', + 'ui.admin.appearance.message_reader.title' => 'Lector de mensajes', + 'ui.admin.appearance.message_reader.layout' => 'Diseno', + 'ui.admin.appearance.message_reader.scrollable_body' => 'Cuerpo de mensaje desplazable', + 'ui.admin.appearance.message_reader.scrollable_body_help' => 'Cuando esta habilitado, el encabezado del mensaje (De, Para, Asunto, Fecha) permanece fijo arriba mientras el cuerpo se desplaza por separado. Cuando esta deshabilitado, todo el modal se desplaza junto (comportamiento predeterminado).', + 'ui.admin.appearance.message_reader.save' => 'Guardar ajustes del lector de mensajes', + + // Admin Binkp Config + 'ui.admin.binkp_config.load_failed' => 'No se pudo cargar la configuracion', + 'ui.admin.binkp_config.save_failed' => 'No se pudo guardar la configuracion', + 'ui.admin.binkp_config.reload_confirm' => 'Recargar la configuracion del demonio binkp? Esto enviara SIGHUP al demonio.', + 'ui.admin.binkp_config.reload_failed' => 'No se pudo recargar la configuracion', + 'ui.admin.binkp_config.remove_uplink_confirm' => 'Eliminar este uplink?', + 'ui.admin.binkp_config.page_title' => 'Configuracion de Binkp', + 'ui.admin.binkp_config.heading' => 'Configuracion de Binkp', + 'ui.admin.binkp_config.restart_notice' => 'Despues de guardar, reinicie los demonios para aplicar cambios.', + 'ui.admin.binkp_config.save_configuration' => 'Guardar configuracion', + 'ui.admin.binkp_config.reload_config' => 'Recargar configuracion', + 'ui.admin.binkp_config.reload_title' => 'Recargar configuracion sin reiniciar el demonio', + 'ui.admin.binkp_config.config_not_loaded' => 'Configuracion no cargada.', + 'ui.admin.binkp_config.configuration_saved' => 'Configuracion guardada.', + 'ui.admin.binkp_config.reloading' => 'Recargando configuracion...', + 'ui.admin.binkp_config.reloaded_success' => 'Configuracion recargada correctamente. El demonio aplico los cambios.', + 'ui.admin.binkp_config.current' => 'actual', + 'ui.admin.binkp_config.system.title' => 'Sistema', + 'ui.admin.binkp_config.system.system_name' => 'Nombre del sistema', + 'ui.admin.binkp_config.system.system_address' => 'Direccion del sistema', + 'ui.admin.binkp_config.system.sysop_name' => 'Nombre del sysop', + 'ui.admin.binkp_config.system.select_admin_user' => 'Seleccione un usuario admin.', + 'ui.admin.binkp_config.system.location' => 'Ubicacion', + 'ui.admin.binkp_config.system.timezone' => 'Zona horaria', + 'ui.admin.binkp_config.system.origin_line' => 'Linea de origen', + 'ui.admin.binkp_config.binkp.title' => 'Binkp', + 'ui.admin.binkp_config.binkp.timeout_seconds' => 'Tiempo de espera (segundos)', + 'ui.admin.binkp_config.binkp.max_connections' => 'Maximas conexiones', + 'ui.admin.binkp_config.binkp.bind_address' => 'Direccion de enlace', + 'ui.admin.binkp_config.binkp.preserve_processed_packets' => 'Preservar paquetes procesados', + 'ui.admin.binkp_config.security.title' => 'Seguridad', + 'ui.admin.binkp_config.security.allow_insecure_inbound' => 'Permitir entrada insegura', + 'ui.admin.binkp_config.security.insecure_receive_only' => 'Inseguro solo recepcion', + 'ui.admin.binkp_config.security.require_allowlist_for_insecure' => 'Requerir allowlist para inseguro', + 'ui.admin.binkp_config.security.max_insecure_sessions_per_hour' => 'Max sesiones inseguras por hora', + 'ui.admin.binkp_config.security.allow_plaintext_fallback' => 'Permitir fallback en texto plano', + 'ui.admin.binkp_config.crashmail.title' => 'Crashmail', + 'ui.admin.binkp_config.crashmail.enable_crashmail' => 'Habilitar crashmail', + 'ui.admin.binkp_config.crashmail.max_attempts' => 'Max intentos', + 'ui.admin.binkp_config.crashmail.retry_interval_minutes' => 'Intervalo de reintento (minutos)', + 'ui.admin.binkp_config.crashmail.use_nodelist_for_routing' => 'Usar nodelist para enrutamiento', + 'ui.admin.binkp_config.crashmail.fallback_port' => 'Puerto fallback', + 'ui.admin.binkp_config.crashmail.allow_insecure_crash_delivery' => 'Permitir entrega crash insegura', + 'ui.admin.binkp_config.uplinks.title' => 'Uplinks', + 'ui.admin.binkp_config.uplinks.add_uplink' => 'Agregar uplink', + 'ui.admin.binkp_config.uplinks.edit_uplink' => 'Editar uplink', + 'ui.admin.binkp_config.uplinks.none_configured' => 'No hay uplinks configurados.', + 'ui.admin.binkp_config.uplinks.real_name' => 'Nombre real', + 'ui.admin.binkp_config.uplinks.username' => 'Usuario', + 'ui.admin.binkp_config.uplinks.table.me' => 'Yo', + 'ui.admin.binkp_config.uplinks.table.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.table.domain' => 'Dominio', + 'ui.admin.binkp_config.uplinks.table.schedule' => 'Horario', + 'ui.admin.binkp_config.uplinks.table.markdown' => 'Markdown', + 'ui.admin.binkp_config.uplinks.table.posting_name' => 'Nombre de publicacion', + 'ui.admin.binkp_config.uplinks.table.adr_domain' => 'ADR @Dominio', + 'ui.admin.binkp_config.uplinks.table.enabled' => 'Habilitado', + 'ui.admin.binkp_config.uplinks.table.default' => 'Predeterminado', + 'ui.admin.binkp_config.uplinks.table.actions' => 'Acciones', + 'ui.admin.binkp_config.uplinks.modal.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.modal.me_address' => 'Direccion mia', + 'ui.admin.binkp_config.uplinks.modal.uplink_address' => 'Direccion uplink', + 'ui.admin.binkp_config.uplinks.modal.domain' => 'Dominio', + 'ui.admin.binkp_config.uplinks.modal.binkp_session_password' => 'Contrasena de sesion BinkP', + 'ui.admin.binkp_config.uplinks.modal.packet_password' => 'Contrasena de paquete', + 'ui.admin.binkp_config.uplinks.modal.packet_password_help' => 'Contrasena de cabecera FTN .pkt (max 8 caracteres)', + 'ui.admin.binkp_config.uplinks.modal.tic_password' => 'Contrasena TIC', + 'ui.admin.binkp_config.uplinks.modal.tic_password_help' => 'Contrasena TIC predeterminada; se sobrescribe por area de archivos', + 'ui.admin.binkp_config.uplinks.modal.poll_schedule' => 'Horario de sondeo', + 'ui.admin.binkp_config.uplinks.modal.networks_one_per_line' => 'Redes (una por linea)', + 'ui.admin.binkp_config.uplinks.modal.allow_markup' => 'Permitir marcado', + 'ui.admin.binkp_config.uplinks.modal.allow_markup_help' => 'Habilita soporte de marcado (Markdown, StyleCodes, etc.) en redes compatibles', + 'ui.admin.binkp_config.uplinks.modal.posting_name_policy' => 'Politica de nombre de publicacion', + 'ui.admin.binkp_config.uplinks.modal.send_domain' => 'Enviar @Dominio en ADR', + 'ui.admin.binkp_config.uplinks.modal.send_domain_help' => 'Incluye la parte @Dominio al enviar la direccion ADR a este uplink', + 'ui.admin.binkp_config.uplinks.modal.enabled_help' => 'Habilita este uplink para enrutamiento normal, sondeo y entrega saliente', + 'ui.admin.binkp_config.uplinks.modal.compression' => 'Compresion', + 'ui.admin.binkp_config.uplinks.modal.compression_help' => 'Solicita transferencias comprimidas cuando el sistema remoto lo soporte', + 'ui.admin.binkp_config.uplinks.modal.crypt' => 'Crypt', + 'ui.admin.binkp_config.uplinks.modal.crypt_help' => 'Usa autenticacion estilo CRAM-MD5 para este uplink cuando se soporte', + 'ui.admin.binkp_config.uplinks.modal.default_help' => 'Marca este uplink como predeterminado cuando no haya una ruta mas especifica', + 'ui.admin.binkp_config.uplinks.modal.save_uplink' => 'Guardar uplink', + 'ui.admin.binkp_config.validation.system_required' => 'Nombre del sistema, direccion y sysop son obligatorios.', + 'ui.admin.binkp_config.validation.port_range' => 'El puerto Binkp debe estar entre 1 y 65535.', + 'ui.admin.binkp_config.validation.timeout_positive' => 'El tiempo de espera de Binkp debe ser un numero positivo.', + 'ui.admin.binkp_config.validation.max_connections_positive' => 'Las conexiones maximas deben ser un numero positivo.', + 'ui.admin.binkp_config.validation.uplink_hostname_required' => 'El hostname del uplink es obligatorio.', + 'ui.admin.binkp_config.validation.uplink_port_range' => 'El puerto del uplink debe estar entre 1 y 65535.', + + // Admin Ads + 'ui.admin.ads.load_failed_with_status' => 'No se pudieron cargar los anuncios ({status})', + 'ui.admin.ads.load_failed' => 'No se pudieron cargar los anuncios', + 'ui.admin.ads.select_file_to_upload' => 'Seleccione un archivo ANSI para cargar.', + 'ui.admin.ads.upload_failed_with_status' => 'Fallo la carga ({status})', + 'ui.admin.ads.uploaded' => 'Anuncio cargado correctamente.', + 'ui.admin.ads.upload_failed' => 'Fallo la carga', + 'ui.admin.ads.delete_confirm' => 'Eliminar {name}?', + 'ui.admin.ads.delete_failed_with_status' => 'Fallo la eliminacion ({status})', + 'ui.admin.ads.deleted' => 'Anuncio eliminado correctamente.', + 'ui.admin.ads.delete_failed' => 'Fallo la eliminacion', + 'ui.admin.ads.unexpected_response' => 'Respuesta inesperada ({status})', + 'ui.admin.ads.page_title' => 'Anuncios', + 'ui.admin.ads.heading' => 'Anuncios', + 'ui.admin.ads.info_text_prefix' => 'Cargue anuncios ANSI (`.ans`) en el directorio', + 'ui.admin.ads.info_text_suffix' => '. Los anuncios se muestran aleatoriamente en el panel.', + 'ui.admin.ads.upload_new' => 'Cargar nuevo anuncio', + 'ui.admin.ads.ansi_file' => 'Archivo ANSI (.ans)', + 'ui.admin.ads.save_as_optional' => 'Guardar como (opcional)', + 'ui.admin.ads.save_as_placeholder' => 'retro-sale.ans', + 'ui.admin.ads.save_as_help' => 'Solo letras, numeros, punto, guion y guion bajo.', + 'ui.admin.ads.upload' => 'Cargar', + 'ui.admin.ads.current_advertisements' => 'Anuncios actuales', + 'ui.admin.ads.size' => 'Tamano', + 'ui.admin.ads.updated' => 'Actualizado', + 'ui.admin.ads.actions' => 'Acciones', + 'ui.admin.ads.loading_ads' => 'Cargando anuncios...', + 'ui.admin.ads.none_uploaded' => 'No hay anuncios cargados.', + 'ui.admin.ads.view' => 'Ver', + + // Admin Dashboard + 'ui.admin.dashboard.page_title' => 'Panel de administracion', + 'ui.admin.dashboard.heading' => 'Panel de administracion', + 'ui.admin.dashboard.total_users' => 'Usuarios totales', + 'ui.admin.dashboard.active_users' => 'Usuarios activos', + 'ui.admin.dashboard.total_netmail' => 'Netmail total', + 'ui.admin.dashboard.total_echomail' => 'Echomail total', + 'ui.admin.dashboard.quick_actions' => 'Acciones rapidas', + 'ui.admin.dashboard.system_information' => 'Informacion del sistema', + 'ui.admin.dashboard.admin_users' => 'Usuarios admin:', + 'ui.admin.dashboard.active_sessions' => 'Sesiones activas:', + 'ui.admin.dashboard.system_address' => 'Direccion del sistema:', + 'ui.admin.dashboard.version' => 'Version:', + 'ui.admin.dashboard.git_branch_commit' => 'Rama / Commit de Git:', + 'ui.admin.dashboard.database_version' => 'Version de base de datos:', + 'ui.admin.dashboard.service_status' => 'Estado de servicios', + 'ui.admin.dashboard.service.admin_daemon' => 'Daemon admin', + 'ui.admin.dashboard.service.binkp_scheduler' => 'Programador Binkp', + 'ui.admin.dashboard.service.binkp_server' => 'Servidor Binkp', + 'ui.admin.dashboard.service.telnetd' => 'Servidor Telnet', + 'ui.admin.dashboard.running' => 'En ejecucion', + 'ui.admin.dashboard.stopped' => 'Detenido', + 'ui.admin.dashboard.pid' => 'PID', + + // Admin Users (legacy users page) + 'ui.admin.users.load_failed' => 'Error al cargar usuarios', + 'ui.admin.users.load_details_failed' => 'Error al cargar detalles del usuario', + 'ui.admin.users.updated_success' => 'Usuario actualizado correctamente', + 'ui.admin.users.created_success' => 'Usuario creado correctamente', + 'ui.admin.users.save_failed' => 'Error al guardar el usuario', + 'ui.admin.users.delete_confirm' => 'Esta seguro de que desea eliminar este usuario? Esta accion no se puede deshacer.', + 'ui.admin.users.deleted_success' => 'Usuario eliminado correctamente', + 'ui.admin.users.delete_failed' => 'Error al eliminar el usuario', + 'ui.admin.users.page_title' => 'Gestion de usuarios', + 'ui.admin.users.heading' => 'Gestion de usuarios', + 'ui.admin.users.add_new_user' => 'Agregar nuevo usuario', + 'ui.admin.users.search_users_placeholder' => 'Buscar usuarios...', + 'ui.admin.users.active' => 'Activo', + 'ui.admin.users.inactive' => 'Inactivo', + 'ui.admin.users.admins' => 'Admins', + 'ui.admin.users.username' => 'Usuario', + 'ui.admin.users.real_name' => 'Nombre real', + 'ui.admin.users.fidonet_address' => 'Direccion Fidonet', + 'ui.admin.users.status' => 'Estado', + 'ui.admin.users.admin' => 'Admin', + 'ui.admin.users.created' => 'Creado', + 'ui.admin.users.actions' => 'Acciones', + 'ui.admin.users.password_help' => 'Deje en blanco al editar para mantener la contrasena actual', + 'ui.admin.users.fidonet_address_placeholder' => '1:123/456.789', + 'ui.admin.users.active_user' => 'Usuario activo', + 'ui.admin.users.administrator' => 'Administrador', + 'ui.admin.users.save_user' => 'Guardar usuario', + 'ui.admin.users.user_details' => 'Detalles del usuario', + 'ui.admin.users.view_details' => 'Ver detalles', + 'ui.admin.users.view_profile' => 'Ver perfil', + 'ui.admin.users.pagination_aria' => 'Paginacion de usuarios', + 'ui.admin.users.previous' => 'Anterior', + 'ui.admin.users.next' => 'Siguiente', + 'ui.admin.users.edit_user' => 'Editar usuario', + 'ui.admin.users.user_information' => 'Informacion del usuario', + 'ui.admin.users.last_login' => 'Ultimo inicio de sesion', + 'ui.admin.users.statistics' => 'Estadisticas', + 'ui.admin.users.netmail_received' => 'Netmail recibido', + 'ui.admin.users.netmail_sent' => 'Netmail enviado', + 'ui.admin.users.echomail_posted' => 'Echomail publicado', + 'ui.admin.users.active_sessions' => 'Sesiones activas', + + // Admin Chat Rooms + 'ui.admin.chat_rooms.load_failed' => 'Error al cargar salas de chat', + 'ui.admin.chat_rooms.not_found' => 'Sala de chat no encontrada', + 'ui.admin.chat_rooms.load_single_failed' => 'Error al cargar la sala de chat', + 'ui.admin.chat_rooms.updated_success' => 'Sala actualizada correctamente', + 'ui.admin.chat_rooms.created_success' => 'Sala creada correctamente', + 'ui.admin.chat_rooms.save_failed' => 'Error al guardar la sala', + 'ui.admin.chat_rooms.delete_confirm' => 'Eliminar esta sala de chat? Esta accion no se puede deshacer.', + 'ui.admin.chat_rooms.deleted_success' => 'Sala eliminada correctamente', + 'ui.admin.chat_rooms.delete_failed' => 'Error al eliminar la sala', + 'ui.admin.chat_rooms.page_title' => 'Salas de chat', + 'ui.admin.chat_rooms.heading' => 'Salas de chat', + 'ui.admin.chat_rooms.add_room' => 'Agregar sala', + 'ui.admin.chat_rooms.status' => 'Estado', + 'ui.admin.chat_rooms.created' => 'Creado', + 'ui.admin.chat_rooms.actions' => 'Acciones', + 'ui.admin.chat_rooms.add_chat_room' => 'Agregar sala de chat', + 'ui.admin.chat_rooms.edit_chat_room' => 'Editar sala de chat', + 'ui.admin.chat_rooms.active_room' => 'Sala activa', + 'ui.admin.chat_rooms.save_room' => 'Guardar sala', + 'ui.admin.chat_rooms.no_description' => '-', + 'ui.admin.chat_rooms.active' => 'Activa', + 'ui.admin.chat_rooms.inactive' => 'Inactiva', + + // Admin Binkp Sessions + 'ui.admin.binkp_sessions.page_title' => 'Sesiones Binkp - Admin', + 'ui.admin.binkp_sessions.heading' => 'Registro de sesiones Binkp', + 'ui.admin.binkp_sessions.back_to_dashboard' => 'Volver al panel', + 'ui.admin.binkp_sessions.total_24h' => 'Total (24h)', + 'ui.admin.binkp_sessions.secure' => 'Segura', + 'ui.admin.binkp_sessions.insecure' => 'Insegura', + 'ui.admin.binkp_sessions.crash_out' => 'Crash saliente', + 'ui.admin.binkp_sessions.successful' => 'Exitosas', + 'ui.admin.binkp_sessions.failed' => 'Fallidas', + 'ui.admin.binkp_sessions.filter_sessions' => 'Filtrar sesiones', + 'ui.admin.binkp_sessions.session_type' => 'Tipo de sesion', + 'ui.admin.binkp_sessions.crash_outbound' => 'Crash saliente', + 'ui.admin.binkp_sessions.success' => 'Exito', + 'ui.admin.binkp_sessions.rejected' => 'Rechazada', + 'ui.admin.binkp_sessions.remote_address' => 'Direccion remota', + 'ui.admin.binkp_sessions.remote_address_placeholder' => '1:123/456', + 'ui.admin.binkp_sessions.apply_filter' => 'Aplicar filtro', + 'ui.admin.binkp_sessions.recent_sessions' => 'Sesiones recientes', + 'ui.admin.binkp_sessions.time' => 'Hora', + 'ui.admin.binkp_sessions.ip' => 'IP', + 'ui.admin.binkp_sessions.type' => 'Tipo', + 'ui.admin.binkp_sessions.direction' => 'Direccion', + 'ui.admin.binkp_sessions.msgs_in' => 'Msgs entrantes', + 'ui.admin.binkp_sessions.msgs_out' => 'Msgs salientes', + 'ui.admin.binkp_sessions.duration' => 'Duracion', + 'ui.admin.binkp_sessions.load_stats_failed' => 'Error al cargar estadisticas', + 'ui.admin.binkp_sessions.no_sessions_found' => 'No se encontraron sesiones', + 'ui.admin.binkp_sessions.in' => 'Entrante', + 'ui.admin.binkp_sessions.out' => 'Saliente', + 'ui.admin.binkp_sessions.load_sessions_failed' => 'Error al cargar sesiones', + 'ui.admin.binkp_sessions.crash' => 'Crash', + 'ui.admin.binkp_sessions.active' => 'Activa', + + // Admin Polls + 'ui.admin.polls.load_failed' => 'Error al cargar encuestas', + 'ui.admin.polls.not_found' => 'Encuesta no encontrada', + 'ui.admin.polls.load_single_failed' => 'Error al cargar la encuesta', + 'ui.admin.polls.min_options_required' => 'Se requieren al menos dos opciones.', + 'ui.admin.polls.question_required' => 'La pregunta es obligatoria.', + 'ui.admin.polls.provide_min_options' => 'Proporcione al menos dos opciones.', + 'ui.admin.polls.updated_success' => 'Encuesta actualizada correctamente', + 'ui.admin.polls.created_success' => 'Encuesta creada correctamente', + 'ui.admin.polls.save_failed' => 'Error al guardar la encuesta', + 'ui.admin.polls.delete_confirm' => 'Eliminar esta encuesta? Esta accion no se puede deshacer.', + 'ui.admin.polls.deleted_success' => 'Encuesta eliminada correctamente', + 'ui.admin.polls.delete_failed' => 'Error al eliminar la encuesta', + 'ui.admin.polls.page_title' => 'Encuestas', + 'ui.admin.polls.heading' => 'Encuestas', + 'ui.admin.polls.add_poll' => 'Agregar encuesta', + 'ui.admin.polls.edit_poll' => 'Editar encuesta', + 'ui.admin.polls.question' => 'Pregunta', + 'ui.admin.polls.status' => 'Estado', + 'ui.admin.polls.votes' => 'Votos', + 'ui.admin.polls.created_by' => 'Creado por', + 'ui.admin.polls.created' => 'Creado', + 'ui.admin.polls.actions' => 'Acciones', + 'ui.admin.polls.options' => 'Opciones', + 'ui.admin.polls.add_option' => 'Agregar opcion', + 'ui.admin.polls.active_poll' => 'Encuesta activa', + 'ui.admin.polls.editing_options_resets_votes' => 'Editar opciones reiniciara los votos de esta encuesta.', + 'ui.admin.polls.save_poll' => 'Guardar encuesta', + 'ui.admin.polls.active' => 'Activa', + 'ui.admin.polls.inactive' => 'Inactiva', + 'ui.admin.polls.option_text_placeholder' => 'Texto de opcion', + + // Admin Template Editor + 'ui.admin.template_editor.load_templates_failed' => 'No se pudieron cargar las plantillas.', + 'ui.admin.template_editor.no_templates_found' => 'No se encontraron plantillas.', + 'ui.admin.template_editor.unsaved_changes_confirm' => 'Tiene cambios sin guardar. Continuar y descartarlos?', + 'ui.admin.template_editor.load_template_failed' => 'No se pudo cargar la plantilla', + 'ui.admin.template_editor.enter_template_path' => 'Ingrese una ruta de plantilla (ej., systemnews.twig).', + 'ui.admin.template_editor.save_template_failed' => 'No se pudo guardar la plantilla', + 'ui.admin.template_editor.template_saved_success' => 'Plantilla guardada correctamente.', + 'ui.admin.template_editor.delete_confirm' => 'Eliminar {template}? Esta accion no se puede deshacer.', + 'ui.admin.template_editor.delete_template_failed' => 'No se pudo eliminar la plantilla', + 'ui.admin.template_editor.no_template_selected' => 'Ninguna plantilla seleccionada', + 'ui.admin.template_editor.template_deleted_success' => 'Plantilla eliminada.', + 'ui.admin.template_editor.install_template_failed' => 'No se pudo instalar la plantilla', + 'ui.admin.template_editor.template_installed_success' => 'Plantilla instalada desde el ejemplo.', + 'ui.admin.template_editor.page_title_prefix' => 'Admin: Editor de plantillas', + 'ui.admin.template_editor.heading' => 'Editor de plantillas', + 'ui.admin.template_editor.restricted_edits_prefix' => 'Las ediciones estan restringidas a', + 'ui.admin.template_editor.custom_templates' => 'Plantillas personalizadas', + 'ui.admin.template_editor.search_templates_placeholder' => 'Buscar plantillas...', + 'ui.admin.template_editor.loading_templates' => 'Cargando plantillas...', + 'ui.admin.template_editor.new_template' => 'Nueva plantilla', + 'ui.admin.template_editor.path_relative_custom' => 'Ruta (relativa a templates/custom)', + 'ui.admin.template_editor.new_template_path_placeholder' => 'systemnews.twig', + 'ui.admin.template_editor.create' => 'Crear', + 'ui.admin.template_editor.editor' => 'Editor', + 'ui.admin.template_editor.install' => 'Instalar', + 'ui.admin.template_editor.changes_apply_immediately' => 'Los cambios se aplican inmediatamente al guardar. Asegurese de que la sintaxis Twig sea valida.', + + // Admin Language Overlay Editor + 'ui.admin.i18n_overrides.page_title' => 'Admin: Ajustes de idioma', + 'ui.admin.i18n_overrides.heading' => 'Ajustes de idioma', + 'ui.admin.i18n_overrides.description' => 'Personalice frases individuales sin editar los archivos de traduccion base. Los ajustes se aplican sobre el catalogo base del idioma seleccionado.', + 'ui.admin.i18n_overrides.locale_label' => 'Idioma', + 'ui.admin.i18n_overrides.catalog_label' => 'Catalogo', + 'ui.admin.i18n_overrides.load_btn' => 'Cargar', + 'ui.admin.i18n_overrides.search_placeholder' => 'Filtrar claves...', + 'ui.admin.i18n_overrides.show_overrides_only' => 'Mostrar solo ajustes', + 'ui.admin.i18n_overrides.col_key' => 'Clave', + 'ui.admin.i18n_overrides.col_base' => 'Valor base', + 'ui.admin.i18n_overrides.col_override' => 'Su ajuste', + 'ui.admin.i18n_overrides.save_btn' => 'Guardar ajustes', + 'ui.admin.i18n_overrides.clear_btn' => 'Limpiar', + 'ui.admin.i18n_overrides.overridden_badge' => 'Ajustado', + 'ui.admin.i18n_overrides.no_keys' => 'No se encontraron claves.', + 'ui.admin.i18n_overrides.loading' => 'Cargando...', + 'ui.admin.i18n_overrides.saved' => 'Ajustes guardados.', + 'ui.admin.i18n_overrides.save_failed' => 'Error al guardar los ajustes.', + 'ui.admin.i18n_overrides.load_failed' => 'Error al cargar el catalogo.', + 'ui.admin.i18n_overrides.select_locale_catalog' => 'Seleccione un idioma y catalogo, luego haga clic en Cargar.', + + // Admin Upgrade Notes + 'ui.admin.upgrade_notes.none_for_version' => 'No se encontraron notas de actualizacion para la version {version}.', + + // Admin Insecure Nodes + 'ui.admin.insecure_nodes.error_prefix' => 'Error: ', + 'ui.admin.insecure_nodes.add_failed' => 'No se pudo agregar el nodo', + 'ui.admin.insecure_nodes.added_success' => 'Nodo agregado correctamente', + 'ui.admin.insecure_nodes.update_failed' => 'No se pudo actualizar el nodo', + 'ui.admin.insecure_nodes.updated_success' => 'Nodo actualizado correctamente', + 'ui.admin.insecure_nodes.delete_confirm' => 'Eliminar {address} de la lista permitida?', + 'ui.admin.insecure_nodes.delete_failed' => 'No se pudo eliminar el nodo', + 'ui.admin.insecure_nodes.deleted_success' => 'Nodo eliminado correctamente', + 'ui.admin.insecure_nodes.page_title' => 'Nodos inseguros - Admin', + 'ui.admin.insecure_nodes.heading' => 'Allowlist de nodos inseguros', + 'ui.admin.insecure_nodes.add_node' => 'Agregar nodo', + 'ui.admin.insecure_nodes.back_to_dashboard' => 'Volver al panel', + 'ui.admin.insecure_nodes.about_insecure_sessions' => 'Sobre sesiones inseguras:', + 'ui.admin.insecure_nodes.info_text_1' => 'Los nodos en esta allowlist pueden conectarse por binkp sin autenticacion por contrasena.', + 'ui.admin.insecure_nodes.info_text_2' => 'Use con cuidado: las sesiones inseguras suelen ser solo recepcion (pueden entregarle correo, pero no recogerlo).', + 'ui.admin.insecure_nodes.allowed_nodes' => 'Nodos permitidos', + 'ui.admin.insecure_nodes.address' => 'Direccion', + 'ui.admin.insecure_nodes.receive' => 'Recibir', + 'ui.admin.insecure_nodes.send' => 'Enviar', + 'ui.admin.insecure_nodes.max_msgs' => 'Max msgs', + 'ui.admin.insecure_nodes.last_session' => 'Ultima sesion', + 'ui.admin.insecure_nodes.active' => 'Activo', + 'ui.admin.insecure_nodes.inactive' => 'Inactivo', + 'ui.admin.insecure_nodes.actions' => 'Acciones', + 'ui.admin.insecure_nodes.add_node_to_allowlist' => 'Agregar nodo a la allowlist', + 'ui.admin.insecure_nodes.ftn_address' => 'Direccion FTN', + 'ui.admin.insecure_nodes.address_placeholder' => '1:123/456', + 'ui.admin.insecure_nodes.description_placeholder' => 'Descripcion del nodo', + 'ui.admin.insecure_nodes.ftn_address_help' => 'La direccion FidoNet del nodo (ej.: 1:123/456)', + 'ui.admin.insecure_nodes.max_messages_per_session' => 'Maximo de mensajes por sesion', + 'ui.admin.insecure_nodes.allow_receive' => 'Permitir recibir', + 'ui.admin.insecure_nodes.allow_receive_help' => 'Permitir que este nodo nos entregue correo', + 'ui.admin.insecure_nodes.allow_send_pickup' => 'Permitir enviar (pickup)', + 'ui.admin.insecure_nodes.allow_send_help' => 'Permitir que este nodo recoja correo de nosotros (use con cuidado)', + 'ui.admin.insecure_nodes.edit_node' => 'Editar nodo', + 'ui.admin.insecure_nodes.save_changes' => 'Guardar cambios', + 'ui.admin.insecure_nodes.no_nodes' => 'No hay nodos en la allowlist', + 'ui.admin.insecure_nodes.load_nodes_failed' => 'Error al cargar nodos', + + // Admin Crashmail Queue + 'ui.admin.crashmail_queue.attempt_delivery_confirm' => 'Intentar entrega para el crashmail pendiente ahora?', + 'ui.admin.crashmail_queue.delivery_attempt_started' => 'Intento de entrega iniciado.', + 'ui.admin.crashmail_queue.delivery_attempt_failed_prefix' => 'Fallo el intento de entrega: ', + 'ui.admin.crashmail_queue.error_prefix' => 'Error: ', + 'ui.admin.crashmail_queue.retry_confirm' => 'Reintentar esta entrega de crashmail?', + 'ui.admin.crashmail_queue.retry_success' => 'Reintento de crashmail encolado.', + 'ui.admin.crashmail_queue.retry_failed_prefix' => 'No se pudo reintentar: ', + 'ui.admin.crashmail_queue.cancel_confirm' => 'Cancelar este crashmail? El mensaje permanecera en su bandeja de salida pero no se entregara como crashmail.', + 'ui.admin.crashmail_queue.cancel_success' => 'Crashmail cancelado.', + 'ui.admin.crashmail_queue.cancel_failed_prefix' => 'No se pudo cancelar: ', + 'ui.admin.crashmail_queue.page_title' => 'Cola de crashmail - Admin', + 'ui.admin.crashmail_queue.heading' => 'Cola de crashmail', + 'ui.admin.crashmail_queue.attempt_delivery' => 'Intentar entrega', + 'ui.admin.crashmail_queue.back_to_dashboard' => 'Volver al panel', + 'ui.admin.crashmail_queue.pending' => 'Pendiente', + 'ui.admin.crashmail_queue.attempting' => 'Intentando', + 'ui.admin.crashmail_queue.sent_24h' => 'Enviado (24h)', + 'ui.admin.crashmail_queue.sent' => 'Enviado', + 'ui.admin.crashmail_queue.failed' => 'Fallido', + 'ui.admin.crashmail_queue.filter_queue' => 'Filtrar cola', + 'ui.admin.crashmail_queue.status' => 'Estado', + 'ui.admin.crashmail_queue.limit' => 'Limite', + 'ui.admin.crashmail_queue.apply_filter' => 'Aplicar filtro', + 'ui.admin.crashmail_queue.queue_items' => 'Elementos en cola', + 'ui.admin.crashmail_queue.to' => 'Para', + 'ui.admin.crashmail_queue.subject' => 'Asunto', + 'ui.admin.crashmail_queue.destination' => 'Destino', + 'ui.admin.crashmail_queue.attempts' => 'Intentos', + 'ui.admin.crashmail_queue.next_attempt' => 'Siguiente intento', + 'ui.admin.crashmail_queue.actions' => 'Acciones', + 'ui.admin.crashmail_queue.no_items' => 'No hay elementos en cola', + 'ui.admin.crashmail_queue.no_subject' => '(sin asunto)', + 'ui.admin.crashmail_queue.unresolved' => 'Sin resolver', + 'ui.admin.crashmail_queue.load_stats_failed' => 'Error al cargar estadisticas', + 'ui.admin.crashmail_queue.load_queue_failed' => 'Error al cargar la cola', + + // Admin File Area Rules + 'ui.admin.filearea_rules.load_failed' => 'No se pudieron cargar las reglas', + 'ui.admin.filearea_rules.active' => 'Activo', + 'ui.admin.filearea_rules.missing_config' => 'Configuracion faltante', + 'ui.admin.filearea_rules.error' => 'Error', + 'ui.admin.filearea_rules.invalid_json' => 'El JSON de reglas no es valido. Corrija los errores antes de guardar.', + 'ui.admin.filearea_rules.save_failed' => 'No se pudieron guardar las reglas', + 'ui.admin.filearea_rules.saved_success' => 'Reglas guardadas correctamente.', + 'ui.admin.filearea_rules.reset_confirm' => 'Reemplazar las reglas actuales con el archivo de ejemplo?', + 'ui.admin.filearea_rules.loaded_example' => 'Reglas de ejemplo cargadas. Guarde para aplicarlas.', + 'ui.admin.filearea_rules.page_title_prefix' => 'Reglas de area de archivos', + 'ui.admin.filearea_rules.heading' => 'Reglas de area de archivos', + 'ui.admin.filearea_rules.config_filename' => 'filearea_rules.json', + 'ui.admin.filearea_rules.reset_to_example' => 'Restablecer al ejemplo', + 'ui.admin.filearea_rules.save_rules' => 'Guardar reglas', + 'ui.admin.filearea_rules.editing_requires_valid_json' => 'La edicion requiere JSON valido.', + 'ui.admin.filearea_rules.macros' => 'Macros', + 'ui.admin.filearea_rules.macro_basedir' => 'Directorio base de la aplicacion', + 'ui.admin.filearea_rules.macro_filepath' => 'Ruta completa al archivo', + 'ui.admin.filearea_rules.macro_filename' => 'Solo nombre de archivo', + 'ui.admin.filearea_rules.macro_filesize' => 'Tamano del archivo en bytes', + 'ui.admin.filearea_rules.macro_domain' => 'Dominio del area de archivos', + 'ui.admin.filearea_rules.macro_areatag' => 'Tag del area de archivos', + 'ui.admin.filearea_rules.macro_uploader' => 'Nombre/direccion del subidor', + 'ui.admin.filearea_rules.macro_ticfile' => 'Ruta TIC (si esta disponible)', + 'ui.admin.filearea_rules.macro_tempdir' => 'Directorio temporal', + 'ui.admin.filearea_rules.tips' => 'Consejos', + 'ui.admin.filearea_rules.tip_1' => 'Las reglas se ejecutan en orden: primero globales y luego las especificas del area.', + 'ui.admin.filearea_rules.tip_2' => 'Las reglas de area pueden usar TAG o TAG@DOMAIN (la especifica por dominio tiene prioridad).', + 'ui.admin.filearea_rules.tip_3' => 'Use success_action y fail_action con +stop para detener el procesamiento adicional.', + + // Admin DOS Doors Config + 'ui.admin.dosdoors_config.load_config_failed' => 'No se pudo cargar la configuracion', + 'ui.admin.dosdoors_config.load_config_error_prefix' => 'Error al cargar la configuracion: ', + 'ui.admin.dosdoors_config.load_doors_failed' => 'No se pudieron cargar las puertas', + 'ui.admin.dosdoors_config.load_doors_error_prefix' => 'Error al cargar las puertas disponibles: ', + 'ui.admin.dosdoors_config.invalid_json_toggle' => 'JSON invalido: no se puede alternar la puerta', + 'ui.admin.dosdoors_config.enabled_all_editor' => 'Todas las puertas habilitadas en el editor. Haga clic en Guardar para aplicar.', + 'ui.admin.dosdoors_config.invalid_json_enable_all' => 'JSON invalido: no se pueden habilitar todas', + 'ui.admin.dosdoors_config.disabled_all_editor' => 'Todas las puertas deshabilitadas en el editor. Haga clic en Guardar para aplicar.', + 'ui.admin.dosdoors_config.invalid_json_disable_all' => 'JSON invalido: no se pueden deshabilitar todas', + 'ui.admin.dosdoors_config.json_formatted' => 'JSON formateado correctamente', + 'ui.admin.dosdoors_config.cannot_format_prefix' => 'No se puede formatear - JSON invalido: ', + 'ui.admin.dosdoors_config.empty_config' => 'Configuracion vacia', + 'ui.admin.dosdoors_config.valid_json' => 'JSON valido', + 'ui.admin.dosdoors_config.invalid_json_prefix' => 'JSON invalido: ', + 'ui.admin.dosdoors_config.save_failed' => 'No se pudo guardar', + 'ui.admin.dosdoors_config.saved_success' => 'Configuracion guardada correctamente.', + 'ui.admin.dosdoors_config.save_error_prefix' => 'Error al guardar la configuracion: ', + 'ui.admin.dosdoors_config.cannot_save_invalid_json_prefix' => 'No se puede guardar - JSON invalido: ', + 'ui.admin.dosdoors_config.page_title' => 'Config DOS Doors', + 'ui.admin.dosdoors_config.heading' => 'Configuracion de DOS Doors', + 'ui.admin.dosdoors_config.info_text_1' => 'Los DOS doors son juegos clasicos de BBS que se ejecutan en DOSBox. La configuracion se guarda en', + 'ui.admin.dosdoors_config.info_text_2' => 'Los doors se detectan escaneando', + 'ui.admin.dosdoors_config.info_text_3' => 'en busca de manifiestos', + 'ui.admin.dosdoors_config.info_text_4' => '.', + 'ui.admin.dosdoors_config.installed_doors' => 'Doors instalados', + 'ui.admin.dosdoors_config.loading_doors' => 'Cargando doors...', + 'ui.admin.dosdoors_config.quick_actions' => 'Acciones rapidas', + 'ui.admin.dosdoors_config.enable_all_doors' => 'Habilitar todos los doors', + 'ui.admin.dosdoors_config.disable_all_doors' => 'Deshabilitar todos los doors', + 'ui.admin.dosdoors_config.config_filename' => 'dosdoors.json', + 'ui.admin.dosdoors_config.format' => 'Formatear', + 'ui.admin.dosdoors_config.loading_config' => 'Cargando configuracion...', + 'ui.admin.dosdoors_config.json_validation_before_save' => 'La validacion JSON se ejecuta antes de guardar', + 'ui.admin.dosdoors_config.configuration_options' => 'Opciones de configuracion', + 'ui.admin.dosdoors_config.option_enabled_help' => 'Si el door esta disponible para usuarios', + 'ui.admin.dosdoors_config.option_credit_cost_help' => 'Creditos requeridos para jugar (0 = gratis)', + 'ui.admin.dosdoors_config.option_max_time_help' => 'Tiempo maximo de juego por sesion', + 'ui.admin.dosdoors_config.option_cpu_cycles_help' => 'Velocidad de CPU de DOSBox (10000 = tipico)', + 'ui.admin.dosdoors_config.option_max_concurrent_help' => 'Maximo de jugadores simultaneos', + 'ui.admin.dosdoors_config.no_doors_found_prefix' => 'No se encontraron doors. Instale doors en', + 'ui.admin.dosdoors_config.no_description' => 'Sin descripcion', + 'ui.admin.dosdoors_config.credits' => 'creditos', + 'ui.admin.dosdoors_config.free' => 'Gratis', + 'ui.admin.dosdoors_config.disable' => 'Deshabilitar', + 'ui.admin.dosdoors_config.enable' => 'Habilitar', + + // Admin Native Doors Config + 'ui.admin.nativedoors_config.load_config_failed' => 'No se pudo cargar la configuracion', + 'ui.admin.nativedoors_config.load_config_error_prefix' => 'Error al cargar la configuracion: ', + 'ui.admin.nativedoors_config.load_doors_failed' => 'No se pudieron cargar las puertas', + 'ui.admin.nativedoors_config.load_doors_error_prefix' => 'Error al cargar las puertas disponibles: ', + 'ui.admin.nativedoors_config.invalid_json_toggle' => 'JSON invalido: no se puede alternar la puerta', + 'ui.admin.nativedoors_config.enabled_all_editor' => 'Todas las puertas habilitadas en el editor. Haga clic en Guardar para aplicar.', + 'ui.admin.nativedoors_config.invalid_json_enable_all' => 'JSON invalido: no se pueden habilitar todas', + 'ui.admin.nativedoors_config.disabled_all_editor' => 'Todas las puertas deshabilitadas en el editor. Haga clic en Guardar para aplicar.', + 'ui.admin.nativedoors_config.invalid_json_disable_all' => 'JSON invalido: no se pueden deshabilitar todas', + 'ui.admin.nativedoors_config.json_formatted' => 'JSON formateado correctamente', + 'ui.admin.nativedoors_config.cannot_format_prefix' => 'No se puede formatear - JSON invalido: ', + 'ui.admin.nativedoors_config.empty_config' => 'Configuracion vacia', + 'ui.admin.nativedoors_config.valid_json' => 'JSON valido', + 'ui.admin.nativedoors_config.invalid_json_prefix' => 'JSON invalido: ', + 'ui.admin.nativedoors_config.save_failed' => 'No se pudo guardar', + 'ui.admin.nativedoors_config.saved_success' => 'Configuracion guardada correctamente.', + 'ui.admin.nativedoors_config.save_error_prefix' => 'Error al guardar la configuracion: ', + 'ui.admin.nativedoors_config.cannot_save_invalid_json_prefix' => 'No se puede guardar - JSON invalido: ', + 'ui.admin.nativedoors_config.page_title' => 'Config Native Doors', + 'ui.admin.nativedoors_config.heading' => 'Configuracion de Native Doors', + 'ui.admin.nativedoors_config.info_text_1' => 'Los native doors son programas Linux que se ejecutan directamente por PTY (sin emulador). La configuracion se guarda en', + 'ui.admin.nativedoors_config.info_text_2' => 'Los doors se detectan escaneando', + 'ui.admin.nativedoors_config.info_text_3' => 'en busca de', + 'ui.admin.nativedoors_config.info_text_4' => 'manifiestos.', + 'ui.admin.nativedoors_config.installed_doors' => 'Doors instalados', + 'ui.admin.nativedoors_config.loading_doors' => 'Cargando doors...', + 'ui.admin.nativedoors_config.quick_actions' => 'Acciones rapidas', + 'ui.admin.nativedoors_config.enable_all_doors' => 'Habilitar todos los doors', + 'ui.admin.nativedoors_config.disable_all_doors' => 'Deshabilitar todos los doors', + 'ui.admin.nativedoors_config.config_filename' => 'nativedoors.json', + 'ui.admin.nativedoors_config.format' => 'Formatear', + 'ui.admin.nativedoors_config.loading_config' => 'Cargando configuracion...', + 'ui.admin.nativedoors_config.json_validation_before_save' => 'La validacion JSON se ejecuta antes de guardar', + 'ui.admin.nativedoors_config.configuration_options' => 'Opciones de configuracion', + 'ui.admin.nativedoors_config.option_enabled_help' => 'Si el door esta disponible para usuarios', + 'ui.admin.nativedoors_config.option_credit_cost_help' => 'Creditos requeridos para jugar (0 = gratis)', + 'ui.admin.nativedoors_config.option_max_time_help' => 'Tiempo maximo de juego por sesion', + 'ui.admin.nativedoors_config.option_max_concurrent_help' => 'Maximo de jugadores simultaneos', + 'ui.admin.nativedoors_config.no_doors_found_prefix' => 'No se encontraron doors. Instale doors en', + 'ui.admin.nativedoors_config.no_description' => 'Sin descripcion', + 'ui.admin.nativedoors_config.credits' => 'creditos', + 'ui.admin.nativedoors_config.free' => 'Gratis', + 'ui.admin.nativedoors_config.disable' => 'Deshabilitar', + 'ui.admin.nativedoors_config.enable' => 'Habilitar', + + // Admin Webdoors Config + 'ui.admin.webdoors_config.load_webdoors_failed' => 'No se pudo cargar la configuracion de webdoors', + 'ui.admin.webdoors_config.load_failed' => 'No se pudo cargar la configuracion', + 'ui.admin.webdoors_config.json_valid' => 'JSON valido', + 'ui.admin.webdoors_config.json_has_errors' => 'El JSON tiene errores', + 'ui.admin.webdoors_config.invalid_json' => 'JSON invalido.', + 'ui.admin.webdoors_config.no_doors_defined' => 'No hay puertas definidas.', + 'ui.admin.webdoors_config.cannot_format_invalid_json' => 'No se puede formatear: JSON invalido', + 'ui.admin.webdoors_config.fix_json_before_save' => 'Corrija los errores de JSON antes de guardar.', + 'ui.admin.webdoors_config.save_failed' => 'No se pudo guardar la configuracion', + 'ui.admin.webdoors_config.saved_success' => 'Configuracion de webdoors guardada.', + 'ui.admin.webdoors_config.activate_failed' => 'No se pudo activar webdoors', + 'ui.admin.webdoors_config.activated_success' => 'Webdoors activado.', + 'ui.admin.webdoors_config.page_title' => 'Config Webdoors', + 'ui.admin.webdoors_config.heading' => 'Config Webdoors', + 'ui.admin.webdoors_config.info_text_prefix' => 'Webdoors se habilita al tener un archivo', + 'ui.admin.webdoors_config.info_text_suffix' => '. Esta pagina permite activarlo y editarlo.', + 'ui.admin.webdoors_config.status' => 'Estado', + 'ui.admin.webdoors_config.activate_webdoors' => 'Activar webdoors', + 'ui.admin.webdoors_config.config_file' => 'Archivo de config:', + 'ui.admin.webdoors_config.config_never_deleted' => 'Una vez activado, el archivo de config nunca se elimina.', + 'ui.admin.webdoors_config.json_controls_help' => 'Editar JSON controla como aparecen los doors en el sistema. La UI solo conoce enabled.', + 'ui.admin.webdoors_config.doors' => 'Doors', + 'ui.admin.webdoors_config.config_filename' => 'webdoors.json', + 'ui.admin.webdoors_config.no_config_loaded' => 'No hay config cargada.', + 'ui.admin.webdoors_config.format_json' => 'Formatear JSON', + 'ui.admin.webdoors_config.waiting_for_config' => 'Esperando config...', + 'ui.admin.webdoors_config.json_validation_before_save' => 'La validacion JSON se ejecuta antes de guardar.', + 'ui.admin.webdoors_config.active_present' => 'Activo (webdoors.json presente)', + 'ui.admin.webdoors_config.not_active_using_example' => 'No activo (usando ejemplo)', + 'ui.admin.webdoors_config.enabled_label' => 'enabled', + 'ui.admin.webdoors_config.true' => 'true', + 'ui.admin.webdoors_config.false' => 'false', + 'ui.admin.webdoors_config.not_in_config' => '(no esta en config)', + + // Admin BBS Settings + 'ui.admin.bbs_settings.load_failed' => 'No se pudo cargar la configuracion', + 'ui.admin.bbs_settings.save_failed' => 'No se pudo guardar la configuracion', + 'ui.admin.bbs_settings.saved_success' => 'Funciones del BBS guardadas correctamente.', + 'ui.admin.bbs_settings.load_system_failed' => 'No se pudo cargar la configuracion del sistema', + 'ui.admin.bbs_settings.save_system_failed' => 'No se pudo guardar la configuracion del sistema', + 'ui.admin.bbs_settings.system_saved_success' => 'Configuracion del sistema guardada correctamente.', + 'ui.admin.bbs_settings.save_credits_failed' => 'No se pudo guardar la configuracion de creditos', + 'ui.admin.bbs_settings.credits_saved_success' => 'Configuracion de creditos guardada correctamente.', + 'ui.admin.bbs_settings.load_taglines_failed' => 'No se pudieron cargar los taglines', + 'ui.admin.bbs_settings.save_taglines_failed' => 'No se pudieron guardar los taglines', + 'ui.admin.bbs_settings.taglines_saved_success' => 'Taglines guardados correctamente.', + 'ui.admin.bbs_settings.page_title' => 'Configuracion del BBS', + 'ui.admin.bbs_settings.heading' => 'Configuracion del BBS', + 'ui.admin.bbs_settings.current' => 'actual', + 'ui.admin.bbs_settings.system.title' => 'Configuracion del sistema', + 'ui.admin.bbs_settings.system.system_name' => 'Nombre del sistema', + 'ui.admin.bbs_settings.system.sysop_name' => 'Nombre del sysop', + 'ui.admin.bbs_settings.system.select_admin_user' => 'Seleccione un usuario admin.', + 'ui.admin.bbs_settings.system.location' => 'Ubicacion', + 'ui.admin.bbs_settings.system.timezone' => 'Zona horaria', + 'ui.admin.bbs_settings.system.system_address' => 'Direccion del sistema', + 'ui.admin.bbs_settings.system.origin_line' => 'Linea de origen', + 'ui.admin.bbs_settings.system.save' => 'Guardar configuracion del sistema', + 'ui.admin.bbs_settings.features.title' => 'Funciones del BBS', + 'ui.admin.bbs_settings.features.enable_webdoors' => 'Habilitar webdoors', + 'ui.admin.bbs_settings.features.inactive' => 'inactivo', + 'ui.admin.bbs_settings.features.activate' => 'Activar', + 'ui.admin.bbs_settings.features.enable_shoutbox' => 'Habilitar shoutbox', + 'ui.admin.bbs_settings.features.enable_advertising' => 'Habilitar publicidad', + 'ui.admin.bbs_settings.features.enable_voting_booth' => 'Habilitar cabina de votacion', + 'ui.admin.bbs_settings.features.enable_chat' => 'Habilitar chat', + 'ui.admin.bbs_settings.features.enable_file_areas' => 'Habilitar areas de archivos', + 'ui.admin.bbs_settings.features.enable_guest_doors_page' => 'Habilitar pagina de puertas de invitados', + 'ui.admin.bbs_settings.features.guest_doors_page_help' => 'Muestra una pagina publica /guest-doors con puertas de acceso anonimo. Tambien muestra un enlace en la pagina de inicio de sesion.', + 'ui.admin.bbs_settings.features.default_echo_interface' => 'Interfaz de echo predeterminada', + 'ui.admin.bbs_settings.features.echo_list_forum' => 'Lista de echo (vista foro)', + 'ui.admin.bbs_settings.features.reader_message_list' => 'Lector (lista de mensajes)', + 'ui.admin.bbs_settings.features.default_echo_help' => 'Interfaz predeterminada para ver echomail. Los usuarios pueden sobrescribirla en su configuracion.', + 'ui.admin.bbs_settings.features.max_cross_post_areas' => 'Maximo de areas de cross-post', + 'ui.admin.bbs_settings.features.max_cross_post_help' => 'Numero maximo de areas adicionales a las que un usuario puede cross-postear (2-20).', + 'ui.admin.bbs_settings.features.save' => 'Guardar configuracion', + 'ui.admin.bbs_settings.credits.title' => 'Configuracion del sistema de creditos', + 'ui.admin.bbs_settings.credits.enabled' => 'Sistema de creditos habilitado', + 'ui.admin.bbs_settings.credits.currency_symbol' => 'Simbolo de moneda', + 'ui.admin.bbs_settings.credits.currency_symbol_help' => 'Ejemplo: $, USD (max 5 caracteres). Deje en blanco para no usar simbolo.', + 'ui.admin.bbs_settings.credits.daily_login_bonus_amount' => 'Monto del bono diario de inicio de sesion', + 'ui.admin.bbs_settings.credits.daily_login_bonus_help' => 'Cantidad de creditos otorgados a usuarios en el inicio diario.', + 'ui.admin.bbs_settings.credits.daily_login_delay_minutes' => 'Retraso de inicio diario (minutos)', + 'ui.admin.bbs_settings.credits.daily_login_delay_help' => 'Minutos despues del inicio antes de otorgar el bono diario.', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus' => 'Bono por aprobacion de nuevo usuario', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus_help' => 'Creditos otorgados cuando se aprueba un usuario pendiente.', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus' => 'Bono de hito de 14 dias para nuevo usuario', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus_help' => 'Bono unico otorgado cuando el usuario inicia sesion 14+ dias despues de crear la cuenta.', + 'ui.admin.bbs_settings.credits.netmail_cost' => 'Costo de netmail', + 'ui.admin.bbs_settings.credits.netmail_cost_help' => 'Creditos cobrados al enviar un mensaje netmail.', + 'ui.admin.bbs_settings.credits.echomail_reward' => 'Recompensa de echomail', + 'ui.admin.bbs_settings.credits.echomail_reward_help' => 'Creditos ganados al publicar un mensaje echomail. Se duplica cuando tiene >= 1200 caracteres.', + 'ui.admin.bbs_settings.credits.crashmail_cost' => 'Costo de crashmail', + 'ui.admin.bbs_settings.credits.crashmail_cost_help' => 'Creditos cobrados al enviar entrega directa/crashmail.', + 'ui.admin.bbs_settings.credits.poll_creation_cost' => 'Costo de creacion de encuesta', + 'ui.admin.bbs_settings.credits.poll_creation_cost_help' => 'Creditos cobrados al crear una nueva encuesta.', + 'ui.admin.bbs_settings.credits.transfer_fee_percentage' => 'Porcentaje de tarifa por transferencia', + 'ui.admin.bbs_settings.credits.transfer_fee_help' => 'Porcentaje de transferencias de credito tomado como tarifa (0.05 = 5%). Distribuido a sysops.', + 'ui.admin.bbs_settings.credits.referral_system' => 'Sistema de referidos', + 'ui.admin.bbs_settings.credits.enable_referral_system' => 'Habilitar sistema de referidos', + 'ui.admin.bbs_settings.credits.referral_bonus' => 'Bono de referido', + 'ui.admin.bbs_settings.credits.referral_bonus_help' => 'Creditos otorgados cuando un usuario referido es aprobado por admin.', + 'ui.admin.bbs_settings.credits.save' => 'Guardar configuracion de creditos', + 'ui.admin.bbs_settings.taglines.title' => 'Taglines', + 'ui.admin.bbs_settings.taglines.one_per_line' => 'Taglines (uno por linea)', + 'ui.admin.bbs_settings.taglines.placeholder' => 'Un tagline por linea.', + 'ui.admin.bbs_settings.taglines.help' => 'Aparecen como taglines seleccionables cuando los usuarios redactan mensajes.', + 'ui.admin.bbs_settings.taglines.save' => 'Guardar taglines', + 'ui.admin.bbs_settings.validation.max_cross_post_areas_range' => 'El maximo de areas de cross-post debe ser un entero entre 2 y 20.', + 'ui.admin.bbs_settings.validation.currency_symbol_length' => 'El simbolo de moneda debe tener 0-5 caracteres.', + 'ui.admin.bbs_settings.validation.daily_login_amount_non_negative' => 'El monto diario de inicio debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.daily_login_delay_non_negative' => 'El retraso diario de inicio debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.approval_bonus_non_negative' => 'El bono de aprobacion debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.netmail_cost_non_negative' => 'El costo de netmail debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.echomail_reward_non_negative' => 'La recompensa de echomail debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.crashmail_cost_non_negative' => 'El costo de crashmail debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.poll_creation_cost_non_negative' => 'El costo de creacion de encuesta debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.return_14_days_non_negative' => 'El bono de retorno de 14 dias debe ser un entero no negativo.', + 'ui.admin.bbs_settings.validation.transfer_fee_range' => 'La tarifa de transferencia debe estar entre 0 y 1 (0% a 100%).', + 'ui.admin.bbs_settings.validation.referral_bonus_non_negative' => 'El bono de referido debe ser un entero no negativo.', + + // Admin MRC Settings + 'ui.admin.mrc_settings.page_title' => 'Configuracion MRC', + 'ui.admin.mrc_settings.heading' => 'Configuracion de chat MRC', + 'ui.admin.mrc_settings.note_label' => 'Nota', + 'ui.admin.mrc_settings.note_text' => 'MRC (Multi Relay Chat) esta disponible como WebDoor.', + 'ui.admin.mrc_settings.note_enable_text' => 'Configure aqui los ajustes del daemon y luego habilite la puerta en', + 'ui.admin.mrc_settings.webdoors_admin' => 'administracion de WebDoors', + 'ui.admin.mrc_settings.load_failed' => 'No se pudo cargar la configuracion de MRC', + 'ui.admin.mrc_settings.load_error' => 'Error al cargar la configuracion de MRC', + 'ui.admin.mrc_settings.saved_success' => 'Configuracion de MRC guardada correctamente', + 'ui.admin.mrc_settings.restart_required' => 'Configuracion guardada. Reinicie el daemon para aplicar los cambios.', + 'ui.admin.mrc_settings.save_failed' => 'No se pudo guardar la configuracion de MRC', + 'ui.admin.mrc_settings.save_error' => 'Error al guardar la configuracion de MRC', + 'ui.admin.mrc_settings.general_settings' => 'Configuracion general', + 'ui.admin.mrc_settings.enable_daemon' => 'Habilitar daemon MRC', + 'ui.admin.mrc_settings.server_connection' => 'Conexion del servidor', + 'ui.admin.mrc_settings.server_host' => 'Host del servidor MRC', + 'ui.admin.mrc_settings.server_port_non_ssl' => 'Puerto del servidor (sin SSL)', + 'ui.admin.mrc_settings.server_port_ssl' => 'Puerto del servidor (SSL)', + 'ui.admin.mrc_settings.use_ssl_connection' => 'Usar conexion SSL', + 'ui.admin.mrc_settings.use_ssl_help' => 'Recomendado para comunicacion segura', + 'ui.admin.mrc_settings.save_general_settings' => 'Guardar configuracion general', + 'ui.admin.mrc_settings.bbs_identity' => 'Identidad del BBS', + 'ui.admin.mrc_settings.bbs_name' => 'Nombre del BBS', + 'ui.admin.mrc_settings.bbs_name_help' => 'Maximo 64 caracteres', + 'ui.admin.mrc_settings.platform_info' => 'Informacion de plataforma', + 'ui.admin.mrc_settings.platform_info_help' => 'Identificador de software y plataforma', + 'ui.admin.mrc_settings.sysop_name' => 'Nombre del sysop', + 'ui.admin.mrc_settings.bbs_information' => 'Informacion del BBS', + 'ui.admin.mrc_settings.website_url' => 'URL del sitio web', + 'ui.admin.mrc_settings.info_optional_help' => 'Opcional - para el comando INFO', + 'ui.admin.mrc_settings.telnet_address' => 'Direccion Telnet', + 'ui.admin.mrc_settings.bbs_description' => 'Descripcion del BBS', + 'ui.admin.mrc_settings.placeholder.server_host' => 'mrc.bottomlessabyss.net', + 'ui.admin.mrc_settings.placeholder.bbs_name' => 'BinktermPHP BBS', + 'ui.admin.mrc_settings.placeholder.platform' => 'BINKTERMPHP/Linux64/1.0.0', + 'ui.admin.mrc_settings.placeholder.sysop' => 'Sysop', + 'ui.admin.mrc_settings.placeholder.website' => 'https://example.com', + 'ui.admin.mrc_settings.placeholder.telnet' => 'telnet://bbs.example.com:23', + 'ui.admin.mrc_settings.placeholder.description' => 'Un BBS moderno...', + 'ui.admin.mrc_settings.save_identity_settings' => 'Guardar configuracion de identidad', + 'ui.admin.mrc_settings.connection_settings' => 'Configuracion de conexion', + 'ui.admin.mrc_settings.auto_reconnect' => 'Reconectar automaticamente', + 'ui.admin.mrc_settings.auto_reconnect_help' => 'Reconectar automaticamente si se pierde la conexion', + 'ui.admin.mrc_settings.reconnect_delay' => 'Retardo de reconexion (segundos)', + 'ui.admin.mrc_settings.reconnect_delay_help' => 'Tiempo de espera antes de reconectar (5-300 segundos)', + 'ui.admin.mrc_settings.ping_interval' => 'Intervalo de PING (segundos)', + 'ui.admin.mrc_settings.ping_interval_help' => 'Intervalo de PING esperado del servidor (30-300 segundos)', + 'ui.admin.mrc_settings.keepalive_timeout' => 'Tiempo de espera de keepalive (segundos)', + 'ui.admin.mrc_settings.keepalive_timeout_help' => 'Desconectar si no se recibe PING (60-600 segundos)', + 'ui.admin.mrc_settings.save_connection_settings' => 'Guardar configuracion de conexion', + 'ui.admin.mrc_settings.room_settings' => 'Configuracion de salas', + 'ui.admin.mrc_settings.default_room' => 'Sala predeterminada', + 'ui.admin.mrc_settings.default_room_help' => 'Sala predeterminada para conexiones nuevas', + 'ui.admin.mrc_settings.auto_join_rooms' => 'Unirse automaticamente a salas', + 'ui.admin.mrc_settings.auto_join_rooms_help' => 'Lista de salas separadas por comas para unirse automaticamente', + 'ui.admin.mrc_settings.message_settings' => 'Configuracion de mensajes', + 'ui.admin.mrc_settings.max_message_length' => 'Longitud maxima del mensaje', + 'ui.admin.mrc_settings.max_message_length_help' => 'Maximo de caracteres por mensaje (80-255)', + 'ui.admin.mrc_settings.history_limit' => 'Limite de historial', + 'ui.admin.mrc_settings.history_limit_help' => 'Mensajes a conservar por sala (100-10000)', + 'ui.admin.mrc_settings.prune_after_days' => 'Eliminar despues de (dias)', + 'ui.admin.mrc_settings.prune_after_days_help' => 'Eliminar mensajes mas antiguos que este valor (1-365 dias)', + 'ui.admin.mrc_settings.save_room_settings' => 'Guardar configuracion de salas', + + // Admin Activity Stats + 'ui.admin.activity_stats.page_title' => 'Estadisticas de actividad', + 'ui.admin.activity_stats.heading' => 'Estadisticas de actividad', + 'ui.admin.activity_stats.period' => 'Periodo', + 'ui.admin.activity_stats.period_7d' => 'Ultimos 7 dias', + 'ui.admin.activity_stats.period_30d' => 'Ultimos 30 dias', + 'ui.admin.activity_stats.period_90d' => 'Ultimos 90 dias', + 'ui.admin.activity_stats.period_all' => 'Todo el tiempo', + 'ui.admin.activity_stats.exclude_admins' => 'Excluir administradores', + 'ui.admin.activity_stats.dashboard' => 'Panel', + 'ui.admin.activity_stats.loading_activity_data' => 'Cargando datos de actividad...', + 'ui.admin.activity_stats.tab_overview' => 'Resumen', + 'ui.admin.activity_stats.tab_popular_areas' => 'Areas populares', + 'ui.admin.activity_stats.tab_doors' => 'Doors', + 'ui.admin.activity_stats.tab_file_activity' => 'Actividad de archivos', + 'ui.admin.activity_stats.tab_nodelist' => 'Nodelist', + 'ui.admin.activity_stats.tab_top_users' => 'Usuarios principales', + 'ui.admin.activity_stats.tab_hourly' => 'Por hora', + 'ui.admin.activity_stats.activity_by_category' => 'Actividad por categoria', + 'ui.admin.activity_stats.category' => 'Categoria', + 'ui.admin.activity_stats.events' => 'Eventos', + 'ui.admin.activity_stats.daily_activity_last_30' => 'Actividad diaria (ultimos 30 dias)', + 'ui.admin.activity_stats.date' => 'Fecha', + 'ui.admin.activity_stats.most_viewed_echoareas' => 'Echoareas mas vistas', + 'ui.admin.activity_stats.most_active_echoareas' => 'Echoareas mas activas (publicaciones)', + 'ui.admin.activity_stats.area' => 'Area', + 'ui.admin.activity_stats.views' => 'Vistas', + 'ui.admin.activity_stats.posts' => 'Publicaciones', + 'ui.admin.activity_stats.most_played_webdoors' => 'WebDoors mas jugados', + 'ui.admin.activity_stats.most_played_dos_doors' => 'DOS Doors mas jugados', + 'ui.admin.activity_stats.game' => 'Juego', + 'ui.admin.activity_stats.door' => 'Door', + 'ui.admin.activity_stats.sessions' => 'Sesiones', + 'ui.admin.activity_stats.top_downloaded_files' => 'Archivos mas descargados', + 'ui.admin.activity_stats.file' => 'Archivo', + 'ui.admin.activity_stats.downloads' => 'Descargas', + 'ui.admin.activity_stats.most_browsed_file_areas' => 'Areas de archivos mas exploradas', + 'ui.admin.activity_stats.most_searched_nodelist_queries' => 'Consultas de nodelist mas buscadas', + 'ui.admin.activity_stats.query' => 'Consulta', + 'ui.admin.activity_stats.searches' => 'Busquedas', + 'ui.admin.activity_stats.most_viewed_nodes' => 'Nodos mas vistos', + 'ui.admin.activity_stats.node' => 'Nodo', + 'ui.admin.activity_stats.most_active_users' => 'Usuarios mas activos', + 'ui.admin.activity_stats.user' => 'Usuario', + 'ui.admin.activity_stats.activity_by_hour_of_day' => 'Actividad por hora del dia', + 'ui.admin.activity_stats.hour' => 'Hora', + 'ui.admin.activity_stats.utc' => 'UTC', + 'ui.admin.activity_stats.no_data_for_period' => 'No hay datos para este periodo', + 'ui.admin.activity_stats.no_data' => 'Sin datos', + 'ui.admin.activity_stats.failed_load_statistics_prefix' => 'No se pudieron cargar las estadisticas. ', + 'ui.admin.activity_stats.echomail' => 'Echomail', + 'ui.admin.activity_stats.netmail' => 'Netmail', + 'ui.admin.activity_stats.files' => 'Archivos', + 'ui.admin.activity_stats.door_plays' => 'Partidas de doors', + 'ui.admin.activity_stats.logins' => 'Inicios de sesion', + 'ui.admin.activity_stats.total' => 'Total', + 'ui.admin.activity_stats.views_sent' => 'Vistas: {views} - Enviados: {sent}', + 'ui.admin.activity_stats.read_sent' => 'Leidos: {read} - Enviados: {sent}', + 'ui.admin.activity_stats.area_views' => 'Vistas de area', + 'ui.admin.activity_stats.sent' => 'Enviados', + 'ui.admin.activity_stats.read' => 'Leidos', + 'ui.admin.activity_stats.doors' => 'Doors', + 'ui.admin.activity_stats.nodelist' => 'Nodelist', + 'ui.admin.activity_stats.chat' => 'Chat', + 'ui.admin.activity_stats.auth' => 'Autenticacion', + 'ui.admin.activity_stats.anonymous' => '(anon)', + + // Admin Economy Viewer + 'ui.admin.economy.page_title' => 'Visor de economia', + 'ui.admin.economy.heading' => 'Visor de economia', + 'ui.admin.economy.period' => 'Periodo:', + 'ui.admin.economy.period_7d' => 'Ultimos 7 dias', + 'ui.admin.economy.period_30d' => 'Ultimos 30 dias', + 'ui.admin.economy.period_90d' => 'Ultimos 90 dias', + 'ui.admin.economy.period_all' => 'Todo el tiempo', + 'ui.admin.economy.dashboard' => 'Panel', + 'ui.admin.economy.loading_statistics' => 'Cargando estadisticas de economia...', + 'ui.admin.economy.period_snapshot' => 'Resumen del periodo', + 'ui.admin.economy.transactions' => 'Transacciones', + 'ui.admin.economy.active_users' => 'Usuarios activos', + 'ui.admin.economy.credits_earned' => 'Creditos ganados', + 'ui.admin.economy.credits_spent' => 'Creditos gastados', + 'ui.admin.economy.net_flow' => 'Flujo neto', + 'ui.admin.economy.current_distribution' => 'Distribucion actual', + 'ui.admin.economy.funded_users' => 'Usuarios con saldo', + 'ui.admin.economy.average_balance' => 'Saldo promedio', + 'ui.admin.economy.median_balance' => 'Saldo mediano', + 'ui.admin.economy.largest_balance' => 'Mayor saldo', + 'ui.admin.economy.richest_user' => 'Usuario mas rico', + 'ui.admin.economy.transaction_types' => 'Tipos de transaccion', + 'ui.admin.economy.type' => 'Tipo', + 'ui.admin.economy.count' => 'Cantidad', + 'ui.admin.economy.net' => 'Neto', + 'ui.admin.economy.top_earners' => 'Mayores ganadores', + 'ui.admin.economy.user' => 'Usuario', + 'ui.admin.economy.tx_short' => 'Tx', + 'ui.admin.economy.earned' => 'Ganado', + 'ui.admin.economy.spent' => 'Gastado', + 'ui.admin.economy.top_spenders' => 'Mayores gastadores', + 'ui.admin.economy.richest_accounts' => 'Cuentas mas ricas', + 'ui.admin.economy.balance' => 'Saldo', + 'ui.admin.economy.recent_transactions' => 'Transacciones recientes', + 'ui.admin.economy.amount' => 'Monto', + 'ui.admin.economy.na' => 'n/a', + 'ui.admin.economy.credits_in_circulation' => 'Creditos en circulacion', + 'ui.admin.economy.funded_wallets' => 'Billeteras con saldo', + 'ui.admin.economy.credits_disabled_notice' => 'Los creditos estan deshabilitados actualmente. Los saldos historicos y datos del libro mayor aun se muestran.', + 'ui.admin.economy.users' => 'usuarios', + 'ui.admin.economy.no_transactions_period' => 'No hay transacciones para este periodo', + 'ui.admin.economy.no_earning_activity_period' => 'No hay actividad de ganancias para este periodo', + 'ui.admin.economy.no_spending_activity_period' => 'No hay actividad de gasto para este periodo', + 'ui.admin.economy.no_funded_users_yet' => 'Aun no hay usuarios con saldo', + 'ui.admin.economy.no_description' => 'Sin descripcion', + 'ui.admin.economy.bal_short' => 'Sal', + 'ui.admin.economy.no_recent_transactions_period' => 'No hay transacciones recientes en este periodo', + 'ui.admin.economy.load_failed' => 'No se pudieron cargar las estadisticas de economia', + + // File Areas Page + 'ui.fileareas.page_title' => 'Areas de archivos', + 'ui.fileareas.heading' => 'Gestion de areas de archivos', + 'ui.fileareas.add_file_area' => 'Agregar area de archivos', + 'ui.fileareas.edit_file_area' => 'Editar area de archivos', + 'ui.fileareas.list_title' => 'Areas de archivos', + 'ui.fileareas.loading_file_areas' => 'Cargando areas de archivos...', + 'ui.fileareas.load_failed' => 'No se pudieron cargar las areas de archivos', + 'ui.fileareas.load_details_failed' => 'No se pudieron cargar los detalles del area de archivos', + 'ui.fileareas.updated_success' => 'Area de archivos actualizada', + 'ui.fileareas.created_success' => 'Area de archivos creada', + 'ui.fileareas.deleted_success' => 'Area de archivos eliminada', + 'ui.fileareas.active_areas' => 'Areas activas:', + 'ui.fileareas.total_files' => 'Archivos totales:', + 'ui.fileareas.total_size' => 'Tamano total:', + 'ui.fileareas.settings_title' => 'Configuracion de area de archivos', + 'ui.fileareas.replace_existing_label' => 'Reemplazar existentes:', + 'ui.fileareas.replace_existing_help' => 'Cuando esta habilitado, los archivos nuevos con el mismo nombre reemplazan los anteriores (util para NODELIST, etc.)', + 'ui.fileareas.versioning_label' => 'Versionado:', + 'ui.fileareas.versioning_help' => 'Cuando esta deshabilitado, los archivos con nombres duplicados reciben sufijos de version (_1, _2, etc.)', + 'ui.fileareas.tag_required' => 'Tag *', + 'ui.fileareas.tag_help' => 'Tag de area de archivos (ej.: NODELIST, GENERAL_FILES)', + 'ui.fileareas.description_required' => 'Descripcion *', + 'ui.fileareas.description_help' => 'Descripcion breve del area de archivos', + 'ui.fileareas.network_domain' => 'Dominio de red', + 'ui.fileareas.max_file_size_mb' => 'Tamano maximo de archivo (MB)', + 'ui.fileareas.maximum_file_size' => 'Tamano maximo de archivo', + 'ui.fileareas.tic_password_optional' => 'Contrasena TIC (opcional)', + 'ui.fileareas.tic_password_help' => 'Contrasena de file echo para archivos TIC (campo FSC-87 Pw)', + 'ui.fileareas.allowed_extensions' => 'Extensiones permitidas', + 'ui.fileareas.extensions_placeholder_allowed' => 'ej.: zip,txt,pdf', + 'ui.fileareas.allowed_extensions_help' => 'Lista separada por comas (dejar vacio para todas)', + 'ui.fileareas.blocked_extensions' => 'Extensiones bloqueadas', + 'ui.fileareas.extensions_placeholder_blocked' => 'ej.: exe,bat,sh', + 'ui.fileareas.blocked_extensions_help' => 'Lista separada por comas', + 'ui.fileareas.replace_existing_files' => 'Reemplazar archivos existentes', + 'ui.fileareas.replace_existing_files_help' => 'Reemplazar archivos con el mismo nombre en lugar de versionar', + 'ui.fileareas.allow_duplicate_content' => 'Permitir contenido duplicado', + 'ui.fileareas.allow_duplicate_content_help' => 'Permitir el mismo contenido de archivo (hash) con nombres distintos', + 'ui.fileareas.local_only' => 'Solo local', + 'ui.fileareas.local_only_help' => 'No reenviar archivos a uplinks', + 'ui.fileareas.upload_permission' => 'Permiso de carga', + 'ui.fileareas.upload_users_can_upload' => 'Los usuarios pueden subir', + 'ui.fileareas.upload_admin_only' => 'Solo admin', + 'ui.fileareas.upload_read_only' => 'Solo lectura (sin cargas)', + 'ui.fileareas.upload_permission_help' => 'Controla quien puede subir archivos a esta area', + 'ui.fileareas.virus_scanning' => 'Escaneo de virus', + 'ui.fileareas.virus_scanning_help' => 'Escanear archivos subidos en busca de virus (requiere ClamAV)', + 'ui.fileareas.file_area_is_active' => 'El area de archivos esta activa', + 'ui.fileareas.edit_rules' => 'Editar reglas', + 'ui.fileareas.delete_confirm_prefix' => 'Esta seguro de que desea eliminar esta area de archivos:', + 'ui.fileareas.delete_confirm_warning' => 'Esto tambien eliminara todos los archivos en esta area.', + 'ui.fileareas.no_file_areas_found' => 'No se encontraron areas de archivos', + 'ui.fileareas.tag' => 'Tag', + 'ui.fileareas.files' => 'Archivos', + 'ui.fileareas.size' => 'Tamano', + 'ui.fileareas.local' => 'Local', + 'ui.fileareas.replace' => 'Reemplazar', + + // Admin Auto Feed + 'ui.admin.auto_feed.load_details_failed' => 'No se pudieron cargar los detalles del feed', + 'ui.admin.auto_feed.operation_failed' => 'La operacion fallo', + 'ui.admin.auto_feed.deleted_success' => 'Feed eliminado correctamente', + 'ui.admin.auto_feed.updated_success' => 'Feed actualizado correctamente', + 'ui.admin.auto_feed.created_success' => 'Feed creado correctamente', + 'ui.admin.auto_feed.delete_failed' => 'Error al eliminar', + 'ui.admin.auto_feed.check_failed' => 'Error al verificar', + 'ui.admin.auto_feed.checked_articles_posted' => 'Feed verificado: {count} articulo(s) nuevo(s) publicado(s)', + 'ui.admin.auto_feed.page_title' => 'Auto Feed', + 'ui.admin.auto_feed.heading' => 'Auto Feed', + 'ui.admin.auto_feed.statistics' => 'Estadisticas', + 'ui.admin.auto_feed.total_feeds' => 'Total de feeds', + 'ui.admin.auto_feed.add_feed' => 'Agregar feed', + 'ui.admin.auto_feed.edit_feed' => 'Editar feed', + 'ui.admin.auto_feed.rss_atom_feeds' => 'Feeds RSS/Atom', + 'ui.admin.auto_feed.loading_feeds' => 'Cargando feeds...', + 'ui.admin.auto_feed.loading_echo_areas' => 'Cargando areas de eco...', + 'ui.admin.auto_feed.loading_users' => 'Cargando usuarios...', + 'ui.admin.auto_feed.active_label' => 'Activo', + 'ui.admin.auto_feed.inactive_label' => 'Inactivo', + 'ui.admin.auto_feed.articles_posted' => 'Articulos publicados', + 'ui.admin.auto_feed.about_title' => 'Acerca de', + 'ui.admin.auto_feed.about_text' => 'Auto Feed monitorea feeds RSS/Atom y publica automaticamente nuevos articulos en las echoareas especificadas.', + 'ui.admin.auto_feed.cron_hint_prefix' => 'Ejecute', + 'ui.admin.auto_feed.cron_hint_suffix' => 'por cron para verificar los feeds periodicamente.', + 'ui.admin.auto_feed.feed_name' => 'Nombre del feed', + 'ui.admin.auto_feed.feed_name_placeholder' => 'ej.: Tech News, BBC Headlines', + 'ui.admin.auto_feed.feed_name_help' => 'Nombre descriptivo para este feed', + 'ui.admin.auto_feed.feed_url_required' => 'URL del feed *', + 'ui.admin.auto_feed.feed_url_placeholder' => 'https://example.com/feed.rss', + 'ui.admin.auto_feed.feed_url_help' => 'URL de feed RSS o Atom', + 'ui.admin.auto_feed.echo_area_required' => 'Area de eco *', + 'ui.admin.auto_feed.echo_area' => 'Area de eco', + 'ui.admin.auto_feed.echo_area_help' => 'Area de eco donde publicar articulos', + 'ui.admin.auto_feed.post_as_user_required' => 'Publicar como usuario *', + 'ui.admin.auto_feed.post_as' => 'Publicar como', + 'ui.admin.auto_feed.post_as_user_help' => 'Cuenta de usuario para publicar mensajes', + 'ui.admin.auto_feed.max_articles' => 'Maximo de articulos', + 'ui.admin.auto_feed.per_check' => 'Por verificacion', + 'ui.admin.auto_feed.confirm_delete_title' => 'Confirmar eliminacion', + 'ui.admin.auto_feed.confirm_delete_text_prefix' => 'Esta seguro de que desea eliminar el feed', + 'ui.admin.auto_feed.confirm_delete_warning' => 'Esta accion no se puede deshacer.', + 'ui.admin.auto_feed.invalid_api_response' => 'Respuesta de API invalida', + 'ui.admin.auto_feed.failed_to_load_feeds_prefix' => 'No se pudieron cargar los feeds:', + 'ui.admin.auto_feed.no_feeds_configured' => 'No hay feeds configurados', + 'ui.admin.auto_feed.never' => 'Nunca', + 'ui.admin.auto_feed.unnamed_feed' => 'Feed sin nombre', + 'ui.admin.auto_feed.unknown' => 'Desconocido', + 'ui.admin.auto_feed.check_now' => 'Verificar ahora', + 'ui.admin.auto_feed.checking_feed' => 'Verificando feed...', + 'ui.admin.auto_feed.select_echo_area' => 'Seleccione area de eco...', + 'ui.admin.auto_feed.name_url' => 'Nombre/URL', + 'ui.admin.auto_feed.articles' => 'Articulos', + 'ui.admin.auto_feed.last_check' => 'Ultima verificacion', + 'ui.admin.auto_feed.actions' => 'Acciones', + 'ui.admin.auto_feed.user_prefix' => 'Usuario #', + + // Echo Areas Page + 'ui.echoareas.page_title' => 'Areas de eco', + 'ui.echoareas.heading' => 'Gestion de areas de eco', + 'ui.echoareas.add_echo_area' => 'Agregar area de eco', + 'ui.echoareas.edit_echo_area' => 'Editar area de eco', + 'ui.echoareas.active_echo_areas' => 'Areas de eco activas', + 'ui.echoareas.search_tags_placeholder' => 'Buscar etiquetas...', + 'ui.echoareas.loading_echo_areas' => 'Cargando areas de eco...', + 'ui.echoareas.statistics' => 'Estadisticas', + 'ui.echoareas.active_areas' => 'Areas activas:', + 'ui.echoareas.total_messages' => 'Mensajes totales:', + 'ui.echoareas.todays_messages' => 'Mensajes de hoy:', + 'ui.echoareas.color_legend' => 'Leyenda de colores', + 'ui.echoareas.color_legend_help' => 'Las areas de eco pueden codificarse por color para facilitar su identificacion:', + 'ui.echoareas.color_green' => 'Verde', + 'ui.echoareas.color_blue' => 'Azul', + 'ui.echoareas.color_red' => 'Rojo', + 'ui.echoareas.color_yellow' => 'Amarillo', + 'ui.echoareas.color_purple' => 'Morado', + 'ui.echoareas.color_orange' => 'Naranja', + 'ui.echoareas.color_teal' => 'Verde azulado', + 'ui.echoareas.color_cyan' => 'Cian', + 'ui.echoareas.color_light_blue' => 'Azul claro', + 'ui.echoareas.color_indigo' => 'Indigo', + 'ui.echoareas.color_pink' => 'Rosa', + 'ui.echoareas.color_gray' => 'Gris', + 'ui.echoareas.legend_general_topics' => 'Temas generales', + 'ui.echoareas.legend_technical_testing' => 'Tecnico/Pruebas', + 'ui.echoareas.legend_important_official' => 'Importante/Oficial', + 'ui.echoareas.legend_administrative' => 'Administrativo', + 'ui.echoareas.legend_special_interest' => 'Interes especial', + 'ui.echoareas.legend_regional' => 'Regional', + 'ui.echoareas.tag_required' => 'Etiqueta *', + 'ui.echoareas.tag_help' => 'Etiqueta del area de eco (ej.: FIDONET.GEN, LOCAL.TEST)', + 'ui.echoareas.description_required' => 'Descripcion *', + 'ui.echoareas.description_help' => 'Descripcion breve del area de eco', + 'ui.echoareas.uplink_address' => 'Direccion uplink', + 'ui.echoareas.uplink_address_help' => 'Sobrescribir direccion FidoNet del uplink', + 'ui.echoareas.color' => 'Color', + 'ui.echoareas.selected_color' => 'Seleccionado:', + 'ui.echoareas.network_domain_help' => 'Dominio de red (ej: fidonet, fsxnet, etc.)', + 'ui.echoareas.moderator' => 'Moderador', + 'ui.echoareas.moderator_help' => 'Nombre del moderador del area de eco', + 'ui.echoareas.posting_name_policy' => 'Politica de nombre al publicar', + 'ui.echoareas.posting_name_policy_inherit' => 'Usar predeterminado del uplink', + 'ui.echoareas.posting_name_policy_real_name' => 'Nombre real', + 'ui.echoareas.posting_name_policy_username' => 'Usuario', + 'ui.echoareas.posting_name_policy_help' => 'Sobrescribe la politica de nombre al publicar del uplink para esta area de eco.', + 'ui.echoareas.local_only' => 'Solo local', + 'ui.echoareas.local_only_help' => 'Los mensajes en areas locales no se transmiten a uplinks', + 'ui.echoareas.sysop_access_only' => 'Solo acceso sysop', + 'ui.echoareas.sysop_access_only_help' => 'Restringir esta area de eco solo a usuarios sysop/admin', + 'ui.echoareas.public_gemini_access' => 'Acceso publico Gemini', + 'ui.echoareas.public_gemini_access_help' => 'Exponer esta area de eco como contenido de solo lectura en el servidor de capsula Gemini', + 'ui.echoareas.delete_confirm_prefix' => 'Esta seguro de que desea eliminar el area de eco', + 'ui.echoareas.delete_confirm_warning' => 'Esta accion no se puede deshacer y afectara el enrutamiento de mensajes.', + 'ui.echoareas.none_found' => 'No se encontraron areas de eco', + 'ui.echoareas.tag' => 'Etiqueta', + 'ui.echoareas.messages' => 'Mensajes', + 'ui.echoareas.uplink' => 'Uplink', + 'ui.echoareas.local' => 'Local', + 'ui.echoareas.sysop' => 'Sysop', + 'ui.echoareas.local_only_title' => 'Solo local - no se transmite a uplinks', + 'ui.echoareas.sysop_access_only_title' => 'Solo acceso sysop', + 'ui.echoareas.mod_prefix' => 'Mod:', + 'ui.echoareas.load_failed' => 'No se pudieron cargar las areas de eco', + 'ui.echoareas.load_details_failed' => 'No se pudieron cargar los detalles del area de eco', + 'ui.echoareas.updated_success' => 'Area de eco actualizada correctamente', + 'ui.echoareas.created_success' => 'Area de eco creada correctamente', + 'ui.echoareas.deleted_success' => 'Area de eco eliminada correctamente', + 'ui.echoareas.sync_not_implemented' => 'La funcionalidad de sincronizacion aun no esta implementada', + 'ui.echoareas.export_not_implemented' => 'La funcionalidad de exportacion aun no esta implementada', + 'ui.echoareas.validate_not_implemented' => 'La funcionalidad de validacion aun no esta implementada', + + // BBS Menu Shell + 'ui.shell.menu' => 'Menu', + 'ui.shell.menu_main_q_title' => 'Menu principal (Q)', + + // Login Page + 'ui.login.title' => 'Iniciar sesion', + 'ui.login.login_to_system' => 'Iniciar sesion en {system_name}', + 'ui.login.username' => 'Usuario', + 'ui.login.password' => 'Contrasena', + 'ui.login.remember_me_30_days' => 'Recordarme (30 dias)', + 'ui.login.no_account' => 'No tiene una cuenta?', + 'ui.login.request_access' => 'Solicitar acceso', + 'ui.login.forgot_password' => 'Olvido su contrasena?', + 'ui.login.reset_password' => 'Restablecer contrasena', + 'ui.login.sending' => 'Enviando...', + 'ui.login.reminder_sent_netmail_email_suffix' => '(enviado por netmail y correo electronico)', + 'ui.login.reminder_sent_netmail_only_suffix' => '(enviado por netmail)', + 'ui.login.connect_via_telnet' => 'Conectar via Telnet', + + // Guest Doors Page + 'ui.guest_doors.page_title' => 'Puertas de invitados', + 'ui.guest_doors.heading' => 'Acceso de invitados', + 'ui.guest_doors.subtitle' => 'Elige una puerta a continuacion para comenzar.', + 'ui.guest_doors.no_doors' => 'No hay puertas de invitados disponibles actualmente.', + 'ui.guest_doors.connect' => 'Conectar', + 'ui.guest_doors.signin_link' => 'Iniciar sesion', + 'ui.guest_doors.register_link' => 'solicitar una cuenta', + + // Register Page + 'ui.register.title' => 'Registro', + 'ui.register.create_account' => 'Crear cuenta', + 'ui.register.approval_required' => 'El registro requiere aprobacion.', + 'ui.register.approval_required_help' => 'Su cuenta sera revisada por un administrador antes de activarse.', + 'ui.register.subject_to_rules' => 'Todas las cuentas estan sujetas a', + 'ui.register.honeypot_website_leave_blank' => 'Sitio web (dejar en blanco)', + 'ui.register.username' => 'Usuario', + 'ui.register.username_help' => '3-20 caracteres, solo letras, numeros y guiones bajos', + 'ui.register.password' => 'Contrasena', + 'ui.register.password_help' => 'Minimo 8 caracteres. Use una mezcla de letras, numeros y simbolos para mayor seguridad.', + 'ui.register.confirm_password' => 'Confirmar contrasena', + 'ui.register.email_address' => 'Correo electronico', + 'ui.register.email_help' => 'Opcional - para recuperacion de cuenta y notificaciones', + 'ui.register.real_name' => 'Nombre real', + 'ui.register.real_name_help' => 'Su nombre real (requerido para FidoNet)', + 'ui.register.location' => 'Ubicacion', + 'ui.register.location_placeholder' => 'Ciudad, Estado/Pais', + 'ui.register.location_help' => 'Su ubicacion (opcional, se muestra en quien esta en linea)', + 'ui.register.reason_for_joining' => 'Motivo para unirse', + 'ui.register.reason_placeholder' => 'Cuentenos por que desea unirse ...', + 'ui.register.submit_registration' => 'Enviar registro', + 'ui.register.already_have_account' => 'Ya tiene una cuenta?', + 'ui.register.sign_in' => 'Iniciar sesion', + 'ui.register.password_strength.weak' => 'Debil - Agregue mas variedad', + 'ui.register.password_strength.fair' => 'Regular - Puede ser mas fuerte', + 'ui.register.password_strength.good' => 'Buena - Buena contrasena!', + 'ui.register.password_strength.strong' => 'Fuerte - Excelente!', + 'ui.register.passwords_do_not_match' => 'Las contrasenas no coinciden.', + 'ui.register.submitting' => 'Enviando...', + 'ui.register.submitted_success' => 'Registro enviado correctamente. Se le notificara cuando su cuenta sea aprobada.', + 'ui.register.go_to_login' => 'Ir al inicio de sesion', + + // Forgot Password Page + 'ui.forgot_password.title' => 'Olvido su contrasena', + 'ui.forgot_password.reset_password' => 'Restablecer contrasena', + 'ui.forgot_password.instructions' => 'Ingrese su nombre de usuario o correo electronico y le enviaremos un enlace para restablecer su contrasena.', + 'ui.forgot_password.username_or_email' => 'Usuario o correo electronico', + 'ui.forgot_password.username_or_email_help' => 'Ingrese el nombre de usuario o correo electronico asociado a su cuenta.', + 'ui.forgot_password.send_reset_link' => 'Enviar enlace de restablecimiento', + 'ui.forgot_password.remember_password' => 'Recuerda su contrasena?', + 'ui.forgot_password.back_to_login' => 'Volver al inicio de sesion', + 'ui.forgot_password.sending' => 'Enviando...', + 'ui.forgot_password.check_email_notice' => 'Revise su bandeja de correo (y carpeta de spam) para el enlace de restablecimiento.', + 'ui.forgot_password.request_failed' => 'No se pudo procesar la solicitud', + 'ui.forgot_password.reset_link_sent_if_exists' => 'Si existe una cuenta con ese usuario o correo, se ha enviado un enlace para restablecer la contrasena.', + 'ui.password_reset_email.subject' => 'Solicitud de restablecimiento de contrasena - {system_name}', + 'ui.password_reset_email.header' => 'Solicitud de restablecimiento de contrasena', + 'ui.password_reset_email.greeting' => 'Hola {name},', + 'ui.password_reset_email.request_received' => 'Recibimos una solicitud para restablecer la contrasena de su cuenta en {system_name}.', + 'ui.password_reset_email.click_link_below' => 'Para restablecer su contrasena, haga clic en el siguiente enlace:', + 'ui.password_reset_email.click_button_below' => 'Para restablecer su contrasena, haga clic en el boton de abajo:', + 'ui.password_reset_email.button' => 'Restablecer su contrasena', + 'ui.password_reset_email.copy_link' => 'O copie y pegue este enlace en su navegador:', + 'ui.password_reset_email.expires_in_hours' => 'Este enlace expirara en {hours} horas.', + 'ui.password_reset_email.security_notes' => 'Notas de seguridad:', + 'ui.password_reset_email.note_never_share' => 'Nunca comparta este enlace con nadie', + 'ui.password_reset_email.note_one_time' => 'Este enlace solo se puede usar una vez', + 'ui.password_reset_email.note_request_new' => 'Si necesita otro enlace, solicite uno nuevo desde la pagina de inicio de sesion', + 'ui.password_reset_email.if_not_requested' => 'Si no solicito un restablecimiento de contrasena, puede ignorar este correo.', + 'ui.password_reset_email.password_unchanged_notice' => 'Su contrasena no cambiara a menos que haga clic en el enlace y cree una nueva contrasena.', + 'ui.password_reset_email.best_regards' => 'Saludos,', + 'ui.password_reset_email.footer_automated' => 'Este es un mensaje automatico de {system_name}', + 'ui.password_reset_email.footer_no_reply' => 'Por favor no responda a este correo.', + + // Reset Password Page + 'ui.reset_password.title' => 'Restablecer contrasena', + 'ui.reset_password.create_new_password' => 'Crear nueva contrasena', + 'ui.reset_password.validating_token' => 'Validando token de restablecimiento...', + 'ui.reset_password.invalid_or_expired_token' => 'Token invalido o expirado', + 'ui.reset_password.invalid_token_help_line1' => 'Este enlace de restablecimiento es invalido o ha expirado.', + 'ui.reset_password.invalid_token_help_line2' => 'Solicite un nuevo enlace de restablecimiento.', + 'ui.reset_password.enter_new_password' => 'Ingrese su nueva contrasena a continuacion.', + 'ui.reset_password.new_password' => 'Nueva contrasena', + 'ui.reset_password.password_min_length_help' => 'La contrasena debe tener al menos 8 caracteres.', + 'ui.reset_password.confirm_new_password' => 'Confirmar nueva contrasena', + 'ui.reset_password.passwords_do_not_match' => 'Las contrasenas no coinciden.', + 'ui.reset_password.request_new_reset_link' => 'Solicitar nuevo enlace de restablecimiento', + 'ui.reset_password.resetting' => 'Restableciendo...', + 'ui.reset_password.redirect_notice' => 'Sera redirigido a la pagina de inicio de sesion en 3 segundos...', + 'ui.reset_password.success_reset_complete' => 'La contrasena se restablecio correctamente. Ya puede iniciar sesion con su nueva contrasena.', + + // Profile Page + 'ui.profile.title' => 'Perfil', + 'ui.profile.my_profile' => 'Mi perfil', + 'ui.profile.profile_information' => 'Informacion del perfil', + 'ui.profile.username' => 'Usuario', + 'ui.profile.username_readonly_help' => 'El nombre de usuario no se puede cambiar', + 'ui.profile.real_name' => 'Nombre real', + 'ui.profile.real_name_help' => 'Su nombre real como se muestra en los mensajes', + 'ui.profile.email_address' => 'Correo electronico', + 'ui.profile.email_help' => 'Correo electronico (opcional, no se muestra publicamente)', + 'ui.profile.location' => 'Ubicacion', + 'ui.profile.location_placeholder' => 'Ciudad, Estado/Pais', + 'ui.profile.location_help' => 'Su ubicacion (se muestra en quien esta en linea)', + 'ui.profile.change_password' => 'Cambiar contrasena', + 'ui.profile.current_password' => 'Contrasena actual', + 'ui.profile.new_password' => 'Nueva contrasena', + 'ui.profile.password_blank_keep_help' => 'Deje los campos de contrasena en blanco para mantener la contrasena actual', + 'ui.profile.account_created' => 'Cuenta creada', + 'ui.profile.last_login' => 'Ultimo inicio de sesion', + 'ui.profile.never' => 'Nunca', + 'ui.profile.update_profile' => 'Actualizar perfil', + 'ui.profile.system_information' => 'Informacion del sistema', + 'ui.profile.system' => 'Sistema', + 'ui.profile.address' => 'Direccion', + 'ui.profile.role' => 'Rol', + 'ui.profile.role_administrator' => 'Administrador', + 'ui.profile.role_user' => 'Usuario', + 'ui.profile.activity_summary' => 'Resumen de actividad', + 'ui.profile.credits' => 'Creditos', + 'ui.profile.disabled' => 'Deshabilitado', + 'ui.profile.netmail_sent' => 'Netmail enviado', + 'ui.profile.echomail_posted' => 'Echomail publicado', + 'ui.profile.total_messages' => 'Mensajes totales', + 'ui.profile.credit_system' => 'Sistema de creditos', + 'ui.profile.credit_costs_rewards' => 'Costos y recompensas de creditos:', + 'ui.profile.daily_login' => 'Inicio diario', + 'ui.profile.crashmail' => 'Crashmail', + 'ui.profile.credit_note' => 'Las puertas web y otros elementos del sistema pueden tener costos o recompensas adicionales.', + 'ui.profile.updating_profile' => 'Actualizando perfil...', + 'ui.profile.updated_successfully' => 'Perfil actualizado correctamente.', + + // Public User Profile Page + 'ui.user_profile.title_with_username' => 'Perfil de {username}', + 'ui.user_profile.edit_my_profile' => 'Editar mi perfil', + 'ui.user_profile.user_information' => 'Informacion del usuario', + 'ui.user_profile.username' => 'Usuario', + 'ui.user_profile.real_name' => 'Nombre real', + 'ui.user_profile.not_specified' => 'No especificado', + 'ui.user_profile.location' => 'Ubicacion', + 'ui.user_profile.fidonet_address' => 'Direccion FidoNet', + 'ui.user_profile.member_since' => 'Miembro desde', + 'ui.user_profile.last_seen' => 'Ultima vez visto', + 'ui.user_profile.role' => 'Rol', + 'ui.user_profile.role_administrator' => 'Administrador', + 'ui.user_profile.role_user' => 'Usuario', + 'ui.user_profile.credits' => 'Creditos', + 'ui.user_profile.current_balance' => 'Saldo actual', + 'ui.user_profile.send_credits' => 'Enviar creditos', + 'ui.user_profile.amount' => 'Cantidad', + 'ui.user_profile.send_credits_limit_fee' => 'Maximo 200 creditos por transaccion. Se aplica una comision de {fee_percent}%.', + 'ui.user_profile.message_optional' => 'Mensaje (opcional)', + 'ui.user_profile.message_optional_placeholder' => 'Para que es esto?', + 'ui.user_profile.send_credits_fee_note' => 'Comision de transaccion del {fee_percent}% distribuida a operadores del sistema', + 'ui.user_profile.activity_summary' => 'Resumen de actividad', + 'ui.user_profile.netmail_sent' => 'Netmail enviado', + 'ui.user_profile.echomail_posted' => 'Echomail publicado', + 'ui.user_profile.total_messages' => 'Mensajes totales', + 'ui.user_profile.additional_info_private' => 'La informacion adicional del perfil es privada', + 'ui.user_profile.transaction_history' => 'Historial de transacciones', + 'ui.user_profile.admin_view' => 'Vista de admin', + 'ui.user_profile.date' => 'Fecha', + 'ui.user_profile.type' => 'Tipo', + 'ui.user_profile.description' => 'Descripcion', + 'ui.user_profile.balance' => 'Saldo', + 'ui.user_profile.load_more_transactions' => 'Cargar mas transacciones', + 'ui.user_profile.no_transactions_yet' => 'Aun no hay transacciones', + 'ui.user_profile.no_more_transactions' => 'No hay mas transacciones', + 'ui.user_profile.load_transactions_failed' => 'No se pudieron cargar las transacciones', + 'ui.user_profile.sending' => 'Enviando...', + 'ui.user_profile.send_credits_success' => 'Se enviaron correctamente {symbol}{amount} a {username}. (Comision: {symbol}{fee}, Recibio: {symbol}{received})', + 'ui.user_profile.transaction_type.daily_login' => 'Inicio diario', + 'ui.user_profile.transaction_type.system_reward' => 'Recompensa del sistema', + 'ui.user_profile.transaction_type.payment' => 'Pago', + 'ui.user_profile.transaction_type.admin_adjustment' => 'Ajuste de administrador', + 'ui.user_profile.transaction_type.unknown' => 'Desconocido', + + // Shared Message Page + 'ui.shared_message.title' => 'Mensaje compartido', + 'ui.shared_message.loading' => 'Cargando mensaje compartido...', + 'ui.shared_message.login_to_reply' => 'Inicie sesion para responder', + 'ui.shared_message.unable_to_load' => 'No se pudo cargar el mensaje', + 'ui.shared_message.go_to_main_site' => 'Ir al sitio principal', + 'ui.shared_message.login_required' => 'Inicio de sesion requerido', + 'ui.shared_message.login_required_help' => 'Este mensaje compartido requiere que inicie sesion para verlo.', + 'ui.shared_message.about_this_message' => 'Acerca de este mensaje', + 'ui.shared_message.about_text' => 'Este mensaje proviene de un sistema de echomail. Echomail es parte de una red mundial de tablones que permite a los usuarios enviar mensajes y participar en areas de discusion (echoareas) entre diferentes sistemas.', + 'ui.shared_message.join_conversation' => 'Quiere unirse a la conversacion?', + 'ui.shared_message.join_conversation_help' => 'Para ver mas mensajes, participar en discusiones y responder publicaciones como esta, debe registrarse para tener una cuenta en este sistema.', + 'ui.shared_message.register_now' => 'Registrarse ahora', + 'ui.shared_message.already_have_account' => 'Ya tiene cuenta?', + 'ui.shared_message.network' => 'Red', + 'ui.shared_message.powered_by' => 'Impulsado por', + 'ui.shared_message.shared_on' => 'Este mensaje fue compartido', + 'ui.shared_message.shared_by' => 'Compartido por {name}', + 'ui.shared_message.viewed_times' => 'Visto {count} veces', + 'ui.shared_message.local_bulletin_board' => 'Tablon local', + 'ui.shared_message.from_label' => 'De:', + 'ui.shared_message.to_label' => 'Para:', + 'ui.shared_message.date_label' => 'Fecha:', + 'ui.shared_message.subject_label' => 'Asunto:', + 'ui.shared_message.kludge_lines' => 'Lineas Kludge', + 'ui.shared_message.show_kludge_lines' => 'Mostrar lineas Kludge', + 'ui.shared_message.load_failed' => 'No se pudo cargar el mensaje compartido', + 'ui.shared_message.not_available' => 'Este mensaje compartido no esta disponible. Puede haber expirado o sido revocado.', + 'ui.shared_message.load_failed_retry' => 'No se pudo cargar el mensaje compartido. Intentelo de nuevo mas tarde.', + + // Netmail Page + 'ui.netmail.title' => 'Netmail', + 'ui.netmail.compose' => 'Redactar', + 'ui.netmail.messages' => 'Mensajes', + 'ui.netmail.delete_selected' => 'Eliminar seleccionados', + 'ui.netmail.quick_stats' => 'Estadisticas rapidas', + + // Address Book + 'ui.address_book.title' => 'Libreta de direcciones', + 'ui.address_book.search_placeholder' => 'Buscar direcciones...', + 'ui.address_book.entries' => 'entradas', + 'ui.address_book.add_entry' => 'Agregar entrada a la libreta', + 'ui.address_book.edit_entry' => 'Editar entrada de la libreta', + 'ui.address_book.no_entries_found' => 'No se encontraron entradas', + 'ui.address_book.unnamed' => 'Sin nombre', + 'ui.address_book.no_address' => 'Sin direccion', + 'ui.address_book.user_id' => 'ID de usuario', + 'ui.address_book.node_address' => 'Direccion de nodo', + 'ui.address_book.email_address' => 'Correo electronico', + 'ui.address_book.name_placeholder' => 'Ingrese un nombre descriptivo (ej., Juan Perez)', + 'ui.address_book.name_help' => 'Nombre descriptivo para este contacto', + 'ui.address_book.user_id_placeholder' => 'Ingrese el ID de usuario para mensajeria', + 'ui.address_book.user_id_help' => 'ID/alias que se usa al enviar mensajes', + 'ui.address_book.node_address_help' => 'Formato: zona:red/nodo o zona:red/nodo.punto', + 'ui.address_book.email_help' => 'Solo para referencia; no se usa para mensajeria', + 'ui.address_book.description_placeholder' => 'Notas sobre este contacto...', + 'ui.address_book.always_crashmail' => 'Usar siempre crashmail para este destinatario', + 'ui.address_book.always_crashmail_help' => 'Activa crashmail automaticamente al redactar mensajes para este contacto.', + + // Echomail Page + 'ui.echomail.title' => 'Echomail', + 'ui.echomail.post_message' => 'Publicar mensaje', + 'ui.echomail.viewing_prefix' => 'Viendo:', + 'ui.echomail.viewing_all' => 'Viendo: Todos los mensajes', + 'ui.echomail.echo_list' => 'Lista de ecos', + 'ui.echomail.search_areas_placeholder' => 'Buscar areas...', + 'ui.echomail.loading_areas' => 'Cargando areas...', + 'ui.echomail.recent_messages' => 'Mensajes recientes', + 'ui.echomail.to_me' => 'Para mi', + 'ui.echomail.saved_items' => 'Guardados', + 'ui.echomail.echo_areas' => 'Areas de eco', + 'ui.echomail.areas' => 'Areas', + 'ui.echomail.manage_subscriptions' => 'Gestionar suscripciones', + 'ui.echomail.share_message' => 'Compartir mensaje', + 'ui.echomail.share_message_help' => 'Comparta este mensaje de echomail con otros mediante un enlace web.', + 'ui.echomail.allow_anonymous_access' => 'Permitir acceso anonimo', + 'ui.echomail.allow_anonymous_access_help' => 'Cualquiera con el enlace puede verlo sin iniciar sesion', + 'ui.echomail.link_expires_after' => 'El enlace expira en:', + 'ui.echomail.never_expires' => 'Nunca expira', + 'ui.echomail.share_link_created_success' => 'Enlace creado correctamente. Ahora puede copiar y compartir esta URL.', + 'ui.echomail.create_share_link' => 'Crear enlace para compartir', + 'ui.echomail.get_friendly_url' => 'Obtener URL amigable', + 'ui.echomail.revoke_share' => 'Revocar enlace', + 'ui.echomail.messages_refreshed' => 'Mensajes actualizados', + 'ui.echomail.saved_items.removed' => 'Mensaje eliminado de guardados', + 'ui.echomail.saved_items.saved' => 'Mensaje guardado para despues', + 'ui.echomail.shares.using_existing' => 'Usando enlace compartido existente', + 'ui.echomail.shares.created_success' => 'Enlace compartido creado correctamente', + 'ui.echomail.shares.friendly_url_generated' => 'URL amigable generada', + 'ui.echomail.shares.revoked' => 'Enlace compartido revocado', + 'ui.echomail.shares.url_copied' => 'URL compartida copiada al portapapeles', + + // Files Page + 'ui.files.title' => 'Áreas de Archivos', + 'ui.files.security_notice_label' => 'Aviso de seguridad:', + 'ui.files.security_notice_text' => 'Usted es responsable de asegurarse de que los archivos que descarga sean seguros. Utilice software de protección contra malware adecuado.', + 'ui.files.recent_uploads' => 'Subidas recientes', + 'ui.files.upload_file' => 'Subir archivo', + 'ui.files.search_placeholder' => 'Buscar archivos...', + 'ui.files.loading_recent_uploads' => 'Cargando subidas recientes...', + 'ui.files.statistics' => 'Estadisticas', + 'ui.files.total_areas' => 'Total de areas', + 'ui.files.total_files' => 'Total de archivos', + 'ui.files.total_size' => 'Tamano total', + 'ui.files.file_details' => 'Detalles del archivo', + 'ui.files.filename' => 'Nombre de archivo', + 'ui.files.size' => 'Tamano', + 'ui.files.uploaded' => 'Subido', + 'ui.files.from' => 'De', + 'ui.files.virus_scan' => 'Escaneo de virus', + 'ui.files.download' => 'Descargar', + 'ui.files.share_file' => 'Compartir archivo', + 'ui.files.link_expiry' => 'Vencimiento del enlace', + 'ui.files.share_link' => 'Enlace para compartir', + 'ui.files.revoke_link' => 'Revocar enlace', + 'ui.files.create_share_link' => 'Crear enlace para compartir', + 'ui.files.select_file_required' => 'Seleccionar archivo *', + 'ui.files.maximum_file_size' => 'Tamano maximo de archivo', + 'ui.files.short_description_required' => 'Descripcion corta *', + 'ui.files.short_description_help' => 'Descripcion breve mostrada en la lista de archivos.', + 'ui.files.long_description' => 'Descripcion larga', + 'ui.files.long_description_help' => 'Descripcion extendida opcional (admite texto plano).', + 'ui.files.upload' => 'Subir', + 'ui.files.recent_uploads_load_failed' => 'No se pudieron cargar las subidas recientes', + 'ui.files.no_recent_uploads' => 'Todavia no se han subido archivos', + 'ui.files.area' => 'Area', + 'ui.files.actions' => 'Acciones', + 'ui.files.virus_scan_now' => 'Escanear virus', + 'ui.files.scanning' => 'Escaneando…', + 'ui.files.scan_failed' => 'Error al escanear virus', + 'ui.files.virus_scan_clean' => 'Escaneo de virus: Limpio', + 'ui.files.virus_detected_prefix' => 'Virus detectado: ', + 'ui.files.virus_scan_error' => 'Escaneo de virus: Error', + 'ui.files.virus_scan_skipped' => 'Escaneo de virus: Omitido', + 'ui.files.shared' => 'Compartido', + 'ui.files.file_areas_load_failed' => 'No se pudieron cargar las areas de archivos', + 'ui.files.no_file_areas' => 'No hay areas de archivos disponibles', + 'ui.files.search_areas_placeholder' => 'Buscar áreas…', + 'ui.files.files_count_label' => 'archivos', + 'ui.files.loading_files' => 'Cargando archivos...', + 'ui.files.load_files_failed' => 'No se pudieron cargar los archivos', + 'ui.files.no_files_in_area' => 'No hay archivos en esta area', + 'ui.files.not_scanned' => 'Sin escanear', + 'ui.files.clean' => 'Limpio', + 'ui.files.infected' => 'Infectado', + 'ui.files.scan_error' => 'Error de escaneo', + 'ui.files.skipped' => 'Omitido', + 'ui.files.no_description' => 'Sin descripcion', + 'ui.files.load_details_failed' => 'No se pudieron cargar los detalles del archivo', + 'ui.files.select_area_first' => 'Seleccione primero un area de archivos', + 'ui.files.select_file_prompt' => 'Seleccione un archivo', + 'ui.files.upload_success' => 'Archivo subido correctamente', + 'ui.files.delete_confirm' => 'Esta seguro de que desea eliminar "{filename}"? Esta accion no se puede deshacer.', + 'ui.files.delete_success' => 'Archivo eliminado correctamente', + 'ui.files.rename' => 'Renombrar', + 'ui.files.rename_file' => 'Renombrar archivo', + 'ui.files.new_filename' => 'Nuevo nombre de archivo', + 'ui.files.rename_success' => 'Archivo renombrado correctamente', + 'ui.files.edit' => 'Editar', + 'ui.files.edit_file' => 'Editar archivo', + 'ui.files.short_description' => 'Descripción corta', + 'ui.files.edit_success' => 'Archivo actualizado correctamente', + 'ui.files.previous_file' => 'Archivo anterior', + 'ui.files.next_file' => 'Archivo siguiente', + 'ui.files.move_to_area' => 'Mover al área', + 'ui.files.active_share_exists' => 'Este archivo ya tiene un enlace compartido activo.', + 'ui.files.revoke_confirm' => 'Esta seguro de que desea revocar este enlace compartido? Cualquiera con el enlace ya no podra acceder.', + 'ui.files.share_revoked' => 'Enlace compartido revocado', + 'ui.files.share_link_copied_clipboard' => 'Enlace compartido copiado al portapapeles', + 'ui.files.share_link_copied' => 'Enlace compartido copiado', + + // Polls Page + 'ui.polls.title' => 'Encuestas', + 'ui.polls.create' => 'Crear encuesta', + 'ui.polls.loading' => 'Cargando encuesta...', + 'ui.polls.description' => 'Explore encuestas activas y vote para ver resultados.', + 'ui.polls.previous' => 'Encuesta anterior', + 'ui.polls.next' => 'Siguiente encuesta', + 'ui.polls.create.title' => 'Crear encuesta', + 'ui.polls.create.heading' => 'Crear nueva encuesta', + 'ui.polls.create.cost_badge' => 'Costo: {cost} creditos', + 'ui.polls.create.insufficient_credits' => 'Creditos insuficientes', + 'ui.polls.create.insufficient_credits_help' => 'Necesita {cost} creditos para crear una encuesta, pero solo tiene {balance} creditos.', + 'ui.polls.create.cost_info_prefix' => 'Crear una encuesta cuesta', + 'ui.polls.create.current_balance' => 'Su saldo actual:', + 'ui.polls.create.credits' => 'creditos', + 'ui.polls.create.question_required' => 'Pregunta de la encuesta *', + 'ui.polls.create.question_placeholder' => 'Que le gustaria preguntar?', + 'ui.polls.create.characters_minimum' => 'caracteres (minimo 10)', + 'ui.polls.create.options_required' => 'Opciones de encuesta * (2-10)', + 'ui.polls.create.option_placeholder' => 'Ingrese una opcion', + 'ui.polls.create.add_option' => 'Agregar opcion', + 'ui.polls.create.options_help' => 'Cada opcion debe ser unica y tener menos de 200 caracteres', + 'ui.polls.create.note_label' => 'Nota:', + 'ui.polls.create.note_text' => 'Una vez creada, la encuesta no puede editarse ni eliminarse. Verifique que la pregunta y las opciones sean correctas antes de enviarla.', + 'ui.polls.create.submit' => 'Crear encuesta ({cost} creditos)', + 'ui.polls.create.max_options_allowed' => 'Maximo 10 opciones permitidas', + 'ui.polls.create.question_min_length' => 'La pregunta debe tener al menos 10 caracteres', + 'ui.polls.create.min_options_required' => 'Proporcione al menos 2 opciones', + 'ui.polls.create.options_unique_required' => 'Todas las opciones deben ser unicas', + 'ui.polls.create.creating' => 'Creando...', + 'ui.polls.create.created_success_spent' => 'Encuesta creada correctamente. Gasto {spent} creditos.', + 'ui.polls.create.error_prefix' => 'Error: ', + + // Shoutbox Page + 'ui.shoutbox.title' => 'Shoutbox', + 'ui.shoutbox.loading' => 'Cargando shouts...', + 'ui.shoutbox.load_older' => 'Cargar shouts anteriores', + 'ui.shoutbox.leave_shout' => 'Deja un shout...', + 'ui.shoutbox.post' => 'Publicar', + 'ui.shoutbox.max_chars' => 'Maximo 280 caracteres.', + 'ui.admin.shoutbox.page_title' => 'Shoutbox', + 'ui.admin.shoutbox.heading' => 'Moderacion de shoutbox', + 'ui.admin.shoutbox.user' => 'Usuario', + 'ui.admin.shoutbox.message' => 'Mensaje', + 'ui.admin.shoutbox.status' => 'Estado', + 'ui.admin.shoutbox.created' => 'Creado', + 'ui.admin.shoutbox.actions' => 'Acciones', + 'ui.admin.shoutbox.error_loading_shouts' => 'Error al cargar shouts', + 'ui.admin.shoutbox.hidden' => 'Oculto', + 'ui.admin.shoutbox.visible' => 'Visible', + 'ui.admin.shoutbox.unhide' => 'Mostrar', + 'ui.admin.shoutbox.hide' => 'Ocultar', + 'ui.admin.shoutbox.hidden_success' => 'Shout ocultado correctamente', + 'ui.admin.shoutbox.unhidden_success' => 'Shout mostrado correctamente', + 'ui.admin.shoutbox.deleted_success' => 'Shout eliminado correctamente', + 'ui.admin.shoutbox.failed_to_update' => 'No se pudo actualizar el shout', + 'ui.admin.shoutbox.failed_to_delete' => 'No se pudo eliminar el shout', + 'ui.admin.shoutbox.confirm_delete' => 'Eliminar este shout?', + + // Chat Page + 'ui.chat.page_title' => 'Chat', + 'ui.chat.heading' => 'Chat', + 'ui.chat.rooms' => 'Salas', + 'ui.chat.direct' => 'Directo', + 'ui.chat.lobby' => 'Lobby', + 'ui.chat.load_older_messages' => 'Cargar mensajes anteriores', + 'ui.chat.type_message_placeholder' => 'Escriba un mensaje...', + 'ui.chat.kick' => 'Expulsar', + 'ui.chat.ban' => 'Banear', + 'ui.chat.no_one_online' => 'Nadie en linea', + 'ui.chat.room' => 'Sala', + 'ui.chat.moderation_hint' => 'Clic derecho o clic para moderar', + 'ui.chat.system' => 'Sistema', + 'ui.chat.confirm_kick' => 'Expulsar a este usuario de la sala?', + 'ui.chat.confirm_ban' => 'Banear a este usuario de la sala?', + 'ui.chat.send_failed' => 'No se pudo enviar el mensaje', + 'ui.chat.moderation_failed' => 'La moderacion fallo', + + // BinkP Page + 'ui.binkp.page_title' => 'Estado de Binkp', + 'ui.binkp.heading' => 'Estado de Binkp', + 'ui.binkp.subheading' => 'Monitoree y gestione sus conexiones Binkp TCP/IP', + 'ui.binkp.system_status' => 'Estado del sistema', + 'ui.binkp.inbound_queue' => 'Cola de entrada', + 'ui.binkp.outbound_queue' => 'Cola de salida', + 'ui.binkp.uplinks' => 'Uplinks', + 'ui.binkp.status_tab' => 'Estado', + 'ui.binkp.uplinks_tab' => 'Uplinks', + 'ui.binkp.queues_tab' => 'Colas', + 'ui.binkp.logs_tab' => 'Logs', + 'ui.binkp.system_information' => 'Informacion del sistema', + 'ui.binkp.loading_system_information' => 'Cargando informacion del sistema...', + 'ui.binkp.uplink_status' => 'Estado de uplinks', + 'ui.binkp.loading_uplink_status' => 'Cargando estado de uplinks...', + 'ui.binkp.poll_all' => 'Consultar todos', + 'ui.binkp.configured_uplinks' => 'Uplinks configurados', + 'ui.binkp.uplink' => 'Uplink', + 'ui.binkp.status' => 'Estado', + 'ui.binkp.schedule' => 'Programacion', + 'ui.binkp.actions' => 'Acciones', + 'ui.binkp.address' => 'Direccion', + 'ui.binkp.sysop' => 'Sysop', + 'ui.binkp.location' => 'Ubicacion', + 'ui.binkp.process' => 'Procesar', + 'ui.binkp.send' => 'Enviar', + 'ui.binkp.poll' => 'Consultar', + 'ui.binkp.enabled' => 'Habilitado', + 'ui.binkp.disabled' => 'Deshabilitado', + 'ui.binkp.online' => 'En linea', + 'ui.binkp.offline' => 'Fuera de linea', + 'ui.binkp.no_uplinks_configured' => 'No hay uplinks configurados', + 'ui.binkp.error_loading_status_prefix' => 'Error al cargar el estado:', + 'ui.binkp.error_loading_uplinks_prefix' => 'Error al cargar uplinks:', + 'ui.binkp.file_count' => '{count} archivos', + 'ui.binkp.pending' => 'Pendientes', + 'ui.binkp.errors' => 'Errores', + 'ui.binkp.stdout' => 'STDOUT', + 'ui.binkp.stderr' => 'STDERR', + 'ui.binkp.http_status_error' => 'HTTP {code}: {text}', + 'ui.binkp.inbound_api_http_error' => 'API entrante: HTTP {code}', + 'ui.binkp.outbound_api_http_error' => 'API saliente: HTTP {code}', + 'ui.binkp.zero_bytes' => '0 Bytes', + 'ui.binkp.byte_units.bytes' => 'Bytes', + 'ui.binkp.byte_units.kb' => 'KB', + 'ui.binkp.byte_units.mb' => 'MB', + 'ui.binkp.byte_units.gb' => 'GB', + 'ui.binkp.poll_schedule_cron' => 'Programacion de consulta (formato cron)', + 'ui.binkp.poll_schedule_placeholder' => '0 */4 * * *', + 'ui.binkp.logs_heading' => 'Logs de Binkp', + 'ui.binkp.lines_option' => '{count} lineas', + 'ui.binkp.loading_logs' => 'Cargando logs...', + 'ui.binkp.add_new_uplink' => 'Agregar nuevo uplink', + 'ui.binkp.ftn_address_required' => 'Direccion FTN *', + 'ui.binkp.ftn_address_placeholder' => '1:123/456', + 'ui.binkp.hostname_required' => 'Hostname *', + 'ui.binkp.hostname_placeholder' => 'bbs.example.com', + 'ui.binkp.default_every_4_hours' => 'Predeterminado: cada 4 horas', + 'ui.binkp.add_uplink' => 'Agregar uplink', + 'ui.binkp.configured_count' => '{count} configurados', + 'ui.binkp.message_count' => '{count} msgs', + 'ui.binkp.poll_completed_exit' => 'Consulta completada (salida {code})', + 'ui.binkp.poll_failed_prefix' => 'La consulta fallo: ', + 'ui.binkp.error_polling_uplink_prefix' => 'Error al consultar uplink: ', + 'ui.binkp.all_uplinks_polled_exit' => 'Todos los uplinks consultados (salida {code})', + 'ui.binkp.polling_failed_prefix' => 'La consulta fallo: ', + 'ui.binkp.error_polling_uplinks_prefix' => 'Error al consultar uplinks: ', + 'ui.binkp.inbound_processing_completed' => 'Procesamiento de entrada completado', + 'ui.binkp.processing_failed_prefix' => 'El procesamiento fallo: ', + 'ui.binkp.error_processing_inbound_prefix' => 'Error al procesar entrada: ', + 'ui.binkp.outbound_processing_completed' => 'Procesamiento de salida completado', + 'ui.binkp.error_processing_outbound_prefix' => 'Error al procesar salida: ', + 'ui.binkp.uplink_added_success' => 'Uplink agregado correctamente', + 'ui.binkp.add_uplink_failed_prefix' => 'No se pudo agregar el uplink: ', + 'ui.binkp.error_adding_uplink_prefix' => 'Error al agregar uplink: ', + 'ui.binkp.remove_uplink_confirm' => 'Eliminar uplink {address}?', + 'ui.binkp.uplink_removed_success' => 'Uplink eliminado correctamente', + 'ui.binkp.remove_uplink_failed_prefix' => 'No se pudo quitar el uplink: ', + 'ui.binkp.error_removing_uplink_prefix' => 'Error al quitar uplink: ', + 'ui.binkp.packet_processing_failed_prefix' => 'El procesamiento de paquetes fallo: ', + 'ui.binkp.packet_processing_completed_exit' => 'Procesamiento de paquetes completado (salida {code})', + 'ui.binkp.error_processing_packets_prefix' => 'Error al procesar paquetes: ', + 'ui.binkp.poll_failed' => 'La consulta fallo', + 'ui.binkp.polling_failed' => 'La consulta de uplinks fallo', + 'ui.binkp.processing_failed' => 'El procesamiento fallo', + 'ui.binkp.add_uplink_failed' => 'No se pudo agregar el uplink', + 'ui.binkp.remove_uplink_failed' => 'No se pudo quitar el uplink', + 'ui.binkp.unknown_error' => 'Error desconocido', + + // Echolist Page + 'ui.echolist.search_min_chars' => 'Ingrese al menos 2 caracteres para buscar', + 'ui.echolist.page_title' => 'Areas de eco', + 'ui.echolist.heading' => 'Lista de ecos', + 'ui.echolist.filter_heading' => 'Filtrar areas de eco', + 'ui.echolist.show_subscribed_only' => 'Mostrar solo areas suscritas', + 'ui.echolist.show_unread_only' => 'Mostrar solo areas con mensajes sin leer', + 'ui.echolist.area_filter_placeholder' => 'Escriba para filtrar por nombre o descripcion...', + 'ui.echolist.area_filter_help' => 'Filtra la lista de abajo en tiempo real', + 'ui.echolist.search_heading' => 'Buscar mensajes', + 'ui.echolist.search_placeholder' => 'Buscar en el contenido del mensaje...', + 'ui.echolist.search_help' => 'Buscar en todo el contenido de mensajes echomail', + 'ui.echolist.loading' => 'Cargando ecos...', + 'ui.echolist.stats.total_echos' => 'Total de ecos', + 'ui.echolist.stats.total_messages' => 'Total de mensajes', + 'ui.echolist.stats.networks' => 'Redes', + 'ui.echolist.stats.recent_messages' => 'Mensajes en 24h', + 'ui.echolist.local' => 'Local', + 'ui.echolist.unknown' => 'Desconocido', + 'ui.echolist.load_failed' => 'No se pudieron cargar las areas de eco', + 'ui.echolist.none_available' => 'No hay areas de eco disponibles.', + 'ui.echolist.new_post' => 'Nueva publicacion', + 'ui.echolist.new_messages' => 'Mensajes nuevos', + 'ui.echolist.local_areas' => 'Areas locales', + 'ui.echolist.lovlynet_network' => 'Red LOVLYNET', + 'ui.echolist.network_suffix' => 'Red', + 'ui.echolist.area_count' => 'area', + 'ui.echolist.plural_suffix' => 's', + 'ui.echolist.sysop_badge' => 'Sysop', + 'ui.echolist.no_description' => 'Sin descripcion', + 'ui.echolist.moderator_prefix' => 'Moderador:', + 'ui.echolist.unread_of_posts' => '{unread} sin leer de {total} publicaciones', + 'ui.echolist.post_count' => '{total} publicaciones', + 'ui.echolist.by_author' => 'por {author}', + 'ui.echolist.time.never' => 'Nunca', + 'ui.echolist.time.just_now' => 'Ahora mismo', + 'ui.echolist.time.minutes_ago' => 'hace {count}m', + 'ui.echolist.time.hours_ago' => 'hace {count}h', + 'ui.echolist.time.days_ago' => 'hace {count}d', + + // Nodelist Page + 'ui.nodelist.node_details' => 'Detalles del nodo', + 'ui.nodelist.back_to_list' => 'Volver a la lista', + 'ui.nodelist.system_information' => 'Informacion del sistema', + 'ui.nodelist.system_name' => 'Nombre del sistema', + 'ui.nodelist.sysop' => 'Sysop', + 'ui.nodelist.location' => 'Ubicacion', + 'ui.nodelist.phone' => 'Telefono', + 'ui.nodelist.baud_rate' => 'Velocidad de baudios', + 'ui.nodelist.not_specified' => 'No especificado', + 'ui.nodelist.unpublished' => 'No publicado', + 'ui.nodelist.address_components' => 'Componentes de direccion', + 'ui.nodelist.zone' => 'Zona', + 'ui.nodelist.net' => 'Net', + 'ui.nodelist.node' => 'Nodo', + 'ui.nodelist.point' => 'Punto', + 'ui.nodelist.full' => 'Completa', + 'ui.nodelist.capabilities_flags' => 'Capacidades y banderas del nodo', + 'ui.nodelist.quick_actions' => 'Acciones rapidas', + 'ui.nodelist.send_netmail' => 'Enviar netmail', + 'ui.nodelist.login_to_send_netmail' => 'Inicie sesion para enviar netmail', + 'ui.nodelist.file_request' => 'Solicitud de archivo', + 'ui.nodelist.connection_information' => 'Informacion de conexion', + 'ui.nodelist.internet_binkp' => 'Internet (BinkP)', + 'ui.nodelist.internet_telnet' => 'Internet (Telnet)', + 'ui.nodelist.dialup_pots' => 'Dial-up (POTS)', + 'ui.nodelist.no_connection_info' => 'No hay informacion de conexion disponible', + 'ui.nodelist.continuous_mail' => 'Correo continuo (24/7)', + 'ui.nodelist.mail_on_hold' => 'Correo en espera', + 'ui.nodelist.system_down' => 'Sistema caido', + 'ui.nodelist.http' => 'HTTP', + 'ui.nodelist.telnet' => 'Telnet', + 'ui.nodelist.ssh' => 'SSH', + 'ui.nodelist.file_request_coming_soon' => 'La solicitud de archivos estara disponible pronto', + 'ui.nodelist.index.heading' => 'Explorador de nodelist', + 'ui.nodelist.index.import_nodelist' => 'Importar nodelist', + 'ui.nodelist.index.nodes' => 'nodos', + 'ui.nodelist.index.zones' => 'zonas', + 'ui.nodelist.index.nets' => 'nets', + 'ui.nodelist.index.points' => 'puntos', + 'ui.nodelist.index.special' => 'especiales', + 'ui.nodelist.index.last_imported' => 'Ultima importacion:', + 'ui.nodelist.index.search_placeholder' => 'Direccion de nodo (2:5034/10), sysop, ubicacion o nombre del sistema', + 'ui.nodelist.index.all_zones' => 'Todas las zonas', + 'ui.nodelist.index.zone_prefix' => 'Zona', + 'ui.nodelist.index.all_nets' => 'Todos los nets', + 'ui.nodelist.index.net_prefix' => 'Net', + 'ui.nodelist.index.search_results' => 'Resultados de busqueda ({count} nodos)', + 'ui.nodelist.index.address' => 'Direccion', + 'ui.nodelist.index.type' => 'Tipo', + 'ui.nodelist.index.phone_host' => 'Telefono/Host', + 'ui.nodelist.index.speed' => 'Velocidad', + 'ui.nodelist.index.actions' => 'Acciones', + 'ui.nodelist.index.no_nodes_found_search' => 'No se encontraron nodos que coincidan con su busqueda.', + 'ui.nodelist.index.use_search_form' => 'Use el formulario de busqueda de arriba para encontrar nodos en el nodelist.', + 'ui.nodelist.index.error_fetching_nets' => 'Error al obtener nets:', + 'ui.nodelist.import.heading' => 'Importar nodelist', + 'ui.nodelist.import.upload_new_nodelist' => 'Subir nuevo nodelist', + 'ui.nodelist.import.network_domain' => 'Dominio de red', + 'ui.nodelist.import.domain_placeholder' => 'ej.: fidonet, fsxnet, agoranet', + 'ui.nodelist.import.domain_title' => 'El dominio debe contener solo letras, numeros, guiones bajos y guiones', + 'ui.nodelist.import.domain_help_1' => 'El dominio FTN al que pertenece este nodelist (ej.: fidonet, fsxnet).', + 'ui.nodelist.import.domain_help_2' => 'Se usa para distinguir nodos de diferentes redes.', + 'ui.nodelist.import.nodelist_file' => 'Archivo nodelist', + 'ui.nodelist.import.nodelist_help_1' => 'Suba un archivo nodelist de FidoNet (formato NODELIST.xxx).', + 'ui.nodelist.import.nodelist_help_2' => 'Soportado: texto plano (.txt, .lst, .nodelist) y comprimido ZIP (.Zxxx)', + 'ui.nodelist.import.archive_before_import' => 'Archivar el nodelist actual antes de importar', + 'ui.nodelist.import.archive_help' => 'Recomendado: conservar el nodelist actual como respaldo inactivo antes de importar el nuevo.', + 'ui.nodelist.import.warning' => 'Advertencia:', + 'ui.nodelist.import.warning_text_1' => 'Importar un nuevo nodelist reemplazara los datos activos del nodelist para este dominio.', + 'ui.nodelist.import.warning_text_2' => 'Esta operacion no se puede deshacer. Asegurese de tener respaldo si es necesario.', + 'ui.nodelist.import.import_nodelist' => 'Importar nodelist', + 'ui.nodelist.import.current_status' => 'Estado actual del nodelist', + 'ui.nodelist.import.file' => 'Archivo:', + 'ui.nodelist.import.day_of_year' => 'Dia del ano:', + 'ui.nodelist.import.release_date' => 'Fecha de publicacion:', + 'ui.nodelist.import.total_nodes' => 'Nodos totales:', + 'ui.nodelist.import.imported' => 'Importado:', + 'ui.nodelist.import.no_active_nodelist' => 'No se encontro un nodelist activo.', + 'ui.nodelist.import.node_statistics' => 'Estadisticas de nodos', + 'ui.nodelist.import.total_nodes_label' => 'Nodos totales', + 'ui.nodelist.import.guidelines' => 'Guia de importacion', + 'ui.nodelist.import.guideline_1' => 'Formato estandar FTS-0005', + 'ui.nodelist.import.guideline_2' => 'Texto ASCII con finales CR/LF', + 'ui.nodelist.import.guideline_3' => 'Encabezado correcto con CRC', + 'ui.nodelist.import.guideline_4' => 'Jerarquia Zona/Net/Nodo', + 'ui.nodelist.import.guideline_5' => 'Archivos comprimidos ZIP (.Zxxx)', + 'ui.nodelist.import.guideline_note' => 'Para archivos ARC, ARJ, LZH, RAR, use el script de importacion por linea de comandos', + 'ui.nodelist.import.importing' => 'Importando...', + 'ui.nodelist.import.success' => 'Se importaron correctamente {count} nodos desde {filename} (Dia {day}) para el dominio @{domain}', + 'ui.dosdoor_player.page_title' => 'Reproductor de puerta DOS', + 'ui.dosdoor_player.document_title_suffix' => 'Puerta DOS', + 'ui.dosdoor_player.status_prefix' => 'Estado:', + 'ui.dosdoor_player.status_disconnected' => 'Desconectado', + 'ui.dosdoor_player.status_launching' => 'Iniciando...', + 'ui.dosdoor_player.status_launch_failed' => 'Inicio fallido', + 'ui.dosdoor_player.status_connecting' => 'Conectando...', + 'ui.dosdoor_player.status_connected' => 'Conectado', + 'ui.dosdoor_player.status_connection_error' => 'Error de conexion', + 'ui.dosdoor_player.status_error' => 'Error', + 'ui.dosdoor_player.end_session' => 'Finalizar sesion', + 'ui.dosdoor_player.launching_door_line' => 'Iniciando juego de puerta...', + 'ui.dosdoor_player.failed_launch_line' => 'No se pudo iniciar la sesion de la puerta.', + 'ui.dosdoor_player.connecting_to_prefix' => 'Conectando a', + 'ui.dosdoor_player.connected_line' => 'Conectado!', + 'ui.dosdoor_player.connection_closed_line' => '[Conexion cerrada]', + 'ui.dosdoor_player.connection_error_line' => '[Error de conexion]', + 'ui.dosdoor_player.failed_to_connect_prefix' => 'No se pudo conectar:', + 'ui.dosdoor_player.confirm_end_session' => 'Esta seguro de que desea finalizar esta sesion de puerta?', + 'ui.dosdoor_player.failed_end_session' => 'No se pudo finalizar la sesion', + 'ui.dosdoor_player.error_ending_session' => 'Error al finalizar la sesion', + 'ui.dosdoor_player.error_no_door_specified' => 'Error: no se especifico ID de puerta', + 'ui.dosdoor_player.failed_launch_door' => 'No se pudo iniciar la puerta', + + // Who's Online + 'ui.whos_online.page_title' => 'Quien esta en linea', + 'ui.whos_online.heading' => 'Quien esta en linea', + 'ui.whos_online.active_last_minutes' => 'Activo en los ultimos {minutes} minutos', + 'ui.whos_online.online_users' => 'Usuarios en linea', + 'ui.whos_online.service' => 'Servicio', + 'ui.whos_online.activity' => 'Actividad', + 'ui.whos_online.idle' => 'Inactivo', + 'ui.whos_online.no_users_online' => 'No hay usuarios en linea en este momento.', + + // Doors and Games + 'ui.webdoors.page_title' => 'Puertas y juegos', + 'ui.webdoors.heading' => 'Puertas y juegos', + 'ui.webdoors.description' => 'Acceda a juegos de puertas DOS clasicos y juegos web modernos.', + 'ui.webdoors.top_scores_all_games' => 'Puntuaciones mas altas (todos los juegos)', + 'ui.webdoors.leaderboard_month_navigation' => 'Navegacion mensual del ranking', + 'ui.webdoors.previous_month' => 'Mes anterior', + 'ui.webdoors.next_month' => 'Mes siguiente', + 'ui.webdoors.current_month' => 'Mes actual', + 'ui.webdoors.rank' => 'Rango', + 'ui.webdoors.player' => 'Jugador', + 'ui.webdoors.game' => 'Juego', + 'ui.webdoors.board' => 'Tablero', + 'ui.webdoors.score' => 'Puntuacion', + 'ui.webdoors.date' => 'Fecha', + 'ui.webdoors.no_scores_for_month' => 'No hay puntuaciones registradas para {month}.', + 'ui.webdoors.players_count' => 'Jugadores: {count}', + 'ui.webdoors.by_author' => 'por {author}', + 'ui.webdoors.version_by_author' => 'v{version} por {author}', + 'ui.webdoors.launch' => 'Iniciar', + 'ui.webdoors.no_games_available' => 'Todavia no hay juegos disponibles. Vuelva mas tarde.', + 'ui.webdoors.errors.system_disabled' => 'Lo sentimos, el sistema de juegos no esta habilitado.', + 'ui.webdoors.errors.admin_only' => 'Este door esta restringido a administradores.', + 'ui.webdoors.errors.requirements_not_met' => 'Este juego requiere funciones que actualmente no estan habilitadas en este sistema.', + 'ui.webdoor_play.page_title_suffix' => 'Puertas y juegos', + 'ui.webdoor_play.back_to_doors' => 'Volver a puertas', + 'ui.webdoor_play.fullscreen' => 'Pantalla completa', + + // Shared File Page + 'ui.shared_file.shared_file' => 'Archivo compartido', + 'ui.shared_file.meta_description_from_system' => 'Archivo compartido desde {system_name}', + 'ui.shared_file.shared_by' => 'Compartido por', + 'ui.shared_file.on_date_prefix' => 'el', + 'ui.shared_file.viewed_count' => 'visto {count} vez/veces', + 'ui.shared_file.expires_prefix' => 'expira', + 'ui.shared_file.clean' => 'Limpio', + 'ui.shared_file.size' => 'Tamano:', + 'ui.shared_file.uploaded' => 'Subido:', + 'ui.shared_file.area' => 'Area:', + 'ui.shared_file.description' => 'Descripcion:', + 'ui.shared_file.details' => 'Detalles:', + 'ui.shared_file.download' => 'Descargar', + 'ui.shared_file.browse_area' => 'Explorar area {tag}', + 'ui.shared_file.join_system_to_download' => 'Unase a {system_name} para descargar', + 'ui.shared_file.register_prompt' => 'Cree una cuenta gratuita para descargar este archivo y acceder a todo nuestro archivo de archivos.', + 'ui.shared_file.register' => 'Registrarse', + 'ui.shared_file.login' => 'Iniciar sesion', + 'ui.shared_file.about_system' => 'Acerca de {system_name}', + 'ui.shared_file.about_system_text' => '{system_name} es un BBS conectado a FidoNet con areas publicas de archivos. Los miembros pueden subir, descargar y compartir archivos de un archivo en crecimiento.', + 'ui.shared_file.powered_by' => 'Desarrollado por BinktermPHP', + 'ui.shared_file.not_available_title' => 'Archivo no disponible', + 'ui.shared_file.not_available_body' => 'Este enlace de archivo compartido no esta disponible. Puede haber expirado o sido revocado.', + 'ui.shared_file.go_to_main_site' => 'Ir al sitio principal', + + // Ads + 'ui.ads.advertisement' => 'Anuncio', + 'ui.ads.none_available' => 'No hay anuncios disponibles.', + + // BBS Menu Shell + 'ui.bbs_menu.main_menu' => 'Menu principal', + 'ui.bbs_menu.press_key_to_navigate' => 'Presione una tecla para navegar', + 'ui.bbs_menu.quit_return' => 'Salir/Volver', + 'ui.bbs_menu.tap_item_to_navigate' => 'Toque un elemento para navegar', + 'ui.bbs_menu.press_highlighted_key' => 'Presione la tecla resaltada para navegar', + 'ui.bbs_menu.returns_to_menu' => 'vuelve a este menu', + 'ui.bbs_menu.tap_card_to_navigate' => 'Toque una tarjeta para navegar', + + // Recent Updates Fragment + 'ui.recent_updates.badge_feature' => 'Caracteristica', + 'ui.recent_updates.badge_fix' => 'Correccion', + 'ui.recent_updates.badge_improvement' => 'Mejora', + 'ui.recent_updates.badge_update' => 'Actualizacion', + + // House Rules Fragment + 'ui.rules.intro' => 'Sea excelente con los demas y ayude a mantener este sistema acogedor para todos.', + 'ui.rules.item_1' => 'Mantenga el respeto. Sin acoso, discurso de odio ni ataques personales.', + 'ui.rules.item_2' => 'Sin spam ni flooding.', + 'ui.rules.item_3' => 'Respete la privacidad. No comparta informacion privada sin consentimiento.', + 'ui.rules.item_4' => 'Siga la etiqueta de Fidonet y las politicas locales de red.', + 'ui.rules.item_5' => 'Las decisiones del sysop son finales.', + + // Dashboard + 'ui.dashboard.title' => 'Panel', + 'ui.dashboard.unread_netmail' => 'Netmail sin leer', + 'ui.dashboard.unread_echomail' => 'Echomail sin leer', + 'ui.dashboard.system_news' => 'Noticias del sistema', + 'ui.dashboard.system_information' => 'Informacion del sistema', + 'ui.dashboard.sysop' => 'Sysop', + 'ui.dashboard.user' => 'Usuario', + 'ui.dashboard.addresses' => 'Direcciones', + 'ui.dashboard.my_referral_link' => 'Mi enlace de referidos', + 'ui.dashboard.referral_invite_prefix' => 'Invite amigos y gane', + 'ui.dashboard.referral_invite_suffix' => 'creditos por cada referido exitoso.', + 'ui.dashboard.referrals.label' => 'Referidos', + 'ui.dashboard.referrals.earned' => 'Ganado', + 'ui.dashboard.voting_booth' => 'Cabina de votacion', + 'ui.dashboard.create_poll_title' => 'Crear encuesta', + 'ui.dashboard.polls.none_active' => 'No hay encuestas activas en este momento.', + 'ui.dashboard.polls.load_failed' => 'No se pudo cargar la encuesta.', + 'ui.dashboard.polls.results' => 'Resultados', + 'ui.dashboard.polls.no_votes' => 'Aun no hay votos.', + 'ui.dashboard.polls.vote_to_see_results' => 'Vote para ver resultados', + 'ui.dashboard.polls.submit_vote' => 'Enviar voto', + 'ui.dashboard.echoareas.none_available' => 'No hay areas de eco disponibles', + 'ui.dashboard.shoutbox.none_yet' => 'Aun no hay shouts. Sea el primero.', + 'ui.dashboard.shoutbox.load_failed' => 'No se pudieron cargar los shouts.', + 'ui.dashboard.shoutbox.post_failed' => 'No se pudo publicar el shout.', + 'ui.dashboard.referrals.error_prefix' => 'Error de estadisticas de referidos:', + 'ui.dashboard.referrals.recent' => 'Referidos recientes', + 'ui.dashboard.referrals.copy_failed_prefix' => 'No se pudo copiar:', + 'ui.dashboard.echoareas.header_area' => 'Area de eco', + 'ui.dashboard.echoareas.header_unread_total' => 'No leidos Total', + + // Settings + 'ui.settings.title' => 'Configuracion', + 'ui.settings.display_preferences' => 'Preferencias de visualizacion', + 'ui.settings.messages_per_page' => 'Mensajes por pagina', + 'ui.settings.messages_per_page_help' => 'Cantidad de mensajes a mostrar por pagina', + 'ui.settings.timezone' => 'Zona horaria', + 'ui.settings.timezone_help' => 'Su zona horaria local para mostrar fechas', + 'ui.settings.language' => 'Idioma', + 'ui.settings.language_help' => 'Idioma preferido para la interfaz', + 'ui.settings.date_format' => 'Formato de fecha', + 'ui.settings.date_format_help' => 'Su formato preferido de fecha y hora', + 'ui.settings.date_format.option.en_us' => 'Americano (MM/DD/YYYY) - 31 ene 2026, 10:30 a. m.', + 'ui.settings.date_format.option.en_gb' => 'Britanico (DD/MM/YYYY) - 31 ene 2026, 10:30', + 'ui.settings.date_format.option.en_ca' => 'Canadiense (YYYY-MM-DD) - 2026-01-31, 10:30 a. m.', + 'ui.settings.date_format.option.de_de' => 'Aleman (DD.MM.YYYY) - 31.01.2026, 10:30', + 'ui.settings.date_format.option.fr_fr' => 'Frances (DD/MM/YYYY) - 31/01/2026 10:30', + 'ui.settings.date_format.option.es_es' => 'Espanol (DD/MM/YYYY) - 31/1/2026, 10:30', + 'ui.settings.date_format.option.it_it' => 'Italiano (DD/MM/YYYY) - 31/01/2026, 10:30', + 'ui.settings.date_format.option.ja_jp' => 'Japones (YYYY/MM/DD) - 2026/01/31 10:30', + 'ui.settings.date_format.option.zh_cn' => 'Chino (YYYY/M/D) - 2026/1/31, 10:30', + 'ui.settings.date_format.option.sv_se' => 'Sueco (YYYY-MM-DD) - 2026-01-31 10:30', + 'ui.settings.theme' => 'Tema', + 'ui.settings.theme_help' => 'Elija su esquema de color preferido', + 'ui.settings.interface_style' => 'Estilo de interfaz', + 'ui.settings.interface_style.web' => 'Interfaz web (barra de navegacion y menus predeterminados)', + 'ui.settings.interface_style.bbs_menu' => 'Menu BBS (navegacion por teclas rapidas)', + 'ui.settings.interface_style_help' => 'Elija su estilo de navegacion preferido.', + 'ui.settings.default_echo_list' => 'Lista predeterminada de areas de eco', + 'ui.settings.system_default' => 'Predeterminado del sistema', + 'ui.settings.default_echo_list.reader' => 'Lector (lista de mensajes)', + 'ui.settings.default_echo_list.echolist' => 'Lista de ecos (agrupada por red)', + 'ui.settings.default_echo_list_help' => 'Elija la vista predeterminada al acceder a areas de eco desde el menu', + 'ui.settings.message_font' => 'Fuente de mensajes', + 'ui.settings.message_font_help' => 'Familia tipografica para mostrar mensajes netmail y echomail', + 'ui.settings.message_font_size' => 'Tamano de fuente de mensajes', + 'ui.settings.message_font_size_help' => 'Tamano de fuente para mostrar mensajes netmail y echomail', + 'ui.settings.message_signature' => 'Firma del mensaje', + 'ui.settings.message_signature_placeholder' => 'Su firma (max 4 lineas)', + 'ui.settings.message_signature_help' => 'Se agrega a los mensajes netmail y echomail salientes. Maximo 4 lineas.', + 'ui.settings.default_tagline' => 'Tagline predeterminado', + 'ui.settings.no_tagline' => 'Sin tagline', + 'ui.settings.random_tagline' => 'Tagline aleatorio', + 'ui.settings.default_tagline_help' => 'Se usa como tagline predeterminado al redactar mensajes. Elija "Tagline aleatorio" para seleccionar uno al azar cada vez.', + 'ui.settings.threading_preferences' => 'Preferencias de hilos', + 'ui.settings.threaded_view_echomail' => 'Activar vista en hilos para echomail', + 'ui.settings.threaded_view_echomail_help' => 'Agrupa mensajes de echomail por hilos de conversacion', + 'ui.settings.threaded_view_netmail' => 'Activar vista en hilos para netmail', + 'ui.settings.threaded_view_netmail_help' => 'Agrupa mensajes de netmail por hilos de conversacion', + 'ui.settings.quote_display' => 'Visualizacion de citas', + 'ui.settings.quote_coloring' => 'Colorear texto citado por profundidad', + 'ui.settings.quote_coloring_help' => 'Muestra lineas citadas (que empiezan con >) en diferentes colores segun nivel de anidacion', + 'ui.settings.session_security' => 'Sesion y seguridad', + 'ui.settings.active_sessions' => 'Sesiones activas', + 'ui.settings.active_sessions_help' => 'Administre sus sesiones de inicio de sesion activas', + 'ui.settings.view_sessions' => 'Ver sesiones', + 'ui.settings.security_actions' => 'Acciones de seguridad', + 'ui.settings.logout_all_help' => 'Cerrar sesion en todos los dispositivos', + 'ui.settings.logout_all' => 'Cerrar todas', + 'ui.settings.save_settings' => 'Guardar configuracion', + 'ui.settings.system_status' => 'Estado del sistema', + 'ui.settings.binkp_status' => 'Estado de BinkP', + 'ui.settings.online' => 'En linea', + 'ui.settings.last_poll' => 'Ultima consulta', + 'ui.settings.messages_today' => 'Mensajes de hoy', + 'ui.settings.never' => 'Nunca', + 'ui.settings.quick_actions' => 'Acciones rapidas', + 'ui.settings.compose_netmail' => 'Redactar netmail', + 'ui.settings.post_echomail' => 'Publicar echomail', + 'ui.settings.help_support' => 'Ayuda y soporte', + 'ui.settings.load_failed_console' => 'No se pudo cargar la configuracion', + 'ui.settings.saving' => 'Guardando configuracion...', + 'ui.settings.saved_successfully' => 'Configuracion guardada correctamente.', + 'ui.settings.sessions.none_active' => 'No se encontraron sesiones activas.', + 'ui.settings.sessions.loading' => 'Cargando sesiones...', + 'ui.settings.sessions.load_failed' => 'No se pudieron cargar las sesiones', + 'ui.settings.sessions.current' => 'Actual', + 'ui.settings.sessions.created' => 'Creada', + 'ui.settings.sessions.expires' => 'Expira', + 'ui.settings.sessions.revoke' => 'Revocar', + 'ui.settings.sessions.revoke_confirm' => 'Esta seguro de que desea revocar esta sesion?', + 'ui.settings.sessions.revoked_success' => 'Sesion revocada correctamente', + 'ui.settings.sessions.logged_out_all_success' => 'Sesion cerrada en todos los dispositivos', + 'ui.settings.sessions.logout_all_confirm' => 'Esta seguro de que desea cerrar sesion en todos los dispositivos? Debera iniciar sesion de nuevo.', + 'ui.settings.polling_uplinks' => 'Consultando uplinks... (esto puede tardar un momento)', + 'ui.settings.poll_complete_prefix' => 'Consulta de uplink completada: ', + 'ui.settings.poll_complete_exit' => 'Consulta de uplink completada (salida {code})', + 'ui.settings.poll_failed_prefix' => 'Fallo la consulta: ', + + // Compose / Drafts / Address Book + 'ui.compose.title_prefix' => 'Redactar', + 'ui.compose.back_to_prefix' => 'Volver a', + 'ui.compose.to_address_label' => 'Direccion destino (dejar en blanco para local)', + 'ui.compose.fidonet_address_help' => 'Direccion Fidonet (ej., 1:123/456)', + 'ui.compose.to_name_required' => 'Nombre destino *', + 'ui.compose.attach_file' => 'Adjuntar archivo', + 'ui.compose.attach_file_help_short' => 'requiere crashmail; el asunto se establecera con el nombre del archivo', + 'ui.compose.echo_area_required' => 'Area de eco *', + 'ui.compose.select_echo_area' => 'Seleccione area de eco...', + 'ui.compose.to_name' => 'Nombre destino', + 'ui.compose.to_name_public_help' => "Dejelo como 'All' para mensaje publico", + 'ui.compose.cross_post_to_other_areas' => 'Publicar en otras areas', + 'ui.compose.cross_post_help_prefix' => 'Seleccione areas adicionales para publicar este mensaje (max', + 'ui.compose.cross_post_help_suffix' => 'Cada area recibe una copia independiente.', + 'ui.compose.stylecodes.inverse_title' => 'Inverso (#inverse#)', + 'ui.compose.stylecodes.active_prefix' => 'StyleCodes activo -', + 'ui.compose.markdown.mode' => 'Modo Markdown', + 'ui.compose.markdown.toolbar_aria' => 'Formato Markdown', + 'ui.compose.markdown.bold_title' => 'Negrita (Ctrl+B)', + 'ui.compose.markdown.italic_title' => 'Cursiva (Ctrl+I)', + 'ui.compose.markdown.heading_1_title' => 'Encabezado 1', + 'ui.compose.markdown.heading_2_title' => 'Encabezado 2', + 'ui.compose.markdown.heading_3_title' => 'Encabezado 3', + 'ui.compose.markdown.inline_code_title' => 'Codigo en linea', + 'ui.compose.markdown.code_block_title' => 'Bloque de codigo', + 'ui.compose.markdown.link_title' => 'Enlace (Ctrl+K)', + 'ui.compose.markdown.unordered_list_title' => 'Lista desordenada', + 'ui.compose.markdown.ordered_list_title' => 'Lista ordenada', + 'ui.compose.markdown.blockquote_title' => 'Cita', + 'ui.compose.markdown.horizontal_rule_title' => 'Regla horizontal', + 'ui.compose.markdown.edit_mode_title' => 'Modo edicion', + 'ui.compose.markdown.preview_rendered_title' => 'Vista previa renderizada', + 'ui.compose.markdown.quick_reference_title' => 'Referencia rapida de Markdown', + 'ui.compose.markdown.write_placeholder' => 'Escriba aqui su mensaje en Markdown...', + 'ui.compose.markdown.click_preview_hint' => 'Haga clic en Vista previa para ver el resultado renderizado.', + 'ui.compose.markdown.formatting_active' => 'Formato Markdown activo.', + 'ui.compose.markdown.quick_reference' => 'Referencia rapida', + 'ui.compose.markup_format' => 'Formato de marcado', + 'ui.compose.plain_text' => 'Texto plano', + 'ui.compose.markdown_label' => 'Markdown', + 'ui.compose.stylecodes_label' => 'StyleCodes (GoldEd, SemPoint Rich Text, Synchronet Markup)', + 'ui.compose.markup_format_help' => 'Enviar con formato de marcado. Markdown y StyleCodes son compatibles en redes compatibles.', + 'ui.compose.hide_panel' => 'Ocultar panel', + 'ui.compose.show_panel' => 'Mostrar panel', + 'ui.compose.netmail_guidelines' => 'Guia de Netmail:', + 'ui.compose.netmail_guideline_private' => 'Mensajes privados entre nodos', + 'ui.compose.netmail_guideline_identity' => 'Identidad de publicacion para este destino: Nombre real', + 'ui.compose.netmail_guideline_identity_destination_real_name' => 'Identidad de publicacion para este destino: Nombre real', + 'ui.compose.netmail_guideline_identity_destination_username' => 'Identidad de publicacion para este destino: Usuario/Alias', + 'ui.compose.netmail_guideline_addressing' => 'Use direccionamiento Fidonet correcto', + 'ui.compose.netmail_guideline_respectful' => 'Sea respetuoso y profesional', + 'ui.compose.netmail_guideline_routed' => 'Los mensajes se enrutan a traves de la red', + 'ui.compose.netmail_cost' => 'Costo: {cost} {currency}', + 'ui.compose.crashmail.direct_delivery_label' => 'Crashmail (Entrega directa)', + 'ui.compose.crashmail.help_direct' => 'Intenta una entrega directa inmediata al nodo de destino, omitiendo el enrutamiento por hub.', + 'ui.compose.crashmail.help_reachable' => 'Requiere que el destino sea alcanzable directamente.', + 'ui.compose.crashmail.help_cost' => 'Costo {cost} {currency}.', + 'ui.compose.echomail_guidelines' => 'Guia de Echomail:', + 'ui.compose.echomail_guideline_identity' => 'Identidad de publicacion: seleccione un area de eco para ver si este mensaje usa Nombre real o Usuario/Alias', + 'ui.compose.echomail_guideline_identity_area_real_name' => 'Identidad de publicacion para esta area: Nombre real', + 'ui.compose.echomail_guideline_identity_area_username' => 'Identidad de publicacion para esta area: Usuario/Alias', + 'ui.compose.echomail_guideline_public' => 'Mensajes de foro publico', + 'ui.compose.echomail_guideline_topic' => 'Mantengase en tema para el area de eco', + 'ui.compose.echomail_guideline_respectful' => 'Sea respetuoso con todos los participantes', + 'ui.compose.echomail_guideline_rules' => 'Siga las reglas del eco y la moderacion', + 'ui.compose.tech_note_plain_text_only' => 'Solo texto plano (sin HTML)', + 'ui.compose.tech_note_line_length' => 'Las lineas deben tener menos de 79 caracteres', + 'ui.compose.tech_note_origin_line' => 'La linea de origen se agrega automaticamente', + 'ui.compose.invalid_fidonet_address_format' => 'Formato de direccion Fidonet invalido (p. ej., 1:123/456)', + 'ui.compose.send_failed_type' => 'No se pudo enviar {type}', + 'ui.compose.upload_attachment_failed' => 'No se pudo cargar el adjunto', + 'ui.compose.reply_attribution' => 'El {date}, {name} escribió:', + 'ui.compose.markdown.help.text_formatting' => 'Formato de texto', + 'ui.compose.markdown.help.syntax' => 'Sintaxis', + 'ui.compose.markdown.help.result' => 'Resultado', + 'ui.compose.markdown.help.hyperlink' => 'hipervinculo', + 'ui.compose.markdown.help.headings' => 'Encabezados', + 'ui.compose.markdown.help.lists_blocks' => 'Listas y bloques', + 'ui.compose.markdown.help.bullet_list' => 'Lista con vietas', + 'ui.compose.markdown.help.numbered_list' => 'Lista numerada', + 'ui.compose.markdown.help.blockquote' => 'Cita', + 'ui.compose.markdown.help.horizontal_rule' => 'Regla horizontal', + 'ui.compose.markdown.help.code_block' => 'Bloque de codigo', + 'ui.compose.markdown.help.keyboard_shortcuts' => 'Atajos de teclado', + 'ui.compose.markdown.help.shortcut_bold' => 'Negrita', + 'ui.compose.markdown.help.shortcut_italic' => 'Cursiva', + 'ui.compose.markdown.help.shortcut_link' => 'Enlace', + 'ui.compose.markdown.help.shortcut_indent' => 'Sangria (4 espacios)', + 'ui.compose.subject_required' => 'Asunto *', + 'ui.compose.message_required' => 'Mensaje *', + 'ui.compose.plain_text_hint' => 'Use texto plano. Se aplica formato estandar de Fidonet.', + 'ui.compose.message_guidelines' => 'Guia de mensajes', + 'ui.compose.technical_notes' => 'Notas tecnicas', + 'ui.compose.your_info' => 'Su informacion', + 'ui.compose.system' => 'Sistema', + 'ui.compose.address' => 'Direccion', + 'ui.compose.tagline_help' => 'Seleccione un tagline del sysop para agregar debajo de su firma.', + 'ui.compose.save_draft' => 'Guardar borrador', + 'ui.compose.send_prefix' => 'Enviar', + 'ui.compose.sending' => 'Enviando...', + 'ui.compose.draft.empty_content' => 'Agregue contenido antes de guardar el borrador', + 'ui.compose.draft.saved_success' => 'Borrador guardado correctamente', + 'ui.compose.sent_cross_post_suffix' => ' (publicado en {count} areas)', + 'ui.compose.address_book.load_failed_short' => 'No se pudo cargar', + 'ui.compose.address_book.select_title' => 'Seleccionar desde la libreta de direcciones', + 'ui.compose.address_book.create_title' => 'Crear nueva entrada en la libreta de direcciones', + 'ui.compose.address_book.entry_added' => 'Entrada agregada correctamente', + 'ui.compose.address_book.use_entry_confirm' => 'Usar esta entrada para el mensaje actual?', + 'ui.address_book.already_exists' => 'Este contacto ya esta en su libreta de direcciones', + 'ui.address_book.load_failed' => 'No se pudo cargar la libreta de direcciones', + 'ui.address_book.added_from_netmail' => 'Agregado desde mensaje netmail', + 'ui.address_book.added_from_netmail_replyto_detail' => 'Agregado desde mensaje netmail. Remitente original: {original_name} ({original_address}), Reply-to: {replyto_name} ({replyto_address})', + 'ui.address_book.added_from_netmail_sender_detail' => 'Agregado desde mensaje netmail. Remitente: {sender_name} ({sender_address})', + 'ui.address_book.check_existing_failed' => 'No se pudo verificar contactos existentes', + 'ui.address_book.entry_updated' => 'Entrada actualizada correctamente', + 'ui.address_book.entry_deleted' => 'Entrada eliminada correctamente', + 'ui.address_book.sender_added' => '{name} agregado a la libreta de direcciones', + 'ui.address_book.saved_to_address_book' => 'Guardado en la libreta de direcciones', + 'ui.address_book.delete_confirm' => 'Esta seguro de que desea eliminar "{name}" de su libreta de direcciones?', + 'ui.address_book.node_address_placeholder' => '1:234/567', + 'ui.address_book.node_address_title' => 'Ingrese una direccion Fidonet valida (ej.: 1:234/567 o 1:234/567.0)', + 'ui.drafts.delete_confirm' => 'Esta seguro de que desea eliminar este borrador? Esta accion no se puede deshacer.', + 'ui.drafts.deleted_success' => 'Borrador eliminado correctamente', + 'ui.messages.none_selected' => 'No hay mensajes seleccionados', + + // Netmail + 'ui.netmail.search.failed' => 'La busqueda fallo', + 'ui.netmail.message_deleted_success' => 'Mensaje eliminado correctamente', + 'ui.netmail.delete_message_confirm' => 'Esta seguro de que desea eliminar este mensaje? Esta accion no se puede deshacer.', + 'ui.netmail.bulk_delete.confirm' => 'Esta seguro de que desea eliminar {count} mensaje(s)?', + 'ui.netmail.bulk_delete.success' => 'Se eliminaron {count} mensaje(s)', + 'ui.netmail.address_book.load_entry_failed_prefix' => 'No se pudo cargar la entrada: ', + 'ui.netmail.bulk_delete.failed' => 'No se pudieron eliminar los mensajes', + 'ui.netmail.no_drafts_found' => 'No se encontraron borradores', + 'ui.netmail.to' => 'Para', + 'ui.netmail.from_to' => 'De/Para', + 'ui.netmail.last_updated' => 'Ultima actualizacion', + 'ui.netmail.received' => 'Recibido', + 'ui.netmail.badge_netmail' => 'NETMAIL', + 'ui.netmail.badge_new' => 'NUEVO', + 'ui.netmail.received_insecure_session_title' => 'Recibido por sesion insegura', + 'ui.netmail.received_insecure_badge_title' => 'Este mensaje se recibio mediante una sesion binkp insegura/no autenticada', + 'ui.netmail.received_insecurely' => 'Recibido de forma insegura', + 'ui.netmail.not_authenticated' => 'Este mensaje no fue autenticado', + + // Echomail + 'ui.echomail.search.failed' => 'La busqueda fallo', + 'ui.echomail.view_all_echo_areas' => 'Ver todas las areas de eco', + 'ui.echomail.search_found_count' => '{count} encontrados', + 'ui.echomail.no_echoareas_available' => 'No hay areas de eco disponibles', + 'ui.echomail.no_drafts_found' => 'No se encontraron borradores', + 'ui.echomail.to_echo_area' => 'Para / Area de eco', + 'ui.echomail.last_updated' => 'Ultima actualizacion', + 'ui.echomail.to_prefix' => 'para:', + 'ui.echomail.bulk_mark_read_failed' => 'No se pudieron marcar los mensajes como leidos', + 'ui.echomail.bulk_mark_read_success' => 'Se marcaron {count} mensaje(s) como leidos', + 'ui.echomail.bulk_marking' => 'Marcando...', + 'ui.echomail.viewing_all_messages' => 'Viendo: Todos los mensajes', + 'ui.echomail.no_area' => 'Sin area', + 'ui.echomail.save_status.update_failed' => 'No se pudo actualizar el estado de guardado', + 'ui.echomail.bulk_delete.failed' => 'No se pudieron eliminar los mensajes', + 'ui.echomail.bulk_delete.success' => 'Se eliminaron {count} mensaje(s)', + 'ui.echomail.bulk_delete.confirm' => 'Esta seguro de que desea eliminar {count} mensaje(s) seleccionados para todos?', + 'ui.echomail.bulk_deleting' => 'Eliminando...', + 'ui.echomail.shares.check_failed' => 'No se pudieron verificar los compartidos existentes', + 'ui.echomail.shares.friendly_url_failed' => 'No se pudo generar la URL amigable', + 'ui.echomail.shares.revoke_confirm' => 'Esta seguro de que desea revocar este enlace compartido? Ya no sera accesible para otros.', + 'ui.echomail.plain_text_mode' => 'Modo texto plano', + 'ui.echomail.press_a_to_toggle' => 'presione A para alternar', + 'ui.echomail.shortcuts.title' => 'Atajos de teclado', + 'ui.echomail.shortcuts.prev_next' => 'Mensaje anterior / siguiente', + 'ui.echomail.shortcuts.toggle_ansi' => 'Alternar renderizado ANSI / texto plano', + 'ui.echomail.shortcuts.download' => 'Descargar mensaje', + 'ui.echomail.shortcuts.fullscreen' => 'Alternar pantalla completa', + 'ui.echomail.shortcuts.help' => 'Mostrar / ocultar atajos de teclado', + 'ui.echomail.shortcuts.close' => 'Cerrar mensaje', + 'ui.echomail.shortcuts.dismiss' => 'Presione ? o H para cerrar esta ayuda', + + // Admin subscriptions + 'ui.admin_subscriptions.page_title' => 'Admin: Gestionar suscripciones', + 'ui.admin_subscriptions.heading' => 'Gestion de suscripciones', + 'ui.admin_subscriptions.breadcrumb_aria' => 'miga de pan', + 'ui.admin_subscriptions.breadcrumb_current' => 'Suscripciones', + 'ui.admin_subscriptions.total_echoareas' => 'Total de echoareas', + 'ui.admin_subscriptions.default_echoareas' => 'Echoareas predeterminadas', + 'ui.admin_subscriptions.total_subscriptions' => 'Total de suscripciones', + 'ui.admin_subscriptions.active_subscribers' => 'Suscriptores activos', + 'ui.admin_subscriptions.echoarea_default_subscriptions' => 'Suscripciones predeterminadas de echoareas', + 'ui.admin_subscriptions.manage_default_help' => 'Gestione que echoareas se suscriben automaticamente para usuarios nuevos', + 'ui.admin_subscriptions.default' => 'Predeterminado', + 'ui.admin_subscriptions.echoarea' => 'Echoarea', + 'ui.admin_subscriptions.subscribers' => 'Suscriptores', + 'ui.admin_subscriptions.user_subs' => 'Suscripciones usuario', + 'ui.admin_subscriptions.auto_subs' => 'Suscripciones auto', + 'ui.admin_subscriptions.messages' => 'Mensajes', + 'ui.admin_subscriptions.default_subscription_title' => 'Suscripcion predeterminada', + 'ui.admin_subscriptions.legend' => 'Leyenda:', + 'ui.admin_subscriptions.legend_default' => 'Suscripcion predeterminada (usuarios nuevos auto-suscritos)', + 'ui.admin_subscriptions.legend_total_badge' => 'Total', + 'ui.admin_subscriptions.legend_total' => 'Todos los suscriptores activos', + 'ui.admin_subscriptions.legend_user_badge' => 'Usuario', + 'ui.admin_subscriptions.legend_user' => 'Suscripciones iniciadas por usuario', + 'ui.admin_subscriptions.legend_auto_badge' => 'Auto', + 'ui.admin_subscriptions.legend_auto' => 'Suscripciones automaticas/admin', + 'ui.admin_subscriptions.how_it_works' => 'Como funciona:', + 'ui.admin_subscriptions.how_default_toggle' => 'Cambie "Predeterminado" para auto-suscribir echoareas a usuarios nuevos', + 'ui.admin_subscriptions.how_existing_users' => 'Al habilitarlo, todos los usuarios existentes tambien se auto-suscriben', + 'ui.admin_subscriptions.how_unsubscribe' => 'Los usuarios aun pueden desuscribirse de echoareas predeterminadas', + 'ui.admin_subscriptions.action_enabled' => 'habilitada', + 'ui.admin_subscriptions.action_disabled' => 'deshabilitada', + 'ui.admin_subscriptions.update_success' => 'Suscripcion predeterminada {action} correctamente. La pagina se recargara para mostrar estadisticas actualizadas.', + 'ui.admin_subscriptions.default_enabled_success' => 'Suscripcion predeterminada habilitada correctamente. La pagina se recargara para mostrar estadisticas actualizadas.', + 'ui.admin_subscriptions.default_disabled_success' => 'Suscripcion predeterminada deshabilitada correctamente. La pagina se recargara para mostrar estadisticas actualizadas.', + 'ui.admin_subscriptions.update_failed' => 'No se pudo actualizar la suscripcion predeterminada. Intente de nuevo.', + 'ui.admin_subscriptions.network_error' => 'Error de red. Intente de nuevo.', + + // User subscriptions + 'ui.user_subscriptions.page_title' => 'Gestionar sus suscripciones de echoareas', + 'ui.user_subscriptions.heading' => 'Gestionar sus suscripciones de echoareas', + 'ui.user_subscriptions.heading_help' => 'Suscribase a echoareas para ver sus mensajes', + 'ui.user_subscriptions.search_placeholder' => 'Buscar echoareas...', + 'ui.user_subscriptions.default_subscription_title' => 'Suscripcion predeterminada', + 'ui.user_subscriptions.message_count' => '{count} mensajes', + 'ui.user_subscriptions.dot_subscribed' => '- Suscrito', + 'ui.user_subscriptions.automatic' => '(automatico)', + 'ui.user_subscriptions.view_messages' => 'Ver mensajes', + 'ui.user_subscriptions.subscription_info' => 'Informacion de suscripcion', + 'ui.user_subscriptions.subscribed_label' => 'Suscrito:', + 'ui.user_subscriptions.how_subscriptions_work' => 'Como funcionan las suscripciones', + 'ui.user_subscriptions.how_new_users' => 'Los usuarios nuevos se suscriben automaticamente a echoareas populares', + 'ui.user_subscriptions.how_subscribe_unsubscribe' => 'Puede suscribirse/desuscribirse de cualquier echoarea', + 'ui.user_subscriptions.how_only_subscribed' => 'Solo echoareas suscritas aparecen en sus listas de mensajes', + 'ui.user_subscriptions.how_star_means_default' => 'indica echoareas predeterminadas', + 'ui.user_subscriptions.subscribed_success' => 'Suscripcion realizada correctamente', + 'ui.user_subscriptions.unsubscribed_success' => 'Desuscripcion realizada correctamente', + 'ui.user_subscriptions.update_failed' => 'No se pudo actualizar la suscripcion. Intente de nuevo.', + 'ui.user_subscriptions.network_error' => 'Error de red. Intente de nuevo.', + + // Echoareas import + 'ui.echoareas_import.page_title' => 'Importar echoareas', + 'ui.echoareas_import.heading' => 'Importar echoareas', + 'ui.echoareas_import.back_to_echo_areas' => 'Volver a echoareas', + 'ui.echoareas_import.processed' => 'Procesadas:', + 'ui.echoareas_import.created' => 'Creadas:', + 'ui.echoareas_import.updated' => 'Actualizadas:', + 'ui.echoareas_import.skipped_blank_rows' => 'Filas vacias omitidas:', + 'ui.echoareas_import.errors' => 'Errores:', + 'ui.echoareas_import.import_errors' => 'Errores de importacion', + 'ui.echoareas_import.upload_csv' => 'Subir CSV', + 'ui.echoareas_import.csv_file' => 'Archivo CSV', + 'ui.echoareas_import.csv_help' => 'Se recomienda CSV UTF-8. La fila de encabezado es opcional.', + 'ui.echoareas_import.import_echo_areas' => 'Importar echoareas', + 'ui.echoareas_import.csv_format' => 'Formato CSV', + 'ui.echoareas_import.each_row_fields' => 'Cada fila debe contener estos campos en este orden:', + 'ui.echoareas_import.example' => 'Ejemplo:', + 'ui.echoareas_import.import_rules' => 'Reglas de importacion', + 'ui.echoareas_import.rule_domain_blank' => 'DOMAIN puede quedar vacio. DOMAIN vacio importa el area como solo local.', + 'ui.echoareas_import.rule_domain_exists' => 'Si se proporciona DOMAIN, ya debe existir en su configuracion de red BinkP.', + 'ui.echoareas_import.rule_existing_area' => 'Si ya existe un area con el mismo ECHOTAG y DOMAIN, se actualiza su descripcion y se reactiva.', + 'ui.echoareas_import.rule_new_areas' => 'Las areas nuevas se crean activas con configuracion predeterminada; puede ajustarlas luego en Gestion de Echo Areas.', + 'ui.echoareas_import.rule_header_optional' => 'Se acepta fila de encabezado ECHOTAG,DESCRIPTION,DOMAIN, pero no es obligatoria.', + 'ui.echoareas_import.importing' => 'Importando...', + 'ui.echoareas_import.error_line_prefix' => 'Linea {line}: {message}', + 'ui.echoareas_import.error_open_csv' => 'No se pudo abrir el archivo CSV subido.', + 'ui.echoareas_import.error_duplicate_row' => 'Combinacion ECHOTAG/DOMAIN duplicada dentro del archivo CSV.', + 'ui.echoareas_import.error_tag_description_required' => 'ECHOTAG y DESCRIPTION son obligatorios.', + 'ui.echoareas_import.error_invalid_tag' => 'ECHOTAG invalido. Use solo letras, numeros, puntos, guiones bajos y guiones.', + 'ui.echoareas_import.error_invalid_domain' => 'DOMAIN invalido. Use solo letras, numeros, guiones bajos y guiones.', + 'ui.echoareas_import.error_unknown_domain' => 'DOMAIN desconocido \'{domain}\'. Agregue primero el dominio de red en la configuracion BinkP.', + 'ui.echoareas_import.error_apply_failed' => 'La importacion fallo y no se aplicaron cambios.', + 'ui.echoareas_import.error_unexpected' => 'Error inesperado durante la importacion.', + 'ui.echoareas_import.error_choose_csv' => 'Seleccione un archivo CSV para importar.', + 'ui.echoareas_import.error_upload_failed' => 'La carga fallo. Intente de nuevo.', + 'ui.echoareas_import.error_invalid_upload' => 'Archivo subido no valido.', + + // Mensajes de exito de API + 'ui.api.reminder.sent' => 'Recordatorio de cuenta enviado correctamente', + 'ui.api.files.uploaded' => 'Archivo subido correctamente', + 'ui.api.files.deleted' => 'Archivo eliminado correctamente', + 'ui.api.files.renamed' => 'Archivo renombrado correctamente', + 'ui.api.messages.sent' => 'Mensaje enviado correctamente', + 'ui.api.messages.bulk_mark_read' => 'Mensajes marcados como leidos', + 'ui.api.messages.bulk_deleted' => 'Mensajes eliminados', + 'ui.api.messages.saved' => 'Mensaje guardado', + 'ui.api.messages.unsaved' => 'Mensaje desmarcado como guardado', + 'ui.api.messages.share_revoked' => 'Enlace compartido revocado', + 'ui.api.credits.sent' => 'Creditos enviados correctamente', + 'ui.api.debug.delete_endpoint_accessible' => 'El endpoint de eliminacion es accesible', + 'ui.api.admin.mrc_restart_initiated' => 'Reinicio de daemon MRC iniciado', + 'ui.api.admin.binkp_config_reloaded' => 'Recarga de configuracion BinkP solicitada', + 'ui.api.binkp.uplink_added' => 'Uplink agregado correctamente', + 'ui.api.binkp.uplink_updated' => 'Uplink actualizado correctamente', + 'ui.api.binkp.uplink_removed' => 'Uplink eliminado correctamente', + 'ui.api.binkp.file_deleted' => 'Archivo eliminado correctamente', + 'ui.api.binkp.file_retry_started' => 'Reintento de archivo iniciado correctamente', + 'ui.api.binkp.config_updated' => 'Configuracion actualizada correctamente', + 'ui.api.binkp.poll_triggered' => 'Sondeo BinkP iniciado', + 'ui.api.binkp.poll_all_triggered' => 'Sondeo global BinkP iniciado', + 'ui.api.binkp.process_packets_started' => 'Procesamiento de paquetes iniciado', + 'ui.api.door.session_resumed' => 'Reanudando sesion existente', +]; diff --git a/config/i18n/es/errors.php b/config/i18n/es/errors.php new file mode 100644 index 000000000..ab516022a --- /dev/null +++ b/config/i18n/es/errors.php @@ -0,0 +1,448 @@ + 'Ocurrio un error inesperado', + + // Auth + 'errors.auth.authentication_required' => 'Autenticacion requerida', + 'errors.auth.invalid_csrf_token' => 'Token CSRF invalido', + 'errors.auth.missing_credentials' => 'Se requieren nombre de usuario y contrasena', + 'errors.auth.invalid_credentials' => 'Credenciales invalidas', + 'errors.auth.invalid_api_key' => 'Clave API invalida', + 'errors.auth.gateway_token_missing_fields' => 'userid y token son obligatorios', + 'errors.auth.invalid_or_expired_gateway_token' => 'Token invalido o expirado', + 'errors.auth.username_or_email_required' => 'Se requiere nombre de usuario o correo electronico', + 'errors.auth.token_required' => 'Se requiere token', + 'errors.auth.invalid_or_expired_token' => 'Token invalido o expirado', + 'errors.auth.token_and_password_required' => 'Se requieren token y nueva contrasena', + 'errors.auth.weak_password' => 'La contrasena debe tener al menos 8 caracteres', + 'errors.auth.reset_failed' => 'No se pudo restablecer la contrasena', + + // Registration + 'errors.register.invalid_submission' => 'Envio invalido', + 'errors.register.too_fast' => 'Tome su tiempo para completar el formulario.', + 'errors.register.session_expired' => 'La sesion expiro. Actualice la pagina e intentelo de nuevo.', + 'errors.register.rate_limited' => 'Demasiados intentos de registro. Intentelo de nuevo mas tarde.', + 'errors.register.required_fields' => 'Se requieren nombre de usuario, contrasena y nombre real', + 'errors.register.invalid_username_format' => 'El nombre de usuario debe tener 3-20 caracteres, solo letras, numeros y guiones bajos', + 'errors.register.restricted_name' => 'Este nombre de usuario o nombre real no esta permitido', + 'errors.register.weak_password' => 'La contrasena debe tener al menos 8 caracteres', + 'errors.register.user_exists' => 'Ya existe un usuario con ese nombre de usuario o nombre. Intente iniciar sesion o contacte al sysop para obtener ayuda.', + 'errors.register.failed' => 'El registro fallo. Intentelo de nuevo mas tarde.', + + // Reminder + 'errors.reminder.username_required' => 'Se requiere nombre de usuario', + 'errors.reminder.user_not_found_or_logged_in' => 'Usuario no encontrado o ya ha iniciado sesion', + 'errors.reminder.send_failed' => 'No se pudo enviar el recordatorio. Intentelo de nuevo mas tarde.', + + // Settings + 'errors.settings.invalid_input' => 'Entrada invalida', + 'errors.settings.update_failed' => 'No se pudo actualizar la configuracion', + 'errors.settings.exception' => 'No se pudo actualizar la configuracion', + + // Messages + 'errors.messages.share_create_failed' => 'No se pudo crear el enlace compartido', + 'errors.messages.share_lookup_failed' => 'No se pudieron cargar los enlaces compartidos', + 'errors.messages.share_revoke_failed' => 'No se pudo revocar el enlace compartido', + 'errors.messages.netmail.not_found' => 'Mensaje no encontrado', + 'errors.messages.netmail.delete_failed' => 'No se pudo eliminar el mensaje', + 'errors.messages.netmail.bulk_delete.invalid_input' => 'Se requiere una lista de IDs de mensajes no vacia', + 'errors.messages.echomail.bulk_read.invalid_input' => 'Se requiere una lista de IDs de mensajes no vacia', + 'errors.messages.echomail.bulk_read.failed' => 'No se pudieron marcar los mensajes como leidos', + 'errors.messages.echomail.bulk_delete.admin_required' => 'Se requieren privilegios de administrador', + 'errors.messages.echomail.bulk_delete.invalid_input' => 'Se requiere una lista de IDs de mensajes no vacia', + 'errors.messages.echomail.bulk_delete.user_not_found' => 'Usuario no encontrado', + 'errors.messages.echomail.stats.subscription_required' => 'Se requiere suscripcion para esta area de eco', + 'errors.messages.echomail.not_found' => 'Mensaje no encontrado', + 'errors.messages.netmail.attachment.no_file' => 'No se subio ningun archivo adjunto', + 'errors.messages.netmail.attachment.upload_error' => 'La carga del archivo adjunto fallo', + 'errors.messages.netmail.attachment.too_large' => 'El archivo adjunto excede el tamano maximo permitido', + 'errors.messages.netmail.attachment.store_failed' => 'No se pudo guardar el archivo adjunto cargado', + 'errors.messages.send.invalid_type' => 'Tipo de mensaje invalido', + 'errors.messages.send.failed' => 'No se pudo enviar el mensaje', + 'errors.messages.send.exception' => 'No se pudo enviar el mensaje', + + // Notify + 'errors.notify.user_id_missing' => 'No se pudo resolver la sesion del usuario', + 'errors.notify.invalid_state' => 'Carga de estado de notificacion invalida', + 'errors.notify.invalid_target' => 'Destino de notificacion invalido', + + // Polls + 'errors.polls.option_required' => 'Se requiere una opcion de encuesta', + 'errors.polls.not_found' => 'Encuesta no encontrada', + 'errors.polls.invalid_option' => 'Opcion de encuesta invalida', + 'errors.polls.vote_failed' => 'No se pudo registrar el voto', + 'errors.polls.insufficient_credits' => 'No se pudieron descontar creditos. Puede que no tenga saldo suficiente.', + 'errors.polls.question_required' => 'Se requiere la pregunta de la encuesta', + 'errors.polls.question_length_invalid' => 'La pregunta de la encuesta debe tener entre 10 y 500 caracteres', + 'errors.polls.options_count_invalid' => 'La encuesta debe incluir entre 2 y 10 opciones', + 'errors.polls.option_empty' => 'Las opciones de la encuesta no pueden estar vacias', + 'errors.polls.option_length_invalid' => 'Las opciones de la encuesta deben tener 200 caracteres o menos', + 'errors.polls.options_duplicate' => 'Las opciones de la encuesta deben ser unicas', + 'errors.polls.create_failed' => 'No se pudo crear la encuesta', + + // Shoutbox + 'errors.shoutbox.message_required' => 'Se requiere mensaje', + 'errors.shoutbox.message_too_long' => 'El mensaje no puede exceder 280 caracteres', + + // Chat + 'errors.chat.feature_disabled' => 'El chat esta deshabilitado', + 'errors.chat.invalid_message_query' => 'Consulta de mensajes de chat invalida', + 'errors.chat.invalid_send_target' => 'Destino de chat invalido', + 'errors.chat.message_length_invalid' => 'El mensaje debe tener entre 1 y 1000 caracteres', + 'errors.chat.room_not_found' => 'Sala de chat no encontrada', + 'errors.chat.user_banned' => 'Estas bloqueado en esta sala', + 'errors.chat.recipient_not_found' => 'Destinatario no encontrado', + 'errors.chat.send_blocked' => 'No se pudo enviar el mensaje', + 'errors.chat.admin_required' => 'Se requieren privilegios de administrador', + 'errors.chat.invalid_moderation_request' => 'Solicitud de moderacion invalida', + 'errors.chat.user_not_found' => 'Usuario no encontrado', + + // Echo Areas + 'errors.echoareas.admin_required' => 'Se requieren privilegios de administrador', + 'errors.echoareas.not_found' => 'Area de eco no encontrada', + 'errors.echoareas.invalid_posting_name_policy' => 'Politica de nombre de publicacion invalida', + 'errors.echoareas.tag_description_required' => 'Se requieren etiqueta y descripcion', + 'errors.echoareas.invalid_tag_format' => 'Formato de etiqueta invalido', + 'errors.echoareas.invalid_color_format' => 'Formato de color invalido', + 'errors.echoareas.create_failed' => 'No se pudo crear el area de eco', + 'errors.echoareas.not_found_or_unchanged' => 'Area de eco no encontrada o sin cambios', + 'errors.echoareas.update_failed' => 'No se pudo actualizar el area de eco', + 'errors.echoareas.delete_blocked_has_messages' => 'No se puede eliminar un area de eco con mensajes existentes', + 'errors.echoareas.delete_failed' => 'No se pudo eliminar el area de eco', + + // File Areas + 'errors.fileareas.not_found' => 'Area de archivos no encontrada', + 'errors.fileareas.create_failed' => 'No se pudo crear el area de archivos', + 'errors.fileareas.update_failed' => 'No se pudo actualizar el area de archivos', + 'errors.fileareas.delete_failed' => 'No se pudo eliminar el area de archivos', + + // Files + 'errors.files.feature_disabled' => 'La funcion de areas de archivos esta deshabilitada', + 'errors.files.area_id_required' => 'Se requiere el ID del area de archivos', + 'errors.files.access_denied' => 'Acceso denegado a esta area de archivos', + 'errors.files.not_found' => 'Archivo no encontrado', + 'errors.files.share_not_found_or_forbidden' => 'Enlace compartido no encontrado o no permitido', + 'errors.files.delete_failed' => 'No se pudo eliminar el archivo', + 'errors.files.scan_forbidden' => 'Se requiere acceso de administrador para escanear archivos', + 'errors.files.scan_failed' => 'Error al realizar el análisis de virus', + 'errors.files.rename_filename_required' => 'Se requiere el nuevo nombre de archivo', + 'errors.files.rename_forbidden' => 'No tiene permiso para renombrar este archivo', + 'errors.files.rename_conflict' => 'Ya existe un archivo con ese nombre en esta area', + 'errors.files.rename_failed' => 'No se pudo renombrar el archivo', + 'errors.files.short_description_required' => 'Se requiere una descripcion corta', + 'errors.files.edit_forbidden' => 'No tiene permiso para editar este archivo', + 'errors.files.edit_failed' => 'No se pudo actualizar el archivo', + 'errors.files.move_forbidden' => 'Solo los administradores pueden mover archivos entre areas', + 'errors.files.move_conflict' => 'Ya existe un archivo con ese nombre en el area de destino', + 'errors.files.move_failed' => 'No se pudo mover el archivo', + 'errors.files.upload.no_file' => 'No se subio ningun archivo', + 'errors.files.upload.area_id_required' => 'Se requiere el ID del area de archivos', + 'errors.files.upload.short_description_required' => 'Se requiere una descripcion corta', + 'errors.files.upload.area_not_found' => 'Area de archivos no encontrada', + 'errors.files.upload.read_only' => 'Esta area de archivos es de solo lectura', + 'errors.files.upload.admin_only' => 'Solo los administradores pueden subir archivos a esta area', + 'errors.files.upload.virus_detected' => 'Archivo rechazado: virus detectado', + 'errors.files.upload.failed' => 'No se pudo subir el archivo', + + // Admin Users + 'errors.admin.users.not_found' => 'Usuario no encontrado', + 'errors.admin.users.create_failed' => 'No se pudo crear el usuario', + 'errors.admin.users.update_failed' => 'No se pudo actualizar el usuario', + 'errors.admin.users.delete_failed' => 'No se pudo eliminar el usuario', + + // Admin Polls + 'errors.admin.polls.question_required' => 'Se requiere una pregunta', + 'errors.admin.polls.options_required' => 'Se requieren al menos dos opciones', + 'errors.admin.polls.not_found' => 'Encuesta no encontrada', + 'errors.admin.polls.create_failed' => 'No se pudo crear la encuesta', + 'errors.admin.polls.update_failed' => 'No se pudo actualizar la encuesta', + 'errors.admin.polls.delete_failed' => 'No se pudo eliminar la encuesta', + + // Message Drafts + 'errors.messages.drafts.invalid_input' => 'Carga de borrador invalida', + 'errors.messages.drafts.user_id_missing' => 'No se pudo resolver la sesion del usuario', + 'errors.messages.drafts.save_failed' => 'No se pudo guardar el borrador', + 'errors.messages.drafts.list_failed' => 'No se pudieron cargar los borradores', + 'errors.messages.drafts.not_found' => 'Borrador no encontrado', + 'errors.messages.drafts.get_failed' => 'No se pudo cargar el borrador', + 'errors.messages.drafts.delete_failed' => 'No se pudo eliminar el borrador', + 'errors.messages.netmail.get_failed' => 'No se pudo cargar el mensaje', + 'errors.messages.echomail.get_failed' => 'No se pudo cargar el mensaje', + + // Message Search / Read State + 'errors.messages.search.query_too_short' => 'La busqueda debe tener al menos 2 caracteres', + 'errors.messages.read.user_id_missing' => 'No se pudo resolver la sesion del usuario', + 'errors.messages.read.invalid_type' => 'Tipo de mensaje invalido', + 'errors.messages.read.mark_failed' => 'No se pudo marcar el mensaje como leido', + 'errors.messages.save.user_id_missing' => 'No se pudo resolver la sesion del usuario', + 'errors.messages.save.invalid_type' => 'Tipo de mensaje invalido', + 'errors.messages.save.failed' => 'No se pudo guardar el mensaje', + 'errors.messages.unsave.user_id_missing' => 'No se pudo resolver la sesion del usuario', + 'errors.messages.unsave.invalid_type' => 'Tipo de mensaje invalido', + 'errors.messages.unsave.failed' => 'No se pudo quitar de guardados el mensaje', + 'errors.messages.unsave.not_saved' => 'El mensaje no estaba guardado o ya fue eliminado', + + // User Profile / Stats / Transactions + 'errors.user.profile.current_password_incorrect' => 'La contrasena actual es incorrecta', + 'errors.user.profile.new_password_too_short' => 'La nueva contrasena debe tener al menos 6 caracteres', + 'errors.user.profile.update_failed' => 'No se pudo actualizar el perfil', + 'errors.user.stats.admin_required' => 'Se requieren privilegios de administrador', + 'errors.user.stats.user_not_found' => 'Usuario no encontrado', + 'errors.user.transactions.admin_required' => 'Se requieren privilegios de administrador', + 'errors.user.transactions.user_not_found' => 'Usuario no encontrado', + 'errors.user.transactions.list_failed' => 'No se pudieron cargar las transacciones', + + // Credits + 'errors.credits.feature_disabled' => 'La funcion de creditos esta deshabilitada', + 'errors.credits.send.invalid_amount' => 'La cantidad debe estar entre 1 y 200', + 'errors.credits.send.self_transfer_forbidden' => 'No puedes enviarte creditos a ti mismo', + 'errors.credits.send.recipient_not_found' => 'Destinatario no encontrado', + 'errors.credits.send.insufficient_balance' => 'Saldo insuficiente', + 'errors.credits.send.debit_failed' => 'No se pudo debitar la cuenta del remitente', + 'errors.credits.send.credit_failed' => 'No se pudo acreditar la cuenta del destinatario', + 'errors.credits.send.failed' => 'La transferencia de creditos fallo', + + // User Sessions / Activity + 'errors.user.sessions.revoke_failed' => 'No se pudo revocar la sesion', + 'errors.user.sessions.revoke_all_failed' => 'No se pudieron revocar las sesiones', + 'errors.user.activity.session_missing' => 'Se requiere una sesion activa', + + // BinkP + 'errors.binkp.admin_required' => 'Se requiere acceso de administrador', + 'errors.binkp.status_failed' => 'No se pudo cargar el estado de BinkP', + 'errors.binkp.poll_failed' => 'No se pudo consultar el uplink BinkP', + 'errors.binkp.poll_all_failed' => 'No se pudieron consultar todos los uplinks BinkP', + 'errors.binkp.process_packets_failed' => 'No se pudieron procesar los paquetes', + 'errors.binkp.uplinks_list_failed' => 'No se pudieron cargar los uplinks BinkP', + 'errors.binkp.files_inbound_failed' => 'No se pudieron cargar los archivos de entrada de BinkP', + 'errors.binkp.files_outbound_failed' => 'No se pudieron cargar los archivos de salida de BinkP', + 'errors.binkp.process_outbound_failed' => 'No se pudo procesar la cola de salida', + 'errors.binkp.connection_test_failed' => 'No se pudo probar la conexion BinkP', + 'errors.binkp.logs.failed' => 'No se pudieron cargar los registros de BinkP', + 'errors.binkp.uplink.address_hostname_required' => 'Se requieren direccion y hostname', + 'errors.binkp.uplink.poll_failed' => 'No se pudo consultar el uplink BinkP', + 'errors.binkp.uplink.poll_all_failed' => 'No se pudieron consultar todos los uplinks BinkP', + 'errors.binkp.uplink.add_failed' => 'No se pudo agregar el uplink BinkP', + 'errors.binkp.uplink.update_failed' => 'No se pudo actualizar el uplink BinkP', + 'errors.binkp.uplink.remove_failed' => 'No se pudo eliminar el uplink BinkP', + 'errors.binkp.files.inbound_failed' => 'No se pudieron cargar los archivos de entrada de BinkP', + 'errors.binkp.files.outbound_failed' => 'No se pudieron cargar los archivos de salida de BinkP', + 'errors.binkp.files.process_inbound_failed' => 'No se pudo procesar la cola de entrada de BinkP', + 'errors.binkp.files.process_outbound_failed' => 'No se pudo procesar la cola de salida de BinkP', + 'errors.binkp.files.delete_outbound_failed' => 'No se pudo eliminar el archivo saliente de BinkP', + 'errors.binkp.files.retry_error_failed' => 'No se pudo reintentar el archivo de error entrante', + 'errors.binkp.config.invalid_section' => 'Seccion de configuracion BinkP invalida', + 'errors.binkp.config.update_failed' => 'No se pudo actualizar la configuracion de BinkP', + + // Admin Pending Users + 'errors.admin.pending_users.admin_required' => 'Se requieren privilegios de administrador', + 'errors.admin.pending_users.list_failed' => 'No se pudieron cargar los usuarios pendientes', + 'errors.admin.pending_users.not_found' => 'Usuario pendiente no encontrado', + 'errors.admin.pending_users.get_failed' => 'No se pudo cargar el usuario pendiente', + 'errors.admin.pending_users.approve_failed' => 'No se pudo aprobar el usuario pendiente', + 'errors.admin.pending_users.reject_failed' => 'No se pudo rechazar el usuario pendiente', + + // Admin Users (extended) + 'errors.admin.users.admin_required' => 'Se requieren privilegios de administrador', + 'errors.admin.users.list_failed' => 'No se pudieron cargar los usuarios', + 'errors.admin.users.get_failed' => 'No se pudo cargar el usuario', + 'errors.admin.users.real_name_required' => 'Se requiere el nombre real', + 'errors.admin.users.password_too_short' => 'La contrasena debe tener al menos 8 caracteres', + 'errors.admin.users.update_status_failed' => 'No se pudo actualizar el estado del usuario', + 'errors.admin.users.create_required_fields' => 'Se requieren nombre de usuario, nombre real y contrasena', + 'errors.admin.users.invalid_username_format' => 'Formato de nombre de usuario invalido', + 'errors.admin.users.restricted_name' => 'Este nombre de usuario o nombre real no esta permitido', + 'errors.admin.users.username_exists' => 'El nombre de usuario ya existe', + 'errors.admin.users.cleanup_failed' => 'No se pudo limpiar los registros', + 'errors.admin.users.reminder_not_allowed' => 'El usuario no es elegible para recordatorio', + 'errors.admin.users.reminder_send_failed' => 'No se pudo enviar el recordatorio', + 'errors.admin.users.need_reminders_failed' => 'No se pudieron cargar los candidatos a recordatorio', + + // Admin Debug + 'errors.admin.debug_failed' => 'No se pudo cargar la informacion de depuracion de administrador', + + // Admin BBS / Appearance / Shell + 'errors.admin.bbs_settings.load_failed' => 'No se pudo cargar la configuracion del BBS', + 'errors.admin.bbs_settings.invalid_payload' => 'Carga de configuracion invalida', + 'errors.admin.bbs_settings.invalid_credits_config' => 'Configuracion de creditos invalida', + 'errors.admin.bbs_settings.save_failed' => 'No se pudo guardar la configuracion del BBS', + 'errors.admin.appearance.load_failed' => 'No se pudo cargar la configuracion de apariencia', + 'errors.admin.appearance.branding.invalid_accent_color' => 'Formato de color de acento invalido', + 'errors.admin.appearance.branding.footer_too_long' => 'El texto del pie de pagina debe tener 500 caracteres o menos', + 'errors.admin.appearance.branding.save_failed' => 'No se pudo guardar la configuracion de marca', + 'errors.admin.appearance.content.save_failed' => 'No se pudo guardar la configuracion de contenido', + 'errors.admin.appearance.navigation.save_failed' => 'No se pudo guardar la configuracion de navegacion', + 'errors.admin.appearance.seo.save_failed' => 'No se pudo guardar la configuracion de SEO', + 'errors.admin.appearance.shell.save_failed' => 'No se pudo guardar la configuracion del shell', + 'errors.admin.appearance.message_reader.save_failed' => 'No se pudo guardar la configuracion del lector de mensajes', + 'errors.admin.appearance.markdown_preview.failed' => 'No se pudo renderizar la vista previa de markdown', + 'errors.admin.shell_art.list_failed' => 'No se pudo listar los archivos de arte de shell', + 'errors.admin.shell_art.upload.no_file' => 'No se subio ningun archivo de arte de shell', + 'errors.admin.shell_art.upload.upload_error' => 'La carga del arte de shell fallo', + 'errors.admin.shell_art.upload.file_too_large' => 'El archivo de arte de shell excede el limite de tamano', + 'errors.admin.shell_art.upload.failed' => 'No se pudo subir el arte de shell', + 'errors.admin.shell_art.delete.invalid_name' => 'Nombre de archivo de arte de shell invalido', + 'errors.admin.shell_art.delete.failed' => 'No se pudo eliminar el arte de shell', + 'errors.admin.taglines.load_failed' => 'No se pudieron cargar las frases', + 'errors.admin.taglines.save_failed' => 'No se pudieron guardar las frases', + 'errors.admin.mrc_settings.load_failed' => 'No se pudo cargar la configuracion de MRC', + 'errors.admin.mrc_settings.save_failed' => 'No se pudo guardar la configuracion de MRC', + 'errors.admin.mrc_settings.restart_failed' => 'No se pudo reiniciar el daemon de MRC', + 'errors.admin.bbs_system.load_failed' => 'No se pudo cargar la configuracion del sistema', + 'errors.admin.bbs_system.save_failed' => 'No se pudo guardar la configuracion del sistema', + 'errors.admin.binkp_config.load_failed' => 'No se pudo cargar la configuracion de BinkP', + 'errors.admin.binkp_config.save_failed' => 'No se pudo guardar la configuracion de BinkP', + 'errors.admin.binkp_config.reload_failed' => 'No se pudo recargar la configuracion de BinkP', + 'errors.admin.dosdoors_config.load_failed' => 'No se pudo cargar la configuracion de DOS doors', + 'errors.admin.dosdoors_config.save_failed' => 'No se pudo guardar la configuracion de DOS doors', + 'errors.admin.native_doors.load_failed' => 'No se pudo cargar la configuracion de puertas nativas', + 'errors.admin.native_doors.save_failed' => 'No se pudo guardar la configuracion de puertas nativas', + 'errors.admin.native_doors.sync_failed' => 'No se pudieron sincronizar las puertas nativas', + 'errors.admin.webdoors_config.load_failed' => 'No se pudo cargar la configuracion de webdoors', + 'errors.admin.webdoors_config.save_failed' => 'No se pudo guardar la configuracion de webdoors', + 'errors.admin.webdoors_config.activate_failed' => 'No se pudo activar la configuracion de webdoors', + 'errors.admin.filearea_rules.load_failed' => 'No se pudieron cargar las reglas del area de archivos', + 'errors.admin.filearea_rules.save_failed' => 'No se pudieron guardar las reglas del area de archivos', + 'errors.admin.ads.list_failed' => 'No se pudieron cargar los anuncios', + 'errors.admin.ads.upload.no_file' => 'No se subio ningun archivo de anuncio', + 'errors.admin.ads.upload.upload_error' => 'La carga del anuncio fallo', + 'errors.admin.ads.upload.file_too_large' => 'El archivo de anuncio excede el limite de tamano', + 'errors.admin.ads.upload.read_failed' => 'No se pudo leer el archivo de anuncio subido', + 'errors.admin.ads.upload.failed' => 'No se pudo subir el anuncio', + 'errors.admin.ads.delete_failed' => 'No se pudo eliminar el anuncio', + 'errors.admin.chat_rooms.invalid_name_length' => 'El nombre de la sala debe tener entre 1 y 64 caracteres', + 'errors.admin.chat_rooms.create_failed' => 'No se pudo crear la sala de chat', + 'errors.admin.chat_rooms.not_found' => 'Sala de chat no encontrada', + 'errors.admin.chat_rooms.lobby_name_locked' => 'El nombre del lobby no se puede cambiar', + 'errors.admin.chat_rooms.update_failed' => 'No se pudo actualizar la sala de chat', + 'errors.admin.chat_rooms.lobby_delete_forbidden' => 'El lobby no se puede eliminar', + 'errors.admin.chat_rooms.delete_failed' => 'No se pudo eliminar la sala de chat', + 'errors.admin.shoutbox.hide_failed' => 'No se pudo ocultar el shout', + 'errors.admin.shoutbox.unhide_failed' => 'No se pudo volver a mostrar el shout', + 'errors.admin.shoutbox.delete_failed' => 'No se pudo eliminar el shout', + 'errors.admin.insecure_nodes.create_failed' => 'No se pudo agregar el nodo inseguro', + 'errors.admin.insecure_nodes.update_failed' => 'No se pudo actualizar el nodo inseguro', + 'errors.admin.insecure_nodes.delete_failed' => 'No se pudo eliminar el nodo inseguro', + 'errors.admin.crashmail.retry_failed' => 'No se pudo reintentar el elemento de crashmail', + 'errors.admin.crashmail.cancel_failed' => 'No se pudo cancelar el elemento de crashmail', + 'errors.admin.crashmail.poll_failed' => 'No se pudo ejecutar el sondeo de crashmail', + 'errors.admin.custom_templates.list_failed' => 'No se pudo listar las plantillas personalizadas', + 'errors.admin.custom_templates.get_failed' => 'No se pudo cargar la plantilla personalizada', + 'errors.admin.custom_templates.save_failed' => 'No se pudo guardar la plantilla personalizada', + 'errors.admin.custom_templates.delete_failed' => 'No se pudo eliminar la plantilla personalizada', + 'errors.admin.custom_templates.install_failed' => 'No se pudo instalar la plantilla personalizada', + 'errors.admin.auto_feed.not_found' => 'Fuente de feed no encontrada', + 'errors.admin.auto_feed.required_fields' => 'Se requieren URL del feed, area de eco y usuario de publicacion', + 'errors.admin.auto_feed.invalid_url' => 'La URL del feed es invalida', + 'errors.admin.auto_feed.echoarea_not_found' => 'Area de eco no encontrada', + 'errors.admin.auto_feed.user_not_found' => 'Usuario de publicacion no encontrado', + 'errors.admin.auto_feed.duplicate_source' => 'La fuente de feed ya existe', + 'errors.admin.auto_feed.create_failed' => 'No se pudo crear la fuente de feed', + 'errors.admin.auto_feed.update_failed' => 'No se pudo actualizar la fuente de feed', + 'errors.admin.auto_feed.check_failed' => 'La verificacion del feed fallo', + 'errors.admin.activity_stats.table_missing' => 'La tabla de registro de actividad no esta disponible', + + // Address Book + 'errors.address_book.list_failed' => 'No se pudieron cargar las entradas de la libreta de direcciones', + 'errors.address_book.not_found' => 'Entrada no encontrada', + 'errors.address_book.get_failed' => 'No se pudo cargar la entrada de la libreta de direcciones', + 'errors.address_book.user_not_found' => 'ID de usuario no encontrado en los datos de autenticacion', + 'errors.address_book.invalid_fidonet_format' => 'Formato de direccion Fidonet invalido. Use formato como 1:234/567 o 1:234/567.0', + 'errors.address_book.required_fields' => 'Se requieren nombre y direccion Fidonet', + 'errors.address_book.duplicate_entry' => 'La entrada de la libreta de direcciones ya existe', + 'errors.address_book.create_failed' => 'No se pudo crear la entrada de la libreta de direcciones', + 'errors.address_book.update_failed' => 'No se pudo actualizar la entrada de la libreta de direcciones', + 'errors.address_book.delete_failed' => 'No se pudo eliminar la entrada de la libreta de direcciones', + 'errors.address_book.search_failed' => 'No se pudieron buscar las entradas de la libreta de direcciones', + 'errors.address_book.stats_failed' => 'No se pudieron cargar las estadisticas de la libreta de direcciones', + + // Shared Messages + 'errors.messages.shared.lookup_failed' => 'No se pudo cargar el mensaje compartido', + 'errors.messages.shared.user_shares_failed' => 'No se pudieron cargar los recursos compartidos del usuario', + 'errors.messages.shared.access_denied' => 'Mensaje no encontrado o acceso denegado', + 'errors.messages.shared.sharing_disabled' => 'El uso compartido esta deshabilitado para tu cuenta', + 'errors.messages.shared.max_active_reached' => 'Se alcanzo el numero maximo de recursos compartidos activos', + 'errors.messages.shared.not_found_or_expired' => 'Recurso compartido no encontrado o expirado', + 'errors.messages.shared.login_required' => 'Se requiere iniciar sesion para acceder a este recurso compartido', + 'errors.messages.shared.original_not_found' => 'Mensaje original no encontrado', + 'errors.messages.shared.not_found' => 'Recurso compartido no encontrado', + 'errors.messages.shared.friendly_url_only_echomail' => 'Las URL amigables solo estan disponibles para recursos compartidos de echomail', + 'errors.messages.shared.slug_generation_failed' => 'No se puede generar un slug para compartir de este mensaje', + + // Subscriptions + 'errors.subscriptions.echoarea_id_required' => 'Se requiere el ID del area de eco', + 'errors.subscriptions.invalid_action' => 'Accion invalida', + 'errors.subscriptions.admin_required' => 'Se requiere acceso de administrador', + + // Nodelist API + 'errors.nodelist.api.endpoint_not_found' => 'Punto final de API no encontrado', + 'errors.nodelist.api.address_required' => 'Se requiere el parametro de direccion', + 'errors.nodelist.api.node_not_found' => 'Nodo no encontrado', + 'errors.nodelist.api.zone_required' => 'Se requiere el parametro de zona', + 'errors.nodelist.api.internal_error' => 'No se pudo procesar la solicitud de API de nodelist', + 'errors.nodelist.admin_required' => 'Se requiere acceso de administrador', + 'errors.nodelist.import.domain_required' => 'Especifique un dominio de red', + 'errors.nodelist.import.domain_invalid' => 'El dominio debe contener solo letras, numeros, guiones bajos y guiones', + 'errors.nodelist.import.file_required' => 'Seleccione un archivo de nodelist valido', + 'errors.nodelist.import.invalid_format' => 'Formato de nodelist invalido', + 'errors.nodelist.import.zip_extension_missing' => 'La extension ZIP no esta disponible', + 'errors.nodelist.import.zip_open_failed' => 'No se pudo abrir el archivo ZIP', + 'errors.nodelist.import.archive_unsupported' => 'El formato de archivo {format} no es compatible en la interfaz web (use linea de comandos)', + 'errors.nodelist.import.archive_nodelist_missing' => 'No se pudo encontrar un archivo de nodelist dentro del archivo comprimido', + 'errors.nodelist.import.extract_failed' => 'No se pudo extraer el archivo comprimido', + 'errors.nodelist.import.failed' => 'No se pudo importar el nodelist', + + // Settings / Taglines + 'errors.settings.load_failed' => 'No se pudo cargar la configuracion del usuario', + 'errors.taglines.load_failed' => 'No se pudieron cargar las frases', + + // Referrals + 'errors.referrals.code_not_found' => 'Codigo de referido no encontrado', + 'errors.referrals.stats_failed' => 'No se pudieron cargar las estadisticas de referidos', + 'errors.referrals.admin_stats_failed' => 'No se pudieron cargar las estadisticas de referidos de administrador', + + // WebDoor + 'errors.webdoor.feature_disabled' => 'El sistema de juegos no esta habilitado', + 'errors.webdoor.auth_required' => 'No autenticado', + 'errors.webdoor.invalid_slot' => 'Numero de ranura invalido', + 'errors.webdoor.save_too_large' => 'Los datos guardados exceden el tamano maximo', + 'errors.webdoor.save_not_found' => 'Guardado no encontrado', + + // Door API + 'errors.door.door_name_required' => 'Se requiere el nombre de la puerta', + 'errors.door.admin_only' => 'Esta puerta esta restringida a administradores', + 'errors.door.insufficient_credits' => 'Creditos insuficientes', + 'errors.door.insufficient_credits_detail' => 'Esta puerta cuesta {required} creditos. Tienes {balance} creditos.', + 'errors.door.capacity_reached' => 'La puerta esta a capacidad maxima', + 'errors.door.capacity_reached_detail' => 'Esta puerta esta actualmente en uso. Solo se permiten {max_nodes} jugador(es) al mismo tiempo. Intentalo de nuevo mas tarde.', + 'errors.door.launch_failed' => 'No se pudo iniciar la sesion de la puerta', + 'errors.door.session_id_required' => 'Se requiere ID de sesion', + 'errors.door.session_unauthorized' => 'No autorizado', + 'errors.door.session_end_failed' => 'No se pudo finalizar la sesion', + 'errors.door.session_get_failed' => 'No se pudo obtener la sesion', + 'errors.door.asset.invalid_type' => 'Tipo de recurso invalido', + 'errors.door.asset.door_not_found' => 'Puerta no encontrada', + 'errors.door.asset.not_defined' => 'Recurso no definido en el manifiesto', + 'errors.door.asset.file_not_found' => 'Archivo de recurso no encontrado', + 'errors.door.asset.access_denied' => 'Acceso denegado', + + // TIC Processing + 'errors.tic.file_area_create_failed' => 'No se pudo crear el area de archivos desde metadatos TIC', + 'errors.tic.validation_failed' => 'La validacion del archivo TIC fallo', + 'errors.tic.virus_detected' => 'Archivo rechazado: virus detectado', + 'errors.tic.processing_failed' => 'El procesamiento TIC fallo', + + // Virus Scanner + 'errors.virus_scanner.not_available' => 'El analisis de virus no esta disponible', + 'errors.virus_scanner.file_not_found' => 'Archivo no encontrado para analisis de virus', + 'errors.virus_scanner.scan_error' => 'Error en el analisis de virus', + + // Language overlay editor + 'errors.admin.i18n_overrides.invalid_locale' => 'Idioma no valido o no admitido', + 'errors.admin.i18n_overrides.missing_params' => 'Se requieren el idioma y el nombre del catalogo', + 'errors.admin.i18n_overrides.load_failed' => 'Error al cargar el catalogo', + 'errors.admin.i18n_overrides.save_failed' => 'Error al guardar los ajustes', +]; + diff --git a/config/i18n/es/terminalserver.php b/config/i18n/es/terminalserver.php new file mode 100644 index 000000000..9198cf336 --- /dev/null +++ b/config/i18n/es/terminalserver.php @@ -0,0 +1,239 @@ + 'Demasiados intentos fallidos de inicio de sesión. Por favor, inténtelo más tarde.', + + // --- Login / register menu (pre-auth) --- + 'ui.terminalserver.server.login_menu.prompt' => '¿Qué desea hacer?', + 'ui.terminalserver.server.login_menu.login' => ' (L) Iniciar sesión en cuenta existente', + 'ui.terminalserver.server.login_menu.register' => ' (R) Registrar nueva cuenta', + 'ui.terminalserver.server.login_menu.quit' => ' (Q) Salir', + 'ui.terminalserver.server.login_menu.choice' => 'Su elección: ', + 'ui.terminalserver.server.goodbye' => '¡Adiós!', + 'ui.terminalserver.server.press_enter_disconnect' => 'Presione Enter para desconectarse.', + + // --- Login --- + 'ui.terminalserver.server.login.username_prompt' => 'Usuario: ', + 'ui.terminalserver.server.login.password_prompt' => 'Contraseña: ', + 'ui.terminalserver.server.login.success' => 'Inicio de sesión exitoso.', + 'ui.terminalserver.server.login.failed_remaining' => 'Error de inicio de sesión. {remaining} intento(s) restante(s).', + 'ui.terminalserver.server.login.failed_max' => 'Error de inicio de sesión. Máximo de intentos superado.', + + // --- Registration --- + 'ui.terminalserver.server.registration.title' => '=== Registro de Nuevo Usuario ===', + 'ui.terminalserver.server.registration.intro' => 'Por favor, proporcione la siguiente información para crear su cuenta.', + 'ui.terminalserver.server.registration.cancel_hint' => '(Escriba "cancel" en cualquier solicitud para cancelar el registro)', + 'ui.terminalserver.server.registration.username' => 'Usuario (3-20 caracteres, letras/números/guión bajo): ', + 'ui.terminalserver.server.registration.password' => 'Contraseña (mínimo 8 caracteres): ', + 'ui.terminalserver.server.registration.confirm' => 'Confirmar contraseña: ', + 'ui.terminalserver.server.registration.password_mismatch' => 'Error: Las contraseñas no coinciden.', + 'ui.terminalserver.server.registration.realname' => 'Nombre real: ', + 'ui.terminalserver.server.registration.email' => 'Correo electrónico (opcional): ', + 'ui.terminalserver.server.registration.location' => 'Ubicación (opcional): ', + 'ui.terminalserver.server.registration.submitting' => 'Enviando registro...', + 'ui.terminalserver.server.registration.success' => '¡Registro exitoso!', + 'ui.terminalserver.server.registration.pending' => 'Su cuenta ha sido creada y está pendiente de aprobación.', + 'ui.terminalserver.server.registration.pending_review' => 'Será notificado una vez que un administrador haya revisado su registro.', + + // --- Anti-bot challenge --- + 'ui.terminalserver.server.press_esc' => 'Presione ESC dos veces para continuar...', + + // --- Banner (login screen) --- + 'ui.terminalserver.server.banner.title' => 'Terminal BinktermPHP', + 'ui.terminalserver.server.banner.system' => 'Sistema: ', + 'ui.terminalserver.server.banner.location' => 'Ubicación: ', + 'ui.terminalserver.server.banner.origin' => 'Origen: ', + 'ui.terminalserver.server.banner.web' => 'Web: ', + 'ui.terminalserver.server.banner.visit_web' => 'Para una buena experiencia visítenos en la web @ {url}', + 'ui.terminalserver.server.banner.tls' => 'Conectado usando TLS', + 'ui.terminalserver.server.banner.no_tls' => 'Conectado sin TLS - use el puerto {port} para una conexión cifrada', + 'ui.terminalserver.server.ssh_banner.welcome' => 'Bienvenido a {system}.', + 'ui.terminalserver.server.ssh_banner.line2' => 'Inicie sesión con sus credenciales, o introduzca cualquier usuario/contraseña', + 'ui.terminalserver.server.ssh_banner.line3' => 'para continuar a la pantalla principal de inicio de sesión del BBS.', + + // --- Main menu --- + 'ui.terminalserver.server.menu.title' => 'Menú Principal', + 'ui.terminalserver.server.menu.select_option' => 'Seleccione una opción:', + 'ui.terminalserver.server.menu.netmail' => 'N) Netmail ({count} mensajes)', + 'ui.terminalserver.server.menu.echomail' => 'E) Echomail ({count} mensajes)', + 'ui.terminalserver.server.menu.whos_online' => 'W) Quién está en línea', + 'ui.terminalserver.server.menu.shoutbox' => 'S) Shoutbox', + 'ui.terminalserver.server.menu.polls' => 'P) Encuestas', + 'ui.terminalserver.server.menu.doors' => 'D) Juegos de Puertas', + 'ui.terminalserver.server.menu.files' => 'F) Archivos', + 'ui.terminalserver.server.menu.quit' => 'Q) Salir', + + // --- Farewell --- + 'ui.terminalserver.server.farewell' => '¡Gracias por visitarnos, que tenga un excelente día!', + 'ui.terminalserver.server.visit_web' => 'Vuelva a visitarnos en la web en {url}', + + // --- Who's Online --- + 'ui.terminalserver.server.whos_online.title' => 'Quién está en línea (últimos {minutes} minutos)', + 'ui.terminalserver.server.whos_online.empty' => 'No hay usuarios en línea.', + + // --- Idle timeout --- + 'ui.terminalserver.server.idle.disconnect' => 'Tiempo de inactividad agotado - desconectando...', + 'ui.terminalserver.server.idle.warning_line' => '¿Sigue ahí? (Presione Enter para continuar)', + 'ui.terminalserver.server.idle.warning_key' => '¿Sigue ahí? (Presione cualquier tecla para continuar)', + + // --- Shared UI prompts --- + 'ui.terminalserver.server.press_any_key' => 'Presione cualquier tecla para volver...', + 'ui.terminalserver.server.press_continue' => 'Presione cualquier tecla para continuar...', + + // --- Message editor (full screen) --- + 'ui.terminalserver.editor.title' => 'EDITOR DE MENSAJES - MODO PANTALLA COMPLETA', + 'ui.terminalserver.editor.shortcuts' => 'Ctrl+K=Ayuda Ctrl+Z=Enviar Ctrl+C=Cancelar', + 'ui.terminalserver.editor.cancelled' => 'Mensaje cancelado.', + 'ui.terminalserver.editor.saved' => 'Mensaje guardado y listo para enviar.', + 'ui.terminalserver.editor.starting_text' => 'Comenzando con texto citado. Ingrese su respuesta a continuación.', + 'ui.terminalserver.editor.instructions' => 'Ingrese el texto del mensaje. Termine con una línea que contenga solo ".". Escriba "/abort" para cancelar.', + + // --- Message editor help --- + 'ui.terminalserver.editor.help.title' => 'AYUDA DEL EDITOR DE MENSAJES', + 'ui.terminalserver.editor.help.separator' => '-------------------', + 'ui.terminalserver.editor.help.navigate' => 'Flechas = Mover cursor', + 'ui.terminalserver.editor.help.edit' => 'Retroceso/Supr = Editar texto', + 'ui.terminalserver.editor.help.help' => 'Ctrl+K = Ayuda', + 'ui.terminalserver.editor.help.start_of_line' => 'Ctrl+A = Inicio de línea', + 'ui.terminalserver.editor.help.end_of_line' => 'Ctrl+E = Fin de línea', + 'ui.terminalserver.editor.help.delete_line' => 'Ctrl+Y = Borrar línea completa', + 'ui.terminalserver.editor.help.save' => 'Ctrl+Z = Guardar mensaje y enviar', + 'ui.terminalserver.editor.help.cancel' => 'Ctrl+C = Cancelar y descartar mensaje', + + // --- Compose (shared between netmail and echomail) --- + 'ui.terminalserver.compose.to_name' => 'Para: ', + 'ui.terminalserver.compose.to_address' => 'Dirección de destino: ', + 'ui.terminalserver.compose.subject' => 'Asunto: ', + 'ui.terminalserver.compose.no_recipient' => 'Se requiere nombre del destinatario. Mensaje cancelado.', + 'ui.terminalserver.compose.enter_message' => 'Ingrese su mensaje a continuación:', + 'ui.terminalserver.compose.select_tagline' => 'Seleccione una etiqueta:', + 'ui.terminalserver.compose.no_tagline' => ' 0) Ninguna', + 'ui.terminalserver.compose.tagline_default' => 'Etiqueta # [{default}] (Enter para predeterminada): ', + 'ui.terminalserver.compose.tagline_none' => 'Etiqueta # (Enter para ninguna): ', + 'ui.terminalserver.compose.message_cancelled' => 'Mensaje cancelado (vacío).', + + // --- Echomail --- + 'ui.terminalserver.echomail.no_areas' => 'No hay áreas de echomail disponibles.', + 'ui.terminalserver.echomail.areas_header' => 'Áreas de echomail (página {page}/{total}):', + 'ui.terminalserver.echomail.areas_nav' => 'Ingrese #, n/p (siguiente/anterior), q (salir)', + 'ui.terminalserver.echomail.no_messages' => 'No hay mensajes de echomail.', + 'ui.terminalserver.echomail.messages_header' => 'Echomail: {area} (página {page}/{total})', + 'ui.terminalserver.echomail.compose_title' => '=== Redactar Echomail ===', + 'ui.terminalserver.echomail.area_label' => 'Área: {area}', + 'ui.terminalserver.echomail.posting' => 'Publicando echomail...', + 'ui.terminalserver.echomail.post_success' => '✓ ¡Echomail publicado exitosamente!', + 'ui.terminalserver.echomail.post_failed' => '✗ Error al publicar echomail: {error}', + + // --- Netmail --- + 'ui.terminalserver.netmail.no_messages' => 'No hay mensajes de netmail.', + 'ui.terminalserver.netmail.header' => 'Netmail (página {page}/{total}):', + 'ui.terminalserver.netmail.compose_title' => '=== Redactar Netmail ===', + 'ui.terminalserver.netmail.sending' => 'Enviando netmail...', + 'ui.terminalserver.netmail.send_success' => '✓ ¡Netmail enviado exitosamente!', + 'ui.terminalserver.netmail.send_failed' => '✗ Error al enviar netmail: {error}', + 'ui.terminalserver.netmail.attachments_none' => 'No hay archivos adjuntos en este mensaje.', + 'ui.terminalserver.netmail.attachments_header' => 'Adjuntos:', + 'ui.terminalserver.netmail.attachment_download_prompt' => 'Adjunto # para descargar (Enter para cancelar): ', + + // --- Polls --- + 'ui.terminalserver.polls.disabled' => 'La cabina de votación está deshabilitada.', + 'ui.terminalserver.polls.title' => 'Encuestas', + 'ui.terminalserver.polls.no_polls' => 'No hay encuestas activas.', + 'ui.terminalserver.polls.detail_title' => 'Detalle de encuesta', + 'ui.terminalserver.polls.total_votes' => 'Total de votos: {count}', + 'ui.terminalserver.polls.enter_poll' => 'Ingrese # de encuesta o Q para volver: ', + 'ui.terminalserver.polls.vote_prompt' => 'Vote con opción # o Q para volver: ', + 'ui.terminalserver.polls.voted' => 'Voto registrado.', + + // --- Shoutbox --- + 'ui.terminalserver.shoutbox.title' => 'Shoutbox', + 'ui.terminalserver.shoutbox.recent_title' => 'Shoutbox reciente', + 'ui.terminalserver.shoutbox.no_messages' => 'No hay mensajes en el shoutbox.', + 'ui.terminalserver.shoutbox.menu' => '[P]ublicar [R]efrescar [Q]uit: ', + 'ui.terminalserver.shoutbox.new_shout' => 'Nuevo mensaje (en blanco para cancelar): ', + 'ui.terminalserver.shoutbox.posted' => 'Mensaje publicado.', + 'ui.terminalserver.shoutbox.post_failed' => 'Error al publicar el mensaje.', + + // --- File areas --- + 'ui.terminalserver.files.no_areas' => 'No hay áreas de archivos disponibles.', + 'ui.terminalserver.files.areas_header' => 'Áreas de archivos (página {page}/{total}):', + 'ui.terminalserver.files.areas_nav' => 'Ingrese #, n/p (siguiente/anterior), q (salir)', + 'ui.terminalserver.files.area_header' => 'Archivos: {area} (página {page}/{total})', + 'ui.terminalserver.files.no_files' => 'No hay archivos en esta área.', + 'ui.terminalserver.files.files_nav' => 'D)escargar n/p (siguiente/anterior) Q)salir', + 'ui.terminalserver.files.files_nav_upload' => 'D)escargar S)ubir n/p (siguiente/anterior) Q)salir', + 'ui.terminalserver.files.files_nav_upload_only' => 'S)ubir n/p (siguiente/anterior) Q)salir', + 'ui.terminalserver.files.files_nav_none' => 'n/p (siguiente/anterior) Q)salir', + 'ui.terminalserver.files.transfer_unavailable' => 'ZMODEM deshabilitado: instale lrzsz (sz/rz) en el servidor para habilitar transferencias.', + 'ui.terminalserver.files.invalid_selection' => 'Selección inválida.', + 'ui.terminalserver.files.download_prompt' => 'Número de archivo a descargar (Enter para cancelar): ', + 'ui.terminalserver.files.download_error' => 'Archivo no encontrado en el servidor.', + 'ui.terminalserver.files.download_starting' => 'Iniciando descarga ZMODEM: {name}', + 'ui.terminalserver.files.download_hint' => 'Inicie la recepción ZMODEM en su terminal ahora...', + 'ui.terminalserver.files.download_done' => 'Transferencia completada.', + 'ui.terminalserver.files.download_failed' => 'La transferencia falló o fue cancelada.', + 'ui.terminalserver.files.upload_title' => '=== Subir Archivo ===', + 'ui.terminalserver.files.upload_area' => 'Área: {area}', + 'ui.terminalserver.files.upload_desc_prompt' => 'Descripción breve (en blanco para cancelar): ', + 'ui.terminalserver.files.upload_cancelled' => 'Subida cancelada.', + 'ui.terminalserver.files.upload_starting' => 'Inicie el envío ZMODEM en su terminal ahora...', + 'ui.terminalserver.files.upload_failed' => 'La transferencia falló o fue cancelada.', + 'ui.terminalserver.files.upload_done' => 'Archivo subido exitosamente (ID: {id}).', + 'ui.terminalserver.files.upload_error' => 'Error al subir: {error}', + 'ui.terminalserver.files.upload_duplicate' => 'Este archivo ya existe en esta área.', + 'ui.terminalserver.files.upload_readonly' => 'Esta área es de solo lectura. No se permiten subidas.', + 'ui.terminalserver.files.upload_admin_only' => 'Solo los administradores pueden subir a esta área.', + + // --- Main menu: terminal settings --- + 'ui.terminalserver.server.menu.terminal_settings' => 'T) Configuración de Terminal', + + // --- Terminal settings page --- + 'ui.terminalserver.settings.title' => '=== Configuración de Terminal ===', + 'ui.terminalserver.settings.charset_label' => 'Juego de caracteres : {value}', + 'ui.terminalserver.settings.ansi_label' => 'Color ANSI : {value}', + 'ui.terminalserver.settings.not_set' => 'No configurado', + 'ui.terminalserver.settings.menu_detect' => 'D) Ejecutar asistente de detección', + 'ui.terminalserver.settings.menu_charset' => 'C) Cambiar juego de caracteres manualmente', + 'ui.terminalserver.settings.menu_ansi' => 'A) Alternar color ANSI', + 'ui.terminalserver.settings.menu_quit' => 'Q) Volver al menú principal', + 'ui.terminalserver.settings.saved' => 'Configuración guardada.', + 'ui.terminalserver.settings.save_failed' => 'Advertencia: no se pudo guardar la configuración.', + 'ui.terminalserver.settings.invalid_choice' => 'Opción inválida.', + 'ui.terminalserver.settings.charset_prompt' => 'Seleccione: (U)TF-8, (C)P437, (A)SCII: ', + + // --- Terminal detection wizard --- + 'ui.terminalserver.detect.title' => '=== Configuración de Terminal ===', + 'ui.terminalserver.detect.intro' => 'El BBS comprobará ahora su terminal para garantizar que el contenido se muestre correctamente.', + 'ui.terminalserver.detect.charset_intro' => 'Prueba de juego de caracteres:', + 'ui.terminalserver.detect.charset_question' => '¿Los caracteres anteriores aparecen como flechas, marcas de verificación y letras acentuadas? (S/N): ', + 'ui.terminalserver.detect.charset_utf8' => 'Juego de caracteres UTF-8 activado.', + 'ui.terminalserver.detect.charset_cp437_intro' => 'Prueba de caracteres de caja CP437:', + 'ui.terminalserver.detect.charset_cp437_question' => '¿Los caracteres anteriores aparecen como una caja dibujada con líneas y esquinas? (S/N): ', + 'ui.terminalserver.detect.charset_cp437' => 'Juego de caracteres CP437 (DOS/ANSI) activado.', + 'ui.terminalserver.detect.charset_ascii' => 'Modo ASCII activado.', + 'ui.terminalserver.detect.ansi_intro' => 'Prueba de color:', + 'ui.terminalserver.detect.ansi_question' => '¿Las palabras anteriores aparecen en diferentes colores? (S/N): ', + 'ui.terminalserver.detect.ansi_yes' => 'Color ANSI activado.', + 'ui.terminalserver.detect.ansi_no' => 'Color ANSI desactivado.', + 'ui.terminalserver.detect.complete' => 'Configuración de terminal completa. Ajustes guardados.', + 'ui.terminalserver.detect.press_enter' => 'Presione Enter para continuar...', + + // --- Door games --- + 'ui.terminalserver.doors.no_doors' => 'No hay puertas disponibles actualmente.', + 'ui.terminalserver.doors.title' => '=== Juegos de Puertas ===', + 'ui.terminalserver.doors.enter_choice' => 'Ingrese número para jugar, o Q para volver: ', + 'ui.terminalserver.doors.invalid' => 'Selección inválida.', + 'ui.terminalserver.doors.launching' => 'Iniciando {name}...', + 'ui.terminalserver.doors.launch_error' => 'Error: {error}', + 'ui.terminalserver.doors.connecting' => 'Conectando al servidor de juegos...', + 'ui.terminalserver.doors.connect_failed' => 'No se pudo conectar al puente de juegos. ¿Está ejecutándose el puente de puertas DOS?', + 'ui.terminalserver.doors.connected' => '¡Conectado! Iniciando juego...', + 'ui.terminalserver.doors.returned' => 'Regresó de {name}.', + + 'ui.terminalserver.message.headers_title' => '=== Encabezados del mensaje ===', + 'ui.terminalserver.message.no_headers' => '(Sin encabezados de mensaje)', +]; + diff --git a/config/i18n/fr/common.php b/config/i18n/fr/common.php new file mode 100644 index 000000000..82564c55e --- /dev/null +++ b/config/i18n/fr/common.php @@ -0,0 +1,2531 @@ + 'Bientôt', + 'time.in_hours' => 'Dans {count} heure{suffix}', + 'time.tomorrow' => 'Demain', + 'time.in_days' => 'Dans {count} jours', + 'time.just_now' => 'À l\'instant', + 'time.minutes_ago' => 'Il y a {count} minutes', + 'time.hours_ago' => 'Il y a {count} heure{suffix}', + 'time.yesterday' => 'Hier', + 'time.days_ago' => 'Il y a {count} jours', + 'time.suffix_plural' => 's', + 'time.suffix_singular' => '', + 'errors.failed_load_messages' => 'Échec du chargement des messages', + 'messages.none_found' => 'Aucun message trouvé', + 'messages.no_subject' => '(Sans objet)', + 'ui.common.success' => 'Succès', + 'ui.common.error' => 'Erreur', + 'ui.common.unknown_error' => 'Erreur inconnue', + 'ui.common.saving' => 'Enregistrement...', + 'ui.common.copy_failed_manual' => 'Échec de la copie dans le presse-papiers. Veuillez copier manuellement.', + 'ui.common.copy_not_supported_manual' => 'Copie dans le presse-papiers non prise en charge. Veuillez copier manuellement.', + 'ui.common.loading' => 'Chargement...', + 'ui.common.loading_messages' => 'Chargement des messages...', + 'ui.common.loading_message' => 'Chargement du message...', + 'ui.common.search' => 'Rechercher', + 'ui.common.search_messages' => 'Rechercher des messages', + 'ui.common.import' => 'Importer', + 'ui.common.search_placeholder' => 'Rechercher...', + 'ui.common.search_messages_placeholder' => 'Rechercher des messages...', + 'ui.common.all' => 'Tous', + 'ui.common.all_messages' => 'Tous les messages', + 'ui.common.unread' => 'Non lu', + 'ui.common.read' => 'Lu', + 'ui.common.sent' => 'Envoyé', + 'ui.common.drafts' => 'Brouillons', + 'ui.common.has_attachment' => 'Contient une pièce jointe', + 'ui.common.select' => 'Sélectionner', + 'ui.common.selected' => 'Sélectionné', + 'ui.common.total' => 'Total', + 'ui.common.recent' => 'Récent', + 'ui.common.statistics' => 'Statistiques', + 'ui.common.manage' => 'Gérer', + 'ui.common.mark_as_read' => 'Marquer comme lu', + 'ui.common.message' => 'Message', + 'ui.common.delete' => 'Supprimer', + 'ui.common.confirm_delete' => 'Confirmer la suppression', + 'ui.common.edit' => 'Modifier', + 'ui.common.close' => 'Fermer', + 'ui.common.refresh' => 'Actualiser', + 'ui.common.hostname' => 'Nom d\'hôte', + 'ui.common.port' => 'Port', + 'ui.common.password' => 'Mot de passe', + 'ui.common.reply' => 'Répondre', + 'ui.common.username' => 'Nom d\'utilisateur', + 'ui.common.real_name' => 'Nom réel', + 'ui.common.email' => 'E-mail', + 'ui.common.email_address' => 'Adresse e-mail', + 'ui.common.location' => 'Localisation', + 'ui.common.address' => 'Adresse', + 'ui.common.domain' => 'Domaine', + 'ui.common.ip_address' => 'Adresse IP', + 'ui.common.status' => 'Statut', + 'ui.common.from' => 'De', + 'ui.common.actions' => 'Actions', + 'ui.common.view' => 'Voir', + 'ui.common.active' => 'Actif', + 'ui.common.inactive' => 'Inactif', + 'ui.common.user' => 'Utilisateur', + 'ui.common.admin' => 'Administrateur', + 'ui.common.created' => 'Créé', + 'ui.common.last_login' => 'Dernière connexion', + 'ui.common.today' => 'Aujourd\'hui', + 'ui.common.one_day_ago' => 'Il y a 1 jour', + 'ui.common.days_ago' => 'Il y a {count} jours', + 'ui.common.enable' => 'Activer', + 'ui.common.disable' => 'Désactiver', + 'ui.common.previous' => 'Précédent', + 'ui.common.next' => 'Suivant', + 'ui.common.go_back' => 'Retour', + 'ui.common.previous_message' => 'Message précédent', + 'ui.common.next_message' => 'Message suivant', + 'ui.common.toggle_fullscreen' => 'Basculer en plein écran', + 'ui.common.cancel' => 'Annuler', + 'ui.common.clear_search' => 'Effacer la recherche', + 'ui.common.save' => 'Enregistrer', + 'ui.common.save_changes' => 'Enregistrer les modifications', + 'ui.common.saved' => 'Enregistré !', + 'ui.common.saved_short' => 'Enregistré', + 'ui.common.error_click_retry' => 'Erreur - cliquer pour réessayer', + 'ui.common.share' => 'Partager', + 'ui.common.share_url' => 'URL de partage :', + 'ui.common.copy' => 'Copier', + 'ui.common.optional' => 'optionnel', + 'ui.common.never' => 'Jamais', + 'ui.common.unknown' => 'Inconnu', + 'ui.common.not_configured' => 'Non configuré', + 'ui.common.name' => 'Nom', + 'ui.common.description' => 'Description', + 'ui.common.label' => 'Étiquette', + 'ui.common.url' => 'URL', + 'ui.common.new_tab' => 'Nouvel onglet', + 'ui.common.key' => 'Clé', + 'ui.common.id' => 'ID', + 'ui.common.preview' => 'Aperçu', + 'ui.common.yes' => 'Oui', + 'ui.common.no' => 'Non', + 'ui.common.not_applicable' => 'n/a', + 'ui.common.sort.newest_first' => 'Plus récent d\'abord', + 'ui.common.sort.oldest_first' => 'Plus ancien d\'abord', + 'ui.common.sort.by_subject' => 'Par objet', + 'ui.common.sort.by_author' => 'Par auteur', + 'ui.common.threading.show_threaded' => 'Afficher en fils', + 'ui.common.threading.show_flat' => 'Afficher à plat', + 'ui.common.from_label' => 'De :', + 'ui.common.to_label' => 'À :', + 'ui.common.date_label' => 'Date :', + 'ui.common.subject_label' => 'Objet :', + 'ui.common.subject_label_short' => 'Objet', + 'ui.common.area_label' => 'Zone :', + 'ui.common.sent_prefix' => 'Envoyé :', + 'ui.common.received_prefix' => 'Reçu :', + 'ui.common.written_prefix' => 'Écrit :', + 'ui.common.shared' => 'Partagé', + 'ui.common.remove_from_saved' => 'Retirer des enregistrés', + 'ui.common.save_for_later' => 'Enregistrer pour plus tard', + 'ui.common.delete_message' => 'Supprimer le message', + 'ui.common.delete_draft' => 'Supprimer le brouillon', + 'ui.common.already_in_address_book' => 'Déjà dans le carnet d\'adresses', + 'ui.common.save_to_address_book' => 'Enregistrer dans le carnet d\'adresses', + 'ui.common.file_attachments_with_count' => 'Pièces jointes ({count})', + 'ui.common.send_netmail_to' => 'Envoyer un netmail à {name}', + 'ui.common.replies_with_count' => '{count} réponses', + 'ui.common.unknown_size' => 'Taille inconnue', + 'ui.common.kludge_lines' => 'Lignes kludge', + 'ui.common.show_kludge_lines' => 'Afficher les lignes kludge', + 'ui.common.hide_kludge_lines' => 'Masquer les lignes kludge', + 'ui.common.no_kludge_lines_found' => 'Aucune ligne kludge trouvée', + 'ui.common.time.1_hour' => '1 heure', + 'ui.common.time.24_hours' => '24 heures', + 'ui.common.time.1_week' => '1 semaine', + 'ui.common.time.30_days' => '30 jours', + 'ui.about.title' => 'À propos', + 'ui.about.about_system' => 'À propos de {system_name}', + 'ui.about.system_name' => 'Nom du système', + 'ui.about.sysop' => 'Sysop', + 'ui.about.network_addresses' => 'Adresses réseau', + 'ui.about.fidonet_address' => 'Adresse Fidonet', + 'ui.about.software' => 'Logiciel', + 'ui.about.links' => 'Liens', + 'ui.about.house_rules' => 'Règles de la maison', + 'ui.footer.powered_by' => 'Propulsé par', + 'ui.error404.title' => 'Page introuvable', + 'ui.error404.description' => 'La page que vous recherchez n\'existe pas ou a été déplacée vers un autre emplacement.', + 'ui.error404.requested_prefix' => 'Demandé :', + 'ui.error404.dashboard' => 'Tableau de bord', + 'ui.error404.go_back' => 'Retour', + 'ui.error404.contact_sysop' => 'Si vous pensez qu\'il s\'agit d\'une erreur, veuillez contacter l\'opérateur système.', + 'ui.error.title' => 'Erreur', + 'ui.error.access_error' => 'Erreur d\'accès', + 'ui.error.processing_request_failed' => 'Une erreur s\'est produite lors du traitement de votre demande.', + 'ui.error.return_to_dashboard' => 'Retour au tableau de bord', + 'ui.web.errors.binkp_admin_only' => 'Seuls les administrateurs peuvent accéder aux fonctionnalités BinkP.', + 'ui.web.errors.chat_disabled' => 'Désolé, le chat n\'est pas activé.', + 'ui.web.errors.user_management_admin_only' => 'Seuls les administrateurs peuvent accéder à la gestion des utilisateurs.', + 'ui.web.errors.profile_user_not_found' => 'Utilisateur introuvable ou inactif.', + 'ui.web.errors.echoareas_admin_only' => 'Seuls les administrateurs peuvent gérer les zones echo.', + 'ui.web.errors.fileareas_admin_only' => 'Seuls les administrateurs peuvent gérer les zones de fichiers.', + 'ui.web.errors.files_feature_disabled' => 'Les zones de fichiers sont désactivées.', + 'ui.web.errors.polls_disabled' => 'Le bureau de vote est désactivé.', + 'ui.web.errors.shoutbox_disabled' => 'La shoutbox est désactivée.', + 'ui.web.errors.compose_type_invalid' => 'Destination de composition invalide.', + 'ui.web.fallback.system_name' => 'Système BinktermPHP', + 'ui.base.update_available' => 'Mise à jour disponible', + 'ui.base.new_version_ready' => 'Une nouvelle version est disponible', + 'ui.base.reload_now' => 'Recharger maintenant', + 'ui.base.messaging' => 'Messagerie', + 'ui.base.netmail' => 'Netmail', + 'ui.base.echomail' => 'Echomail', + 'ui.base.local_chat' => 'Chat local', + 'ui.base.mrc_chat' => 'Chat MRC', + 'ui.base.doors_games' => 'Portes & Jeux', + 'ui.base.files' => 'Fichiers', + 'ui.base.nodelist' => 'Liste des nœuds', + 'ui.base.admin' => 'Admin', + 'ui.base.profile' => 'Profil', + 'ui.base.settings' => 'Paramètres', + 'ui.base.subscriptions' => 'Abonnements', + 'ui.base.logout' => 'Déconnexion', + 'ui.base.login' => 'Connexion', + 'ui.base.guest_doors' => 'Portes invité', + 'ui.base.admin.whos_online' => 'Qui est en ligne', + 'ui.base.admin.dashboard' => 'Tableau de bord', + 'ui.base.admin.binkp_status' => 'État Binkp', + 'ui.base.admin.manage_users' => 'Gérer les utilisateurs', + 'ui.base.admin.ads' => 'Publicités', + 'ui.base.admin.area_management' => 'Gestion des zones', + 'ui.base.admin.echo_areas' => 'Zones echo', + 'ui.base.admin.file_areas' => 'Zones de fichiers', + 'ui.base.admin.file_area_rules' => 'Règles des zones de fichiers', + 'ui.base.admin.subscriptions' => 'Abonnements', + 'ui.base.admin.auto_feed' => 'Alimentation automatique', + 'ui.base.admin.chat_rooms' => 'Salons de chat', + 'ui.base.admin.mrc_settings' => 'Paramètres MRC', + 'ui.base.admin.polls' => 'Sondages', + 'ui.base.admin.shoutbox' => 'Shoutbox', + 'ui.base.admin.doors' => 'Portes', + 'ui.base.admin.doors_dos' => 'Portes DOS', + 'ui.base.admin.doors_native' => 'Portes natives', + 'ui.base.admin.doors_web' => 'Portes web', + 'ui.base.admin.activity_stats' => 'Statistiques d\'activité', + 'ui.base.admin.economy_viewer' => 'Visualiseur d\'économie', + 'ui.base.admin.bbs_settings' => 'Paramètres BBS', + 'ui.base.admin.appearance' => 'Apparence', + 'ui.base.admin.binkp_configuration' => 'Configuration Binkp', + 'ui.base.admin.template_editor' => 'Éditeur de modèles', + 'ui.base.admin.i18n_overrides' => 'Substitutions de langue', + 'ui.base.admin.help' => 'Aide', + 'ui.base.admin.readme' => 'README', + 'ui.base.admin.faq' => 'FAQ', + 'ui.base.admin.upgrade_notes' => 'Notes de mise à jour v{version}', + 'ui.base.admin.claudes_bbs' => 'BBS de Claude', + 'ui.base.admin.report_issue' => 'Signaler un problème', + 'ui.admin_users.pending_users_error' => 'Erreur utilisateurs en attente :', + 'ui.admin_users.pending_users_load_failed_prefix' => 'Échec du chargement des utilisateurs en attente : ', + 'ui.admin_users.all_users_error' => 'Erreur tous les utilisateurs :', + 'ui.admin_users.users_load_failed_prefix' => 'Échec du chargement des utilisateurs : ', + 'ui.admin_users.processing' => 'Traitement en cours...', + 'ui.admin_users.user_approved_success' => 'Utilisateur approuvé avec succès !', + 'ui.admin_users.user_rejected_success' => 'Utilisateur rejeté avec succès !', + 'ui.admin_users.user_action_failed_prefix' => 'Échec de l\'action {action} sur l\'utilisateur : ', + 'ui.admin_users.user_action_failed_short' => 'Échec de l\'action {action} sur l\'utilisateur', + 'ui.admin_users.success_prefix' => 'Succès : ', + 'ui.admin_users.error_prefix' => 'Erreur : ', + 'ui.admin_users.user_data_load_failed_prefix' => 'Échec du chargement des données utilisateur : ', + 'ui.admin_users.password_min_length' => 'Le mot de passe doit comporter au moins 8 caractères', + 'ui.admin_users.user_updated_success' => 'Utilisateur mis à jour avec succès !', + 'ui.admin_users.user_update_failed_prefix' => 'Échec de la mise à jour de l\'utilisateur : ', + 'ui.admin_users.user_toggle_confirm' => 'Êtes-vous sûr de vouloir {action} cet utilisateur ?', + 'ui.admin_users.user_toggled_success' => 'Utilisateur {action} avec succès !', + 'ui.admin_users.required_fields' => 'Le nom d\'utilisateur, le nom réel et le mot de passe sont obligatoires', + 'ui.admin_users.username_validation' => 'Le nom d\'utilisateur doit comporter 3 à 20 caractères, lettres, chiffres et underscores uniquement', + 'ui.admin_users.creating' => 'Création en cours...', + 'ui.admin_users.user_created_success' => 'Utilisateur créé avec succès !', + 'ui.admin_users.user_create_failed_prefix' => 'Échec de la création de l\'utilisateur : ', + 'ui.admin_users.user_create_failed' => 'Échec de la création de l\'utilisateur', + 'ui.admin_users.cleanup_confirm' => 'Cela supprimera tous les enregistrements d\'inscription approuvés et les anciennes inscriptions rejetées (30+ jours). Êtes-vous sûr ?', + 'ui.admin_users.cleaning' => 'Nettoyage en cours...', + 'ui.admin_users.cleanup_success' => 'Nettoyage terminé ! {approved} inscriptions approuvées et {rejected} anciennes inscriptions rejetées supprimées ({total} au total).', + 'ui.admin_users.cleanup_failed_prefix' => 'Échec du nettoyage des inscriptions : ', + 'ui.admin_users.send_reminder_confirm' => 'Envoyer un rappel de compte à {username} ? Un message de rappel sera envoyé par netmail et par e-mail (si disponible).', + 'ui.admin_users.reminder_sent_netmail_email_suffix' => ' (envoyé par netmail et e-mail)', + 'ui.admin_users.reminder_sent_netmail_only_suffix' => ' (envoyé par netmail uniquement)', + 'ui.admin_users.send_reminder_failed_prefix' => 'Échec de l\'envoi du rappel : ', + 'ui.admin_users.send_reminder_failed' => 'Échec de l\'envoi du rappel', + 'ui.admin_users.no_reminders_needed' => 'Aucun utilisateur n\'a besoin de rappel de compte pour le moment. Tous les utilisateurs se sont connectés !', + 'ui.admin_users.reminders_found_header' => '{count} utilisateur(s) ne s\'est/se sont pas encore connecté(s) : + +', + 'ui.admin_users.reminder_user_line' => '- {username} ({real_name}) - créé le {created_date} +', + 'ui.admin_users.no_real_name' => 'Aucun nom réel', + 'ui.admin_users.reminders_found_footer' => ' +Vous pouvez envoyer des rappels individuels en utilisant les boutons « Rappeler » dans la liste des utilisateurs ci-dessous.', + 'ui.admin_users.reminders_load_failed_prefix' => 'Échec du chargement des utilisateurs nécessitant des rappels : ', + 'ui.admin_users.page_title' => 'Gestion des utilisateurs', + 'ui.admin_users.heading' => 'Gestion des utilisateurs', + 'ui.admin_users.create_new_user' => 'Créer un nouvel utilisateur', + 'ui.admin_users.need_reminders' => 'Rappels nécessaires', + 'ui.admin_users.cleanup' => 'Nettoyage', + 'ui.admin_users.pending_registrations' => 'Inscriptions en attente', + 'ui.admin_users.loading_pending_registrations' => 'Chargement des inscriptions en attente...', + 'ui.admin_users.all_users' => 'Tous les utilisateurs', + 'ui.admin_users.search_users_placeholder' => 'Rechercher des utilisateurs...', + 'ui.admin_users.per_page_25' => '25 par page', + 'ui.admin_users.per_page_50' => '50 par page', + 'ui.admin_users.per_page_100' => '100 par page', + 'ui.admin_users.loading_users' => 'Chargement des utilisateurs...', + 'ui.admin_users.approve_user' => 'Approuver l\'utilisateur', + 'ui.admin_users.admin_notes' => 'Notes administrateur', + 'ui.admin_users.optional_notes_placeholder' => 'Notes facultatives sur cette décision...', + 'ui.admin_users.approve' => 'Approuver', + 'ui.admin_users.reject' => 'Rejeter', + 'ui.admin_users.create_user' => 'Créer un utilisateur', + 'ui.admin_users.edit_user' => 'Modifier l\'utilisateur', + 'ui.admin_users.account_is_active' => 'Le compte est actif', + 'ui.admin_users.inactive_cannot_login' => 'Les utilisateurs inactifs ne peuvent pas se connecter', + 'ui.admin_users.admin_privileges' => 'Privilèges administrateur', + 'ui.admin_users.admin_privileges_help' => 'Les administrateurs peuvent gérer les utilisateurs et les paramètres système', + 'ui.admin_users.email_optional_help' => 'Facultatif - pour la récupération de compte et les notifications', + 'ui.admin_users.min_8_characters' => 'Minimum 8 caractères', + 'ui.admin_users.username_cannot_change' => 'Le nom d\'utilisateur ne peut pas être modifié', + 'ui.admin_users.real_name_cannot_change' => 'Le nom réel ne peut pas être modifié', + 'ui.admin_users.new_password' => 'Nouveau mot de passe', + 'ui.admin_users.new_password_help' => 'Laisser vide pour conserver le mot de passe actuel. Minimum 8 caractères si modification.', + 'ui.admin_users.no_pending_registrations' => 'Aucune inscription en attente', + 'ui.admin_users.requested' => 'Demandé', + 'ui.admin_users.last_reminded' => 'Dernier rappel', + 'ui.admin_users.not_provided' => 'Non fourni', + 'ui.admin_users.view_profile' => 'Voir le profil', + 'ui.admin_users.send_account_reminder' => 'Envoyer un rappel de compte', + 'ui.admin_users.remind' => 'Rappeler', + 'ui.admin_users.referred_by' => 'Référé par :', + 'ui.admin_users.basic_information' => 'Informations de base', + 'ui.admin_users.request_details' => 'Détails de la demande', + 'ui.admin_users.reason_for_joining' => 'Raison de l\'inscription :', + 'ui.admin_users.no_reason_provided' => 'Aucune raison fournie', + 'ui.admin_users.user_agent' => 'Agent utilisateur :', + 'ui.admin_users.not_available' => 'Non disponible', + 'ui.admin_users.user_details' => 'Détails de l\'utilisateur', + 'ui.admin_users.approve_user_registration' => 'Approuver l\'inscription de l\'utilisateur', + 'ui.admin_users.reject_user_registration' => 'Rejeter l\'inscription de l\'utilisateur', + 'ui.admin_users.users_pagination_aria' => 'Pagination des utilisateurs', + 'ui.admin_users.showing_users_range' => 'Affichage de {start}-{end} sur {total} utilisateurs', + 'ui.admin.appearance.load_failed_prefix' => 'Échec du chargement de la configuration d\'apparence : ', + 'ui.admin.appearance.save_failed' => 'Échec de l\'enregistrement', + 'ui.admin.appearance.reset_menu_confirm' => 'Réinitialiser les éléments du menu par défaut ?', + 'ui.admin.appearance.shell_saved_reload' => 'Enregistré ! Rechargez la page pour voir le nouveau shell.', + 'ui.admin.appearance.upload_failed_prefix' => 'Échec du téléversement : ', + 'ui.admin.appearance.delete_shell_art_confirm' => 'Supprimer {name} ?', + 'ui.admin.appearance.delete_failed_prefix' => 'Échec de la suppression : ', + 'ui.admin.appearance.error_prefix' => 'Erreur : ', + 'ui.admin.appearance.request_failed' => 'Échec de la requête', + 'ui.admin.appearance.page_title' => 'Apparence & Contenu - Admin', + 'ui.admin.appearance.heading' => 'Apparence & Contenu', + 'ui.admin.appearance.tab_branding' => 'Identité visuelle', + 'ui.admin.appearance.tab_content' => 'Contenu', + 'ui.admin.appearance.tab_navigation' => 'Navigation', + 'ui.admin.appearance.tab_seo' => 'SEO & Public', + 'ui.admin.appearance.tab_shell' => 'Shell', + 'ui.admin.appearance.tab_message_reader' => 'Lecteur de messages', + 'ui.admin.appearance.invalid_hex_color' => 'Couleur hexadécimale invalide', + 'ui.admin.appearance.no_content' => 'Aucun contenu', + 'ui.admin.appearance.preview_failed' => 'Échec de l\'aperçu', + 'ui.admin.appearance.branding.title' => 'Identité visuelle', + 'ui.admin.appearance.branding.accent_color' => 'Couleur d\'accentuation', + 'ui.admin.appearance.branding.pick_accent_color' => 'Choisir une couleur d\'accentuation', + 'ui.admin.appearance.branding.reset' => 'Réinitialiser', + 'ui.admin.appearance.branding.accent_help' => 'Remplace la couleur principale de la navigation et des boutons sur tout le site. Laisser vide pour la valeur par défaut.', + 'ui.admin.appearance.branding.default_theme' => 'Thème par défaut', + 'ui.admin.appearance.branding.system_default' => '(Défaut système)', + 'ui.admin.appearance.branding.default_theme_help' => 'Thème appliqué à tous les utilisateurs par défaut.', + 'ui.admin.appearance.branding.lock_theme' => 'Verrouiller le thème (empêcher les utilisateurs de le modifier)', + 'ui.admin.appearance.branding.logo_url' => 'URL du logo', + 'ui.admin.appearance.branding.logo_url_help' => 'URL d\'une image de logo affichée dans la barre de navigation. Laisser vide pour utiliser le nom du système en texte.', + 'ui.admin.appearance.branding.footer_text' => 'Texte du pied de page', + 'ui.admin.appearance.branding.footer_placeholder' => 'Laisser vide pour la ligne nœud/sysop par défaut', + 'ui.admin.appearance.branding.footer_help' => 'Texte personnalisé pour le pied de page. Laisser vide pour la ligne d\'informations système par défaut.', + 'ui.admin.appearance.branding.save' => 'Enregistrer l\'identité visuelle', + 'ui.admin.appearance.content.system_news_title' => 'Actualités système (MOTD)', + 'ui.admin.appearance.content.system_news_placeholder' => 'Saisir les actualités système en format Markdown...', + 'ui.admin.appearance.content.system_news_help' => 'Prend en charge Markdown. Affiché sur le tableau de bord. Laisser vide pour afficher le systemnews.twig par défaut ou personnalisé.', + 'ui.admin.appearance.content.house_rules_title' => 'Règles de la maison', + 'ui.admin.appearance.content.house_rules_placeholder' => 'Saisir les règles de la maison en format Markdown...', + 'ui.admin.appearance.content.house_rules_help' => 'Prend en charge Markdown. Affiché dans la fenêtre modale des règles de la maison accessible depuis le pied de page.', + 'ui.admin.appearance.content.site_announcement_title' => 'Annonce du site', + 'ui.admin.appearance.content.show_announcement_banner' => 'Afficher la bannière d\'annonce', + 'ui.admin.appearance.content.announcement_text' => 'Texte de l\'annonce', + 'ui.admin.appearance.content.type' => 'Type', + 'ui.admin.appearance.content.type_info' => 'Info (bleu)', + 'ui.admin.appearance.content.type_success' => 'Succès (vert)', + 'ui.admin.appearance.content.type_warning' => 'Avertissement (jaune)', + 'ui.admin.appearance.content.type_danger' => 'Danger (rouge)', + 'ui.admin.appearance.content.type_primary' => 'Principal', + 'ui.admin.appearance.content.expires_at_optional' => 'Expire le (optionnel)', + 'ui.admin.appearance.content.expires_help' => 'Laisser vide pour ne jamais expirer.', + 'ui.admin.appearance.content.dismissible' => 'Masquable', + 'ui.admin.appearance.content.save' => 'Enregistrer le contenu', + 'ui.admin.appearance.navigation.title' => 'Liens de navigation personnalisés', + 'ui.admin.appearance.navigation.add_link' => 'Ajouter un lien', + 'ui.admin.appearance.navigation.help' => 'Jusqu\'à 3 liens apparaissent directement dans la barre de navigation ; 4 ou plus sont regroupés dans un menu déroulant « Liens ».', + 'ui.admin.appearance.navigation.url_placeholder' => '/page ou https://...', + 'ui.admin.appearance.navigation.save' => 'Enregistrer la navigation', + 'ui.admin.appearance.seo.title' => 'SEO & Pages publiques', + 'ui.admin.appearance.seo.enable_about_page' => 'Activer la page publique /about', + 'ui.admin.appearance.seo.about_page_help' => 'Lorsqu\'elle est activée, une page /about accessible publiquement affiche les informations de votre système et les règles de la maison. Désactivée, elle renvoie une erreur 404.', + 'ui.admin.appearance.seo.site_description' => 'Description du site', + 'ui.admin.appearance.seo.max_300_chars' => '(max 300 caractères)', + 'ui.admin.appearance.seo.description_help' => 'Utilisée dans la méta-description et les balises OG pour les moteurs de recherche et les aperçus de liens.', + 'ui.admin.appearance.seo.og_image_url' => 'URL de l\'image OG', + 'ui.admin.appearance.seo.og_image_placeholder' => 'https://example.com/preview.png', + 'ui.admin.appearance.seo.og_image_help' => 'Image affichée lorsque votre site est partagé sur les réseaux sociaux. Doit faire au moins 1200x630 px.', + 'ui.admin.appearance.seo.save' => 'Enregistrer les paramètres SEO', + 'ui.admin.appearance.shell.title' => 'Shell de l\'interface', + 'ui.admin.appearance.shell.default_interface_style' => 'Style d\'interface par défaut', + 'ui.admin.appearance.shell.web_interface' => 'Interface web', + 'ui.admin.appearance.shell.web_interface_help' => 'Barre de navigation complète, menus déroulants, mise en page web standard', + 'ui.admin.appearance.shell.bbs_menu' => 'Menu BBS', + 'ui.admin.appearance.shell.bbs_menu_help' => 'En-tête minimal, navigation par touches de raccourci, menu carte/texte/ANSI', + 'ui.admin.appearance.shell.lock_shell' => 'Verrouiller le shell (empêcher les utilisateurs de le modifier dans leurs paramètres)', + 'ui.admin.appearance.shell.bbs_menu_config' => 'Configuration du menu BBS', + 'ui.admin.appearance.shell.menu_variant' => 'Variante du menu', + 'ui.admin.appearance.shell.variant_cards' => 'Grille de cartes', + 'ui.admin.appearance.shell.variant_text' => 'Texte terminal', + 'ui.admin.appearance.shell.variant_ansi' => 'Art ANSI', + 'ui.admin.appearance.shell.ansi_art_file' => 'Fichier d\'art ANSI', + 'ui.admin.appearance.shell.none_selected' => '(aucun sélectionné)', + 'ui.admin.appearance.shell.ansi_file_help' => 'Sélectionnez un fichier .ans téléversé à afficher au-dessus du menu.', + 'ui.admin.appearance.shell.upload_ans' => 'Téléverser .ans', + 'ui.admin.appearance.shell.max_512_kb' => 'Max 512 Ko.', + 'ui.admin.appearance.shell.delete_selected' => 'Supprimer la sélection', + 'ui.admin.appearance.shell.menu_items' => 'Éléments du menu', + 'ui.admin.appearance.shell.menu_items_help' => 'Définissez les touches de raccourci et les destinations pour le menu principal.', + 'ui.admin.appearance.shell.icon' => 'Icône', + 'ui.admin.appearance.shell.icon_help' => '(nom Font Awesome)', + 'ui.admin.appearance.shell.icon_placeholder' => 'envelope', + 'ui.admin.appearance.shell.add_item' => 'Ajouter un élément', + 'ui.admin.appearance.shell.reset_to_defaults' => 'Réinitialiser par défaut', + 'ui.admin.appearance.shell.save' => 'Enregistrer les paramètres du shell', + 'ui.admin.appearance.shell.could_not_load_file_list' => 'Impossible de charger la liste des fichiers.', + 'ui.admin.appearance.shell.uploading' => 'Téléversement en cours...', + 'ui.admin.appearance.shell.uploaded_prefix' => 'Téléversé : ', + 'ui.admin.appearance.shell.deleted_prefix' => 'Supprimé : ', + 'ui.admin.appearance.default_menu.messages' => 'Messages', + 'ui.admin.appearance.default_menu.netmail' => 'Netmail', + 'ui.admin.appearance.default_menu.files' => 'Fichiers', + 'ui.admin.appearance.default_menu.games_doors' => 'Jeux & Portes', + 'ui.admin.appearance.default_menu.settings' => 'Paramètres', + 'ui.admin.appearance.message_reader.title' => 'Lecteur de messages', + 'ui.admin.appearance.message_reader.layout' => 'Mise en page', + 'ui.admin.appearance.message_reader.scrollable_body' => 'Corps du message défilant', + 'ui.admin.appearance.message_reader.scrollable_body_help' => 'Lorsqu\'il est activé, l\'en-tête du message (De, À, Objet, Date) reste fixé en haut tandis que le corps du message défile indépendamment. Lorsqu\'il est désactivé, toute la fenêtre modale défile ensemble (comportement par défaut).', + 'ui.admin.appearance.message_reader.save' => 'Enregistrer les paramètres du lecteur de messages', + 'ui.admin.binkp_config.load_failed' => 'Échec du chargement de la configuration', + 'ui.admin.binkp_config.save_failed' => 'Échec de l\'enregistrement de la configuration', + 'ui.admin.binkp_config.reload_confirm' => 'Recharger la configuration du démon binkp ? Cela enverra un signal SIGHUP au démon.', + 'ui.admin.binkp_config.reload_failed' => 'Échec du rechargement de la configuration', + 'ui.admin.binkp_config.remove_uplink_confirm' => 'Supprimer ce lien montant ?', + 'ui.admin.binkp_config.page_title' => 'Configuration Binkp', + 'ui.admin.binkp_config.heading' => 'Configuration Binkp', + 'ui.admin.binkp_config.restart_notice' => 'Après l\'enregistrement, redémarrez les démons pour appliquer les modifications.', + 'ui.admin.binkp_config.save_configuration' => 'Enregistrer la configuration', + 'ui.admin.binkp_config.reload_config' => 'Recharger la configuration', + 'ui.admin.binkp_config.reload_title' => 'Recharger la configuration sans redémarrer le démon', + 'ui.admin.binkp_config.config_not_loaded' => 'Configuration non chargée.', + 'ui.admin.binkp_config.configuration_saved' => 'Configuration enregistrée.', + 'ui.admin.binkp_config.reloading' => 'Rechargement de la configuration...', + 'ui.admin.binkp_config.reloaded_success' => 'Configuration rechargée avec succès. Le démon a pris en compte les modifications.', + 'ui.admin.binkp_config.current' => 'actuel', + 'ui.admin.binkp_config.system.title' => 'Système', + 'ui.admin.binkp_config.system.system_name' => 'Nom du système', + 'ui.admin.binkp_config.system.system_address' => 'Adresse du système', + 'ui.admin.binkp_config.system.sysop_name' => 'Nom du sysop', + 'ui.admin.binkp_config.system.select_admin_user' => 'Sélectionner un utilisateur administrateur.', + 'ui.admin.binkp_config.system.location' => 'Emplacement', + 'ui.admin.binkp_config.system.timezone' => 'Fuseau horaire', + 'ui.admin.binkp_config.system.origin_line' => 'Ligne d\'origine', + 'ui.admin.binkp_config.binkp.title' => 'Binkp', + 'ui.admin.binkp_config.binkp.timeout_seconds' => 'Délai d\'expiration (secondes)', + 'ui.admin.binkp_config.binkp.max_connections' => 'Connexions maximales', + 'ui.admin.binkp_config.binkp.bind_address' => 'Adresse de liaison', + 'ui.admin.binkp_config.binkp.preserve_processed_packets' => 'Conserver les paquets traités', + 'ui.admin.binkp_config.security.title' => 'Sécurité', + 'ui.admin.binkp_config.security.allow_insecure_inbound' => 'Autoriser les connexions entrantes non sécurisées', + 'ui.admin.binkp_config.security.insecure_receive_only' => 'Réception uniquement en mode non sécurisé', + 'ui.admin.binkp_config.security.require_allowlist_for_insecure' => 'Exiger la liste d\'autorisation pour les connexions non sécurisées', + 'ui.admin.binkp_config.security.max_insecure_sessions_per_hour' => 'Sessions non sécurisées max / heure', + 'ui.admin.binkp_config.security.allow_plaintext_fallback' => 'Autoriser le repli en texte clair', + 'ui.admin.binkp_config.crashmail.title' => 'Crashmail', + 'ui.admin.binkp_config.crashmail.enable_crashmail' => 'Activer Crashmail', + 'ui.admin.binkp_config.crashmail.max_attempts' => 'Tentatives max', + 'ui.admin.binkp_config.crashmail.retry_interval_minutes' => 'Intervalle de réessai (minutes)', + 'ui.admin.binkp_config.crashmail.use_nodelist_for_routing' => 'Utiliser la Nodelist pour le routage', + 'ui.admin.binkp_config.crashmail.fallback_port' => 'Port de repli', + 'ui.admin.binkp_config.crashmail.allow_insecure_crash_delivery' => 'Autoriser la livraison Crash non sécurisée', + 'ui.admin.binkp_config.uplinks.title' => 'Uplinks', + 'ui.admin.binkp_config.uplinks.add_uplink' => 'Ajouter un uplink', + 'ui.admin.binkp_config.uplinks.edit_uplink' => 'Modifier l\'uplink', + 'ui.admin.binkp_config.uplinks.none_configured' => 'Aucun uplink configuré.', + 'ui.admin.binkp_config.uplinks.real_name' => 'Nom réel', + 'ui.admin.binkp_config.uplinks.username' => 'Nom d\'utilisateur', + 'ui.admin.binkp_config.uplinks.table.me' => 'Moi', + 'ui.admin.binkp_config.uplinks.table.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.table.domain' => 'Domaine', + 'ui.admin.binkp_config.uplinks.table.schedule' => 'Planification', + 'ui.admin.binkp_config.uplinks.table.markdown' => 'Markdown', + 'ui.admin.binkp_config.uplinks.table.posting_name' => 'Nom d\'affichage', + 'ui.admin.binkp_config.uplinks.table.adr_domain' => 'ADR @Domaine', + 'ui.admin.binkp_config.uplinks.table.enabled' => 'Activé', + 'ui.admin.binkp_config.uplinks.table.default' => 'Défaut', + 'ui.admin.binkp_config.uplinks.table.actions' => 'Actions', + 'ui.admin.binkp_config.uplinks.modal.uplink' => 'Uplink', + 'ui.admin.binkp_config.uplinks.modal.me_address' => 'Mon adresse', + 'ui.admin.binkp_config.uplinks.modal.uplink_address' => 'Adresse de l\'uplink', + 'ui.admin.binkp_config.uplinks.modal.domain' => 'Domaine', + 'ui.admin.binkp_config.uplinks.modal.binkp_session_password' => 'Mot de passe de session BinkP', + 'ui.admin.binkp_config.uplinks.modal.packet_password' => 'Mot de passe du paquet', + 'ui.admin.binkp_config.uplinks.modal.packet_password_help' => 'Mot de passe d\'en-tête FTN .pkt (8 caractères max)', + 'ui.admin.binkp_config.uplinks.modal.tic_password' => 'Mot de passe TIC', + 'ui.admin.binkp_config.uplinks.modal.tic_password_help' => 'Mot de passe TIC par défaut ; remplacé par zone de fichiers', + 'ui.admin.binkp_config.uplinks.modal.poll_schedule' => 'Planification des sondages', + 'ui.admin.binkp_config.uplinks.modal.networks_one_per_line' => 'Réseaux (un par ligne)', + 'ui.admin.binkp_config.uplinks.modal.allow_markup' => 'Autoriser le balisage', + 'ui.admin.binkp_config.uplinks.modal.allow_markup_help' => 'Active la prise en charge du balisage (Markdown, StyleCodes, etc.) sur les réseaux compatibles', + 'ui.admin.binkp_config.uplinks.modal.posting_name_policy' => 'Politique de nom d\'affichage', + 'ui.admin.binkp_config.uplinks.modal.send_domain' => 'Envoyer @Domaine dans ADR', + 'ui.admin.binkp_config.uplinks.modal.send_domain_help' => 'Inclure la partie @Domaine lors de l\'envoi de l\'adresse ADR à cet uplink', + 'ui.admin.binkp_config.uplinks.modal.enabled_help' => 'Active cet uplink pour le routage normal, les sondages et la livraison sortante', + 'ui.admin.binkp_config.uplinks.modal.compression' => 'Compression', + 'ui.admin.binkp_config.uplinks.modal.compression_help' => 'Demande des transferts compressés lorsque le système distant le prend en charge', + 'ui.admin.binkp_config.uplinks.modal.crypt' => 'Chiffrement', + 'ui.admin.binkp_config.uplinks.modal.crypt_help' => 'Utilise l\'authentification de style CRAM-MD5 pour cet uplink lorsque pris en charge', + 'ui.admin.binkp_config.uplinks.modal.default_help' => 'Marque cet uplink comme uplink par défaut lorsqu\'aucune route plus spécifique ne correspond', + 'ui.admin.binkp_config.uplinks.modal.save_uplink' => 'Enregistrer l\'uplink', + 'ui.admin.binkp_config.validation.system_required' => 'Le nom du système, l\'adresse et le sysop sont requis.', + 'ui.admin.binkp_config.validation.port_range' => 'Le port Binkp doit être compris entre 1 et 65535.', + 'ui.admin.binkp_config.validation.timeout_positive' => 'Le délai d\'expiration Binkp doit être un nombre positif.', + 'ui.admin.binkp_config.validation.max_connections_positive' => 'Le nombre maximum de connexions doit être un nombre positif.', + 'ui.admin.binkp_config.validation.uplink_hostname_required' => 'Le nom d\'hôte de l\'uplink est requis.', + 'ui.admin.binkp_config.validation.uplink_port_range' => 'Le port de l\'uplink doit être compris entre 1 et 65535.', + 'ui.admin.ads.load_failed_with_status' => 'Échec du chargement des annonces ({status})', + 'ui.admin.ads.load_failed' => 'Échec du chargement des annonces', + 'ui.admin.ads.select_file_to_upload' => 'Sélectionnez un fichier ANSI à téléverser.', + 'ui.admin.ads.upload_failed_with_status' => 'Échec du téléversement ({status})', + 'ui.admin.ads.uploaded' => 'Annonce téléversée.', + 'ui.admin.ads.upload_failed' => 'Échec du téléversement', + 'ui.admin.ads.delete_confirm' => 'Supprimer {name} ?', + 'ui.admin.ads.delete_failed_with_status' => 'Échec de la suppression ({status})', + 'ui.admin.ads.deleted' => 'Annonce supprimée.', + 'ui.admin.ads.delete_failed' => 'Échec de la suppression', + 'ui.admin.ads.unexpected_response' => 'Réponse inattendue ({status})', + 'ui.admin.ads.page_title' => 'Annonces', + 'ui.admin.ads.heading' => 'Annonces', + 'ui.admin.ads.info_text_prefix' => 'Téléversez des annonces ANSI (`.ans`) dans le répertoire', + 'ui.admin.ads.info_text_suffix' => 'Les annonces sont affichées aléatoirement sur le tableau de bord.', + 'ui.admin.ads.upload_new' => 'Téléverser une nouvelle annonce', + 'ui.admin.ads.ansi_file' => 'Fichier ANSI (.ans)', + 'ui.admin.ads.save_as_optional' => 'Enregistrer sous (optionnel)', + 'ui.admin.ads.save_as_placeholder' => 'retro-sale.ans', + 'ui.admin.ads.save_as_help' => 'Lettres, chiffres, point, tiret, underscore uniquement.', + 'ui.admin.ads.upload' => 'Téléverser', + 'ui.admin.ads.current_advertisements' => 'Annonces actuelles', + 'ui.admin.ads.size' => 'Taille', + 'ui.admin.ads.updated' => 'Mis à jour', + 'ui.admin.ads.actions' => 'Actions', + 'ui.admin.ads.loading_ads' => 'Chargement des annonces...', + 'ui.admin.ads.none_uploaded' => 'Aucune annonce téléversée.', + 'ui.admin.ads.view' => 'Voir', + 'ui.admin.dashboard.page_title' => 'Tableau de bord admin', + 'ui.admin.dashboard.heading' => 'Tableau de bord admin', + 'ui.admin.dashboard.total_users' => 'Total utilisateurs', + 'ui.admin.dashboard.active_users' => 'Utilisateurs actifs', + 'ui.admin.dashboard.total_netmail' => 'Total Netmail', + 'ui.admin.dashboard.total_echomail' => 'Total Echomail', + 'ui.admin.dashboard.quick_actions' => 'Actions rapides', + 'ui.admin.dashboard.system_information' => 'Informations système', + 'ui.admin.dashboard.admin_users' => 'Utilisateurs admin :', + 'ui.admin.dashboard.active_sessions' => 'Sessions actives :', + 'ui.admin.dashboard.system_address' => 'Adresse du système :', + 'ui.admin.dashboard.version' => 'Version :', + 'ui.admin.dashboard.git_branch_commit' => 'Branche Git / Commit :', + 'ui.admin.dashboard.database_version' => 'Version de la base de données :', + 'ui.admin.dashboard.service_status' => 'État des services', + 'ui.admin.dashboard.service.admin_daemon' => 'Démon admin', + 'ui.admin.dashboard.service.binkp_scheduler' => 'Planificateur Binkp', + 'ui.admin.dashboard.service.binkp_server' => 'Serveur Binkp', + 'ui.admin.dashboard.service.telnetd' => 'Serveur Telnet', + 'ui.admin.dashboard.running' => 'En cours', + 'ui.admin.dashboard.stopped' => 'Arrêté', + 'ui.admin.dashboard.pid' => 'PID', + 'ui.admin.users.load_failed' => 'Erreur lors du chargement des utilisateurs', + 'ui.admin.users.load_details_failed' => 'Erreur lors du chargement des détails de l\'utilisateur', + 'ui.admin.users.updated_success' => 'Utilisateur mis à jour avec succès', + 'ui.admin.users.created_success' => 'Utilisateur créé avec succès', + 'ui.admin.users.save_failed' => 'Erreur lors de l\'enregistrement de l\'utilisateur', + 'ui.admin.users.delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.', + 'ui.admin.users.deleted_success' => 'Utilisateur supprimé avec succès', + 'ui.admin.users.delete_failed' => 'Erreur lors de la suppression de l\'utilisateur', + 'ui.admin.users.page_title' => 'Gestion des utilisateurs', + 'ui.admin.users.heading' => 'Gestion des utilisateurs', + 'ui.admin.users.add_new_user' => 'Ajouter un nouvel utilisateur', + 'ui.admin.users.search_users_placeholder' => 'Rechercher des utilisateurs...', + 'ui.admin.users.active' => 'Actif', + 'ui.admin.users.inactive' => 'Inactif', + 'ui.admin.users.admins' => 'Admins', + 'ui.admin.users.username' => 'Nom d\'utilisateur', + 'ui.admin.users.real_name' => 'Nom réel', + 'ui.admin.users.fidonet_address' => 'Adresse Fidonet', + 'ui.admin.users.status' => 'Statut', + 'ui.admin.users.admin' => 'Admin', + 'ui.admin.users.created' => 'Créé', + 'ui.admin.users.actions' => 'Actions', + 'ui.admin.users.password_help' => 'Laisser vide lors de la modification pour conserver le mot de passe actuel', + 'ui.admin.users.fidonet_address_placeholder' => '1:123/456.789', + 'ui.admin.users.active_user' => 'Utilisateur actif', + 'ui.admin.users.administrator' => 'Administrateur', + 'ui.admin.users.save_user' => 'Enregistrer l\'utilisateur', + 'ui.admin.users.user_details' => 'Détails de l\'utilisateur', + 'ui.admin.users.view_details' => 'Voir les détails', + 'ui.admin.users.view_profile' => 'Voir le profil', + 'ui.admin.users.pagination_aria' => 'Pagination des utilisateurs', + 'ui.admin.users.previous' => 'Précédent', + 'ui.admin.users.next' => 'Suivant', + 'ui.admin.users.edit_user' => 'Modifier l\'utilisateur', + 'ui.admin.users.user_information' => 'Informations de l\'utilisateur', + 'ui.admin.users.last_login' => 'Dernière connexion', + 'ui.admin.users.statistics' => 'Statistiques', + 'ui.admin.users.netmail_received' => 'Netmail reçu', + 'ui.admin.users.netmail_sent' => 'Netmail envoyé', + 'ui.admin.users.echomail_posted' => 'Echomail publié', + 'ui.admin.users.active_sessions' => 'Sessions actives', + 'ui.admin.chat_rooms.load_failed' => 'Erreur lors du chargement des salons de discussion', + 'ui.admin.chat_rooms.not_found' => 'Salon de discussion introuvable', + 'ui.admin.chat_rooms.load_single_failed' => 'Erreur lors du chargement du salon de discussion', + 'ui.admin.chat_rooms.updated_success' => 'Salon mis à jour avec succès', + 'ui.admin.chat_rooms.created_success' => 'Salon créé avec succès', + 'ui.admin.chat_rooms.save_failed' => 'Erreur lors de l\'enregistrement du salon', + 'ui.admin.chat_rooms.delete_confirm' => 'Supprimer ce salon de discussion ? Cette action est irréversible.', + 'ui.admin.chat_rooms.deleted_success' => 'Salon supprimé avec succès', + 'ui.admin.chat_rooms.delete_failed' => 'Erreur lors de la suppression du salon', + 'ui.admin.chat_rooms.page_title' => 'Salons de discussion', + 'ui.admin.chat_rooms.heading' => 'Salons de discussion', + 'ui.admin.chat_rooms.add_room' => 'Ajouter un salon', + 'ui.admin.chat_rooms.status' => 'Statut', + 'ui.admin.chat_rooms.created' => 'Créé', + 'ui.admin.chat_rooms.actions' => 'Actions', + 'ui.admin.chat_rooms.add_chat_room' => 'Ajouter un salon de discussion', + 'ui.admin.chat_rooms.edit_chat_room' => 'Modifier le salon de discussion', + 'ui.admin.chat_rooms.active_room' => 'Salon actif', + 'ui.admin.chat_rooms.save_room' => 'Enregistrer le salon', + 'ui.admin.chat_rooms.no_description' => '-', + 'ui.admin.chat_rooms.active' => 'Actif', + 'ui.admin.chat_rooms.inactive' => 'Inactif', + 'ui.admin.binkp_sessions.page_title' => 'Sessions Binkp - Admin', + 'ui.admin.binkp_sessions.heading' => 'Journal des sessions Binkp', + 'ui.admin.binkp_sessions.back_to_dashboard' => 'Retour au tableau de bord', + 'ui.admin.binkp_sessions.total_24h' => 'Total (24h)', + 'ui.admin.binkp_sessions.secure' => 'Sécurisé', + 'ui.admin.binkp_sessions.insecure' => 'Non sécurisé', + 'ui.admin.binkp_sessions.crash_out' => 'Crash sortant', + 'ui.admin.binkp_sessions.successful' => 'Réussie', + 'ui.admin.binkp_sessions.failed' => 'Échouée', + 'ui.admin.binkp_sessions.filter_sessions' => 'Filtrer les sessions', + 'ui.admin.binkp_sessions.session_type' => 'Type de session', + 'ui.admin.binkp_sessions.crash_outbound' => 'Crash sortant', + 'ui.admin.binkp_sessions.success' => 'Succès', + 'ui.admin.binkp_sessions.rejected' => 'Rejetée', + 'ui.admin.binkp_sessions.remote_address' => 'Adresse distante', + 'ui.admin.binkp_sessions.remote_address_placeholder' => '1:123/456', + 'ui.admin.binkp_sessions.apply_filter' => 'Appliquer le filtre', + 'ui.admin.binkp_sessions.recent_sessions' => 'Sessions récentes', + 'ui.admin.binkp_sessions.time' => 'Heure', + 'ui.admin.binkp_sessions.ip' => 'IP', + 'ui.admin.binkp_sessions.type' => 'Type', + 'ui.admin.binkp_sessions.direction' => 'Direction', + 'ui.admin.binkp_sessions.msgs_in' => 'Msgs entrants', + 'ui.admin.binkp_sessions.msgs_out' => 'Msgs sortants', + 'ui.admin.binkp_sessions.duration' => 'Durée', + 'ui.admin.binkp_sessions.load_stats_failed' => 'Erreur lors du chargement des statistiques', + 'ui.admin.binkp_sessions.no_sessions_found' => 'Aucune session trouvée', + 'ui.admin.binkp_sessions.in' => 'Entrant', + 'ui.admin.binkp_sessions.out' => 'Sortant', + 'ui.admin.binkp_sessions.load_sessions_failed' => 'Erreur lors du chargement des sessions', + 'ui.admin.binkp_sessions.crash' => 'Crash', + 'ui.admin.binkp_sessions.active' => 'Actif', + 'ui.admin.polls.load_failed' => 'Erreur lors du chargement des sondages', + 'ui.admin.polls.not_found' => 'Sondage introuvable', + 'ui.admin.polls.load_single_failed' => 'Erreur lors du chargement du sondage', + 'ui.admin.polls.min_options_required' => 'Au moins deux options sont requises.', + 'ui.admin.polls.question_required' => 'La question est obligatoire.', + 'ui.admin.polls.provide_min_options' => 'Veuillez fournir au moins deux options.', + 'ui.admin.polls.updated_success' => 'Sondage mis à jour avec succès', + 'ui.admin.polls.created_success' => 'Sondage créé avec succès', + 'ui.admin.polls.save_failed' => 'Erreur lors de l\'enregistrement du sondage', + 'ui.admin.polls.delete_confirm' => 'Supprimer ce sondage ? Cette action est irréversible.', + 'ui.admin.polls.deleted_success' => 'Sondage supprimé avec succès', + 'ui.admin.polls.delete_failed' => 'Erreur lors de la suppression du sondage', + 'ui.admin.polls.page_title' => 'Sondages', + 'ui.admin.polls.heading' => 'Sondages', + 'ui.admin.polls.add_poll' => 'Ajouter un sondage', + 'ui.admin.polls.edit_poll' => 'Modifier le sondage', + 'ui.admin.polls.question' => 'Question', + 'ui.admin.polls.status' => 'Statut', + 'ui.admin.polls.votes' => 'Votes', + 'ui.admin.polls.created_by' => 'Créé par', + 'ui.admin.polls.created' => 'Créé', + 'ui.admin.polls.actions' => 'Actions', + 'ui.admin.polls.options' => 'Options', + 'ui.admin.polls.add_option' => 'Ajouter une option', + 'ui.admin.polls.active_poll' => 'Sondage actif', + 'ui.admin.polls.editing_options_resets_votes' => 'La modification des options réinitialisera les votes de ce sondage.', + 'ui.admin.polls.save_poll' => 'Enregistrer le sondage', + 'ui.admin.polls.active' => 'Actif', + 'ui.admin.polls.inactive' => 'Inactif', + 'ui.admin.polls.option_text_placeholder' => 'Texte de l\'option', + 'ui.admin.template_editor.load_templates_failed' => 'Échec du chargement des modèles.', + 'ui.admin.template_editor.no_templates_found' => 'Aucun modèle trouvé.', + 'ui.admin.template_editor.unsaved_changes_confirm' => 'Vous avez des modifications non enregistrées. Continuer et les ignorer ?', + 'ui.admin.template_editor.load_template_failed' => 'Échec du chargement du modèle', + 'ui.admin.template_editor.enter_template_path' => 'Saisissez un chemin de modèle (ex. : systemnews.twig).', + 'ui.admin.template_editor.save_template_failed' => 'Échec de l\'enregistrement du modèle', + 'ui.admin.template_editor.template_saved_success' => 'Modèle enregistré avec succès.', + 'ui.admin.template_editor.delete_confirm' => 'Supprimer {template} ? Cette action est irréversible.', + 'ui.admin.template_editor.delete_template_failed' => 'Échec de la suppression du modèle', + 'ui.admin.template_editor.no_template_selected' => 'Aucun modèle sélectionné', + 'ui.admin.template_editor.template_deleted_success' => 'Modèle supprimé.', + 'ui.admin.template_editor.install_template_failed' => 'Échec de l\'installation du modèle', + 'ui.admin.template_editor.template_installed_success' => 'Modèle installé depuis l\'exemple.', + 'ui.admin.template_editor.page_title_prefix' => 'Admin : Éditeur de modèles', + 'ui.admin.template_editor.heading' => 'Éditeur de modèles', + 'ui.admin.template_editor.restricted_edits_prefix' => 'Les modifications sont limitées à', + 'ui.admin.template_editor.custom_templates' => 'Modèles personnalisés', + 'ui.admin.template_editor.search_templates_placeholder' => 'Rechercher des modèles...', + 'ui.admin.template_editor.loading_templates' => 'Chargement des modèles...', + 'ui.admin.template_editor.new_template' => 'Nouveau modèle', + 'ui.admin.template_editor.path_relative_custom' => 'Chemin (relatif à templates/custom)', + 'ui.admin.template_editor.new_template_path_placeholder' => 'systemnews.twig', + 'ui.admin.template_editor.create' => 'Créer', + 'ui.admin.template_editor.editor' => 'Éditeur', + 'ui.admin.template_editor.install' => 'Installer', + 'ui.admin.template_editor.changes_apply_immediately' => 'Les modifications s\'appliquent immédiatement après l\'enregistrement. Assurez-vous que votre syntaxe Twig est valide.', + 'ui.admin.i18n_overrides.page_title' => 'Admin : Substitutions de langue', + 'ui.admin.i18n_overrides.heading' => 'Substitutions de langue', + 'ui.admin.i18n_overrides.description' => 'Personnalisez des phrases individuelles sans modifier les fichiers de traduction de base. Les substitutions sont appliquées par-dessus le catalogue de base pour la locale sélectionnée.', + 'ui.admin.i18n_overrides.locale_label' => 'Locale', + 'ui.admin.i18n_overrides.catalog_label' => 'Catalogue', + 'ui.admin.i18n_overrides.load_btn' => 'Charger', + 'ui.admin.i18n_overrides.search_placeholder' => 'Filtrer les clés...', + 'ui.admin.i18n_overrides.show_overrides_only' => 'Afficher uniquement les substitutions', + 'ui.admin.i18n_overrides.col_key' => 'Clé', + 'ui.admin.i18n_overrides.col_base' => 'Valeur de base', + 'ui.admin.i18n_overrides.col_override' => 'Votre substitution', + 'ui.admin.i18n_overrides.save_btn' => 'Enregistrer les substitutions', + 'ui.admin.i18n_overrides.clear_btn' => 'Effacer', + 'ui.admin.i18n_overrides.overridden_badge' => 'Substitué', + 'ui.admin.i18n_overrides.no_keys' => 'Aucune clé trouvée.', + 'ui.admin.i18n_overrides.loading' => 'Chargement...', + 'ui.admin.i18n_overrides.saved' => 'Substitutions enregistrées.', + 'ui.admin.i18n_overrides.save_failed' => 'Échec de l\'enregistrement des substitutions.', + 'ui.admin.i18n_overrides.load_failed' => 'Échec du chargement du catalogue.', + 'ui.admin.i18n_overrides.select_locale_catalog' => 'Sélectionnez une locale et un catalogue, puis cliquez sur Charger.', + 'ui.admin.upgrade_notes.none_for_version' => 'Aucune note de mise à jour trouvée pour la version {version}.', + 'ui.admin.insecure_nodes.error_prefix' => 'Erreur : ', + 'ui.admin.insecure_nodes.add_failed' => 'Échec de l\'ajout du nœud', + 'ui.admin.insecure_nodes.added_success' => 'Nœud ajouté avec succès', + 'ui.admin.insecure_nodes.update_failed' => 'Échec de la mise à jour du nœud', + 'ui.admin.insecure_nodes.updated_success' => 'Nœud mis à jour avec succès', + 'ui.admin.insecure_nodes.delete_confirm' => 'Supprimer {address} de la liste d\'autorisation ?', + 'ui.admin.insecure_nodes.delete_failed' => 'Échec de la suppression du nœud', + 'ui.admin.insecure_nodes.deleted_success' => 'Nœud supprimé avec succès', + 'ui.admin.insecure_nodes.page_title' => 'Nœuds non sécurisés - Admin', + 'ui.admin.insecure_nodes.heading' => 'Liste d\'autorisation des nœuds non sécurisés', + 'ui.admin.insecure_nodes.add_node' => 'Ajouter un nœud', + 'ui.admin.insecure_nodes.back_to_dashboard' => 'Retour au tableau de bord', + 'ui.admin.insecure_nodes.about_insecure_sessions' => 'À propos des sessions non sécurisées :', + 'ui.admin.insecure_nodes.info_text_1' => 'Les nœuds de cette liste d\'autorisation peuvent se connecter via binkp sans authentification par mot de passe.', + 'ui.admin.insecure_nodes.info_text_2' => 'À utiliser avec précaution - les sessions non sécurisées sont généralement en réception uniquement (elles peuvent vous livrer du courrier, mais ne peuvent pas en récupérer).', + 'ui.admin.insecure_nodes.allowed_nodes' => 'Nœuds autorisés', + 'ui.admin.insecure_nodes.address' => 'Adresse', + 'ui.admin.insecure_nodes.receive' => 'Recevoir', + 'ui.admin.insecure_nodes.send' => 'Envoyer', + 'ui.admin.insecure_nodes.max_msgs' => 'Msgs max', + 'ui.admin.insecure_nodes.last_session' => 'Dernière session', + 'ui.admin.insecure_nodes.active' => 'Actif', + 'ui.admin.insecure_nodes.inactive' => 'Inactif', + 'ui.admin.insecure_nodes.actions' => 'Actions', + 'ui.admin.insecure_nodes.add_node_to_allowlist' => 'Ajouter un nœud à la liste autorisée', + 'ui.admin.insecure_nodes.ftn_address' => 'Adresse FTN', + 'ui.admin.insecure_nodes.address_placeholder' => '1:123/456', + 'ui.admin.insecure_nodes.description_placeholder' => 'Description du nœud', + 'ui.admin.insecure_nodes.ftn_address_help' => 'L\'adresse FidoNet du nœud (ex. : 1:123/456)', + 'ui.admin.insecure_nodes.max_messages_per_session' => 'Nombre max de messages par session', + 'ui.admin.insecure_nodes.allow_receive' => 'Autoriser la réception', + 'ui.admin.insecure_nodes.allow_receive_help' => 'Autoriser ce nœud à nous délivrer du courrier', + 'ui.admin.insecure_nodes.allow_send_pickup' => 'Autoriser l\'envoi (récupération)', + 'ui.admin.insecure_nodes.allow_send_help' => 'Autoriser ce nœud à récupérer du courrier chez nous (à utiliser avec précaution)', + 'ui.admin.insecure_nodes.edit_node' => 'Modifier le nœud', + 'ui.admin.insecure_nodes.save_changes' => 'Enregistrer les modifications', + 'ui.admin.insecure_nodes.no_nodes' => 'Aucun nœud dans la liste autorisée', + 'ui.admin.insecure_nodes.load_nodes_failed' => 'Erreur lors du chargement des nœuds', + 'ui.admin.crashmail_queue.attempt_delivery_confirm' => 'Tenter la livraison du crashmail en attente maintenant ?', + 'ui.admin.crashmail_queue.delivery_attempt_started' => 'Tentative de livraison démarrée.', + 'ui.admin.crashmail_queue.delivery_attempt_failed_prefix' => 'Échec de la tentative de livraison : ', + 'ui.admin.crashmail_queue.error_prefix' => 'Erreur : ', + 'ui.admin.crashmail_queue.retry_confirm' => 'Réessayer cette livraison de crashmail ?', + 'ui.admin.crashmail_queue.retry_success' => 'Nouvelle tentative de crashmail mise en file d\'attente.', + 'ui.admin.crashmail_queue.retry_failed_prefix' => 'Échec de la nouvelle tentative : ', + 'ui.admin.crashmail_queue.cancel_confirm' => 'Annuler ce crashmail ? Le message restera dans votre boîte d\'envoi mais ne sera pas livré en mode crash.', + 'ui.admin.crashmail_queue.cancel_success' => 'Crashmail annulé.', + 'ui.admin.crashmail_queue.cancel_failed_prefix' => 'Échec de l\'annulation : ', + 'ui.admin.crashmail_queue.page_title' => 'File d\'attente Crashmail - Admin', + 'ui.admin.crashmail_queue.heading' => 'File d\'attente Crashmail', + 'ui.admin.crashmail_queue.attempt_delivery' => 'Tenter la livraison', + 'ui.admin.crashmail_queue.back_to_dashboard' => 'Retour au tableau de bord', + 'ui.admin.crashmail_queue.pending' => 'En attente', + 'ui.admin.crashmail_queue.attempting' => 'En cours', + 'ui.admin.crashmail_queue.sent_24h' => 'Envoyé (24h)', + 'ui.admin.crashmail_queue.sent' => 'Envoyé', + 'ui.admin.crashmail_queue.failed' => 'Échoué', + 'ui.admin.crashmail_queue.filter_queue' => 'Filtrer la file d\'attente', + 'ui.admin.crashmail_queue.status' => 'Statut', + 'ui.admin.crashmail_queue.limit' => 'Limite', + 'ui.admin.crashmail_queue.apply_filter' => 'Appliquer le filtre', + 'ui.admin.crashmail_queue.queue_items' => 'Éléments en file d\'attente', + 'ui.admin.crashmail_queue.to' => 'À', + 'ui.admin.crashmail_queue.subject' => 'Objet', + 'ui.admin.crashmail_queue.destination' => 'Destination', + 'ui.admin.crashmail_queue.attempts' => 'Tentatives', + 'ui.admin.crashmail_queue.next_attempt' => 'Prochaine tentative', + 'ui.admin.crashmail_queue.actions' => 'Actions', + 'ui.admin.crashmail_queue.no_items' => 'Aucun élément dans la file d\'attente', + 'ui.admin.crashmail_queue.no_subject' => '(sans objet)', + 'ui.admin.crashmail_queue.unresolved' => 'Non résolu', + 'ui.admin.crashmail_queue.load_stats_failed' => 'Erreur lors du chargement des statistiques', + 'ui.admin.crashmail_queue.load_queue_failed' => 'Erreur lors du chargement de la file d\'attente', + 'ui.admin.filearea_rules.load_failed' => 'Échec du chargement des règles', + 'ui.admin.filearea_rules.active' => 'Actif', + 'ui.admin.filearea_rules.missing_config' => 'Configuration manquante', + 'ui.admin.filearea_rules.error' => 'Erreur', + 'ui.admin.filearea_rules.invalid_json' => 'Le JSON des règles est invalide. Veuillez corriger les erreurs avant d\'enregistrer.', + 'ui.admin.filearea_rules.save_failed' => 'Échec de l\'enregistrement des règles', + 'ui.admin.filearea_rules.saved_success' => 'Règles enregistrées avec succès.', + 'ui.admin.filearea_rules.reset_confirm' => 'Remplacer les règles actuelles par le fichier d\'exemple ?', + 'ui.admin.filearea_rules.loaded_example' => 'Règles d\'exemple chargées. Enregistrez pour les appliquer.', + 'ui.admin.filearea_rules.page_title_prefix' => 'Règles des zones de fichiers', + 'ui.admin.filearea_rules.heading' => 'Règles des zones de fichiers', + 'ui.admin.filearea_rules.config_filename' => 'filearea_rules.json', + 'ui.admin.filearea_rules.reset_to_example' => 'Réinitialiser avec l\'exemple', + 'ui.admin.filearea_rules.save_rules' => 'Enregistrer les règles', + 'ui.admin.filearea_rules.editing_requires_valid_json' => 'La modification nécessite un JSON valide.', + 'ui.admin.filearea_rules.macros' => 'Macros', + 'ui.admin.filearea_rules.macro_basedir' => 'Répertoire de base de l\'application', + 'ui.admin.filearea_rules.macro_filepath' => 'Chemin complet du fichier', + 'ui.admin.filearea_rules.macro_filename' => 'Nom du fichier uniquement', + 'ui.admin.filearea_rules.macro_filesize' => 'Taille du fichier en octets', + 'ui.admin.filearea_rules.macro_domain' => 'Domaine de la zone de fichiers', + 'ui.admin.filearea_rules.macro_areatag' => 'Tag de la zone de fichiers', + 'ui.admin.filearea_rules.macro_uploader' => 'Nom/adresse de l\'expéditeur', + 'ui.admin.filearea_rules.macro_ticfile' => 'Chemin TIC (si disponible)', + 'ui.admin.filearea_rules.macro_tempdir' => 'Répertoire temporaire', + 'ui.admin.filearea_rules.tips' => 'Conseils', + 'ui.admin.filearea_rules.tip_1' => 'Les règles s\'exécutent dans l\'ordre : les règles globales d\'abord, puis les règles spécifiques à la zone.', + 'ui.admin.filearea_rules.tip_2' => 'Les règles de zone peuvent être indexées par TAG ou TAG@DOMAIN (la spécificité du domaine est prioritaire).', + 'ui.admin.filearea_rules.tip_3' => 'Utilisez success_action et fail_action avec +stop pour arrêter le traitement.', + 'ui.admin.dosdoors_config.load_config_failed' => 'Échec du chargement de la configuration', + 'ui.admin.dosdoors_config.load_config_error_prefix' => 'Erreur lors du chargement de la configuration : ', + 'ui.admin.dosdoors_config.load_doors_failed' => 'Échec du chargement des portes', + 'ui.admin.dosdoors_config.load_doors_error_prefix' => 'Erreur lors du chargement des portes disponibles : ', + 'ui.admin.dosdoors_config.invalid_json_toggle' => 'JSON invalide - impossible de basculer la porte', + 'ui.admin.dosdoors_config.enabled_all_editor' => 'Toutes les portes activées dans l\'éditeur. Cliquez sur Enregistrer pour appliquer.', + 'ui.admin.dosdoors_config.invalid_json_enable_all' => 'JSON invalide - impossible d\'activer toutes les portes', + 'ui.admin.dosdoors_config.disabled_all_editor' => 'Toutes les portes désactivées dans l\'éditeur. Cliquez sur Enregistrer pour appliquer.', + 'ui.admin.dosdoors_config.invalid_json_disable_all' => 'JSON invalide - impossible de désactiver toutes les portes', + 'ui.admin.dosdoors_config.json_formatted' => 'JSON formaté avec succès', + 'ui.admin.dosdoors_config.cannot_format_prefix' => 'Impossible de formater - JSON invalide : ', + 'ui.admin.dosdoors_config.empty_config' => 'Configuration vide', + 'ui.admin.dosdoors_config.valid_json' => 'JSON valide', + 'ui.admin.dosdoors_config.invalid_json_prefix' => 'JSON invalide : ', + 'ui.admin.dosdoors_config.save_failed' => 'Échec de l\'enregistrement', + 'ui.admin.dosdoors_config.saved_success' => 'Configuration enregistrée avec succès !', + 'ui.admin.dosdoors_config.save_error_prefix' => 'Erreur lors de l\'enregistrement de la configuration : ', + 'ui.admin.dosdoors_config.cannot_save_invalid_json_prefix' => 'Impossible d\'enregistrer - JSON invalide : ', + 'ui.admin.dosdoors_config.page_title' => 'Configuration des portes DOS', + 'ui.admin.dosdoors_config.heading' => 'Configuration des portes DOS', + 'ui.admin.dosdoors_config.info_text_1' => 'Les portes DOS sont des jeux BBS classiques qui s\'exécutent dans DOSBox. La configuration est stockée dans', + 'ui.admin.dosdoors_config.info_text_2' => 'Les portes sont découvertes en analysant', + 'ui.admin.dosdoors_config.info_text_3' => 'pour', + 'ui.admin.dosdoors_config.info_text_4' => 'les manifestes.', + 'ui.admin.dosdoors_config.installed_doors' => 'Portes installées', + 'ui.admin.dosdoors_config.loading_doors' => 'Chargement des portes...', + 'ui.admin.dosdoors_config.quick_actions' => 'Actions rapides', + 'ui.admin.dosdoors_config.enable_all_doors' => 'Activer toutes les portes', + 'ui.admin.dosdoors_config.disable_all_doors' => 'Désactiver toutes les portes', + 'ui.admin.dosdoors_config.config_filename' => 'dosdoors.json', + 'ui.admin.dosdoors_config.format' => 'Formater', + 'ui.admin.dosdoors_config.loading_config' => 'Chargement de la configuration...', + 'ui.admin.dosdoors_config.json_validation_before_save' => 'La validation JSON s\'effectue avant l\'enregistrement', + 'ui.admin.dosdoors_config.configuration_options' => 'Options de configuration', + 'ui.admin.dosdoors_config.option_enabled_help' => 'Indique si la porte est disponible pour les utilisateurs', + 'ui.admin.dosdoors_config.option_credit_cost_help' => 'Crédits requis pour jouer (0 = gratuit)', + 'ui.admin.dosdoors_config.option_max_time_help' => 'Durée maximale de jeu par session', + 'ui.admin.dosdoors_config.option_cpu_cycles_help' => 'Vitesse CPU DOSBox (10000 = typique)', + 'ui.admin.dosdoors_config.option_max_concurrent_help' => 'Nombre maximum de joueurs simultanés', + 'ui.admin.dosdoors_config.no_doors_found_prefix' => 'Aucune porte trouvée. Installez des portes dans', + 'ui.admin.dosdoors_config.no_description' => 'Aucune description', + 'ui.admin.dosdoors_config.credits' => 'crédits', + 'ui.admin.dosdoors_config.free' => 'Gratuit', + 'ui.admin.dosdoors_config.disable' => 'Désactiver', + 'ui.admin.dosdoors_config.enable' => 'Activer', + 'ui.admin.nativedoors_config.load_config_failed' => 'Échec du chargement de la configuration', + 'ui.admin.nativedoors_config.load_config_error_prefix' => 'Erreur lors du chargement de la configuration : ', + 'ui.admin.nativedoors_config.load_doors_failed' => 'Échec du chargement des portes', + 'ui.admin.nativedoors_config.load_doors_error_prefix' => 'Erreur lors du chargement des portes disponibles : ', + 'ui.admin.nativedoors_config.invalid_json_toggle' => 'JSON invalide - impossible de basculer la porte', + 'ui.admin.nativedoors_config.enabled_all_editor' => 'Toutes les portes activées dans l\'éditeur. Cliquez sur Enregistrer pour appliquer.', + 'ui.admin.nativedoors_config.invalid_json_enable_all' => 'JSON invalide - impossible d\'activer toutes les portes', + 'ui.admin.nativedoors_config.disabled_all_editor' => 'Toutes les portes désactivées dans l\'éditeur. Cliquez sur Enregistrer pour appliquer.', + 'ui.admin.nativedoors_config.invalid_json_disable_all' => 'JSON invalide - impossible de désactiver toutes les portes', + 'ui.admin.nativedoors_config.json_formatted' => 'JSON formaté avec succès', + 'ui.admin.nativedoors_config.cannot_format_prefix' => 'Impossible de formater - JSON invalide : ', + 'ui.admin.nativedoors_config.empty_config' => 'Configuration vide', + 'ui.admin.nativedoors_config.valid_json' => 'JSON valide', + 'ui.admin.nativedoors_config.invalid_json_prefix' => 'JSON invalide : ', + 'ui.admin.nativedoors_config.save_failed' => 'Échec de l\'enregistrement', + 'ui.admin.nativedoors_config.saved_success' => 'Configuration enregistrée avec succès !', + 'ui.admin.nativedoors_config.save_error_prefix' => 'Erreur lors de l\'enregistrement de la configuration : ', + 'ui.admin.nativedoors_config.cannot_save_invalid_json_prefix' => 'Impossible d\'enregistrer - JSON invalide : ', + 'ui.admin.nativedoors_config.page_title' => 'Configuration des portes natives', + 'ui.admin.nativedoors_config.heading' => 'Configuration des portes natives', + 'ui.admin.nativedoors_config.info_text_1' => 'Les portes natives sont des programmes Linux qui s\'exécutent directement via PTY (sans émulateur). La configuration est stockée dans', + 'ui.admin.nativedoors_config.info_text_2' => 'Les portes sont découvertes en analysant', + 'ui.admin.nativedoors_config.info_text_3' => 'pour', + 'ui.admin.nativedoors_config.info_text_4' => 'les manifestes.', + 'ui.admin.nativedoors_config.installed_doors' => 'Portes installées', + 'ui.admin.nativedoors_config.loading_doors' => 'Chargement des portes...', + 'ui.admin.nativedoors_config.quick_actions' => 'Actions rapides', + 'ui.admin.nativedoors_config.enable_all_doors' => 'Activer toutes les portes', + 'ui.admin.nativedoors_config.disable_all_doors' => 'Désactiver toutes les portes', + 'ui.admin.nativedoors_config.config_filename' => 'nativedoors.json', + 'ui.admin.nativedoors_config.format' => 'Formater', + 'ui.admin.nativedoors_config.loading_config' => 'Chargement de la configuration...', + 'ui.admin.nativedoors_config.json_validation_before_save' => 'La validation JSON s\'exécute avant l\'enregistrement', + 'ui.admin.nativedoors_config.configuration_options' => 'Options de configuration', + 'ui.admin.nativedoors_config.option_enabled_help' => 'Indique si la porte est disponible pour les utilisateurs', + 'ui.admin.nativedoors_config.option_credit_cost_help' => 'Crédits requis pour jouer (0 = gratuit)', + 'ui.admin.nativedoors_config.option_max_time_help' => 'Durée de jeu maximale par session', + 'ui.admin.nativedoors_config.option_max_concurrent_help' => 'Nombre maximum de joueurs simultanés', + 'ui.admin.nativedoors_config.no_doors_found_prefix' => 'Aucune porte trouvée. Installez des portes dans', + 'ui.admin.nativedoors_config.no_description' => 'Aucune description', + 'ui.admin.nativedoors_config.credits' => 'crédits', + 'ui.admin.nativedoors_config.free' => 'Gratuit', + 'ui.admin.nativedoors_config.disable' => 'Désactiver', + 'ui.admin.nativedoors_config.enable' => 'Activer', + 'ui.admin.webdoors_config.load_webdoors_failed' => 'Échec du chargement de la configuration des webdoors', + 'ui.admin.webdoors_config.load_failed' => 'Échec du chargement de la configuration', + 'ui.admin.webdoors_config.json_valid' => 'Le JSON est valide', + 'ui.admin.webdoors_config.json_has_errors' => 'Le JSON contient des erreurs', + 'ui.admin.webdoors_config.invalid_json' => 'JSON invalide.', + 'ui.admin.webdoors_config.no_doors_defined' => 'Aucune porte définie.', + 'ui.admin.webdoors_config.cannot_format_invalid_json' => 'Impossible de formater : JSON invalide', + 'ui.admin.webdoors_config.fix_json_before_save' => 'Veuillez corriger les erreurs JSON avant d\'enregistrer.', + 'ui.admin.webdoors_config.save_failed' => 'Échec de l\'enregistrement de la configuration', + 'ui.admin.webdoors_config.saved_success' => 'Configuration des webdoors enregistrée.', + 'ui.admin.webdoors_config.activate_failed' => 'Échec de l\'activation des webdoors', + 'ui.admin.webdoors_config.activated_success' => 'Webdoors activées.', + 'ui.admin.webdoors_config.page_title' => 'Configuration des webdoors', + 'ui.admin.webdoors_config.heading' => 'Configuration des webdoors', + 'ui.admin.webdoors_config.info_text_prefix' => 'Les webdoors sont activées en ayant un fichier', + 'ui.admin.webdoors_config.info_text_suffix' => 'Cette page vous permet de l\'activer et de le modifier.', + 'ui.admin.webdoors_config.status' => 'Statut', + 'ui.admin.webdoors_config.activate_webdoors' => 'Activer les webdoors', + 'ui.admin.webdoors_config.config_file' => 'Fichier de configuration :', + 'ui.admin.webdoors_config.config_never_deleted' => 'Une fois activé, le fichier de configuration n\'est jamais supprimé.', + 'ui.admin.webdoors_config.json_controls_help' => 'La modification du JSON contrôle l\'apparence des portes dans le système. Seul « enabled » est reconnu par l\'interface.', + 'ui.admin.webdoors_config.doors' => 'Portes', + 'ui.admin.webdoors_config.config_filename' => 'webdoors.json', + 'ui.admin.webdoors_config.no_config_loaded' => 'Aucune configuration chargée.', + 'ui.admin.webdoors_config.format_json' => 'Formater le JSON', + 'ui.admin.webdoors_config.waiting_for_config' => 'En attente de la configuration...', + 'ui.admin.webdoors_config.json_validation_before_save' => 'La validation JSON s\'exécute avant l\'enregistrement.', + 'ui.admin.webdoors_config.active_present' => 'Actif (webdoors.json présent)', + 'ui.admin.webdoors_config.not_active_using_example' => 'Inactif (utilisation de l\'exemple)', + 'ui.admin.webdoors_config.enabled_label' => 'activé', + 'ui.admin.webdoors_config.true' => 'true', + 'ui.admin.webdoors_config.false' => 'false', + 'ui.admin.webdoors_config.not_in_config' => '(absent de la configuration)', + 'ui.admin.bbs_settings.load_failed' => 'Échec du chargement des paramètres', + 'ui.admin.bbs_settings.save_failed' => 'Échec de l\'enregistrement des paramètres', + 'ui.admin.bbs_settings.saved_success' => 'Fonctionnalités BBS enregistrées avec succès.', + 'ui.admin.bbs_settings.load_system_failed' => 'Échec du chargement des paramètres système', + 'ui.admin.bbs_settings.save_system_failed' => 'Échec de l\'enregistrement des paramètres système', + 'ui.admin.bbs_settings.system_saved_success' => 'Paramètres système enregistrés avec succès.', + 'ui.admin.bbs_settings.save_credits_failed' => 'Échec de l\'enregistrement des paramètres de crédits', + 'ui.admin.bbs_settings.credits_saved_success' => 'Paramètres de crédits enregistrés avec succès.', + 'ui.admin.bbs_settings.load_taglines_failed' => 'Échec du chargement des signatures', + 'ui.admin.bbs_settings.save_taglines_failed' => 'Échec de l\'enregistrement des signatures', + 'ui.admin.bbs_settings.taglines_saved_success' => 'Signatures enregistrées avec succès.', + 'ui.admin.bbs_settings.page_title' => 'Paramètres BBS', + 'ui.admin.bbs_settings.heading' => 'Paramètres BBS', + 'ui.admin.bbs_settings.current' => 'actuel', + 'ui.admin.bbs_settings.system.title' => 'Paramètres système', + 'ui.admin.bbs_settings.system.system_name' => 'Nom du système', + 'ui.admin.bbs_settings.system.sysop_name' => 'Nom du sysop', + 'ui.admin.bbs_settings.system.select_admin_user' => 'Sélectionnez un utilisateur administrateur.', + 'ui.admin.bbs_settings.system.location' => 'Emplacement', + 'ui.admin.bbs_settings.system.timezone' => 'Fuseau horaire', + 'ui.admin.bbs_settings.system.system_address' => 'Adresse du système', + 'ui.admin.bbs_settings.system.origin_line' => 'Ligne d\'origine', + 'ui.admin.bbs_settings.system.save' => 'Enregistrer les paramètres système', + 'ui.admin.bbs_settings.features.title' => 'Fonctionnalités BBS', + 'ui.admin.bbs_settings.features.enable_webdoors' => 'Activer les webdoors', + 'ui.admin.bbs_settings.features.inactive' => 'inactif', + 'ui.admin.bbs_settings.features.activate' => 'Activer', + 'ui.admin.bbs_settings.features.enable_shoutbox' => 'Activer le shoutbox', + 'ui.admin.bbs_settings.features.enable_advertising' => 'Activer la publicité', + 'ui.admin.bbs_settings.features.enable_voting_booth' => 'Activer le bureau de vote', + 'ui.admin.bbs_settings.features.enable_chat' => 'Activer le chat', + 'ui.admin.bbs_settings.features.enable_file_areas' => 'Activer les zones de fichiers', + 'ui.admin.bbs_settings.features.enable_guest_doors_page' => 'Activer la page des portes invité', + 'ui.admin.bbs_settings.features.guest_doors_page_help' => 'Affiche une page publique /guest-doors listant les portes accessibles anonymement. Affiche également un lien sur la page de connexion.', + 'ui.admin.bbs_settings.features.default_echo_interface' => 'Interface echo par défaut', + 'ui.admin.bbs_settings.features.echo_list_forum' => 'Liste echo (vue forum)', + 'ui.admin.bbs_settings.features.reader_message_list' => 'Lecteur (liste de messages)', + 'ui.admin.bbs_settings.features.default_echo_help' => 'Interface par défaut pour la consultation des echomail. Les utilisateurs peuvent la modifier dans leurs paramètres.', + 'ui.admin.bbs_settings.features.max_cross_post_areas' => 'Nombre max de zones de cross-post', + 'ui.admin.bbs_settings.features.max_cross_post_help' => 'Nombre maximum de zones supplémentaires vers lesquelles un utilisateur peut faire un cross-post (2-20).', + 'ui.admin.bbs_settings.features.save' => 'Enregistrer les paramètres', + 'ui.admin.bbs_settings.credits.title' => 'Configuration du système de crédits', + 'ui.admin.bbs_settings.credits.enabled' => 'Système de crédits activé', + 'ui.admin.bbs_settings.credits.currency_symbol' => 'Symbole monétaire', + 'ui.admin.bbs_settings.credits.currency_symbol_help' => 'Exemple : $, USD (5 caractères max). Laisser vide pour aucun symbole.', + 'ui.admin.bbs_settings.credits.daily_login_bonus_amount' => 'Montant du bonus de connexion quotidien', + 'ui.admin.bbs_settings.credits.daily_login_bonus_help' => 'Nombre de crédits attribués aux utilisateurs lors de la connexion quotidienne.', + 'ui.admin.bbs_settings.credits.daily_login_delay_minutes' => 'Délai de connexion quotidien (minutes)', + 'ui.admin.bbs_settings.credits.daily_login_delay_help' => 'Minutes après la connexion avant l\'attribution du bonus quotidien.', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus' => 'Bonus d\'approbation du nouvel utilisateur', + 'ui.admin.bbs_settings.credits.new_user_approval_bonus_help' => 'Crédits attribués lorsqu\'un utilisateur en attente est approuvé.', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus' => 'Bonus de jalon 14 jours pour les nouveaux utilisateurs', + 'ui.admin.bbs_settings.credits.new_user_14_day_bonus_help' => 'Bonus unique attribué lorsqu\'un utilisateur se connecte 14 jours ou plus après la création de son compte.', + 'ui.admin.bbs_settings.credits.netmail_cost' => 'Coût du netmail', + 'ui.admin.bbs_settings.credits.netmail_cost_help' => 'Crédits débités lors de l\'envoi d\'un message netmail.', + 'ui.admin.bbs_settings.credits.echomail_reward' => 'Récompense echomail', + 'ui.admin.bbs_settings.credits.echomail_reward_help' => 'Crédits gagnés lors de la publication d\'un message echomail. Doublés si le message fait >= 1200 caractères.', + 'ui.admin.bbs_settings.credits.crashmail_cost' => 'Coût du crashmail', + 'ui.admin.bbs_settings.credits.crashmail_cost_help' => 'Crédits débités lors d\'un envoi direct/crashmail.', + 'ui.admin.bbs_settings.credits.poll_creation_cost' => 'Coût de création de sondage', + 'ui.admin.bbs_settings.credits.poll_creation_cost_help' => 'Crédits débités lors de la création d\'un nouveau sondage.', + 'ui.admin.bbs_settings.credits.transfer_fee_percentage' => 'Pourcentage de frais de transfert', + 'ui.admin.bbs_settings.credits.transfer_fee_help' => 'Pourcentage des transferts de crédits prélevé en frais (0,05 = 5%). Distribué aux sysops.', + 'ui.admin.bbs_settings.credits.referral_system' => 'Système de parrainage', + 'ui.admin.bbs_settings.credits.enable_referral_system' => 'Activer le système de parrainage', + 'ui.admin.bbs_settings.credits.referral_bonus' => 'Bonus de parrainage', + 'ui.admin.bbs_settings.credits.referral_bonus_help' => 'Crédits attribués lorsqu\'un utilisateur parrainé est approuvé par l\'administrateur.', + 'ui.admin.bbs_settings.credits.save' => 'Enregistrer les paramètres de crédits', + 'ui.admin.bbs_settings.taglines.title' => 'Signatures', + 'ui.admin.bbs_settings.taglines.one_per_line' => 'Signatures (une par ligne)', + 'ui.admin.bbs_settings.taglines.placeholder' => 'Une signature par ligne.', + 'ui.admin.bbs_settings.taglines.help' => 'Ces signatures apparaissent comme options sélectionnables lorsque les utilisateurs rédigent des messages.', + 'ui.admin.bbs_settings.taglines.save' => 'Enregistrer les signatures', + 'ui.admin.bbs_settings.validation.max_cross_post_areas_range' => 'Le nombre max de zones de cross-post doit être un entier compris entre 2 et 20.', + 'ui.admin.bbs_settings.validation.currency_symbol_length' => 'Le symbole monétaire doit comporter entre 0 et 5 caractères.', + 'ui.admin.bbs_settings.validation.daily_login_amount_non_negative' => 'Le montant du bonus de connexion quotidien doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.daily_login_delay_non_negative' => 'Le délai de connexion quotidien doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.approval_bonus_non_negative' => 'Le bonus d\'approbation doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.netmail_cost_non_negative' => 'Le coût du netmail doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.echomail_reward_non_negative' => 'La récompense echomail doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.crashmail_cost_non_negative' => 'Le coût du crashmail doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.poll_creation_cost_non_negative' => 'Le coût de création de sondage doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.return_14_days_non_negative' => 'Le bonus de retour à 14 jours doit être un entier non négatif.', + 'ui.admin.bbs_settings.validation.transfer_fee_range' => 'Les frais de transfert doivent être compris entre 0 et 1 (0% à 100%).', + 'ui.admin.bbs_settings.validation.referral_bonus_non_negative' => 'Le bonus de parrainage doit être un entier non négatif.', + 'ui.admin.mrc_settings.page_title' => 'Paramètres MRC', + 'ui.admin.mrc_settings.heading' => 'Paramètres du chat MRC', + 'ui.admin.mrc_settings.note_label' => 'Remarque', + 'ui.admin.mrc_settings.note_text' => 'MRC (Multi Relay Chat) est disponible en tant que WebDoor.', + 'ui.admin.mrc_settings.note_enable_text' => 'Configurez les paramètres du démon ici, puis activez la porte dans le', + 'ui.admin.mrc_settings.webdoors_admin' => 'Administration des WebDoors', + 'ui.admin.mrc_settings.load_failed' => 'Échec du chargement des paramètres MRC', + 'ui.admin.mrc_settings.load_error' => 'Erreur lors du chargement des paramètres MRC', + 'ui.admin.mrc_settings.saved_success' => 'Paramètres MRC enregistrés avec succès', + 'ui.admin.mrc_settings.restart_required' => 'Paramètres enregistrés. Redémarrez le démon pour appliquer les modifications.', + 'ui.admin.mrc_settings.save_failed' => 'Échec de l\'enregistrement des paramètres MRC', + 'ui.admin.mrc_settings.save_error' => 'Erreur lors de l\'enregistrement des paramètres MRC', + 'ui.admin.mrc_settings.general_settings' => 'Paramètres généraux', + 'ui.admin.mrc_settings.enable_daemon' => 'Activer le démon MRC', + 'ui.admin.mrc_settings.server_connection' => 'Connexion au serveur', + 'ui.admin.mrc_settings.server_host' => 'Hôte du serveur MRC', + 'ui.admin.mrc_settings.server_port_non_ssl' => 'Port du serveur (Non-SSL)', + 'ui.admin.mrc_settings.server_port_ssl' => 'Port du serveur (SSL)', + 'ui.admin.mrc_settings.use_ssl_connection' => 'Utiliser la connexion SSL', + 'ui.admin.mrc_settings.use_ssl_help' => 'Recommandé pour une communication sécurisée', + 'ui.admin.mrc_settings.save_general_settings' => 'Enregistrer les paramètres généraux', + 'ui.admin.mrc_settings.bbs_identity' => 'Identité BBS', + 'ui.admin.mrc_settings.bbs_name' => 'Nom du BBS', + 'ui.admin.mrc_settings.bbs_name_help' => 'Maximum 64 caractères', + 'ui.admin.mrc_settings.platform_info' => 'Informations sur la plateforme', + 'ui.admin.mrc_settings.platform_info_help' => 'Identifiant du logiciel et de la plateforme', + 'ui.admin.mrc_settings.sysop_name' => 'Nom du Sysop', + 'ui.admin.mrc_settings.bbs_information' => 'Informations BBS', + 'ui.admin.mrc_settings.website_url' => 'URL du site web', + 'ui.admin.mrc_settings.info_optional_help' => 'Optionnel - pour la commande INFO', + 'ui.admin.mrc_settings.telnet_address' => 'Adresse Telnet', + 'ui.admin.mrc_settings.bbs_description' => 'Description du BBS', + 'ui.admin.mrc_settings.placeholder.server_host' => 'mrc.bottomlessabyss.net', + 'ui.admin.mrc_settings.placeholder.bbs_name' => 'BinktermPHP BBS', + 'ui.admin.mrc_settings.placeholder.platform' => 'BINKTERMPHP/Linux64/1.0.0', + 'ui.admin.mrc_settings.placeholder.sysop' => 'Sysop', + 'ui.admin.mrc_settings.placeholder.website' => 'https://example.com', + 'ui.admin.mrc_settings.placeholder.telnet' => 'telnet://bbs.example.com:23', + 'ui.admin.mrc_settings.placeholder.description' => 'Un BBS moderne...', + 'ui.admin.mrc_settings.save_identity_settings' => 'Enregistrer les paramètres d\'identité', + 'ui.admin.mrc_settings.connection_settings' => 'Paramètres de connexion', + 'ui.admin.mrc_settings.auto_reconnect' => 'Reconnexion automatique', + 'ui.admin.mrc_settings.auto_reconnect_help' => 'Se reconnecter automatiquement en cas de perte de connexion', + 'ui.admin.mrc_settings.reconnect_delay' => 'Délai de reconnexion (secondes)', + 'ui.admin.mrc_settings.reconnect_delay_help' => 'Temps d\'attente avant la reconnexion (5-300 secondes)', + 'ui.admin.mrc_settings.ping_interval' => 'Intervalle de ping (secondes)', + 'ui.admin.mrc_settings.ping_interval_help' => 'Intervalle PING attendu du serveur (30-300 secondes)', + 'ui.admin.mrc_settings.keepalive_timeout' => 'Délai de maintien de connexion (secondes)', + 'ui.admin.mrc_settings.keepalive_timeout_help' => 'Déconnecter si aucun PING reçu (60-600 secondes)', + 'ui.admin.mrc_settings.save_connection_settings' => 'Enregistrer les paramètres de connexion', + 'ui.admin.mrc_settings.room_settings' => 'Paramètres des salons', + 'ui.admin.mrc_settings.default_room' => 'Salon par défaut', + 'ui.admin.mrc_settings.default_room_help' => 'Salon par défaut pour les nouvelles connexions', + 'ui.admin.mrc_settings.auto_join_rooms' => 'Rejoindre automatiquement les salons', + 'ui.admin.mrc_settings.auto_join_rooms_help' => 'Liste de salons à rejoindre automatiquement, séparés par des virgules', + 'ui.admin.mrc_settings.message_settings' => 'Paramètres des messages', + 'ui.admin.mrc_settings.max_message_length' => 'Longueur maximale des messages', + 'ui.admin.mrc_settings.max_message_length_help' => 'Nombre maximum de caractères par message (80-255)', + 'ui.admin.mrc_settings.history_limit' => 'Limite de l\'historique', + 'ui.admin.mrc_settings.history_limit_help' => 'Messages à conserver par salon (100-10000)', + 'ui.admin.mrc_settings.prune_after_days' => 'Supprimer après (jours)', + 'ui.admin.mrc_settings.prune_after_days_help' => 'Supprimer les messages plus anciens que cette valeur (1-365 jours)', + 'ui.admin.mrc_settings.save_room_settings' => 'Enregistrer les paramètres des salons', + 'ui.admin.activity_stats.page_title' => 'Statistiques d\'activité', + 'ui.admin.activity_stats.heading' => 'Statistiques d\'activité', + 'ui.admin.activity_stats.period' => 'Période', + 'ui.admin.activity_stats.period_7d' => '7 derniers jours', + 'ui.admin.activity_stats.period_30d' => '30 derniers jours', + 'ui.admin.activity_stats.period_90d' => '90 derniers jours', + 'ui.admin.activity_stats.period_all' => 'Toute la période', + 'ui.admin.activity_stats.exclude_admins' => 'Exclure les administrateurs', + 'ui.admin.activity_stats.dashboard' => 'Tableau de bord', + 'ui.admin.activity_stats.loading_activity_data' => 'Chargement des données d\'activité...', + 'ui.admin.activity_stats.tab_overview' => 'Vue d\'ensemble', + 'ui.admin.activity_stats.tab_popular_areas' => 'Zones populaires', + 'ui.admin.activity_stats.tab_doors' => 'Portes', + 'ui.admin.activity_stats.tab_file_activity' => 'Activité fichiers', + 'ui.admin.activity_stats.tab_nodelist' => 'Liste des nœuds', + 'ui.admin.activity_stats.tab_top_users' => 'Meilleurs utilisateurs', + 'ui.admin.activity_stats.tab_hourly' => 'Par heure', + 'ui.admin.activity_stats.activity_by_category' => 'Activité par catégorie', + 'ui.admin.activity_stats.category' => 'Catégorie', + 'ui.admin.activity_stats.events' => 'Événements', + 'ui.admin.activity_stats.daily_activity_last_30' => 'Activité quotidienne (30 derniers jours)', + 'ui.admin.activity_stats.date' => 'Date', + 'ui.admin.activity_stats.most_viewed_echoareas' => 'Zones echo les plus consultées', + 'ui.admin.activity_stats.most_active_echoareas' => 'Zones echo les plus actives (publications)', + 'ui.admin.activity_stats.area' => 'Zone', + 'ui.admin.activity_stats.views' => 'Vues', + 'ui.admin.activity_stats.posts' => 'Publications', + 'ui.admin.activity_stats.most_played_webdoors' => 'WebDoors les plus joués', + 'ui.admin.activity_stats.most_played_dos_doors' => 'Portes DOS les plus jouées', + 'ui.admin.activity_stats.game' => 'Jeu', + 'ui.admin.activity_stats.door' => 'Porte', + 'ui.admin.activity_stats.sessions' => 'Sessions', + 'ui.admin.activity_stats.top_downloaded_files' => 'Fichiers les plus téléchargés', + 'ui.admin.activity_stats.file' => 'Fichier', + 'ui.admin.activity_stats.downloads' => 'Téléchargements', + 'ui.admin.activity_stats.most_browsed_file_areas' => 'Zones de fichiers les plus parcourues', + 'ui.admin.activity_stats.most_searched_nodelist_queries' => 'Requêtes de liste de nœuds les plus recherchées', + 'ui.admin.activity_stats.query' => 'Requête', + 'ui.admin.activity_stats.searches' => 'Recherches', + 'ui.admin.activity_stats.most_viewed_nodes' => 'Nœuds les plus consultés', + 'ui.admin.activity_stats.node' => 'Nœud', + 'ui.admin.activity_stats.most_active_users' => 'Utilisateurs les plus actifs', + 'ui.admin.activity_stats.user' => 'Utilisateur', + 'ui.admin.activity_stats.activity_by_hour_of_day' => 'Activité par heure de la journée', + 'ui.admin.activity_stats.hour' => 'Heure', + 'ui.admin.activity_stats.utc' => 'UTC', + 'ui.admin.activity_stats.no_data_for_period' => 'Aucune donnée pour cette période', + 'ui.admin.activity_stats.no_data' => 'Aucune donnée', + 'ui.admin.activity_stats.failed_load_statistics_prefix' => 'Échec du chargement des statistiques. ', + 'ui.admin.activity_stats.echomail' => 'Echomail', + 'ui.admin.activity_stats.netmail' => 'Netmail', + 'ui.admin.activity_stats.files' => 'Fichiers', + 'ui.admin.activity_stats.door_plays' => 'Parties de portes', + 'ui.admin.activity_stats.logins' => 'Connexions', + 'ui.admin.activity_stats.total' => 'Total', + 'ui.admin.activity_stats.views_sent' => 'Vues : {views} - Envoyés : {sent}', + 'ui.admin.activity_stats.read_sent' => 'Lus : {read} - Envoyés : {sent}', + 'ui.admin.activity_stats.area_views' => 'Vues de la zone', + 'ui.admin.activity_stats.sent' => 'Envoyés', + 'ui.admin.activity_stats.read' => 'Lus', + 'ui.admin.activity_stats.doors' => 'Portes', + 'ui.admin.activity_stats.nodelist' => 'Liste des nœuds', + 'ui.admin.activity_stats.chat' => 'Chat', + 'ui.admin.activity_stats.auth' => 'Auth', + 'ui.admin.activity_stats.anonymous' => '(anon)', + 'ui.admin.economy.page_title' => 'Visionneuse d\'économie', + 'ui.admin.economy.heading' => 'Visionneuse d\'économie', + 'ui.admin.economy.period' => 'Période :', + 'ui.admin.economy.period_7d' => '7 derniers jours', + 'ui.admin.economy.period_30d' => '30 derniers jours', + 'ui.admin.economy.period_90d' => '90 derniers jours', + 'ui.admin.economy.period_all' => 'Toute la période', + 'ui.admin.economy.dashboard' => 'Tableau de bord', + 'ui.admin.economy.loading_statistics' => 'Chargement des statistiques économiques...', + 'ui.admin.economy.period_snapshot' => 'Instantané de la période', + 'ui.admin.economy.transactions' => 'Transactions', + 'ui.admin.economy.active_users' => 'Utilisateurs actifs', + 'ui.admin.economy.credits_earned' => 'Crédits gagnés', + 'ui.admin.economy.credits_spent' => 'Crédits dépensés', + 'ui.admin.economy.net_flow' => 'Flux net', + 'ui.admin.economy.current_distribution' => 'Distribution actuelle', + 'ui.admin.economy.funded_users' => 'Utilisateurs financés', + 'ui.admin.economy.average_balance' => 'Solde moyen', + 'ui.admin.economy.median_balance' => 'Solde médian', + 'ui.admin.economy.largest_balance' => 'Solde le plus élevé', + 'ui.admin.economy.richest_user' => 'Utilisateur le plus riche', + 'ui.admin.economy.transaction_types' => 'Types de transactions', + 'ui.admin.economy.type' => 'Type', + 'ui.admin.economy.count' => 'Nombre', + 'ui.admin.economy.net' => 'Net', + 'ui.admin.economy.top_earners' => 'Meilleurs gagnants', + 'ui.admin.economy.user' => 'Utilisateur', + 'ui.admin.economy.tx_short' => 'Tx', + 'ui.admin.economy.earned' => 'Gagné', + 'ui.admin.economy.spent' => 'Dépensé', + 'ui.admin.economy.top_spenders' => 'Meilleurs dépensiers', + 'ui.admin.economy.richest_accounts' => 'Comptes les plus riches', + 'ui.admin.economy.balance' => 'Solde', + 'ui.admin.economy.recent_transactions' => 'Transactions récentes', + 'ui.admin.economy.amount' => 'Montant', + 'ui.admin.economy.na' => 'n/a', + 'ui.admin.economy.credits_in_circulation' => 'Crédits en circulation', + 'ui.admin.economy.funded_wallets' => 'Portefeuilles financés', + 'ui.admin.economy.credits_disabled_notice' => 'Les crédits sont actuellement désactivés. Les soldes historiques et les données du grand livre sont toujours affichés.', + 'ui.admin.economy.users' => 'utilisateurs', + 'ui.admin.economy.no_transactions_period' => 'Aucune transaction pour cette période', + 'ui.admin.economy.no_earning_activity_period' => 'Aucune activité de gain pour cette période', + 'ui.admin.economy.no_spending_activity_period' => 'Aucune activité de dépense pour cette période', + 'ui.admin.economy.no_funded_users_yet' => 'Aucun utilisateur financé pour l\'instant', + 'ui.admin.economy.no_description' => 'Aucune description', + 'ui.admin.economy.bal_short' => 'Sol', + 'ui.admin.economy.no_recent_transactions_period' => 'Aucune transaction récente pour cette période', + 'ui.admin.economy.load_failed' => 'Échec du chargement des statistiques économiques', + 'ui.fileareas.page_title' => 'Zones de fichiers', + 'ui.fileareas.heading' => 'Gestion des zones de fichiers', + 'ui.fileareas.add_file_area' => 'Ajouter une zone de fichiers', + 'ui.fileareas.edit_file_area' => 'Modifier la zone de fichiers', + 'ui.fileareas.list_title' => 'Zones de fichiers', + 'ui.fileareas.loading_file_areas' => 'Chargement des zones de fichiers...', + 'ui.fileareas.load_failed' => 'Échec du chargement des zones de fichiers', + 'ui.fileareas.load_details_failed' => 'Échec du chargement des détails de la zone de fichiers', + 'ui.fileareas.updated_success' => 'Zone de fichiers mise à jour', + 'ui.fileareas.created_success' => 'Zone de fichiers créée', + 'ui.fileareas.deleted_success' => 'Zone de fichiers supprimée', + 'ui.fileareas.active_areas' => 'Zones actives :', + 'ui.fileareas.total_files' => 'Total des fichiers :', + 'ui.fileareas.total_size' => 'Taille totale :', + 'ui.fileareas.settings_title' => 'Paramètres de la zone de fichiers', + 'ui.fileareas.replace_existing_label' => 'Remplacer les existants :', + 'ui.fileareas.replace_existing_help' => 'Lorsqu\'activé, les nouveaux fichiers portant le même nom remplacent les anciens (utile pour NODELIST, etc.)', + 'ui.fileareas.versioning_label' => 'Versionnage :', + 'ui.fileareas.versioning_help' => 'Lorsque désactivé, les fichiers avec des noms en double reçoivent des suffixes de version (_1, _2, etc.)', + 'ui.fileareas.tag_required' => 'Tag *', + 'ui.fileareas.tag_help' => 'Tag de la zone de fichiers (ex. : NODELIST, GENERAL_FILES)', + 'ui.fileareas.description_required' => 'Description *', + 'ui.fileareas.description_help' => 'Brève description de la zone de fichiers', + 'ui.fileareas.network_domain' => 'Domaine réseau', + 'ui.fileareas.max_file_size_mb' => 'Taille maximale des fichiers (Mo)', + 'ui.fileareas.maximum_file_size' => 'Taille maximale des fichiers', + 'ui.fileareas.tic_password_optional' => 'Mot de passe TIC (Optionnel)', + 'ui.fileareas.tic_password_help' => 'Mot de passe de l\'écho de fichiers pour les fichiers TIC (champ Pw FSC-87)', + 'ui.fileareas.allowed_extensions' => 'Extensions autorisées', + 'ui.fileareas.extensions_placeholder_allowed' => 'ex. : zip,txt,pdf', + 'ui.fileareas.allowed_extensions_help' => 'Liste séparée par des virgules (laisser vide pour tout autoriser)', + 'ui.fileareas.blocked_extensions' => 'Extensions bloquées', + 'ui.fileareas.extensions_placeholder_blocked' => 'ex. : exe,bat,sh', + 'ui.fileareas.blocked_extensions_help' => 'Liste séparée par des virgules', + 'ui.fileareas.replace_existing_files' => 'Remplacer les fichiers existants', + 'ui.fileareas.replace_existing_files_help' => 'Remplacer les fichiers portant le même nom au lieu de les versionner', + 'ui.fileareas.allow_duplicate_content' => 'Autoriser le contenu en double', + 'ui.fileareas.allow_duplicate_content_help' => 'Autoriser le même contenu de fichier (hash) avec des noms de fichiers différents', + 'ui.fileareas.local_only' => 'Local uniquement', + 'ui.fileareas.local_only_help' => 'Ne pas transmettre les fichiers aux liens montants', + 'ui.fileareas.upload_permission' => 'Permission d\'envoi', + 'ui.fileareas.upload_users_can_upload' => 'Les utilisateurs peuvent envoyer', + 'ui.fileareas.upload_admin_only' => 'Administrateur uniquement', + 'ui.fileareas.upload_read_only' => 'Lecture seule (aucun envoi)', + 'ui.fileareas.upload_permission_help' => 'Contrôler qui peut envoyer des fichiers dans cette zone', + 'ui.fileareas.virus_scanning' => 'Analyse antivirus', + 'ui.fileareas.virus_scanning_help' => 'Analyser les fichiers envoyés pour détecter les virus (nécessite ClamAV)', + 'ui.fileareas.file_area_is_active' => 'La zone de fichiers est active', + 'ui.fileareas.edit_rules' => 'Modifier les règles', + 'ui.fileareas.delete_confirm_prefix' => 'Êtes-vous sûr de vouloir supprimer cette zone de fichiers :', + 'ui.fileareas.delete_confirm_warning' => 'Cela supprimera également tous les fichiers de cette zone !', + 'ui.fileareas.no_file_areas_found' => 'Aucune zone de fichiers trouvée', + 'ui.fileareas.tag' => 'Tag', + 'ui.fileareas.files' => 'Fichiers', + 'ui.fileareas.size' => 'Taille', + 'ui.fileareas.local' => 'Local', + 'ui.fileareas.replace' => 'Remplacer', + 'ui.admin.auto_feed.load_details_failed' => 'Échec du chargement des détails du flux', + 'ui.admin.auto_feed.operation_failed' => 'Opération échouée', + 'ui.admin.auto_feed.deleted_success' => 'Flux supprimé avec succès', + 'ui.admin.auto_feed.updated_success' => 'Flux mis à jour avec succès', + 'ui.admin.auto_feed.created_success' => 'Flux créé avec succès', + 'ui.admin.auto_feed.delete_failed' => 'Échec de la suppression', + 'ui.admin.auto_feed.check_failed' => 'Échec de la vérification', + 'ui.admin.auto_feed.checked_articles_posted' => 'Flux vérifié : {count} nouvel(s) article(s) publié(s)', + 'ui.admin.auto_feed.page_title' => 'Flux automatique', + 'ui.admin.auto_feed.heading' => 'Flux automatique', + 'ui.admin.auto_feed.statistics' => 'Statistiques', + 'ui.admin.auto_feed.total_feeds' => 'Total des flux', + 'ui.admin.auto_feed.add_feed' => 'Ajouter un flux', + 'ui.admin.auto_feed.edit_feed' => 'Modifier le flux', + 'ui.admin.auto_feed.rss_atom_feeds' => 'Flux RSS/Atom', + 'ui.admin.auto_feed.loading_feeds' => 'Chargement des flux...', + 'ui.admin.auto_feed.loading_echo_areas' => 'Chargement des zones d\'écho...', + 'ui.admin.auto_feed.loading_users' => 'Chargement des utilisateurs...', + 'ui.admin.auto_feed.active_label' => 'Actif', + 'ui.admin.auto_feed.inactive_label' => 'Inactif', + 'ui.admin.auto_feed.articles_posted' => 'Articles publiés', + 'ui.admin.auto_feed.about_title' => 'À propos', + 'ui.admin.auto_feed.about_text' => 'Le flux automatique surveille les flux RSS/Atom et publie automatiquement les nouveaux articles dans les zones d\'écho spécifiées.', + 'ui.admin.auto_feed.cron_hint_prefix' => 'Exécuter', + 'ui.admin.auto_feed.cron_hint_suffix' => 'via cron pour vérifier les flux périodiquement.', + 'ui.admin.auto_feed.feed_name' => 'Nom du flux', + 'ui.admin.auto_feed.feed_name_placeholder' => 'ex. : Actualités tech, Titres BBC', + 'ui.admin.auto_feed.feed_name_help' => 'Nom convivial pour ce flux', + 'ui.admin.auto_feed.feed_url_required' => 'URL du flux *', + 'ui.admin.auto_feed.feed_url_placeholder' => 'https://example.com/feed.rss', + 'ui.admin.auto_feed.feed_url_help' => 'URL du flux RSS ou Atom', + 'ui.admin.auto_feed.echo_area_required' => 'Zone d\'écho *', + 'ui.admin.auto_feed.echo_area' => 'Zone d\'écho', + 'ui.admin.auto_feed.echo_area_help' => 'Zone d\'écho dans laquelle publier les articles', + 'ui.admin.auto_feed.post_as_user_required' => 'Publier en tant qu\'utilisateur *', + 'ui.admin.auto_feed.post_as' => 'Publier en tant que', + 'ui.admin.auto_feed.post_as_user_help' => 'Compte utilisateur avec lequel publier les messages', + 'ui.admin.auto_feed.max_articles' => 'Articles max', + 'ui.admin.auto_feed.per_check' => 'Par vérification', + 'ui.admin.auto_feed.confirm_delete_title' => 'Confirmer la suppression', + 'ui.admin.auto_feed.confirm_delete_text_prefix' => 'Êtes-vous sûr de vouloir supprimer le flux', + 'ui.admin.auto_feed.confirm_delete_warning' => 'Cette action est irréversible.', + 'ui.admin.auto_feed.invalid_api_response' => 'Réponse API invalide', + 'ui.admin.auto_feed.failed_to_load_feeds_prefix' => 'Échec du chargement des flux :', + 'ui.admin.auto_feed.no_feeds_configured' => 'Aucun flux configuré', + 'ui.admin.auto_feed.never' => 'Jamais', + 'ui.admin.auto_feed.unnamed_feed' => 'Flux sans nom', + 'ui.admin.auto_feed.unknown' => 'Inconnu', + 'ui.admin.auto_feed.check_now' => 'Vérifier maintenant', + 'ui.admin.auto_feed.checking_feed' => 'Vérification du flux...', + 'ui.admin.auto_feed.select_echo_area' => 'Sélectionner une zone d\'écho...', + 'ui.admin.auto_feed.name_url' => 'Nom/URL', + 'ui.admin.auto_feed.articles' => 'Articles', + 'ui.admin.auto_feed.last_check' => 'Dernière vérification', + 'ui.admin.auto_feed.actions' => 'Actions', + 'ui.admin.auto_feed.user_prefix' => 'Utilisateur n°', + 'ui.echoareas.page_title' => 'Zones d\'écho', + 'ui.echoareas.heading' => 'Gestion des zones d\'écho', + 'ui.echoareas.add_echo_area' => 'Ajouter une zone d\'écho', + 'ui.echoareas.edit_echo_area' => 'Modifier la zone d\'écho', + 'ui.echoareas.active_echo_areas' => 'Zones d\'écho actives', + 'ui.echoareas.search_tags_placeholder' => 'Rechercher des tags...', + 'ui.echoareas.loading_echo_areas' => 'Chargement des zones d\'écho...', + 'ui.echoareas.statistics' => 'Statistiques', + 'ui.echoareas.active_areas' => 'Zones actives :', + 'ui.echoareas.total_messages' => 'Total des messages :', + 'ui.echoareas.todays_messages' => 'Messages du jour :', + 'ui.echoareas.color_legend' => 'Légende des couleurs', + 'ui.echoareas.color_legend_help' => 'Les zones d\'écho peuvent être codées par couleur pour une identification facile :', + 'ui.echoareas.color_green' => 'Vert', + 'ui.echoareas.color_blue' => 'Bleu', + 'ui.echoareas.color_red' => 'Rouge', + 'ui.echoareas.color_yellow' => 'Jaune', + 'ui.echoareas.color_purple' => 'Violet', + 'ui.echoareas.color_orange' => 'Orange', + 'ui.echoareas.color_teal' => 'Sarcelle', + 'ui.echoareas.color_cyan' => 'Cyan', + 'ui.echoareas.color_light_blue' => 'Bleu clair', + 'ui.echoareas.color_indigo' => 'Indigo', + 'ui.echoareas.color_pink' => 'Rose', + 'ui.echoareas.color_gray' => 'Gris', + 'ui.echoareas.legend_general_topics' => 'Sujets généraux', + 'ui.echoareas.legend_technical_testing' => 'Technique/Test', + 'ui.echoareas.legend_important_official' => 'Important/Officiel', + 'ui.echoareas.legend_administrative' => 'Administratif', + 'ui.echoareas.legend_special_interest' => 'Intérêt particulier', + 'ui.echoareas.legend_regional' => 'Régional', + 'ui.echoareas.tag_required' => 'Étiquette *', + 'ui.echoareas.tag_help' => 'Étiquette de la zone echo (ex. : FIDONET.GEN, LOCAL.TEST)', + 'ui.echoareas.description_required' => 'Description *', + 'ui.echoareas.description_help' => 'Brève description de la zone echo', + 'ui.echoareas.uplink_address' => 'Adresse de liaison montante', + 'ui.echoareas.uplink_address_help' => 'Remplacer l\'adresse FidoNet de liaison montante', + 'ui.echoareas.color' => 'Couleur', + 'ui.echoareas.selected_color' => 'Sélectionnée :', + 'ui.echoareas.network_domain_help' => 'Domaine réseau (ex. : fidonet, fsxnet, etc.)', + 'ui.echoareas.moderator' => 'Modérateur', + 'ui.echoareas.moderator_help' => 'Nom du modérateur de la zone echo', + 'ui.echoareas.posting_name_policy' => 'Politique de nom de publication', + 'ui.echoareas.posting_name_policy_inherit' => 'Utiliser le défaut de la liaison montante', + 'ui.echoareas.posting_name_policy_real_name' => 'Nom réel', + 'ui.echoareas.posting_name_policy_username' => 'Nom d\'utilisateur', + 'ui.echoareas.posting_name_policy_help' => 'Remplace la politique de nom de publication de la liaison montante pour cette zone echo.', + 'ui.echoareas.local_only' => 'Local uniquement', + 'ui.echoareas.local_only_help' => 'Les messages des zones locales ne sont pas transmis aux liaisons montantes', + 'ui.echoareas.sysop_access_only' => 'Accès sysop uniquement', + 'ui.echoareas.sysop_access_only_help' => 'Restreindre cette zone echo aux utilisateurs sysop/admin uniquement', + 'ui.echoareas.public_gemini_access' => 'Accès Gemini public', + 'ui.echoareas.public_gemini_access_help' => 'Exposer cette zone echo en lecture seule sur le serveur de capsule Gemini', + 'ui.echoareas.delete_confirm_prefix' => 'Êtes-vous sûr de vouloir supprimer la zone echo', + 'ui.echoareas.delete_confirm_warning' => 'Cette action est irréversible et affectera le routage des messages.', + 'ui.echoareas.none_found' => 'Aucune zone echo trouvée', + 'ui.echoareas.tag' => 'Étiquette', + 'ui.echoareas.messages' => 'Messages', + 'ui.echoareas.uplink' => 'Liaison montante', + 'ui.echoareas.local' => 'Local', + 'ui.echoareas.sysop' => 'Sysop', + 'ui.echoareas.local_only_title' => 'Local uniquement - non transmis aux liaisons montantes', + 'ui.echoareas.sysop_access_only_title' => 'Accès sysop uniquement', + 'ui.echoareas.mod_prefix' => 'Mod :', + 'ui.echoareas.load_failed' => 'Échec du chargement des zones echo', + 'ui.echoareas.load_details_failed' => 'Échec du chargement des détails de la zone echo', + 'ui.echoareas.updated_success' => 'Zone echo mise à jour avec succès', + 'ui.echoareas.created_success' => 'Zone echo créée avec succès', + 'ui.echoareas.deleted_success' => 'Zone echo supprimée avec succès', + 'ui.echoareas.sync_not_implemented' => 'La fonctionnalité de synchronisation n\'est pas encore implémentée', + 'ui.echoareas.export_not_implemented' => 'La fonctionnalité d\'exportation n\'est pas encore implémentée', + 'ui.echoareas.validate_not_implemented' => 'La fonctionnalité de validation n\'est pas encore implémentée', + 'ui.shell.menu' => 'Menu', + 'ui.shell.menu_main_q_title' => 'Menu principal (Q)', + 'ui.login.title' => 'Connexion', + 'ui.login.login_to_system' => 'Connexion à {system_name}', + 'ui.login.username' => 'Nom d\'utilisateur', + 'ui.login.password' => 'Mot de passe', + 'ui.login.remember_me_30_days' => 'Se souvenir de moi (30 jours)', + 'ui.login.no_account' => 'Vous n\'avez pas de compte ?', + 'ui.login.request_access' => 'Demander un accès', + 'ui.login.forgot_password' => 'Mot de passe oublié ?', + 'ui.login.reset_password' => 'Réinitialiser le mot de passe', + 'ui.login.sending' => 'Envoi en cours...', + 'ui.login.reminder_sent_netmail_email_suffix' => '(envoyé par netmail et e-mail)', + 'ui.login.reminder_sent_netmail_only_suffix' => '(envoyé par netmail)', + 'ui.login.connect_via_telnet' => 'Se connecter via Telnet', + 'ui.guest_doors.page_title' => 'Portes invité', + 'ui.guest_doors.heading' => 'Accès invité', + 'ui.guest_doors.subtitle' => 'Choisissez une porte ci-dessous pour commencer.', + 'ui.guest_doors.no_doors' => 'Aucune porte invité n\'est actuellement disponible.', + 'ui.guest_doors.connect' => 'Se connecter', + 'ui.guest_doors.signin_link' => 'Se connecter', + 'ui.guest_doors.register_link' => 'demander un compte', + 'ui.register.title' => 'Inscription', + 'ui.register.create_account' => 'Créer un compte', + 'ui.register.approval_required' => 'L\'inscription nécessite une approbation.', + 'ui.register.approval_required_help' => 'Votre compte sera examiné par un administrateur avant activation.', + 'ui.register.subject_to_rules' => 'Tous les comptes sont soumis aux', + 'ui.register.honeypot_website_leave_blank' => 'Site web (laisser vide)', + 'ui.register.username' => 'Nom d\'utilisateur', + 'ui.register.username_help' => '3 à 20 caractères, lettres, chiffres et underscores uniquement', + 'ui.register.password' => 'Mot de passe', + 'ui.register.password_help' => 'Minimum 8 caractères. Utilisez un mélange de lettres, chiffres et symboles pour plus de sécurité.', + 'ui.register.confirm_password' => 'Confirmer le mot de passe', + 'ui.register.email_address' => 'Adresse e-mail', + 'ui.register.email_help' => 'Facultatif - pour la récupération de compte et les notifications', + 'ui.register.real_name' => 'Nom réel', + 'ui.register.real_name_help' => 'Votre vrai nom (requis pour FidoNet)', + 'ui.register.location' => 'Localisation', + 'ui.register.location_placeholder' => 'Ville, État/Pays', + 'ui.register.location_help' => 'Votre localisation (facultatif, affiché dans la liste des connectés)', + 'ui.register.reason_for_joining' => 'Raison de l\'inscription', + 'ui.register.reason_placeholder' => 'Dites-nous pourquoi vous souhaitez nous rejoindre...', + 'ui.register.submit_registration' => 'Soumettre l\'inscription', + 'ui.register.already_have_account' => 'Vous avez déjà un compte ?', + 'ui.register.sign_in' => 'Se connecter', + 'ui.register.password_strength.weak' => 'Faible - Ajoutez plus de variété', + 'ui.register.password_strength.fair' => 'Moyen - Pourrait être plus fort', + 'ui.register.password_strength.good' => 'Bon - Beau mot de passe !', + 'ui.register.password_strength.strong' => 'Fort - Excellent !', + 'ui.register.passwords_do_not_match' => 'Les mots de passe ne correspondent pas.', + 'ui.register.submitting' => 'Envoi en cours...', + 'ui.register.submitted_success' => 'Inscription soumise avec succès ! Vous serez notifié lorsque votre compte sera approuvé.', + 'ui.register.go_to_login' => 'Aller à la connexion', + 'ui.forgot_password.title' => 'Mot de passe oublié', + 'ui.forgot_password.reset_password' => 'Réinitialiser le mot de passe', + 'ui.forgot_password.instructions' => 'Entrez votre nom d\'utilisateur ou votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.', + 'ui.forgot_password.username_or_email' => 'Nom d\'utilisateur ou e-mail', + 'ui.forgot_password.username_or_email_help' => 'Entrez le nom d\'utilisateur ou l\'adresse e-mail associé à votre compte.', + 'ui.forgot_password.send_reset_link' => 'Envoyer le lien de réinitialisation', + 'ui.forgot_password.remember_password' => 'Vous vous souvenez de votre mot de passe ?', + 'ui.forgot_password.back_to_login' => 'Retour à la connexion', + 'ui.forgot_password.sending' => 'Envoi en cours...', + 'ui.forgot_password.check_email_notice' => 'Veuillez vérifier votre boîte de réception (et le dossier spam) pour le lien de réinitialisation.', + 'ui.forgot_password.request_failed' => 'Échec du traitement de la demande', + 'ui.forgot_password.reset_link_sent_if_exists' => 'Si un compte avec ce nom d\'utilisateur ou cet e-mail existe, un lien de réinitialisation du mot de passe a été envoyé.', + 'ui.password_reset_email.subject' => 'Demande de réinitialisation de mot de passe - {system_name}', + 'ui.password_reset_email.header' => 'Demande de réinitialisation de mot de passe', + 'ui.password_reset_email.greeting' => 'Bonjour {name},', + 'ui.password_reset_email.request_received' => 'Nous avons reçu une demande de réinitialisation de votre mot de passe pour votre compte sur {system_name}.', + 'ui.password_reset_email.click_link_below' => 'Pour réinitialiser votre mot de passe, veuillez cliquer sur le lien ci-dessous :', + 'ui.password_reset_email.click_button_below' => 'Pour réinitialiser votre mot de passe, veuillez cliquer sur le bouton ci-dessous :', + 'ui.password_reset_email.button' => 'Réinitialiser votre mot de passe', + 'ui.password_reset_email.copy_link' => 'Ou copiez et collez ce lien dans votre navigateur :', + 'ui.password_reset_email.expires_in_hours' => 'Ce lien expirera dans {hours} heures.', + 'ui.password_reset_email.security_notes' => 'Notes de sécurité :', + 'ui.password_reset_email.note_never_share' => 'Ne partagez jamais ce lien avec quiconque', + 'ui.password_reset_email.note_one_time' => 'Ce lien ne peut être utilisé qu\'une seule fois', + 'ui.password_reset_email.note_request_new' => 'Si vous avez besoin d\'un autre lien de réinitialisation, faites-en la demande depuis la page de connexion', + 'ui.password_reset_email.if_not_requested' => 'Si vous n\'avez pas demandé de réinitialisation de mot de passe, vous pouvez ignorer cet e-mail.', + 'ui.password_reset_email.password_unchanged_notice' => 'Votre mot de passe ne sera pas modifié tant que vous n\'aurez pas cliqué sur le lien ci-dessus et créé un nouveau mot de passe.', + 'ui.password_reset_email.best_regards' => 'Cordialement,', + 'ui.password_reset_email.footer_automated' => 'Ceci est un message automatique de {system_name}', + 'ui.password_reset_email.footer_no_reply' => 'Veuillez ne pas répondre à cet e-mail.', + 'ui.reset_password.title' => 'Réinitialiser le mot de passe', + 'ui.reset_password.create_new_password' => 'Créer un nouveau mot de passe', + 'ui.reset_password.validating_token' => 'Validation du jeton de réinitialisation...', + 'ui.reset_password.invalid_or_expired_token' => 'Jeton invalide ou expiré', + 'ui.reset_password.invalid_token_help_line1' => 'Ce lien de réinitialisation de mot de passe est invalide ou a expiré.', + 'ui.reset_password.invalid_token_help_line2' => 'Veuillez demander un nouveau lien de réinitialisation de mot de passe.', + 'ui.reset_password.enter_new_password' => 'Veuillez entrer votre nouveau mot de passe ci-dessous.', + 'ui.reset_password.new_password' => 'Nouveau mot de passe', + 'ui.reset_password.password_min_length_help' => 'Le mot de passe doit comporter au moins 8 caractères.', + 'ui.reset_password.confirm_new_password' => 'Confirmer le nouveau mot de passe', + 'ui.reset_password.passwords_do_not_match' => 'Les mots de passe ne correspondent pas.', + 'ui.reset_password.request_new_reset_link' => 'Demander un nouveau lien de réinitialisation', + 'ui.reset_password.resetting' => 'Réinitialisation en cours...', + 'ui.reset_password.redirect_notice' => 'Vous serez redirigé vers la page de connexion dans 3 secondes...', + 'ui.reset_password.success_reset_complete' => 'Le mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.', + 'ui.profile.title' => 'Profil', + 'ui.profile.my_profile' => 'Mon profil', + 'ui.profile.profile_information' => 'Informations du profil', + 'ui.profile.username' => 'Nom d\'utilisateur', + 'ui.profile.username_readonly_help' => 'Le nom d\'utilisateur ne peut pas être modifié', + 'ui.profile.real_name' => 'Nom réel', + 'ui.profile.real_name_help' => 'Votre vrai nom tel qu\'affiché dans les messages', + 'ui.profile.email_address' => 'Adresse e-mail', + 'ui.profile.email_help' => 'Adresse e-mail (facultative, non affichée publiquement)', + 'ui.profile.location' => 'Localisation', + 'ui.profile.location_placeholder' => 'Ville, État/Pays', + 'ui.profile.location_help' => 'Votre localisation (affichée dans la liste des connectés)', + 'ui.profile.change_password' => 'Changer le mot de passe', + 'ui.profile.current_password' => 'Mot de passe actuel', + 'ui.profile.new_password' => 'Nouveau mot de passe', + 'ui.profile.password_blank_keep_help' => 'Laissez les champs de mot de passe vides pour conserver le mot de passe actuel', + 'ui.profile.account_created' => 'Compte créé', + 'ui.profile.last_login' => 'Dernière connexion', + 'ui.profile.never' => 'Jamais', + 'ui.profile.update_profile' => 'Mettre à jour le profil', + 'ui.profile.system_information' => 'Informations système', + 'ui.profile.system' => 'Système', + 'ui.profile.address' => 'Adresse', + 'ui.profile.role' => 'Rôle', + 'ui.profile.role_administrator' => 'Administrateur', + 'ui.profile.role_user' => 'Utilisateur', + 'ui.profile.activity_summary' => 'Résumé d\'activité', + 'ui.profile.credits' => 'Crédits', + 'ui.profile.disabled' => 'Désactivé', + 'ui.profile.netmail_sent' => 'Netmails envoyés', + 'ui.profile.echomail_posted' => 'Echomails publiés', + 'ui.profile.total_messages' => 'Total des messages', + 'ui.profile.credit_system' => 'Système de crédits', + 'ui.profile.credit_costs_rewards' => 'Coûts et récompenses en crédits :', + 'ui.profile.daily_login' => 'Connexion quotidienne', + 'ui.profile.crashmail' => 'Crashmail', + 'ui.profile.credit_note' => 'Les portes web et autres éléments du système peuvent avoir des coûts ou récompenses supplémentaires.', + 'ui.profile.updating_profile' => 'Mise à jour du profil...', + 'ui.profile.updated_successfully' => 'Profil mis à jour avec succès !', + 'ui.user_profile.title_with_username' => 'Profil de {username}', + 'ui.user_profile.edit_my_profile' => 'Modifier mon profil', + 'ui.user_profile.user_information' => 'Informations utilisateur', + 'ui.user_profile.username' => 'Nom d\'utilisateur', + 'ui.user_profile.real_name' => 'Vrai nom', + 'ui.user_profile.not_specified' => 'Non spécifié', + 'ui.user_profile.location' => 'Localisation', + 'ui.user_profile.fidonet_address' => 'Adresse FidoNet', + 'ui.user_profile.member_since' => 'Membre depuis', + 'ui.user_profile.last_seen' => 'Dernière visite', + 'ui.user_profile.role' => 'Rôle', + 'ui.user_profile.role_administrator' => 'Administrateur', + 'ui.user_profile.role_user' => 'Utilisateur', + 'ui.user_profile.credits' => 'Crédits', + 'ui.user_profile.current_balance' => 'Solde actuel', + 'ui.user_profile.send_credits' => 'Envoyer des crédits', + 'ui.user_profile.amount' => 'Montant', + 'ui.user_profile.send_credits_limit_fee' => 'Maximum 200 crédits par transaction. Des frais de {fee_percent}% s\'appliquent.', + 'ui.user_profile.message_optional' => 'Message (facultatif)', + 'ui.user_profile.message_optional_placeholder' => 'À quoi cela correspond-il ?', + 'ui.user_profile.send_credits_fee_note' => 'Frais de transaction de {fee_percent}% distribués aux opérateurs système', + 'ui.user_profile.activity_summary' => 'Résumé d\'activité', + 'ui.user_profile.netmail_sent' => 'Netmails envoyés', + 'ui.user_profile.echomail_posted' => 'Echomails publiés', + 'ui.user_profile.total_messages' => 'Total des messages', + 'ui.user_profile.additional_info_private' => 'Les informations supplémentaires du profil sont privées', + 'ui.user_profile.transaction_history' => 'Historique des transactions', + 'ui.user_profile.admin_view' => 'Vue administrateur', + 'ui.user_profile.date' => 'Date', + 'ui.user_profile.type' => 'Type', + 'ui.user_profile.description' => 'Description', + 'ui.user_profile.balance' => 'Solde', + 'ui.user_profile.load_more_transactions' => 'Charger plus de transactions', + 'ui.user_profile.no_transactions_yet' => 'Aucune transaction pour l\'instant', + 'ui.user_profile.no_more_transactions' => 'Plus de transactions', + 'ui.user_profile.load_transactions_failed' => 'Échec du chargement des transactions', + 'ui.user_profile.sending' => 'Envoi en cours...', + 'ui.user_profile.send_credits_success' => '{symbol}{amount} envoyés avec succès à {username} ! (Frais : {symbol}{fee}, Reçu : {symbol}{received})', + 'ui.user_profile.transaction_type.daily_login' => 'Connexion quotidienne', + 'ui.user_profile.transaction_type.system_reward' => 'Récompense système', + 'ui.user_profile.transaction_type.payment' => 'Paiement', + 'ui.user_profile.transaction_type.admin_adjustment' => 'Ajustement administrateur', + 'ui.user_profile.transaction_type.unknown' => 'Inconnu', + 'ui.shared_message.title' => 'Message partagé', + 'ui.shared_message.loading' => 'Chargement du message partagé...', + 'ui.shared_message.login_to_reply' => 'Se connecter pour répondre', + 'ui.shared_message.unable_to_load' => 'Impossible de charger le message', + 'ui.shared_message.go_to_main_site' => 'Aller au site principal', + 'ui.shared_message.login_required' => 'Connexion requise', + 'ui.shared_message.login_required_help' => 'Ce message partagé nécessite d\'être connecté pour le consulter.', + 'ui.shared_message.about_this_message' => 'À propos de ce message', + 'ui.shared_message.about_text' => 'Ce message provient d\'un système echomail. L\'echomail fait partie d\'un réseau mondial de babillards électroniques permettant aux utilisateurs d\'envoyer des messages et de participer à des zones de discussion (echoareas) sur différents systèmes.', + 'ui.shared_message.join_conversation' => 'Envie de rejoindre la conversation ?', + 'ui.shared_message.join_conversation_help' => 'Pour consulter davantage de messages, participer aux discussions et répondre à des publications comme celle-ci, vous devez créer un compte sur ce système.', + 'ui.shared_message.register_now' => 'S\'inscrire maintenant', + 'ui.shared_message.already_have_account' => 'Déjà un compte ?', + 'ui.shared_message.network' => 'Réseau', + 'ui.shared_message.powered_by' => 'Propulsé par', + 'ui.shared_message.shared_on' => 'Ce message a été partagé', + 'ui.shared_message.shared_by' => 'Partagé par {name}', + 'ui.shared_message.viewed_times' => 'Consulté {count} fois', + 'ui.shared_message.local_bulletin_board' => 'Babillard local', + 'ui.shared_message.from_label' => 'De :', + 'ui.shared_message.to_label' => 'À :', + 'ui.shared_message.date_label' => 'Date :', + 'ui.shared_message.subject_label' => 'Sujet :', + 'ui.shared_message.kludge_lines' => 'Lignes kludge', + 'ui.shared_message.show_kludge_lines' => 'Afficher les lignes kludge', + 'ui.shared_message.load_failed' => 'Échec du chargement du message partagé', + 'ui.shared_message.not_available' => 'Ce message partagé n\'est pas disponible. Il a peut-être expiré ou été révoqué.', + 'ui.shared_message.load_failed_retry' => 'Échec du chargement du message partagé. Veuillez réessayer ultérieurement.', + 'ui.netmail.title' => 'Netmail', + 'ui.netmail.compose' => 'Rédiger', + 'ui.netmail.messages' => 'Messages', + 'ui.netmail.delete_selected' => 'Supprimer la sélection', + 'ui.netmail.quick_stats' => 'Statistiques rapides', + 'ui.address_book.title' => 'Carnet d\'adresses', + 'ui.address_book.search_placeholder' => 'Rechercher des adresses...', + 'ui.address_book.entries' => 'entrées', + 'ui.address_book.add_entry' => 'Ajouter une entrée au carnet d\'adresses', + 'ui.address_book.edit_entry' => 'Modifier une entrée du carnet d\'adresses', + 'ui.address_book.no_entries_found' => 'Aucune entrée trouvée', + 'ui.address_book.unnamed' => 'Sans nom', + 'ui.address_book.no_address' => 'Aucune adresse', + 'ui.address_book.user_id' => 'ID utilisateur', + 'ui.address_book.node_address' => 'Adresse de nœud', + 'ui.address_book.email_address' => 'Adresse e-mail', + 'ui.address_book.name_placeholder' => 'Entrez un nom descriptif (ex. : Jean Dupont)', + 'ui.address_book.name_help' => 'Nom descriptif pour ce contact', + 'ui.address_book.user_id_placeholder' => 'Entrez l\'ID utilisateur pour la messagerie', + 'ui.address_book.user_id_help' => 'ID utilisateur/pseudonyme à utiliser lors de l\'envoi de messages', + 'ui.address_book.node_address_help' => 'Format : zone:net/node ou zone:net/node.point', + 'ui.address_book.email_help' => 'Pour votre référence uniquement - non utilisé pour la messagerie', + 'ui.address_book.description_placeholder' => 'Notes sur ce contact...', + 'ui.address_book.always_crashmail' => 'Toujours utiliser le crashmail pour ce destinataire', + 'ui.address_book.always_crashmail_help' => 'Activer automatiquement le crashmail lors de la rédaction de messages à ce contact.', + 'ui.echomail.title' => 'Echomail', + 'ui.echomail.post_message' => 'Publier un message', + 'ui.echomail.viewing_prefix' => 'Affichage :', + 'ui.echomail.viewing_all' => 'Affichage : Tous les messages', + 'ui.echomail.echo_list' => 'Liste des echos', + 'ui.echomail.search_areas_placeholder' => 'Rechercher des zones...', + 'ui.echomail.loading_areas' => 'Chargement des zones...', + 'ui.echomail.recent_messages' => 'Messages récents', + 'ui.echomail.to_me' => 'Pour moi', + 'ui.echomail.saved_items' => 'Éléments sauvegardés', + 'ui.echomail.echo_areas' => 'Zones echo', + 'ui.echomail.areas' => 'Zones', + 'ui.echomail.manage_subscriptions' => 'Gérer les abonnements', + 'ui.echomail.share_message' => 'Partager le message', + 'ui.echomail.share_message_help' => 'Partagez ce message echomail avec d\'autres via un lien web.', + 'ui.echomail.allow_anonymous_access' => 'Autoriser l\'accès anonyme', + 'ui.echomail.allow_anonymous_access_help' => 'Toute personne disposant du lien peut le consulter sans se connecter', + 'ui.echomail.link_expires_after' => 'Le lien expire après :', + 'ui.echomail.never_expires' => 'N\'expire jamais', + 'ui.echomail.share_link_created_success' => 'Lien de partage créé avec succès ! Vous pouvez maintenant copier et partager cette URL.', + 'ui.echomail.create_share_link' => 'Créer un lien de partage', + 'ui.echomail.get_friendly_url' => 'Obtenir une URL conviviale', + 'ui.echomail.revoke_share' => 'Révoquer le partage', + 'ui.echomail.messages_refreshed' => 'Messages actualisés', + 'ui.echomail.saved_items.removed' => 'Message retiré des éléments sauvegardés', + 'ui.echomail.saved_items.saved' => 'Message sauvegardé pour plus tard', + 'ui.echomail.shares.using_existing' => 'Utilisation du lien de partage existant', + 'ui.echomail.shares.created_success' => 'Lien de partage créé avec succès !', + 'ui.echomail.shares.friendly_url_generated' => 'URL conviviale générée !', + 'ui.echomail.shares.revoked' => 'Lien de partage révoqué', + 'ui.echomail.shares.url_copied' => 'URL de partage copiée dans le presse-papiers !', + 'ui.files.title' => 'Zones de Fichiers', + 'ui.files.security_notice_label' => 'Avis de sécurité :', + 'ui.files.security_notice_text' => 'Vous êtes responsable de vous assurer que les fichiers que vous téléchargez sont sûrs. Utilisez un logiciel de protection contre les logiciels malveillants approprié.', + 'ui.files.recent_uploads' => 'Téléversements récents', + 'ui.files.upload_file' => 'Téléverser un fichier', + 'ui.files.search_placeholder' => 'Rechercher des fichiers...', + 'ui.files.loading_recent_uploads' => 'Chargement des téléversements récents...', + 'ui.files.statistics' => 'Statistiques', + 'ui.files.total_areas' => 'Total des zones', + 'ui.files.total_files' => 'Total des fichiers', + 'ui.files.total_size' => 'Taille totale', + 'ui.files.file_details' => 'Détails du fichier', + 'ui.files.filename' => 'Nom du fichier', + 'ui.files.size' => 'Taille', + 'ui.files.uploaded' => 'Téléversé', + 'ui.files.from' => 'De', + 'ui.files.virus_scan' => 'Analyse antivirus', + 'ui.files.download' => 'Télécharger', + 'ui.files.share_file' => 'Partager le fichier', + 'ui.files.link_expiry' => 'Expiration du lien', + 'ui.files.share_link' => 'Lien de partage', + 'ui.files.revoke_link' => 'Révoquer le lien', + 'ui.files.create_share_link' => 'Créer un lien de partage', + 'ui.files.select_file_required' => 'Sélectionner un fichier *', + 'ui.files.maximum_file_size' => 'Taille maximale du fichier', + 'ui.files.short_description_required' => 'Description courte *', + 'ui.files.short_description_help' => 'Brève description affichée dans les listes de fichiers.', + 'ui.files.long_description' => 'Description longue', + 'ui.files.long_description_help' => 'Description étendue facultative (texte brut uniquement).', + 'ui.files.upload' => 'Téléverser', + 'ui.files.recent_uploads_load_failed' => 'Échec du chargement des téléversements récents', + 'ui.files.no_recent_uploads' => 'Aucun fichier n\'a encore été téléversé', + 'ui.files.area' => 'Zone', + 'ui.files.actions' => 'Actions', + 'ui.files.virus_scan_now' => 'Analyse antivirus', + 'ui.files.scanning' => 'Analyse en cours…', + 'ui.files.scan_failed' => 'Échec de l\'analyse antivirus', + 'ui.files.virus_scan_clean' => 'Analyse antivirus : Sain', + 'ui.files.virus_detected_prefix' => 'Virus détecté : ', + 'ui.files.virus_scan_error' => 'Analyse antivirus : Erreur', + 'ui.files.virus_scan_skipped' => 'Analyse antivirus : Ignorée', + 'ui.files.shared' => 'Partagé', + 'ui.files.file_areas_load_failed' => 'Échec du chargement des zones de fichiers', + 'ui.files.no_file_areas' => 'Aucune zone de fichiers disponible', + 'ui.files.search_areas_placeholder' => 'Rechercher des zones…', + 'ui.files.files_count_label' => 'fichiers', + 'ui.files.loading_files' => 'Chargement des fichiers...', + 'ui.files.load_files_failed' => 'Échec du chargement des fichiers', + 'ui.files.no_files_in_area' => 'Aucun fichier dans cette zone', + 'ui.files.not_scanned' => 'Non analysé', + 'ui.files.clean' => 'Sain', + 'ui.files.infected' => 'Infecté', + 'ui.files.scan_error' => 'Erreur d\'analyse', + 'ui.files.skipped' => 'Ignoré', + 'ui.files.no_description' => 'Aucune description', + 'ui.files.load_details_failed' => 'Échec du chargement des détails du fichier', + 'ui.files.select_area_first' => 'Veuillez d\'abord sélectionner une zone de fichiers', + 'ui.files.select_file_prompt' => 'Veuillez sélectionner un fichier', + 'ui.files.upload_success' => 'Fichier téléversé avec succès', + 'ui.files.delete_confirm' => 'Êtes-vous sûr de vouloir supprimer « {filename} » ? Cette action est irréversible.', + 'ui.files.delete_success' => 'Fichier supprimé avec succès', + 'ui.files.rename' => 'Renommer', + 'ui.files.rename_file' => 'Renommer le fichier', + 'ui.files.new_filename' => 'Nouveau nom de fichier', + 'ui.files.rename_success' => 'Fichier renommé avec succès', + 'ui.files.active_share_exists' => 'Ce fichier possède déjà un lien de partage actif.', + 'ui.files.revoke_confirm' => 'Êtes-vous sûr de vouloir révoquer ce lien de partage ? Toute personne disposant du lien ne pourra plus y accéder.', + 'ui.files.share_revoked' => 'Lien de partage révoqué', + 'ui.files.share_link_copied_clipboard' => 'Lien de partage copié dans le presse-papiers', + 'ui.files.share_link_copied' => 'Lien de partage copié', + 'ui.polls.title' => 'Sondages', + 'ui.polls.create' => 'Créer un sondage', + 'ui.polls.loading' => 'Chargement du sondage...', + 'ui.polls.description' => 'Parcourez les sondages actifs et votez pour révéler les résultats.', + 'ui.polls.previous' => 'Sondage précédent', + 'ui.polls.next' => 'Sondage suivant', + 'ui.polls.create.title' => 'Créer un sondage', + 'ui.polls.create.heading' => 'Créer un nouveau sondage', + 'ui.polls.create.cost_badge' => 'Coût : {cost} crédits', + 'ui.polls.create.insufficient_credits' => 'Crédits insuffisants', + 'ui.polls.create.insufficient_credits_help' => 'Vous avez besoin de {cost} crédits pour créer un sondage, mais vous n\'avez que {balance} crédits.', + 'ui.polls.create.cost_info_prefix' => 'La création d\'un sondage coûte', + 'ui.polls.create.current_balance' => 'Votre solde actuel :', + 'ui.polls.create.credits' => 'crédits', + 'ui.polls.create.question_required' => 'Question du sondage *', + 'ui.polls.create.question_placeholder' => 'Que souhaitez-vous demander ?', + 'ui.polls.create.characters_minimum' => 'caractères (minimum 10)', + 'ui.polls.create.options_required' => 'Options du sondage * (2-10)', + 'ui.polls.create.option_placeholder' => 'Saisir une option', + 'ui.polls.create.add_option' => 'Ajouter une option', + 'ui.polls.create.options_help' => 'Chaque option doit être unique et ne pas dépasser 200 caractères', + 'ui.polls.create.note_label' => 'Remarque :', + 'ui.polls.create.note_text' => 'Une fois créés, les sondages ne peuvent pas être modifiés ni supprimés. Assurez-vous que votre question et vos options sont correctes avant de soumettre.', + 'ui.polls.create.submit' => 'Créer le sondage ({cost} crédits)', + 'ui.polls.create.max_options_allowed' => 'Maximum 10 options autorisées', + 'ui.polls.create.question_min_length' => 'La question doit comporter au moins 10 caractères', + 'ui.polls.create.min_options_required' => 'Veuillez fournir au moins 2 options', + 'ui.polls.create.options_unique_required' => 'Toutes les options doivent être uniques', + 'ui.polls.create.creating' => 'Création en cours...', + 'ui.polls.create.created_success_spent' => 'Sondage créé avec succès ! Vous avez dépensé {spent} crédits.', + 'ui.polls.create.error_prefix' => 'Erreur : ', + 'ui.shoutbox.title' => 'Shoutbox', + 'ui.shoutbox.loading' => 'Chargement des messages...', + 'ui.shoutbox.load_older' => 'Charger les anciens messages', + 'ui.shoutbox.leave_shout' => 'Laisser un message...', + 'ui.shoutbox.post' => 'Publier', + 'ui.shoutbox.max_chars' => '280 caractères maximum.', + 'ui.admin.shoutbox.page_title' => 'Shoutbox', + 'ui.admin.shoutbox.heading' => 'Modération de la Shoutbox', + 'ui.admin.shoutbox.user' => 'Utilisateur', + 'ui.admin.shoutbox.message' => 'Message', + 'ui.admin.shoutbox.status' => 'Statut', + 'ui.admin.shoutbox.created' => 'Créé', + 'ui.admin.shoutbox.actions' => 'Actions', + 'ui.admin.shoutbox.error_loading_shouts' => 'Erreur lors du chargement des messages', + 'ui.admin.shoutbox.hidden' => 'Masqué', + 'ui.admin.shoutbox.visible' => 'Visible', + 'ui.admin.shoutbox.unhide' => 'Afficher', + 'ui.admin.shoutbox.hide' => 'Masquer', + 'ui.admin.shoutbox.hidden_success' => 'Message masqué avec succès', + 'ui.admin.shoutbox.unhidden_success' => 'Message affiché avec succès', + 'ui.admin.shoutbox.deleted_success' => 'Message supprimé avec succès', + 'ui.admin.shoutbox.failed_to_update' => 'Échec de la mise à jour du message', + 'ui.admin.shoutbox.failed_to_delete' => 'Échec de la suppression du message', + 'ui.admin.shoutbox.confirm_delete' => 'Supprimer ce message ?', + 'ui.chat.page_title' => 'Chat', + 'ui.chat.heading' => 'Chat', + 'ui.chat.rooms' => 'Salons', + 'ui.chat.direct' => 'Direct', + 'ui.chat.lobby' => 'Accueil', + 'ui.chat.load_older_messages' => 'Charger les anciens messages', + 'ui.chat.type_message_placeholder' => 'Saisir un message...', + 'ui.chat.kick' => 'Expulser', + 'ui.chat.ban' => 'Bannir', + 'ui.chat.no_one_online' => 'Personne en ligne', + 'ui.chat.room' => 'Salon', + 'ui.chat.moderation_hint' => 'Clic droit ou clic pour modérer', + 'ui.chat.system' => 'Système', + 'ui.chat.confirm_kick' => 'Expulser cet utilisateur du salon ?', + 'ui.chat.confirm_ban' => 'Bannir cet utilisateur du salon ?', + 'ui.chat.send_failed' => 'Échec de l\'envoi du message', + 'ui.chat.moderation_failed' => 'Échec de la modération', + 'ui.binkp.page_title' => 'État Binkp', + 'ui.binkp.heading' => 'État Binkp', + 'ui.binkp.subheading' => 'Surveiller et gérer vos connexions Binkp TCP/IP', + 'ui.binkp.system_status' => 'État du système', + 'ui.binkp.inbound_queue' => 'File d\'attente entrante', + 'ui.binkp.outbound_queue' => 'File d\'attente sortante', + 'ui.binkp.uplinks' => 'Uplinks', + 'ui.binkp.status_tab' => 'État', + 'ui.binkp.uplinks_tab' => 'Uplinks', + 'ui.binkp.queues_tab' => 'Files d\'attente', + 'ui.binkp.logs_tab' => 'Journaux', + 'ui.binkp.system_information' => 'Informations système', + 'ui.binkp.loading_system_information' => 'Chargement des informations système...', + 'ui.binkp.uplink_status' => 'État des uplinks', + 'ui.binkp.loading_uplink_status' => 'Chargement de l\'état des uplinks...', + 'ui.binkp.poll_all' => 'Interroger tout', + 'ui.binkp.configured_uplinks' => 'Uplinks configurés', + 'ui.binkp.uplink' => 'Uplink', + 'ui.binkp.status' => 'État', + 'ui.binkp.schedule' => 'Planification', + 'ui.binkp.actions' => 'Actions', + 'ui.binkp.address' => 'Adresse', + 'ui.binkp.sysop' => 'Sysop', + 'ui.binkp.location' => 'Emplacement', + 'ui.binkp.process' => 'Traiter', + 'ui.binkp.send' => 'Envoyer', + 'ui.binkp.poll' => 'Interroger', + 'ui.binkp.enabled' => 'Activé', + 'ui.binkp.disabled' => 'Désactivé', + 'ui.binkp.online' => 'En ligne', + 'ui.binkp.offline' => 'Hors ligne', + 'ui.binkp.no_uplinks_configured' => 'Aucun uplink configuré', + 'ui.binkp.error_loading_status_prefix' => 'Erreur lors du chargement de l\'état :', + 'ui.binkp.error_loading_uplinks_prefix' => 'Erreur lors du chargement des uplinks :', + 'ui.binkp.file_count' => '{count} fichiers', + 'ui.binkp.pending' => 'En attente', + 'ui.binkp.errors' => 'Erreurs', + 'ui.binkp.stdout' => 'STDOUT', + 'ui.binkp.stderr' => 'STDERR', + 'ui.binkp.http_status_error' => 'HTTP {code} : {text}', + 'ui.binkp.inbound_api_http_error' => 'API entrante : HTTP {code}', + 'ui.binkp.outbound_api_http_error' => 'API sortante : HTTP {code}', + 'ui.binkp.zero_bytes' => '0 octet', + 'ui.binkp.byte_units.bytes' => 'Octets', + 'ui.binkp.byte_units.kb' => 'Ko', + 'ui.binkp.byte_units.mb' => 'Mo', + 'ui.binkp.byte_units.gb' => 'Go', + 'ui.binkp.poll_schedule_cron' => 'Planification d\'interrogation (format cron)', + 'ui.binkp.poll_schedule_placeholder' => '0 */4 * * *', + 'ui.binkp.logs_heading' => 'Journaux Binkp', + 'ui.binkp.lines_option' => '{count} lignes', + 'ui.binkp.loading_logs' => 'Chargement des journaux...', + 'ui.binkp.add_new_uplink' => 'Ajouter un nouvel uplink', + 'ui.binkp.ftn_address_required' => 'Adresse FTN *', + 'ui.binkp.ftn_address_placeholder' => '1:123/456', + 'ui.binkp.hostname_required' => 'Nom d\'hôte *', + 'ui.binkp.hostname_placeholder' => 'bbs.example.com', + 'ui.binkp.default_every_4_hours' => 'Par défaut : toutes les 4 heures', + 'ui.binkp.add_uplink' => 'Ajouter l\'uplink', + 'ui.binkp.configured_count' => '{count} configuré(s)', + 'ui.binkp.message_count' => '{count} msg(s)', + 'ui.binkp.poll_completed_exit' => 'Interrogation terminée (sortie {code})', + 'ui.binkp.poll_failed_prefix' => 'Échec de l\'interrogation : ', + 'ui.binkp.error_polling_uplink_prefix' => 'Erreur lors de l\'interrogation de l\'uplink : ', + 'ui.binkp.all_uplinks_polled_exit' => 'Tous les uplinks interrogés (sortie {code})', + 'ui.binkp.polling_failed_prefix' => 'Échec de l\'interrogation : ', + 'ui.binkp.error_polling_uplinks_prefix' => 'Erreur lors de l\'interrogation des uplinks : ', + 'ui.binkp.inbound_processing_completed' => 'Traitement entrant terminé', + 'ui.binkp.processing_failed_prefix' => 'Échec du traitement : ', + 'ui.binkp.error_processing_inbound_prefix' => 'Erreur lors du traitement entrant : ', + 'ui.binkp.outbound_processing_completed' => 'Traitement sortant terminé', + 'ui.binkp.error_processing_outbound_prefix' => 'Erreur lors du traitement sortant : ', + 'ui.binkp.uplink_added_success' => 'Uplink ajouté avec succès', + 'ui.binkp.add_uplink_failed_prefix' => 'Échec de l\'ajout de l\'uplink : ', + 'ui.binkp.error_adding_uplink_prefix' => 'Erreur lors de l\'ajout de l\'uplink : ', + 'ui.binkp.remove_uplink_confirm' => 'Supprimer l\'uplink {address} ?', + 'ui.binkp.uplink_removed_success' => 'Uplink supprimé avec succès', + 'ui.binkp.remove_uplink_failed_prefix' => 'Échec de la suppression de l\'uplink : ', + 'ui.binkp.error_removing_uplink_prefix' => 'Erreur lors de la suppression de l\'uplink : ', + 'ui.binkp.packet_processing_failed_prefix' => 'Échec du traitement du paquet : ', + 'ui.binkp.packet_processing_completed_exit' => 'Traitement du paquet terminé (sortie {code})', + 'ui.binkp.error_processing_packets_prefix' => 'Erreur lors du traitement des paquets : ', + 'ui.binkp.poll_failed' => 'Échec de l\'interrogation', + 'ui.binkp.polling_failed' => 'Échec de l\'interrogation', + 'ui.binkp.processing_failed' => 'Échec du traitement', + 'ui.binkp.add_uplink_failed' => 'Échec de l\'ajout de l\'uplink', + 'ui.binkp.remove_uplink_failed' => 'Échec de la suppression de l\'uplink', + 'ui.binkp.unknown_error' => 'Erreur inconnue', + 'ui.echolist.search_min_chars' => 'Veuillez saisir au moins 2 caractères pour rechercher', + 'ui.echolist.page_title' => 'Zones Echo', + 'ui.echolist.heading' => 'Liste Echo', + 'ui.echolist.filter_heading' => 'Filtrer les zones Echo', + 'ui.echolist.show_subscribed_only' => 'Afficher uniquement les zones abonnées', + 'ui.echolist.show_unread_only' => 'Afficher uniquement les zones avec des messages non lus', + 'ui.echolist.area_filter_placeholder' => 'Tapez pour filtrer par nom ou description...', + 'ui.echolist.area_filter_help' => 'Filtre la liste ci-dessous en temps réel', + 'ui.echolist.search_heading' => 'Rechercher des messages', + 'ui.echolist.search_placeholder' => 'Rechercher dans le contenu des messages...', + 'ui.echolist.search_help' => 'Rechercher dans tout le contenu des messages echomail', + 'ui.echolist.loading' => 'Chargement des echos...', + 'ui.echolist.stats.total_echos' => 'Total des echos', + 'ui.echolist.stats.total_messages' => 'Total des messages', + 'ui.echolist.stats.networks' => 'Réseaux', + 'ui.echolist.stats.recent_messages' => 'Messages (24h)', + 'ui.echolist.local' => 'Local', + 'ui.echolist.unknown' => 'Inconnu', + 'ui.echolist.load_failed' => 'Échec du chargement des zones Echo', + 'ui.echolist.none_available' => 'Aucune zone Echo disponible.', + 'ui.echolist.new_post' => 'Nouveau message', + 'ui.echolist.new_messages' => 'Nouveaux messages', + 'ui.echolist.local_areas' => 'Zones locales', + 'ui.echolist.lovlynet_network' => 'Réseau LOVLYNET', + 'ui.echolist.network_suffix' => 'Réseau', + 'ui.echolist.area_count' => 'zone', + 'ui.echolist.plural_suffix' => 's', + 'ui.echolist.sysop_badge' => 'Sysop', + 'ui.echolist.no_description' => 'Aucune description', + 'ui.echolist.moderator_prefix' => 'Modérateur :', + 'ui.echolist.unread_of_posts' => '{unread} non lu(s) sur {total} messages', + 'ui.echolist.post_count' => '{total} messages', + 'ui.echolist.by_author' => 'par {author}', + 'ui.echolist.time.never' => 'Jamais', + 'ui.echolist.time.just_now' => 'À l\'instant', + 'ui.echolist.time.minutes_ago' => 'il y a {count}min', + 'ui.echolist.time.hours_ago' => 'il y a {count}h', + 'ui.echolist.time.days_ago' => 'il y a {count}j', + 'ui.nodelist.node_details' => 'Détails du nœud', + 'ui.nodelist.back_to_list' => 'Retour à la liste', + 'ui.nodelist.system_information' => 'Informations système', + 'ui.nodelist.system_name' => 'Nom du système', + 'ui.nodelist.sysop' => 'Sysop', + 'ui.nodelist.location' => 'Emplacement', + 'ui.nodelist.phone' => 'Téléphone', + 'ui.nodelist.baud_rate' => 'Débit en bauds', + 'ui.nodelist.not_specified' => 'Non spécifié', + 'ui.nodelist.unpublished' => 'Non publié', + 'ui.nodelist.address_components' => 'Composants de l\'adresse', + 'ui.nodelist.zone' => 'Zone', + 'ui.nodelist.net' => 'Réseau', + 'ui.nodelist.node' => 'Nœud', + 'ui.nodelist.point' => 'Point', + 'ui.nodelist.full' => 'Complet', + 'ui.nodelist.capabilities_flags' => 'Capacités et indicateurs du nœud', + 'ui.nodelist.quick_actions' => 'Actions rapides', + 'ui.nodelist.send_netmail' => 'Envoyer un Netmail', + 'ui.nodelist.login_to_send_netmail' => 'Connexion pour envoyer un Netmail', + 'ui.nodelist.file_request' => 'Demande de fichier', + 'ui.nodelist.connection_information' => 'Informations de connexion', + 'ui.nodelist.internet_binkp' => 'Internet (BinkP)', + 'ui.nodelist.internet_telnet' => 'Internet (Telnet)', + 'ui.nodelist.dialup_pots' => 'Accès commuté (POTS)', + 'ui.nodelist.no_connection_info' => 'Aucune information de connexion disponible', + 'ui.nodelist.continuous_mail' => 'Courrier continu (24h/24)', + 'ui.nodelist.mail_on_hold' => 'Courrier en attente', + 'ui.nodelist.system_down' => 'Système hors ligne', + 'ui.nodelist.http' => 'HTTP', + 'ui.nodelist.telnet' => 'Telnet', + 'ui.nodelist.ssh' => 'SSH', + 'ui.nodelist.file_request_coming_soon' => 'La fonctionnalité de demande de fichiers arrive bientôt !', + 'ui.nodelist.index.heading' => 'Navigateur de liste de nœuds', + 'ui.nodelist.index.import_nodelist' => 'Importer une liste de nœuds', + 'ui.nodelist.index.nodes' => 'nœuds', + 'ui.nodelist.index.zones' => 'zones', + 'ui.nodelist.index.nets' => 'réseaux', + 'ui.nodelist.index.points' => 'points', + 'ui.nodelist.index.special' => 'spécial', + 'ui.nodelist.index.last_imported' => 'Dernière importation :', + 'ui.nodelist.index.search_placeholder' => 'Adresse de nœud (2:5034/10), sysop, localisation ou nom du système', + 'ui.nodelist.index.all_zones' => 'Toutes les zones', + 'ui.nodelist.index.zone_prefix' => 'Zone', + 'ui.nodelist.index.all_nets' => 'Tous les réseaux', + 'ui.nodelist.index.net_prefix' => 'Réseau', + 'ui.nodelist.index.search_results' => 'Résultats de recherche ({count} nœuds)', + 'ui.nodelist.index.address' => 'Adresse', + 'ui.nodelist.index.type' => 'Type', + 'ui.nodelist.index.phone_host' => 'Téléphone/Hôte', + 'ui.nodelist.index.speed' => 'Vitesse', + 'ui.nodelist.index.actions' => 'Actions', + 'ui.nodelist.index.no_nodes_found_search' => 'Aucun nœud trouvé correspondant à vos critères de recherche.', + 'ui.nodelist.index.use_search_form' => 'Utilisez le formulaire de recherche ci-dessus pour trouver des nœuds dans la liste.', + 'ui.nodelist.index.error_fetching_nets' => 'Erreur lors de la récupération des réseaux :', + 'ui.nodelist.import.heading' => 'Importer une liste de nœuds', + 'ui.nodelist.import.upload_new_nodelist' => 'Téléverser une nouvelle liste de nœuds', + 'ui.nodelist.import.network_domain' => 'Domaine réseau', + 'ui.nodelist.import.domain_placeholder' => 'ex. : fidonet, fsxnet, agoranet', + 'ui.nodelist.import.domain_title' => 'Le domaine ne doit contenir que des lettres, des chiffres, des tirets bas et des tirets', + 'ui.nodelist.import.domain_help_1' => 'Le domaine réseau FTN auquel appartient cette liste de nœuds (ex. : fidonet, fsxnet).', + 'ui.nodelist.import.domain_help_2' => 'Ceci permet de distinguer les nœuds de différents réseaux.', + 'ui.nodelist.import.nodelist_file' => 'Fichier de liste de nœuds', + 'ui.nodelist.import.nodelist_help_1' => 'Téléversez un fichier de liste de nœuds FidoNet (format NODELIST.xxx).', + 'ui.nodelist.import.nodelist_help_2' => 'Pris en charge : texte brut (.txt, .lst, .nodelist) et ZIP compressé (.Zxxx)', + 'ui.nodelist.import.archive_before_import' => 'Archiver la liste de nœuds actuelle avant l\'importation', + 'ui.nodelist.import.archive_help' => 'Recommandé : conserver la liste de nœuds actuelle comme sauvegarde inactive avant d\'importer la nouvelle.', + 'ui.nodelist.import.warning' => 'Avertissement :', + 'ui.nodelist.import.warning_text_1' => 'L\'importation d\'une nouvelle liste de nœuds remplacera les données de la liste active actuelle pour ce domaine.', + 'ui.nodelist.import.warning_text_2' => 'Cette opération est irréversible. Assurez-vous d\'avoir une sauvegarde si nécessaire.', + 'ui.nodelist.import.import_nodelist' => 'Importer la liste de nœuds', + 'ui.nodelist.import.current_status' => 'État actuel de la liste de nœuds', + 'ui.nodelist.import.file' => 'Fichier :', + 'ui.nodelist.import.day_of_year' => 'Jour de l\'année :', + 'ui.nodelist.import.release_date' => 'Date de publication :', + 'ui.nodelist.import.total_nodes' => 'Total des nœuds :', + 'ui.nodelist.import.imported' => 'Importé :', + 'ui.nodelist.import.no_active_nodelist' => 'Aucune liste de nœuds active trouvée.', + 'ui.nodelist.import.node_statistics' => 'Statistiques des nœuds', + 'ui.nodelist.import.total_nodes_label' => 'Total des nœuds', + 'ui.nodelist.import.guidelines' => 'Directives d\'importation', + 'ui.nodelist.import.guideline_1' => 'Format standard FTS-0005', + 'ui.nodelist.import.guideline_2' => 'Texte ASCII avec fins de ligne CR/LF', + 'ui.nodelist.import.guideline_3' => 'En-tête correct avec CRC', + 'ui.nodelist.import.guideline_4' => 'Hiérarchie Zone/Réseau/Nœud', + 'ui.nodelist.import.guideline_5' => 'Fichiers compressés ZIP (.Zxxx)', + 'ui.nodelist.import.guideline_note' => 'Pour les fichiers ARC, ARJ, LZH, RAR, utilisez le script d\'importation en ligne de commande', + 'ui.nodelist.import.importing' => 'Importation en cours...', + 'ui.nodelist.import.success' => '{count} nœuds importés avec succès depuis {filename} (Jour {day}) pour le domaine @{domain}', + 'ui.dosdoor_player.page_title' => 'Lecteur de portes DOS', + 'ui.dosdoor_player.document_title_suffix' => 'Porte DOS', + 'ui.dosdoor_player.status_prefix' => 'État :', + 'ui.dosdoor_player.status_disconnected' => 'Déconnecté', + 'ui.dosdoor_player.status_launching' => 'Lancement...', + 'ui.dosdoor_player.status_launch_failed' => 'Échec du lancement', + 'ui.dosdoor_player.status_connecting' => 'Connexion en cours...', + 'ui.dosdoor_player.status_connected' => 'Connecté', + 'ui.dosdoor_player.status_connection_error' => 'Erreur de connexion', + 'ui.dosdoor_player.status_error' => 'Erreur', + 'ui.dosdoor_player.end_session' => 'Terminer la session', + 'ui.dosdoor_player.launching_door_line' => 'Lancement du jeu de porte...', + 'ui.dosdoor_player.failed_launch_line' => 'Échec du lancement de la session de porte.', + 'ui.dosdoor_player.connecting_to_prefix' => 'Connexion à', + 'ui.dosdoor_player.connected_line' => 'Connecté !', + 'ui.dosdoor_player.connection_closed_line' => '[Connexion fermée]', + 'ui.dosdoor_player.connection_error_line' => '[Erreur de connexion]', + 'ui.dosdoor_player.failed_to_connect_prefix' => 'Échec de la connexion :', + 'ui.dosdoor_player.confirm_end_session' => 'Êtes-vous sûr de vouloir terminer cette session de porte ?', + 'ui.dosdoor_player.failed_end_session' => 'Échec de la fin de session', + 'ui.dosdoor_player.error_ending_session' => 'Erreur lors de la fin de session', + 'ui.dosdoor_player.error_no_door_specified' => 'Erreur : aucun identifiant de porte spécifié', + 'ui.dosdoor_player.failed_launch_door' => 'Échec du lancement de la porte', + 'ui.whos_online.page_title' => 'Qui est en ligne', + 'ui.whos_online.heading' => 'Qui est en ligne', + 'ui.whos_online.active_last_minutes' => 'Actif au cours des {minutes} dernières minutes', + 'ui.whos_online.online_users' => 'Utilisateurs en ligne', + 'ui.whos_online.service' => 'Service', + 'ui.whos_online.activity' => 'Activité', + 'ui.whos_online.idle' => 'Inactif', + 'ui.whos_online.no_users_online' => 'Aucun utilisateur n\'est en ligne pour le moment.', + 'ui.webdoors.page_title' => 'Portes et jeux', + 'ui.webdoors.heading' => 'Portes et jeux', + 'ui.webdoors.description' => 'Accédez aux jeux de portes DOS classiques et aux jeux web modernes.', + 'ui.webdoors.top_scores_all_games' => 'Meilleurs scores (tous les jeux)', + 'ui.webdoors.leaderboard_month_navigation' => 'Navigation mensuelle du classement', + 'ui.webdoors.previous_month' => 'Mois précédent', + 'ui.webdoors.next_month' => 'Mois suivant', + 'ui.webdoors.current_month' => 'Mois actuel', + 'ui.webdoors.rank' => 'Rang', + 'ui.webdoors.player' => 'Joueur', + 'ui.webdoors.game' => 'Jeu', + 'ui.webdoors.board' => 'Tableau', + 'ui.webdoors.score' => 'Score', + 'ui.webdoors.date' => 'Date', + 'ui.webdoors.no_scores_for_month' => 'Aucun score enregistré pour {month}.', + 'ui.webdoors.players_count' => 'Joueurs : {count}', + 'ui.webdoors.by_author' => 'par {author}', + 'ui.webdoors.version_by_author' => 'v{version} par {author}', + 'ui.webdoors.launch' => 'Lancer', + 'ui.webdoors.no_games_available' => 'Aucun jeu disponible pour le moment. Revenez plus tard !', + 'ui.webdoors.errors.system_disabled' => 'Désolé, le système de jeux n\'est pas activé.', + 'ui.webdoors.errors.admin_only' => 'Cette porte est réservée aux administrateurs.', + 'ui.webdoors.errors.requirements_not_met' => 'Ce jeu nécessite des fonctionnalités qui ne sont pas actuellement activées sur ce système.', + 'ui.webdoor_play.page_title_suffix' => 'Portes et jeux', + 'ui.webdoor_play.back_to_doors' => 'Retour aux portes', + 'ui.webdoor_play.fullscreen' => 'Plein écran', + 'ui.shared_file.shared_file' => 'Fichier partagé', + 'ui.shared_file.meta_description_from_system' => 'Fichier partagé depuis {system_name}', + 'ui.shared_file.shared_by' => 'Partagé par', + 'ui.shared_file.on_date_prefix' => 'le', + 'ui.shared_file.viewed_count' => 'consulté {count} fois', + 'ui.shared_file.expires_prefix' => 'expire', + 'ui.shared_file.clean' => 'Sain', + 'ui.shared_file.size' => 'Taille :', + 'ui.shared_file.uploaded' => 'Téléversé :', + 'ui.shared_file.area' => 'Zone :', + 'ui.shared_file.description' => 'Description :', + 'ui.shared_file.details' => 'Détails :', + 'ui.shared_file.download' => 'Télécharger', + 'ui.shared_file.browse_area' => 'Parcourir la zone {tag}', + 'ui.shared_file.join_system_to_download' => 'Rejoindre {system_name} pour télécharger', + 'ui.shared_file.register_prompt' => 'Créez un compte gratuit pour télécharger ce fichier et accéder à notre archive complète.', + 'ui.shared_file.register' => 'S\'inscrire', + 'ui.shared_file.login' => 'Connexion', + 'ui.shared_file.about_system' => 'À propos de {system_name}', + 'ui.shared_file.about_system_text' => '{system_name} est un BBS connecté à FidoNet avec des zones de fichiers publiques. Les membres peuvent téléverser, télécharger et partager des fichiers depuis une archive en constante évolution.', + 'ui.shared_file.powered_by' => 'Propulsé par BinktermPHP', + 'ui.shared_file.not_available_title' => 'Fichier non disponible', + 'ui.shared_file.not_available_body' => 'Ce lien de fichier partagé n\'est pas disponible. Il a peut-être expiré ou été révoqué.', + 'ui.shared_file.go_to_main_site' => 'Aller au site principal', + 'ui.ads.advertisement' => 'Publicité', + 'ui.ads.none_available' => 'Aucune publicité disponible.', + 'ui.bbs_menu.main_menu' => 'Menu principal', + 'ui.bbs_menu.press_key_to_navigate' => 'Appuyez sur une touche pour naviguer', + 'ui.bbs_menu.quit_return' => 'Quitter/Retour', + 'ui.bbs_menu.tap_item_to_navigate' => 'Appuyez sur un élément pour naviguer', + 'ui.bbs_menu.press_highlighted_key' => 'Appuyez sur la touche mise en évidence pour naviguer', + 'ui.bbs_menu.returns_to_menu' => 'retourne à ce menu', + 'ui.bbs_menu.tap_card_to_navigate' => 'Appuyez sur une carte pour naviguer', + 'ui.recent_updates.badge_feature' => 'Fonctionnalité', + 'ui.recent_updates.badge_fix' => 'Correction', + 'ui.recent_updates.badge_improvement' => 'Amélioration', + 'ui.recent_updates.badge_update' => 'Mise à jour', + 'ui.rules.intro' => 'Soyez excellents les uns envers les autres et contribuez à garder ce système accueillant pour tous.', + 'ui.rules.item_1' => 'Restez courtois. Pas de harcèlement, de discours haineux ou d\'attaques personnelles.', + 'ui.rules.item_2' => 'Pas de spam ni d\'inondation.', + 'ui.rules.item_3' => 'Respectez la vie privée. Ne partagez pas d\'informations privées sans consentement.', + 'ui.rules.item_4' => 'Respectez l\'étiquette Fidonet et les politiques du réseau local.', + 'ui.rules.item_5' => 'Les décisions du Sysop sont définitives.', + 'ui.dashboard.title' => 'Tableau de bord', + 'ui.dashboard.unread_netmail' => 'Netmail non lu', + 'ui.dashboard.unread_echomail' => 'Echomail non lu', + 'ui.dashboard.system_news' => 'Actualités du système', + 'ui.dashboard.system_information' => 'Informations système', + 'ui.dashboard.sysop' => 'Sysop', + 'ui.dashboard.user' => 'Utilisateur', + 'ui.dashboard.addresses' => 'Adresses', + 'ui.dashboard.my_referral_link' => 'Mon lien de parrainage', + 'ui.dashboard.referral_invite_prefix' => 'Invitez des amis et gagnez', + 'ui.dashboard.referral_invite_suffix' => 'crédits pour chaque parrainage réussi.', + 'ui.dashboard.referrals.label' => 'Parrainages', + 'ui.dashboard.referrals.earned' => 'Gagné', + 'ui.dashboard.voting_booth' => 'Bureau de vote', + 'ui.dashboard.create_poll_title' => 'Créer un sondage', + 'ui.dashboard.polls.none_active' => 'Aucun sondage actif pour le moment.', + 'ui.dashboard.polls.load_failed' => 'Échec du chargement du sondage.', + 'ui.dashboard.polls.results' => 'Résultats', + 'ui.dashboard.polls.no_votes' => 'Aucun vote pour l\'instant.', + 'ui.dashboard.polls.vote_to_see_results' => 'Votez pour voir les résultats', + 'ui.dashboard.polls.submit_vote' => 'Soumettre le vote', + 'ui.dashboard.echoareas.none_available' => 'Aucune zone echo disponible', + 'ui.dashboard.shoutbox.none_yet' => 'Aucun message pour l\'instant. Soyez le premier !', + 'ui.dashboard.shoutbox.load_failed' => 'Échec du chargement des messages.', + 'ui.dashboard.shoutbox.post_failed' => 'Échec de l\'envoi du message.', + 'ui.dashboard.referrals.error_prefix' => 'Erreur de statistiques de parrainage :', + 'ui.dashboard.referrals.recent' => 'Parrainages récents', + 'ui.dashboard.referrals.copy_failed_prefix' => 'Échec de la copie :', + 'ui.dashboard.echoareas.header_area' => 'Zone Echo', + 'ui.dashboard.echoareas.header_unread_total' => 'Total non lu', + 'ui.settings.title' => 'Paramètres', + 'ui.settings.display_preferences' => 'Préférences d\'affichage', + 'ui.settings.messages_per_page' => 'Messages par page', + 'ui.settings.messages_per_page_help' => 'Nombre de messages à afficher par page', + 'ui.settings.timezone' => 'Fuseau horaire', + 'ui.settings.timezone_help' => 'Votre fuseau horaire local pour l\'affichage des dates', + 'ui.settings.language' => 'Langue', + 'ui.settings.language_help' => 'Langue préférée pour l\'interface', + 'ui.settings.date_format' => 'Format de date', + 'ui.settings.date_format_help' => 'Votre format de date et d\'heure préféré', + 'ui.settings.date_format.option.en_us' => 'Américain (MM/DD/YYYY) - Jan 31, 2026, 10:30 AM', + 'ui.settings.date_format.option.en_gb' => 'Britannique (DD/MM/YYYY) - 31 Jan 2026, 10:30', + 'ui.settings.date_format.option.en_ca' => 'Canadien (YYYY-MM-DD) - 2026-01-31, 10:30 a.m.', + 'ui.settings.date_format.option.de_de' => 'Allemand (DD.MM.YYYY) - 31.01.2026, 10:30', + 'ui.settings.date_format.option.fr_fr' => 'Français (DD/MM/YYYY) - 31/01/2026 10:30', + 'ui.settings.date_format.option.es_es' => 'Espagnol (DD/MM/YYYY) - 31/1/2026, 10:30', + 'ui.settings.date_format.option.it_it' => 'Italien (DD/MM/YYYY) - 31/01/2026, 10:30', + 'ui.settings.date_format.option.ja_jp' => 'Japonais (YYYY/MM/DD) - 2026/01/31 10:30', + 'ui.settings.date_format.option.zh_cn' => 'Chinois (YYYY/M/D) - 2026/1/31, 10:30', + 'ui.settings.date_format.option.sv_se' => 'Suédois (YYYY-MM-DD) - 2026-01-31 10:30', + 'ui.settings.theme' => 'Thème', + 'ui.settings.theme_help' => 'Choisissez votre palette de couleurs préférée', + 'ui.settings.interface_style' => 'Style d\'interface', + 'ui.settings.interface_style.web' => 'Interface Web (barre de navigation et menus par défaut)', + 'ui.settings.interface_style.bbs_menu' => 'Menu BBS (navigation par touches de raccourci)', + 'ui.settings.interface_style_help' => 'Choisissez votre style de navigation préféré.', + 'ui.settings.default_echo_list' => 'Liste de zones echo par défaut', + 'ui.settings.system_default' => 'Défaut système', + 'ui.settings.default_echo_list.reader' => 'Lecteur (liste de messages)', + 'ui.settings.default_echo_list.echolist' => 'Liste Echo (groupée par réseau)', + 'ui.settings.default_echo_list_help' => 'Choisissez votre vue par défaut préférée lors de l\'accès aux zones echo depuis le menu', + 'ui.settings.message_font' => 'Police des messages', + 'ui.settings.message_font_help' => 'Famille de polices pour l\'affichage des messages netmail et echomail', + 'ui.settings.message_font_size' => 'Taille de police des messages', + 'ui.settings.message_font_size_help' => 'Taille de police pour l\'affichage des messages netmail et echomail', + 'ui.settings.message_signature' => 'Signature des messages', + 'ui.settings.message_signature_placeholder' => 'Votre signature (4 lignes max)', + 'ui.settings.message_signature_help' => 'Ajoutée aux messages netmail et echomail sortants. 4 lignes maximum.', + 'ui.settings.default_tagline' => 'Accroche par défaut', + 'ui.settings.no_tagline' => 'Pas d\'accroche', + 'ui.settings.random_tagline' => 'Accroche aléatoire', + 'ui.settings.default_tagline_help' => 'Utilisée comme accroche par défaut lors de la rédaction de messages. Choisissez « Accroche aléatoire » pour en sélectionner une au hasard à chaque fois.', + 'ui.settings.threading_preferences' => 'Préférences de fil de discussion', + 'ui.settings.threaded_view_echomail' => 'Activer la vue en fil pour l\'echomail', + 'ui.settings.threaded_view_echomail_help' => 'Regrouper les messages echomail par fils de conversation', + 'ui.settings.threaded_view_netmail' => 'Activer la vue en fil pour le netmail', + 'ui.settings.threaded_view_netmail_help' => 'Regrouper les messages netmail par fils de conversation', + 'ui.settings.quote_display' => 'Affichage des citations', + 'ui.settings.quote_coloring' => 'Coloriser le texte cité par niveau', + 'ui.settings.quote_coloring_help' => 'Afficher les lignes de messages cités (commençant par >) dans différentes couleurs selon le niveau d\'imbrication', + 'ui.settings.session_security' => 'Session et sécurité', + 'ui.settings.active_sessions' => 'Sessions actives', + 'ui.settings.active_sessions_help' => 'Gérez vos sessions de connexion actives', + 'ui.settings.view_sessions' => 'Voir les sessions', + 'ui.settings.security_actions' => 'Actions de sécurité', + 'ui.settings.logout_all_help' => 'Se déconnecter de tous les appareils', + 'ui.settings.logout_all' => 'Tout déconnecter', + 'ui.settings.save_settings' => 'Enregistrer les paramètres', + 'ui.settings.system_status' => 'État du système', + 'ui.settings.binkp_status' => 'État BinkP', + 'ui.settings.online' => 'En ligne', + 'ui.settings.last_poll' => 'Dernier sondage', + 'ui.settings.messages_today' => 'Messages aujourd\'hui', + 'ui.settings.never' => 'Jamais', + 'ui.settings.quick_actions' => 'Actions rapides', + 'ui.settings.compose_netmail' => 'Rédiger un netmail', + 'ui.settings.post_echomail' => 'Publier un echomail', + 'ui.settings.help_support' => 'Aide et support', + 'ui.settings.load_failed_console' => 'Échec du chargement des paramètres', + 'ui.settings.saving' => 'Enregistrement des paramètres...', + 'ui.settings.saved_successfully' => 'Paramètres enregistrés avec succès !', + 'ui.settings.sessions.none_active' => 'Aucune session active trouvée.', + 'ui.settings.sessions.loading' => 'Chargement des sessions...', + 'ui.settings.sessions.load_failed' => 'Échec du chargement des sessions', + 'ui.settings.sessions.current' => 'Actuelle', + 'ui.settings.sessions.created' => 'Créée', + 'ui.settings.sessions.expires' => 'Expire', + 'ui.settings.sessions.revoke' => 'Révoquer', + 'ui.settings.sessions.revoke_confirm' => 'Êtes-vous sûr de vouloir révoquer cette session ?', + 'ui.settings.sessions.revoked_success' => 'Session révoquée avec succès', + 'ui.settings.sessions.logged_out_all_success' => 'Déconnecté de toutes les sessions', + 'ui.settings.sessions.logout_all_confirm' => 'Êtes-vous sûr de vouloir vous déconnecter de tous les appareils ? Vous devrez vous reconnecter.', + 'ui.settings.polling_uplinks' => 'Interrogation des liaisons montantes... (cela peut prendre un moment)', + 'ui.settings.poll_complete_prefix' => 'Interrogation de la liaison montante terminée : ', + 'ui.settings.poll_complete_exit' => 'Interrogation de la liaison montante terminée (sortie {code})', + 'ui.settings.poll_failed_prefix' => 'Échec de l\'interrogation : ', + 'ui.compose.title_prefix' => 'Rédiger', + 'ui.compose.back_to_prefix' => 'Retour à', + 'ui.compose.to_address_label' => 'Adresse du destinataire (laisser vide pour local)', + 'ui.compose.fidonet_address_help' => 'Adresse Fidonet (ex. : 1:123/456)', + 'ui.compose.to_name_required' => 'Nom du destinataire *', + 'ui.compose.attach_file' => 'Joindre un fichier', + 'ui.compose.attach_file_help_short' => 'nécessite crashmail ; le sujet sera défini comme nom de fichier', + 'ui.compose.echo_area_required' => 'Zone Echo *', + 'ui.compose.select_echo_area' => 'Sélectionner une zone echo...', + 'ui.compose.to_name' => 'Nom du destinataire', + 'ui.compose.to_name_public_help' => 'Laisser \'All\' pour un message public', + 'ui.compose.cross_post_to_other_areas' => 'Publier en copie dans d\'autres zones', + 'ui.compose.cross_post_help_prefix' => 'Sélectionnez des zones supplémentaires pour publier ce message (max', + 'ui.compose.cross_post_help_suffix' => 'Chaque zone reçoit une copie indépendante.', + 'ui.compose.stylecodes.inverse_title' => 'Inverse (#inverse#)', + 'ui.compose.stylecodes.active_prefix' => 'StyleCodes actifs -', + 'ui.compose.markdown.mode' => 'Mode Markdown', + 'ui.compose.markdown.toolbar_aria' => 'Mise en forme Markdown', + 'ui.compose.markdown.bold_title' => 'Gras (Ctrl+B)', + 'ui.compose.markdown.italic_title' => 'Italique (Ctrl+I)', + 'ui.compose.markdown.heading_1_title' => 'Titre 1', + 'ui.compose.markdown.heading_2_title' => 'Titre 2', + 'ui.compose.markdown.heading_3_title' => 'Titre 3', + 'ui.compose.markdown.inline_code_title' => 'Code en ligne', + 'ui.compose.markdown.code_block_title' => 'Bloc de code', + 'ui.compose.markdown.link_title' => 'Lien (Ctrl+K)', + 'ui.compose.markdown.unordered_list_title' => 'Liste non ordonnée', + 'ui.compose.markdown.ordered_list_title' => 'Liste ordonnée', + 'ui.compose.markdown.blockquote_title' => 'Citation', + 'ui.compose.markdown.horizontal_rule_title' => 'Règle horizontale', + 'ui.compose.markdown.edit_mode_title' => 'Mode édition', + 'ui.compose.markdown.preview_rendered_title' => 'Aperçu du rendu', + 'ui.compose.markdown.quick_reference_title' => 'Référence rapide Markdown', + 'ui.compose.markdown.write_placeholder' => 'Rédigez votre message Markdown ici...', + 'ui.compose.markdown.click_preview_hint' => 'Cliquez sur Aperçu pour voir le rendu.', + 'ui.compose.markdown.formatting_active' => 'Formatage Markdown actif.', + 'ui.compose.markdown.quick_reference' => 'Référence rapide', + 'ui.compose.markup_format' => 'Format de balisage', + 'ui.compose.plain_text' => 'Texte brut', + 'ui.compose.markdown_label' => 'Markdown', + 'ui.compose.stylecodes_label' => 'StyleCodes (GoldEd, SemPoint Rich Text, Synchronet Markup)', + 'ui.compose.markup_format_help' => 'Envoyer avec mise en forme. Markdown et StyleCodes sont pris en charge sur les réseaux compatibles.', + 'ui.compose.hide_panel' => 'Masquer le panneau', + 'ui.compose.show_panel' => 'Afficher le panneau', + 'ui.compose.netmail_guidelines' => 'Consignes Netmail :', + 'ui.compose.netmail_guideline_private' => 'Messages privés entre nœuds', + 'ui.compose.netmail_guideline_identity' => 'Identité d\'envoi pour cette destination : Nom réel', + 'ui.compose.netmail_guideline_identity_destination_real_name' => 'Identité d\'envoi pour cette destination : Nom réel', + 'ui.compose.netmail_guideline_identity_destination_username' => 'Identité d\'envoi pour cette destination : Nom d\'utilisateur/Alias', + 'ui.compose.netmail_guideline_addressing' => 'Utiliser l\'adressage Fidonet approprié', + 'ui.compose.netmail_guideline_respectful' => 'Soyez respectueux et professionnel', + 'ui.compose.netmail_guideline_routed' => 'Les messages sont acheminés via le réseau', + 'ui.compose.netmail_cost' => 'Coût : {cost} {currency}', + 'ui.compose.crashmail.direct_delivery_label' => 'Crashmail (Livraison directe)', + 'ui.compose.crashmail.help_direct' => 'Tenter une livraison directe immédiate au nœud de destination, en contournant le routage par hub.', + 'ui.compose.crashmail.help_reachable' => 'Nécessite que la destination soit directement accessible.', + 'ui.compose.crashmail.help_cost' => 'Coûte {cost} {currency}.', + 'ui.compose.echomail_guidelines' => 'Consignes Echomail :', + 'ui.compose.echomail_guideline_identity' => 'Identité d\'envoi : Sélectionnez une zone echo pour voir si ce message utilise le Nom réel ou le Nom d\'utilisateur/Alias', + 'ui.compose.echomail_guideline_identity_area_real_name' => 'Identité d\'envoi pour cette zone : Nom réel', + 'ui.compose.echomail_guideline_identity_area_username' => 'Identité d\'envoi pour cette zone : Nom d\'utilisateur/Alias', + 'ui.compose.echomail_guideline_public' => 'Messages de forum public', + 'ui.compose.echomail_guideline_topic' => 'Restez dans le sujet de la zone echo', + 'ui.compose.echomail_guideline_respectful' => 'Soyez respectueux envers tous les participants', + 'ui.compose.echomail_guideline_rules' => 'Respectez les règles et la modération de la zone echo', + 'ui.compose.tech_note_plain_text_only' => 'Texte brut uniquement (pas de HTML)', + 'ui.compose.tech_note_line_length' => 'Les lignes doivent faire moins de 79 caractères', + 'ui.compose.tech_note_origin_line' => 'Ligne d\'origine ajoutée automatiquement', + 'ui.compose.invalid_fidonet_address_format' => 'Format d\'adresse Fidonet invalide (ex. : 1:123/456)', + 'ui.compose.send_failed_type' => 'Échec de l\'envoi de {type}', + 'ui.compose.upload_attachment_failed' => 'Échec du téléversement de la pièce jointe', + 'ui.compose.reply_attribution' => 'Le {date}, {name} a écrit :', + 'ui.compose.markdown.help.text_formatting' => 'Mise en forme du texte', + 'ui.compose.markdown.help.syntax' => 'Syntaxe', + 'ui.compose.markdown.help.result' => 'Résultat', + 'ui.compose.markdown.help.hyperlink' => 'hyperlien', + 'ui.compose.markdown.help.headings' => 'Titres', + 'ui.compose.markdown.help.lists_blocks' => 'Listes & Blocs', + 'ui.compose.markdown.help.bullet_list' => 'Liste à puces', + 'ui.compose.markdown.help.numbered_list' => 'Liste numérotée', + 'ui.compose.markdown.help.blockquote' => 'Citation', + 'ui.compose.markdown.help.horizontal_rule' => 'Règle horizontale', + 'ui.compose.markdown.help.code_block' => 'Bloc de code', + 'ui.compose.markdown.help.keyboard_shortcuts' => 'Raccourcis clavier', + 'ui.compose.markdown.help.shortcut_bold' => 'Gras', + 'ui.compose.markdown.help.shortcut_italic' => 'Italique', + 'ui.compose.markdown.help.shortcut_link' => 'Lien', + 'ui.compose.markdown.help.shortcut_indent' => 'Indenter (4 espaces)', + 'ui.compose.subject_required' => 'Sujet *', + 'ui.compose.message_required' => 'Message *', + 'ui.compose.plain_text_hint' => 'Utilisez du texte brut. La mise en forme standard Fidonet s\'applique.', + 'ui.compose.message_guidelines' => 'Consignes pour les messages', + 'ui.compose.technical_notes' => 'Notes techniques', + 'ui.compose.your_info' => 'Vos informations', + 'ui.compose.system' => 'Système', + 'ui.compose.address' => 'Adresse', + 'ui.compose.tagline_help' => 'Sélectionnez une accroche fournie par le sysop à ajouter sous votre signature.', + 'ui.compose.save_draft' => 'Enregistrer le brouillon', + 'ui.compose.send_prefix' => 'Envoyer', + 'ui.compose.sending' => 'Envoi en cours...', + 'ui.compose.draft.empty_content' => 'Veuillez ajouter du contenu avant d\'enregistrer le brouillon', + 'ui.compose.draft.saved_success' => 'Brouillon enregistré avec succès', + 'ui.compose.sent_cross_post_suffix' => ' (publié dans {count} zones)', + 'ui.compose.address_book.load_failed_short' => 'Échec du chargement', + 'ui.compose.address_book.select_title' => 'Sélectionner dans le carnet d\'adresses', + 'ui.compose.address_book.create_title' => 'Créer une nouvelle entrée dans le carnet d\'adresses', + 'ui.compose.address_book.entry_added' => 'Entrée ajoutée avec succès', + 'ui.compose.address_book.use_entry_confirm' => 'Utiliser cette entrée pour le message actuel ?', + 'ui.address_book.already_exists' => 'Ce contact est déjà dans votre carnet d\'adresses', + 'ui.address_book.load_failed' => 'Échec du chargement du carnet d\'adresses', + 'ui.address_book.added_from_netmail' => 'Ajouté depuis un message netmail', + 'ui.address_book.added_from_netmail_replyto_detail' => 'Ajouté depuis un message netmail. Expéditeur d\'origine : {original_name} ({original_address}), Répondre à : {replyto_name} ({replyto_address})', + 'ui.address_book.added_from_netmail_sender_detail' => 'Ajouté depuis un message netmail. Expéditeur : {sender_name} ({sender_address})', + 'ui.address_book.check_existing_failed' => 'Échec de la vérification des contacts existants', + 'ui.address_book.entry_updated' => 'Entrée mise à jour avec succès', + 'ui.address_book.entry_deleted' => 'Entrée supprimée avec succès', + 'ui.address_book.sender_added' => '{name} ajouté au carnet d\'adresses', + 'ui.address_book.saved_to_address_book' => 'Enregistré dans le carnet d\'adresses', + 'ui.address_book.delete_confirm' => 'Êtes-vous sûr de vouloir supprimer « {name} » de votre carnet d\'adresses ?', + 'ui.address_book.node_address_placeholder' => '1:234/567', + 'ui.address_book.node_address_title' => 'Entrez une adresse Fidonet valide (ex. : 1:234/567 ou 1:234/567.0)', + 'ui.drafts.delete_confirm' => 'Êtes-vous sûr de vouloir supprimer ce brouillon ? Cette action est irréversible.', + 'ui.drafts.deleted_success' => 'Brouillon supprimé avec succès', + 'ui.messages.none_selected' => 'Aucun message sélectionné', + 'ui.netmail.search.failed' => 'Échec de la recherche', + 'ui.netmail.message_deleted_success' => 'Message supprimé avec succès', + 'ui.netmail.delete_message_confirm' => 'Êtes-vous sûr de vouloir supprimer ce message ? Cette action est irréversible.', + 'ui.netmail.bulk_delete.confirm' => 'Êtes-vous sûr de vouloir supprimer {count} message(s) ?', + 'ui.netmail.bulk_delete.success' => '{count} message(s) supprimé(s)', + 'ui.netmail.address_book.load_entry_failed_prefix' => 'Échec du chargement de l\'entrée : ', + 'ui.netmail.bulk_delete.failed' => 'Échec de la suppression des messages', + 'ui.netmail.no_drafts_found' => 'Aucun brouillon trouvé', + 'ui.netmail.to' => 'À', + 'ui.netmail.from_to' => 'De/À', + 'ui.netmail.last_updated' => 'Dernière mise à jour', + 'ui.netmail.received' => 'Reçu', + 'ui.netmail.badge_netmail' => 'NETMAIL', + 'ui.netmail.badge_new' => 'NOUVEAU', + 'ui.netmail.received_insecure_session_title' => 'Reçu via une session non sécurisée', + 'ui.netmail.received_insecure_badge_title' => 'Ce message a été reçu via une session binkp non sécurisée/non authentifiée', + 'ui.netmail.received_insecurely' => 'Reçu de manière non sécurisée', + 'ui.netmail.not_authenticated' => 'Ce message n\'a pas été authentifié', + 'ui.echomail.search.failed' => 'Échec de la recherche', + 'ui.echomail.view_all_echo_areas' => 'Voir toutes les zones echo', + 'ui.echomail.search_found_count' => '{count} trouvé(s)', + 'ui.echomail.no_echoareas_available' => 'Aucune zone echo disponible', + 'ui.echomail.no_drafts_found' => 'Aucun brouillon trouvé', + 'ui.echomail.to_echo_area' => 'À / Zone echo', + 'ui.echomail.last_updated' => 'Dernière mise à jour', + 'ui.echomail.to_prefix' => 'à :', + 'ui.echomail.bulk_mark_read_failed' => 'Échec du marquage des messages comme lus', + 'ui.echomail.bulk_mark_read_success' => '{count} message(s) marqué(s) comme lu(s)', + 'ui.echomail.bulk_marking' => 'Marquage en cours...', + 'ui.echomail.viewing_all_messages' => 'Affichage : Tous les messages', + 'ui.echomail.no_area' => 'Aucune zone', + 'ui.echomail.save_status.update_failed' => 'Échec de la mise à jour du statut d\'enregistrement', + 'ui.echomail.bulk_delete.failed' => 'Échec de la suppression des messages', + 'ui.echomail.bulk_delete.success' => '{count} message(s) supprimé(s)', + 'ui.echomail.bulk_delete.confirm' => 'Êtes-vous sûr de vouloir supprimer {count} message(s) sélectionné(s) pour tout le monde ?', + 'ui.echomail.bulk_deleting' => 'Suppression en cours...', + 'ui.echomail.shares.check_failed' => 'Échec de la vérification des partages existants', + 'ui.echomail.shares.friendly_url_failed' => 'Échec de la génération de l\'URL conviviale', + 'ui.echomail.shares.revoke_confirm' => 'Êtes-vous sûr de vouloir révoquer ce lien de partage ? Il ne sera plus accessible aux autres.', + 'ui.echomail.plain_text_mode' => 'Mode texte brut', + 'ui.echomail.press_a_to_toggle' => 'appuyez sur A pour basculer', + 'ui.echomail.shortcuts.title' => 'Raccourcis clavier', + 'ui.echomail.shortcuts.prev_next' => 'Message précédent / suivant', + 'ui.echomail.shortcuts.toggle_ansi' => 'Basculer rendu ANSI / texte brut', + 'ui.echomail.shortcuts.download' => 'Télécharger le message', + 'ui.echomail.shortcuts.fullscreen' => 'Basculer le plein écran', + 'ui.echomail.shortcuts.help' => 'Afficher / masquer les raccourcis clavier', + 'ui.echomail.shortcuts.close' => 'Fermer le message', + 'ui.echomail.shortcuts.dismiss' => 'Appuyez sur ? ou H pour fermer cette aide', + 'ui.admin_subscriptions.page_title' => 'Admin : Gérer les abonnements', + 'ui.admin_subscriptions.heading' => 'Gestion des abonnements', + 'ui.admin_subscriptions.breadcrumb_aria' => 'fil d\'Ariane', + 'ui.admin_subscriptions.breadcrumb_current' => 'Abonnements', + 'ui.admin_subscriptions.total_echoareas' => 'Total des zones echo', + 'ui.admin_subscriptions.default_echoareas' => 'Zones echo par défaut', + 'ui.admin_subscriptions.total_subscriptions' => 'Total des abonnements', + 'ui.admin_subscriptions.active_subscribers' => 'Abonnés actifs', + 'ui.admin_subscriptions.echoarea_default_subscriptions' => 'Abonnements par défaut aux zones d\'écho', + 'ui.admin_subscriptions.manage_default_help' => 'Gérer les zones d\'écho auxquelles les nouveaux utilisateurs sont automatiquement abonnés', + 'ui.admin_subscriptions.default' => 'Par défaut', + 'ui.admin_subscriptions.echoarea' => 'Zone d\'écho', + 'ui.admin_subscriptions.subscribers' => 'Abonnés', + 'ui.admin_subscriptions.user_subs' => 'Abon. utilisateurs', + 'ui.admin_subscriptions.auto_subs' => 'Abon. automatiques', + 'ui.admin_subscriptions.messages' => 'Messages', + 'ui.admin_subscriptions.default_subscription_title' => 'Abonnement par défaut', + 'ui.admin_subscriptions.legend' => 'Légende :', + 'ui.admin_subscriptions.legend_default' => 'Abonnement par défaut (nouveaux utilisateurs abonnés automatiquement)', + 'ui.admin_subscriptions.legend_total_badge' => 'Total', + 'ui.admin_subscriptions.legend_total' => 'Tous les abonnés actifs', + 'ui.admin_subscriptions.legend_user_badge' => 'Utilisateur', + 'ui.admin_subscriptions.legend_user' => 'Abonnements initiés par l\'utilisateur', + 'ui.admin_subscriptions.legend_auto_badge' => 'Auto', + 'ui.admin_subscriptions.legend_auto' => 'Abonnements automatiques/administrateur', + 'ui.admin_subscriptions.how_it_works' => 'Fonctionnement :', + 'ui.admin_subscriptions.how_default_toggle' => 'Activez « Par défaut » pour abonner automatiquement les nouveaux utilisateurs aux zones d\'écho', + 'ui.admin_subscriptions.how_existing_users' => 'Lorsqu\'activé, tous les utilisateurs existants sont également abonnés automatiquement', + 'ui.admin_subscriptions.how_unsubscribe' => 'Les utilisateurs peuvent toujours se désabonner des zones d\'écho par défaut', + 'ui.admin_subscriptions.action_enabled' => 'activé', + 'ui.admin_subscriptions.action_disabled' => 'désactivé', + 'ui.admin_subscriptions.update_success' => 'Abonnement par défaut {action} avec succès ! La page va se rafraîchir pour afficher les statistiques mises à jour.', + 'ui.admin_subscriptions.default_enabled_success' => 'Abonnement par défaut activé avec succès ! La page va se rafraîchir pour afficher les statistiques mises à jour.', + 'ui.admin_subscriptions.default_disabled_success' => 'Abonnement par défaut désactivé avec succès ! La page va se rafraîchir pour afficher les statistiques mises à jour.', + 'ui.admin_subscriptions.update_failed' => 'Échec de la mise à jour de l\'abonnement par défaut. Veuillez réessayer.', + 'ui.admin_subscriptions.network_error' => 'Erreur réseau. Veuillez réessayer.', + 'ui.user_subscriptions.page_title' => 'Gérer vos abonnements aux zones d\'écho', + 'ui.user_subscriptions.heading' => 'Gérer vos abonnements aux zones d\'écho', + 'ui.user_subscriptions.heading_help' => 'Abonnez-vous aux zones d\'écho pour voir leurs messages', + 'ui.user_subscriptions.search_placeholder' => 'Rechercher des zones d\'écho...', + 'ui.user_subscriptions.default_subscription_title' => 'Abonnement par défaut', + 'ui.user_subscriptions.message_count' => '{count} messages', + 'ui.user_subscriptions.dot_subscribed' => '- Abonné', + 'ui.user_subscriptions.automatic' => '(automatique)', + 'ui.user_subscriptions.view_messages' => 'Voir les messages', + 'ui.user_subscriptions.subscription_info' => 'Informations d\'abonnement', + 'ui.user_subscriptions.subscribed_label' => 'Abonné :', + 'ui.user_subscriptions.how_subscriptions_work' => 'Fonctionnement des abonnements', + 'ui.user_subscriptions.how_new_users' => 'Les nouveaux utilisateurs sont automatiquement abonnés aux zones d\'écho populaires', + 'ui.user_subscriptions.how_subscribe_unsubscribe' => 'Vous pouvez vous abonner/désabonner de n\'importe quelle zone d\'écho', + 'ui.user_subscriptions.how_only_subscribed' => 'Seules les zones d\'écho auxquelles vous êtes abonné apparaissent dans vos listes de messages', + 'ui.user_subscriptions.how_star_means_default' => 'indique les zones d\'écho par défaut', + 'ui.user_subscriptions.subscribed_success' => 'Abonnement effectué avec succès !', + 'ui.user_subscriptions.unsubscribed_success' => 'Désabonnement effectué avec succès !', + 'ui.user_subscriptions.update_failed' => 'Échec de la mise à jour de l\'abonnement. Veuillez réessayer.', + 'ui.user_subscriptions.network_error' => 'Erreur réseau. Veuillez réessayer.', + 'ui.echoareas_import.page_title' => 'Importer des zones d\'écho', + 'ui.echoareas_import.heading' => 'Importer des zones d\'écho', + 'ui.echoareas_import.back_to_echo_areas' => 'Retour aux zones d\'écho', + 'ui.echoareas_import.processed' => 'Traités :', + 'ui.echoareas_import.created' => 'Créés :', + 'ui.echoareas_import.updated' => 'Mis à jour :', + 'ui.echoareas_import.skipped_blank_rows' => 'Lignes vides ignorées :', + 'ui.echoareas_import.errors' => 'Erreurs :', + 'ui.echoareas_import.import_errors' => 'Erreurs d\'importation', + 'ui.echoareas_import.upload_csv' => 'Téléverser un CSV', + 'ui.echoareas_import.csv_file' => 'Fichier CSV', + 'ui.echoareas_import.csv_help' => 'Le CSV en UTF-8 est recommandé. La ligne d\'en-tête est facultative.', + 'ui.echoareas_import.import_echo_areas' => 'Importer des zones d\'écho', + 'ui.echoareas_import.csv_format' => 'Format CSV', + 'ui.echoareas_import.each_row_fields' => 'Chaque ligne doit contenir ces champs dans cet ordre :', + 'ui.echoareas_import.example' => 'Exemple :', + 'ui.echoareas_import.import_rules' => 'Règles d\'importation', + 'ui.echoareas_import.rule_domain_blank' => 'DOMAIN peut être vide. Un domaine vide importe la zone en local uniquement.', + 'ui.echoareas_import.rule_domain_exists' => 'Si DOMAIN est fourni, il doit déjà exister dans votre configuration réseau BinkP.', + 'ui.echoareas_import.rule_existing_area' => 'Si une zone existe déjà avec le même ECHOTAG et DOMAIN, sa description est mise à jour et la zone est réactivée.', + 'ui.echoareas_import.rule_new_areas' => 'Les nouvelles zones sont créées actives avec les paramètres par défaut ; vous pouvez les affiner ensuite dans la gestion des zones d\'écho.', + 'ui.echoareas_import.rule_header_optional' => 'La ligne d\'en-tête ECHOTAG,DESCRIPTION,DOMAIN est acceptée mais non obligatoire.', + 'ui.echoareas_import.importing' => 'Importation en cours...', + 'ui.echoareas_import.error_line_prefix' => 'Ligne {line} : {message}', + 'ui.echoareas_import.error_open_csv' => 'Impossible d\'ouvrir le fichier CSV téléversé.', + 'ui.echoareas_import.error_duplicate_row' => 'Combinaison ECHOTAG/DOMAIN en double dans le fichier CSV.', + 'ui.echoareas_import.error_tag_description_required' => 'ECHOTAG et DESCRIPTION sont obligatoires.', + 'ui.echoareas_import.error_invalid_tag' => 'ECHOTAG invalide. Utilisez uniquement des lettres, des chiffres, des points, des tirets bas et des tirets.', + 'ui.echoareas_import.error_invalid_domain' => 'DOMAIN invalide. Utilisez uniquement des lettres, des chiffres, des tirets bas et des tirets.', + 'ui.echoareas_import.error_unknown_domain' => 'DOMAIN inconnu « {domain} ». Ajoutez d\'abord le domaine réseau dans la configuration BinkP.', + 'ui.echoareas_import.error_apply_failed' => 'L\'importation a échoué et aucune modification n\'a été appliquée.', + 'ui.echoareas_import.error_unexpected' => 'Erreur d\'importation inattendue.', + 'ui.echoareas_import.error_choose_csv' => 'Veuillez choisir un fichier CSV à importer.', + 'ui.echoareas_import.error_upload_failed' => 'Le téléversement a échoué. Veuillez réessayer.', + 'ui.echoareas_import.error_invalid_upload' => 'Fichier téléversé invalide.', + 'ui.api.reminder.sent' => 'Rappel de compte envoyé avec succès', + 'ui.api.files.uploaded' => 'Fichier téléversé avec succès', + 'ui.api.files.deleted' => 'Fichier supprimé avec succès', + 'ui.api.files.renamed' => 'Fichier renommé avec succès', + 'ui.api.messages.sent' => 'Message envoyé avec succès', + 'ui.api.messages.bulk_mark_read' => 'Messages marqués comme lus', + 'ui.api.messages.bulk_deleted' => 'Messages supprimés', + 'ui.api.messages.saved' => 'Message sauvegardé', + 'ui.api.messages.unsaved' => 'Message non sauvegardé', + 'ui.api.messages.share_revoked' => 'Lien de partage révoqué', + 'ui.api.credits.sent' => 'Crédits envoyés avec succès', + 'ui.api.debug.delete_endpoint_accessible' => 'Le point de terminaison de suppression est accessible', + 'ui.api.admin.mrc_restart_initiated' => 'Redémarrage du démon MRC initié', + 'ui.api.admin.binkp_config_reloaded' => 'Rechargement de la configuration BinkP demandé', + 'ui.api.binkp.uplink_added' => 'Lien montant ajouté avec succès', + 'ui.api.binkp.uplink_updated' => 'Lien montant mis à jour avec succès', + 'ui.api.binkp.uplink_removed' => 'Lien montant supprimé avec succès', + 'ui.api.binkp.file_deleted' => 'Fichier supprimé avec succès', + 'ui.api.binkp.file_retry_started' => 'Nouvelle tentative de fichier initiée avec succès', + 'ui.api.binkp.config_updated' => 'Configuration mise à jour avec succès', + 'ui.api.binkp.poll_triggered' => 'Interrogation BinkP déclenchée', + 'ui.api.binkp.poll_all_triggered' => 'Interrogation générale BinkP déclenchée', + 'ui.api.binkp.process_packets_started' => 'Traitement des paquets démarré', + 'ui.api.door.session_resumed' => 'Reprise de la session existante', + 'ui.files.previous_file' => 'Fichier précédent', + 'ui.files.next_file' => 'Fichier suivant', +]; diff --git a/config/i18n/fr/errors.php b/config/i18n/fr/errors.php new file mode 100644 index 000000000..5779fc21a --- /dev/null +++ b/config/i18n/fr/errors.php @@ -0,0 +1,370 @@ + 'Une erreur inattendue s\'est produite', + 'errors.auth.authentication_required' => 'Authentification requise', + 'errors.auth.invalid_csrf_token' => 'Jeton CSRF invalide', + 'errors.auth.missing_credentials' => 'Nom d\'utilisateur et mot de passe requis', + 'errors.auth.invalid_credentials' => 'Identifiants invalides', + 'errors.auth.invalid_api_key' => 'Clé API invalide', + 'errors.auth.gateway_token_missing_fields' => 'L\'identifiant utilisateur et le jeton sont requis', + 'errors.auth.invalid_or_expired_gateway_token' => 'Jeton invalide ou expiré', + 'errors.auth.username_or_email_required' => 'Nom d\'utilisateur ou adresse e-mail requis', + 'errors.auth.token_required' => 'Jeton requis', + 'errors.auth.invalid_or_expired_token' => 'Jeton invalide ou expiré', + 'errors.auth.token_and_password_required' => 'Le jeton et le nouveau mot de passe sont requis', + 'errors.auth.weak_password' => 'Le mot de passe doit comporter au moins 8 caractères', + 'errors.auth.reset_failed' => 'Échec de la réinitialisation du mot de passe', + 'errors.register.invalid_submission' => 'Soumission invalide', + 'errors.register.too_fast' => 'Veuillez prendre le temps de remplir le formulaire.', + 'errors.register.session_expired' => 'Session expirée. Veuillez actualiser la page et réessayer.', + 'errors.register.rate_limited' => 'Trop de tentatives d\'inscription. Veuillez réessayer plus tard.', + 'errors.register.required_fields' => 'Le nom d\'utilisateur, le mot de passe et le nom réel sont requis', + 'errors.register.invalid_username_format' => 'Le nom d\'utilisateur doit comporter entre 3 et 20 caractères, lettres, chiffres et underscores uniquement', + 'errors.register.restricted_name' => 'Ce nom d\'utilisateur ou nom réel n\'est pas autorisé', + 'errors.register.weak_password' => 'Le mot de passe doit comporter au moins 8 caractères', + 'errors.register.user_exists' => 'Un utilisateur avec ce nom d\'utilisateur ou ce nom existe déjà. Veuillez vous connecter ou contacter le sysop pour obtenir de l\'aide.', + 'errors.register.failed' => 'Échec de l\'inscription. Veuillez réessayer plus tard.', + 'errors.reminder.username_required' => 'Le nom d\'utilisateur est requis', + 'errors.reminder.user_not_found_or_logged_in' => 'Utilisateur introuvable ou déjà connecté', + 'errors.reminder.send_failed' => 'Échec de l\'envoi du rappel. Veuillez réessayer plus tard.', + 'errors.settings.invalid_input' => 'Entrée invalide', + 'errors.settings.update_failed' => 'Échec de la mise à jour des paramètres', + 'errors.settings.exception' => 'Échec de la mise à jour des paramètres', + 'errors.messages.share_create_failed' => 'Échec de la création du lien de partage', + 'errors.messages.share_lookup_failed' => 'Échec du chargement des liens de partage', + 'errors.messages.share_revoke_failed' => 'Échec de la révocation du lien de partage', + 'errors.messages.netmail.not_found' => 'Message introuvable', + 'errors.messages.netmail.delete_failed' => 'Échec de la suppression du message', + 'errors.messages.netmail.bulk_delete.invalid_input' => 'Une liste d\'identifiants de messages non vide est requise', + 'errors.messages.echomail.bulk_read.invalid_input' => 'Une liste d\'identifiants de messages non vide est requise', + 'errors.messages.echomail.bulk_read.failed' => 'Échec du marquage des messages comme lus', + 'errors.messages.echomail.bulk_delete.admin_required' => 'Des privilèges administrateur sont requis', + 'errors.messages.echomail.bulk_delete.invalid_input' => 'Une liste d\'identifiants de messages non vide est requise', + 'errors.messages.echomail.bulk_delete.user_not_found' => 'Utilisateur introuvable', + 'errors.messages.echomail.stats.subscription_required' => 'Abonnement requis pour cette zone echo', + 'errors.messages.echomail.not_found' => 'Message introuvable', + 'errors.messages.netmail.attachment.no_file' => 'Aucune pièce jointe téléversée', + 'errors.messages.netmail.attachment.upload_error' => 'Échec du téléversement de la pièce jointe', + 'errors.messages.netmail.attachment.too_large' => 'La pièce jointe dépasse la taille maximale autorisée', + 'errors.messages.netmail.attachment.store_failed' => 'Échec du stockage de la pièce jointe téléversée', + 'errors.messages.send.invalid_type' => 'Type de message invalide', + 'errors.messages.send.failed' => 'Échec de l\'envoi du message', + 'errors.messages.send.exception' => 'Échec de l\'envoi du message', + 'errors.notify.user_id_missing' => 'Impossible de résoudre la session utilisateur', + 'errors.notify.invalid_state' => 'Charge utile d\'état de notification invalide', + 'errors.notify.invalid_target' => 'Cible de notification invalide', + 'errors.polls.option_required' => 'Une option de sondage est requise', + 'errors.polls.not_found' => 'Sondage introuvable', + 'errors.polls.invalid_option' => 'Option de sondage invalide', + 'errors.polls.vote_failed' => 'Échec de l\'enregistrement du vote', + 'errors.polls.insufficient_credits' => 'Échec de la déduction des crédits. Votre solde est peut-être insuffisant.', + 'errors.polls.question_required' => 'La question du sondage est requise', + 'errors.polls.question_length_invalid' => 'La question du sondage doit comporter entre 10 et 500 caractères', + 'errors.polls.options_count_invalid' => 'Le sondage doit inclure entre 2 et 10 options', + 'errors.polls.option_empty' => 'Les options du sondage ne peuvent pas être vides', + 'errors.polls.option_length_invalid' => 'Les options du sondage ne doivent pas dépasser 200 caractères', + 'errors.polls.options_duplicate' => 'Les options du sondage doivent être uniques', + 'errors.polls.create_failed' => 'Échec de la création du sondage', + 'errors.shoutbox.message_required' => 'Le message est requis', + 'errors.shoutbox.message_too_long' => 'Le message ne peut pas dépasser 280 caractères', + 'errors.chat.feature_disabled' => 'Le chat est désactivé', + 'errors.chat.invalid_message_query' => 'Requête de message de chat invalide', + 'errors.chat.invalid_send_target' => 'Cible de chat invalide', + 'errors.chat.message_length_invalid' => 'Le message doit comporter entre 1 et 1000 caractères', + 'errors.chat.room_not_found' => 'Salon de chat introuvable', + 'errors.chat.user_banned' => 'Vous êtes banni de ce salon', + 'errors.chat.recipient_not_found' => 'Destinataire introuvable', + 'errors.chat.send_blocked' => 'Le message n\'a pas pu être envoyé', + 'errors.chat.admin_required' => 'Des privilèges administrateur sont requis', + 'errors.chat.invalid_moderation_request' => 'Demande de modération invalide', + 'errors.chat.user_not_found' => 'Utilisateur introuvable', + 'errors.echoareas.admin_required' => 'Des privilèges administrateur sont requis', + 'errors.echoareas.not_found' => 'Zone echo introuvable', + 'errors.echoareas.invalid_posting_name_policy' => 'Politique de nom de publication invalide', + 'errors.echoareas.tag_description_required' => 'Le tag et la description sont requis', + 'errors.echoareas.invalid_tag_format' => 'Format de tag invalide', + 'errors.echoareas.invalid_color_format' => 'Format de couleur invalide', + 'errors.echoareas.create_failed' => 'Échec de la création de la zone echo', + 'errors.echoareas.not_found_or_unchanged' => 'Zone echo introuvable ou aucune modification effectuée', + 'errors.echoareas.update_failed' => 'Échec de la mise à jour de la zone echo', + 'errors.echoareas.delete_blocked_has_messages' => 'Impossible de supprimer une zone echo contenant des messages', + 'errors.echoareas.delete_failed' => 'Échec de la suppression de la zone echo', + 'errors.fileareas.not_found' => 'Zone de fichiers introuvable', + 'errors.fileareas.create_failed' => 'Échec de la création de la zone de fichiers', + 'errors.fileareas.update_failed' => 'Échec de la mise à jour de la zone de fichiers', + 'errors.fileareas.delete_failed' => 'Échec de la suppression de la zone de fichiers', + 'errors.files.feature_disabled' => 'La fonctionnalité des zones de fichiers est désactivée', + 'errors.files.area_id_required' => 'L\'identifiant de la zone de fichiers est requis', + 'errors.files.access_denied' => 'Accès refusé à cette zone de fichiers', + 'errors.files.not_found' => 'Fichier introuvable', + 'errors.files.share_not_found_or_forbidden' => 'Lien de partage introuvable ou non autorisé', + 'errors.files.delete_failed' => 'Échec de la suppression du fichier', + 'errors.files.scan_forbidden' => 'Accès administrateur requis pour analyser les fichiers', + 'errors.files.scan_failed' => 'Échec de l\'analyse antivirus', + 'errors.files.rename_filename_required' => 'Le nouveau nom de fichier est requis', + 'errors.files.rename_forbidden' => 'Vous n\'avez pas la permission de renommer ce fichier', + 'errors.files.rename_conflict' => 'Un fichier portant ce nom existe déjà dans cette zone', + 'errors.files.rename_failed' => 'Échec du renommage du fichier', + 'errors.files.upload.no_file' => 'Aucun fichier téléversé', + 'errors.files.upload.area_id_required' => 'L\'identifiant de la zone de fichiers est requis', + 'errors.files.upload.short_description_required' => 'Une description courte est requise', + 'errors.files.upload.area_not_found' => 'Zone de fichiers introuvable', + 'errors.files.upload.read_only' => 'Cette zone de fichiers est en lecture seule', + 'errors.files.upload.admin_only' => 'Seuls les administrateurs peuvent téléverser des fichiers dans cette zone', + 'errors.files.upload.virus_detected' => 'Fichier rejeté : virus détecté', + 'errors.files.upload.failed' => 'Échec du téléversement du fichier', + 'errors.admin.users.not_found' => 'Utilisateur introuvable', + 'errors.admin.users.create_failed' => 'Échec de la création de l\'utilisateur', + 'errors.admin.users.update_failed' => 'Échec de la mise à jour de l\'utilisateur', + 'errors.admin.users.delete_failed' => 'Échec de la suppression de l\'utilisateur', + 'errors.admin.polls.question_required' => 'La question est requise', + 'errors.admin.polls.options_required' => 'Au moins deux options sont requises', + 'errors.admin.polls.not_found' => 'Sondage introuvable', + 'errors.admin.polls.create_failed' => 'Échec de la création du sondage', + 'errors.admin.polls.update_failed' => 'Échec de la mise à jour du sondage', + 'errors.admin.polls.delete_failed' => 'Échec de la suppression du sondage', + 'errors.messages.drafts.invalid_input' => 'Charge utile de brouillon invalide', + 'errors.messages.drafts.user_id_missing' => 'Impossible de résoudre la session utilisateur', + 'errors.messages.drafts.save_failed' => 'Échec de l\'enregistrement du brouillon', + 'errors.messages.drafts.list_failed' => 'Échec du chargement des brouillons', + 'errors.messages.drafts.not_found' => 'Brouillon introuvable', + 'errors.messages.drafts.get_failed' => 'Échec du chargement du brouillon', + 'errors.messages.drafts.delete_failed' => 'Échec de la suppression du brouillon', + 'errors.messages.netmail.get_failed' => 'Échec du chargement du message', + 'errors.messages.echomail.get_failed' => 'Échec du chargement du message', + 'errors.messages.search.query_too_short' => 'La requête de recherche doit comporter au moins 2 caractères', + 'errors.messages.read.user_id_missing' => 'Impossible de résoudre la session utilisateur', + 'errors.messages.read.invalid_type' => 'Type de message invalide', + 'errors.messages.read.mark_failed' => 'Échec du marquage du message comme lu', + 'errors.messages.save.user_id_missing' => 'Impossible de résoudre la session utilisateur', + 'errors.messages.save.invalid_type' => 'Type de message invalide', + 'errors.messages.save.failed' => 'Échec de l\'enregistrement du message', + 'errors.messages.unsave.user_id_missing' => 'Impossible de résoudre la session utilisateur', + 'errors.messages.unsave.invalid_type' => 'Type de message invalide', + 'errors.messages.unsave.failed' => 'Échec de la suppression du message enregistré', + 'errors.messages.unsave.not_saved' => 'Le message n\'était pas enregistré ou a déjà été supprimé', + 'errors.user.profile.current_password_incorrect' => 'Le mot de passe actuel est incorrect', + 'errors.user.profile.new_password_too_short' => 'Le nouveau mot de passe doit comporter au moins 6 caractères', + 'errors.user.profile.update_failed' => 'Échec de la mise à jour du profil', + 'errors.user.stats.admin_required' => 'Des privilèges administrateur sont requis', + 'errors.user.stats.user_not_found' => 'Utilisateur introuvable', + 'errors.user.transactions.admin_required' => 'Des privilèges administrateur sont requis', + 'errors.user.transactions.user_not_found' => 'Utilisateur introuvable', + 'errors.user.transactions.list_failed' => 'Échec du chargement des transactions', + 'errors.credits.feature_disabled' => 'La fonctionnalité des crédits est désactivée', + 'errors.credits.send.invalid_amount' => 'Le montant doit être compris entre 1 et 200', + 'errors.credits.send.self_transfer_forbidden' => 'Vous ne pouvez pas vous envoyer des crédits à vous-même', + 'errors.credits.send.recipient_not_found' => 'Destinataire introuvable', + 'errors.credits.send.insufficient_balance' => 'Solde insuffisant', + 'errors.credits.send.debit_failed' => 'Échec du débit du compte expéditeur', + 'errors.credits.send.credit_failed' => 'Échec du crédit du compte destinataire', + 'errors.credits.send.failed' => 'Le transfert de crédits a échoué', + 'errors.user.sessions.revoke_failed' => 'Échec de la révocation de la session', + 'errors.user.sessions.revoke_all_failed' => 'Échec de la révocation des sessions', + 'errors.user.activity.session_missing' => 'Une session active est requise', + 'errors.binkp.admin_required' => 'Accès administrateur requis', + 'errors.binkp.status_failed' => 'Échec du chargement du statut BinkP', + 'errors.binkp.poll_failed' => 'Échec de l\'interrogation du lien montant BinkP', + 'errors.binkp.poll_all_failed' => 'Échec de l\'interrogation de tous les liens montants BinkP', + 'errors.binkp.process_packets_failed' => 'Échec du traitement des paquets', + 'errors.binkp.uplinks_list_failed' => 'Échec du chargement des liens montants BinkP', + 'errors.binkp.files_inbound_failed' => 'Échec du chargement des fichiers BinkP entrants', + 'errors.binkp.files_outbound_failed' => 'Échec du chargement des fichiers BinkP sortants', + 'errors.binkp.process_outbound_failed' => 'Échec du traitement de la file d\'attente sortante', + 'errors.binkp.connection_test_failed' => 'Échec du test de connexion BinkP', + 'errors.binkp.logs.failed' => 'Échec du chargement des journaux BinkP', + 'errors.binkp.uplink.address_hostname_required' => 'L\'adresse et le nom d\'hôte sont requis', + 'errors.binkp.uplink.poll_failed' => 'Échec de l\'interrogation du lien montant BinkP', + 'errors.binkp.uplink.poll_all_failed' => 'Échec de l\'interrogation de tous les liens montants BinkP', + 'errors.binkp.uplink.add_failed' => 'Échec de l\'ajout du lien montant BinkP', + 'errors.binkp.uplink.update_failed' => 'Échec de la mise à jour du lien montant BinkP', + 'errors.binkp.uplink.remove_failed' => 'Échec de la suppression du lien montant BinkP', + 'errors.binkp.files.inbound_failed' => 'Échec du chargement des fichiers BinkP entrants', + 'errors.binkp.files.outbound_failed' => 'Échec du chargement des fichiers BinkP sortants', + 'errors.binkp.files.process_inbound_failed' => 'Échec du traitement de la file d\'attente BinkP entrante', + 'errors.binkp.files.process_outbound_failed' => 'Échec du traitement de la file d\'attente BinkP sortante', + 'errors.binkp.files.delete_outbound_failed' => 'Échec de la suppression du fichier BinkP sortant', + 'errors.binkp.files.retry_error_failed' => 'Échec de la nouvelle tentative sur le fichier d\'erreur entrant', + 'errors.binkp.config.invalid_section' => 'Section de configuration BinkP invalide', + 'errors.binkp.config.update_failed' => 'Échec de la mise à jour de la configuration BinkP', + 'errors.admin.pending_users.admin_required' => 'Les privilèges administrateur sont requis', + 'errors.admin.pending_users.list_failed' => 'Échec du chargement des utilisateurs en attente', + 'errors.admin.pending_users.not_found' => 'Utilisateur en attente introuvable', + 'errors.admin.pending_users.get_failed' => 'Échec du chargement de l\'utilisateur en attente', + 'errors.admin.pending_users.approve_failed' => 'Échec de l\'approbation de l\'utilisateur en attente', + 'errors.admin.pending_users.reject_failed' => 'Échec du rejet de l\'utilisateur en attente', + 'errors.admin.users.admin_required' => 'Les privilèges administrateur sont requis', + 'errors.admin.users.list_failed' => 'Échec du chargement des utilisateurs', + 'errors.admin.users.get_failed' => 'Échec du chargement de l\'utilisateur', + 'errors.admin.users.real_name_required' => 'Le nom réel est requis', + 'errors.admin.users.password_too_short' => 'Le mot de passe doit comporter au moins 8 caractères', + 'errors.admin.users.update_status_failed' => 'Échec de la mise à jour du statut de l\'utilisateur', + 'errors.admin.users.create_required_fields' => 'Le nom d\'utilisateur, le nom réel et le mot de passe sont requis', + 'errors.admin.users.invalid_username_format' => 'Format de nom d\'utilisateur invalide', + 'errors.admin.users.restricted_name' => 'Ce nom d\'utilisateur ou nom réel n\'est pas autorisé', + 'errors.admin.users.username_exists' => 'Le nom d\'utilisateur existe déjà', + 'errors.admin.users.cleanup_failed' => 'Échec du nettoyage des inscriptions', + 'errors.admin.users.reminder_not_allowed' => 'L\'utilisateur n\'est pas éligible au rappel', + 'errors.admin.users.reminder_send_failed' => 'Échec de l\'envoi du rappel', + 'errors.admin.users.need_reminders_failed' => 'Échec du chargement des candidats aux rappels', + 'errors.admin.debug_failed' => 'Échec du chargement des informations de débogage administrateur', + 'errors.admin.bbs_settings.load_failed' => 'Échec du chargement des paramètres BBS', + 'errors.admin.bbs_settings.invalid_payload' => 'Charge utile de configuration invalide', + 'errors.admin.bbs_settings.invalid_credits_config' => 'Configuration des crédits invalide', + 'errors.admin.bbs_settings.save_failed' => 'Échec de l\'enregistrement des paramètres BBS', + 'errors.admin.appearance.load_failed' => 'Échec du chargement des paramètres d\'apparence', + 'errors.admin.appearance.branding.invalid_accent_color' => 'Format de couleur d\'accentuation invalide', + 'errors.admin.appearance.branding.footer_too_long' => 'Le texte du pied de page doit comporter 500 caractères ou moins', + 'errors.admin.appearance.branding.save_failed' => 'Échec de l\'enregistrement des paramètres de marque', + 'errors.admin.appearance.content.save_failed' => 'Échec de l\'enregistrement des paramètres de contenu', + 'errors.admin.appearance.navigation.save_failed' => 'Échec de l\'enregistrement des paramètres de navigation', + 'errors.admin.appearance.seo.save_failed' => 'Échec de l\'enregistrement des paramètres SEO', + 'errors.admin.appearance.shell.save_failed' => 'Échec de l\'enregistrement des paramètres du shell', + 'errors.admin.appearance.message_reader.save_failed' => 'Échec de l\'enregistrement des paramètres du lecteur de messages', + 'errors.admin.appearance.markdown_preview.failed' => 'Échec du rendu de l\'aperçu Markdown', + 'errors.admin.shell_art.list_failed' => 'Échec du listage des fichiers d\'art shell', + 'errors.admin.shell_art.upload.no_file' => 'Aucun fichier d\'art shell téléversé', + 'errors.admin.shell_art.upload.upload_error' => 'Échec du téléversement de l\'art shell', + 'errors.admin.shell_art.upload.file_too_large' => 'Le fichier d\'art shell dépasse la limite de taille', + 'errors.admin.shell_art.upload.failed' => 'Échec du téléversement de l\'art shell', + 'errors.admin.shell_art.delete.invalid_name' => 'Nom de fichier d\'art shell invalide', + 'errors.admin.shell_art.delete.failed' => 'Échec de la suppression de l\'art shell', + 'errors.admin.taglines.load_failed' => 'Échec du chargement des signatures', + 'errors.admin.taglines.save_failed' => 'Échec de l\'enregistrement des signatures', + 'errors.admin.mrc_settings.load_failed' => 'Échec du chargement des paramètres MRC', + 'errors.admin.mrc_settings.save_failed' => 'Échec de l\'enregistrement des paramètres MRC', + 'errors.admin.mrc_settings.restart_failed' => 'Échec du redémarrage du démon MRC', + 'errors.admin.bbs_system.load_failed' => 'Échec du chargement des paramètres système', + 'errors.admin.bbs_system.save_failed' => 'Échec de l\'enregistrement des paramètres système', + 'errors.admin.binkp_config.load_failed' => 'Échec du chargement de la configuration BinkP', + 'errors.admin.binkp_config.save_failed' => 'Échec de l\'enregistrement de la configuration BinkP', + 'errors.admin.binkp_config.reload_failed' => 'Échec du rechargement de la configuration BinkP', + 'errors.admin.dosdoors_config.load_failed' => 'Échec du chargement de la configuration des portes DOS', + 'errors.admin.dosdoors_config.save_failed' => 'Échec de l\'enregistrement de la configuration des portes DOS', + 'errors.admin.native_doors.load_failed' => 'Échec du chargement de la configuration des portes natives', + 'errors.admin.native_doors.save_failed' => 'Échec de l\'enregistrement de la configuration des portes natives', + 'errors.admin.native_doors.sync_failed' => 'Échec de la synchronisation des portes natives', + 'errors.admin.webdoors_config.load_failed' => 'Échec du chargement de la configuration des portes web', + 'errors.admin.webdoors_config.save_failed' => 'Échec de l\'enregistrement de la configuration des portes web', + 'errors.admin.webdoors_config.activate_failed' => 'Échec de l\'activation de la configuration des portes web', + 'errors.admin.filearea_rules.load_failed' => 'Échec du chargement des règles de zone de fichiers', + 'errors.admin.filearea_rules.save_failed' => 'Échec de l\'enregistrement des règles de zone de fichiers', + 'errors.admin.ads.list_failed' => 'Échec du chargement des publicités', + 'errors.admin.ads.upload.no_file' => 'Aucun fichier publicitaire téléversé', + 'errors.admin.ads.upload.upload_error' => 'Échec du téléversement de la publicité', + 'errors.admin.ads.upload.file_too_large' => 'Le fichier publicitaire dépasse la limite de taille', + 'errors.admin.ads.upload.read_failed' => 'Échec de la lecture du fichier publicitaire téléversé', + 'errors.admin.ads.upload.failed' => 'Échec du téléversement de la publicité', + 'errors.admin.ads.delete_failed' => 'Échec de la suppression de la publicité', + 'errors.admin.chat_rooms.invalid_name_length' => 'Le nom du salon doit comporter entre 1 et 64 caractères', + 'errors.admin.chat_rooms.create_failed' => 'Échec de la création du salon de discussion', + 'errors.admin.chat_rooms.not_found' => 'Salon de discussion introuvable', + 'errors.admin.chat_rooms.lobby_name_locked' => 'Le nom du salon principal ne peut pas être modifié', + 'errors.admin.chat_rooms.update_failed' => 'Échec de la mise à jour du salon de discussion', + 'errors.admin.chat_rooms.lobby_delete_forbidden' => 'Le salon principal ne peut pas être supprimé', + 'errors.admin.chat_rooms.delete_failed' => 'Échec de la suppression du salon de discussion', + 'errors.admin.shoutbox.hide_failed' => 'Échec du masquage du message', + 'errors.admin.shoutbox.unhide_failed' => 'Échec de l\'affichage du message', + 'errors.admin.shoutbox.delete_failed' => 'Échec de la suppression du message', + 'errors.admin.insecure_nodes.create_failed' => 'Échec de l\'ajout du nœud non sécurisé', + 'errors.admin.insecure_nodes.update_failed' => 'Échec de la mise à jour du nœud non sécurisé', + 'errors.admin.insecure_nodes.delete_failed' => 'Échec de la suppression du nœud non sécurisé', + 'errors.admin.crashmail.retry_failed' => 'Échec de la nouvelle tentative sur l\'élément crashmail', + 'errors.admin.crashmail.cancel_failed' => 'Échec de l\'annulation de l\'élément crashmail', + 'errors.admin.crashmail.poll_failed' => 'Échec de l\'exécution du sondage crashmail', + 'errors.admin.custom_templates.list_failed' => 'Échec du listage des modèles personnalisés', + 'errors.admin.custom_templates.get_failed' => 'Échec du chargement du modèle personnalisé', + 'errors.admin.custom_templates.save_failed' => 'Échec de l\'enregistrement du modèle personnalisé', + 'errors.admin.custom_templates.delete_failed' => 'Échec de la suppression du modèle personnalisé', + 'errors.admin.custom_templates.install_failed' => 'Échec de l\'installation du modèle personnalisé', + 'errors.admin.auto_feed.not_found' => 'Source de flux introuvable', + 'errors.admin.auto_feed.required_fields' => 'L\'URL du flux, la zone d\'écho et l\'utilisateur publiant sont requis', + 'errors.admin.auto_feed.invalid_url' => 'L\'URL du flux est invalide', + 'errors.admin.auto_feed.echoarea_not_found' => 'Zone d\'écho introuvable', + 'errors.admin.auto_feed.user_not_found' => 'Utilisateur publiant introuvable', + 'errors.admin.auto_feed.duplicate_source' => 'La source de flux existe déjà', + 'errors.admin.auto_feed.create_failed' => 'Échec de la création de la source de flux', + 'errors.admin.auto_feed.update_failed' => 'Échec de la mise à jour de la source de flux', + 'errors.admin.auto_feed.check_failed' => 'La vérification du flux a échoué', + 'errors.admin.activity_stats.table_missing' => 'La table du journal d\'activité n\'est pas disponible', + 'errors.address_book.list_failed' => 'Échec du chargement des entrées du carnet d\'adresses', + 'errors.address_book.not_found' => 'Entrée introuvable', + 'errors.address_book.get_failed' => 'Échec du chargement de l\'entrée du carnet d\'adresses', + 'errors.address_book.user_not_found' => 'Identifiant utilisateur introuvable dans les données d\'authentification', + 'errors.address_book.invalid_fidonet_format' => 'Format d\'adresse Fidonet invalide. Utilisez un format tel que 1:234/567 ou 1:234/567.0', + 'errors.address_book.required_fields' => 'Le nom et l\'adresse Fidonet sont requis', + 'errors.address_book.duplicate_entry' => 'L\'entrée du carnet d\'adresses existe déjà', + 'errors.address_book.create_failed' => 'Échec de la création de l\'entrée du carnet d\'adresses', + 'errors.address_book.update_failed' => 'Échec de la mise à jour de l\'entrée du carnet d\'adresses', + 'errors.address_book.delete_failed' => 'Échec de la suppression de l\'entrée du carnet d\'adresses', + 'errors.address_book.search_failed' => 'Échec de la recherche dans le carnet d\'adresses', + 'errors.address_book.stats_failed' => 'Échec du chargement des statistiques du carnet d\'adresses', + 'errors.messages.shared.lookup_failed' => 'Échec du chargement du message partagé', + 'errors.messages.shared.user_shares_failed' => 'Échec du chargement des partages de l\'utilisateur', + 'errors.messages.shared.access_denied' => 'Message introuvable ou accès refusé', + 'errors.messages.shared.sharing_disabled' => 'Le partage est désactivé pour votre compte', + 'errors.messages.shared.max_active_reached' => 'Nombre maximum de partages actifs atteint', + 'errors.messages.shared.not_found_or_expired' => 'Partage introuvable ou expiré', + 'errors.messages.shared.login_required' => 'Connexion requise pour accéder à ce partage', + 'errors.messages.shared.original_not_found' => 'Message original introuvable', + 'errors.messages.shared.not_found' => 'Partage introuvable', + 'errors.messages.shared.friendly_url_only_echomail' => 'Les URLs conviviales sont uniquement disponibles pour les partages echomail', + 'errors.messages.shared.slug_generation_failed' => 'Impossible de générer un identifiant de partage pour ce message', + 'errors.subscriptions.echoarea_id_required' => 'ID de zone echo requis', + 'errors.subscriptions.invalid_action' => 'Action invalide', + 'errors.subscriptions.admin_required' => 'Accès administrateur requis', + 'errors.nodelist.api.endpoint_not_found' => 'Point de terminaison API introuvable', + 'errors.nodelist.api.address_required' => 'Paramètre d\'adresse requis', + 'errors.nodelist.api.node_not_found' => 'Nœud introuvable', + 'errors.nodelist.api.zone_required' => 'Paramètre de zone requis', + 'errors.nodelist.api.internal_error' => 'Échec du traitement de la requête API nodelist', + 'errors.nodelist.admin_required' => 'Accès administrateur requis', + 'errors.nodelist.import.domain_required' => 'Veuillez spécifier un domaine réseau', + 'errors.nodelist.import.domain_invalid' => 'Le domaine ne doit contenir que des lettres, des chiffres, des tirets bas et des tirets', + 'errors.nodelist.import.file_required' => 'Veuillez sélectionner un fichier nodelist valide', + 'errors.nodelist.import.invalid_format' => 'Format de nodelist invalide', + 'errors.nodelist.import.zip_extension_missing' => 'Extension ZIP non disponible', + 'errors.nodelist.import.zip_open_failed' => 'Impossible d\'ouvrir le fichier ZIP', + 'errors.nodelist.import.archive_unsupported' => 'Le format d\'archive {format} n\'est pas pris en charge dans l\'interface web (utilisez la ligne de commande)', + 'errors.nodelist.import.archive_nodelist_missing' => 'Impossible de trouver le fichier nodelist dans l\'archive', + 'errors.nodelist.import.extract_failed' => 'Échec de l\'extraction de l\'archive', + 'errors.nodelist.import.failed' => 'Échec de l\'importation de la nodelist', + 'errors.settings.load_failed' => 'Échec du chargement des paramètres utilisateur', + 'errors.taglines.load_failed' => 'Échec du chargement des signatures', + 'errors.referrals.code_not_found' => 'Code de parrainage introuvable', + 'errors.referrals.stats_failed' => 'Échec du chargement des statistiques de parrainage', + 'errors.referrals.admin_stats_failed' => 'Échec du chargement des statistiques de parrainage administrateur', + 'errors.webdoor.feature_disabled' => 'Le système de jeu n\'est pas activé', + 'errors.webdoor.auth_required' => 'Non authentifié', + 'errors.webdoor.invalid_slot' => 'Numéro d\'emplacement invalide', + 'errors.webdoor.save_too_large' => 'Les données de sauvegarde dépassent la taille maximale', + 'errors.webdoor.save_not_found' => 'Sauvegarde introuvable', + 'errors.door.door_name_required' => 'Nom de la porte requis', + 'errors.door.admin_only' => 'Cette porte est réservée aux administrateurs', + 'errors.door.insufficient_credits' => 'Crédits insuffisants', + 'errors.door.insufficient_credits_detail' => 'Cette porte coûte {required} crédits. Vous disposez de {balance} crédits.', + 'errors.door.capacity_reached' => 'La porte a atteint sa capacité maximale', + 'errors.door.capacity_reached_detail' => 'Cette porte est actuellement utilisée. Seul(s) {max_nodes} joueur(s) autorisé(s) à la fois. Veuillez réessayer ultérieurement.', + 'errors.door.launch_failed' => 'Échec du démarrage de la session de porte', + 'errors.door.session_id_required' => 'ID de session requis', + 'errors.door.session_unauthorized' => 'Non autorisé', + 'errors.door.session_end_failed' => 'Échec de la fermeture de la session', + 'errors.door.session_get_failed' => 'Échec de la récupération de la session', + 'errors.door.asset.invalid_type' => 'Type de ressource invalide', + 'errors.door.asset.door_not_found' => 'Porte introuvable', + 'errors.door.asset.not_defined' => 'Ressource non définie dans le manifeste', + 'errors.door.asset.file_not_found' => 'Fichier de ressource introuvable', + 'errors.door.asset.access_denied' => 'Accès refusé', + 'errors.tic.file_area_create_failed' => 'Échec de la création de la zone de fichiers à partir des métadonnées TIC', + 'errors.tic.validation_failed' => 'Échec de la validation du fichier TIC', + 'errors.tic.virus_detected' => 'Fichier rejeté : virus détecté', + 'errors.tic.processing_failed' => 'Échec du traitement TIC', + 'errors.virus_scanner.not_available' => 'Analyse antivirus non disponible', + 'errors.virus_scanner.file_not_found' => 'Fichier introuvable pour l\'analyse antivirus', + 'errors.virus_scanner.scan_error' => 'Erreur lors de l\'analyse antivirus', + 'errors.admin.i18n_overrides.invalid_locale' => 'Paramètre régional invalide ou non pris en charge', + 'errors.admin.i18n_overrides.missing_params' => 'Le paramètre régional et le nom du catalogue sont requis', + 'errors.admin.i18n_overrides.load_failed' => 'Échec du chargement du catalogue', + 'errors.admin.i18n_overrides.save_failed' => 'Échec de l\'enregistrement des substitutions', +]; diff --git a/config/i18n/fr/terminalserver.php b/config/i18n/fr/terminalserver.php new file mode 100644 index 000000000..bf84e9b12 --- /dev/null +++ b/config/i18n/fr/terminalserver.php @@ -0,0 +1,194 @@ + 'Trop de tentatives de connexion échouées. Veuillez réessayer plus tard.', + 'ui.terminalserver.server.login_menu.prompt' => 'Que souhaitez-vous faire :', + 'ui.terminalserver.server.login_menu.login' => ' (L) Se connecter à un compte existant', + 'ui.terminalserver.server.login_menu.register' => ' (R) Créer un nouveau compte', + 'ui.terminalserver.server.login_menu.quit' => ' (Q) Quitter', + 'ui.terminalserver.server.login_menu.choice' => 'Votre choix : ', + 'ui.terminalserver.server.goodbye' => 'Au revoir !', + 'ui.terminalserver.server.press_enter_disconnect' => 'Appuyez sur Entrée pour vous déconnecter.', + 'ui.terminalserver.server.login.username_prompt' => 'Nom d\'utilisateur : ', + 'ui.terminalserver.server.login.password_prompt' => 'Mot de passe : ', + 'ui.terminalserver.server.login.success' => 'Connexion réussie.', + 'ui.terminalserver.server.login.failed_remaining' => 'Échec de la connexion. {remaining} tentative(s) restante(s).', + 'ui.terminalserver.server.login.failed_max' => 'Échec de la connexion. Nombre maximum de tentatives dépassé.', + 'ui.terminalserver.server.registration.title' => '=== Inscription d\'un nouvel utilisateur ===', + 'ui.terminalserver.server.registration.intro' => 'Veuillez fournir les informations suivantes pour créer votre compte.', + 'ui.terminalserver.server.registration.cancel_hint' => '(Tapez "cancel" à n\'importe quelle invite pour annuler l\'inscription)', + 'ui.terminalserver.server.registration.username' => 'Nom d\'utilisateur (3-20 caractères, lettres/chiffres/tiret bas) : ', + 'ui.terminalserver.server.registration.password' => 'Mot de passe (8 caractères minimum) : ', + 'ui.terminalserver.server.registration.confirm' => 'Confirmer le mot de passe : ', + 'ui.terminalserver.server.registration.password_mismatch' => 'Erreur : Les mots de passe ne correspondent pas.', + 'ui.terminalserver.server.registration.realname' => 'Nom réel : ', + 'ui.terminalserver.server.registration.email' => 'E-mail (facultatif) : ', + 'ui.terminalserver.server.registration.location' => 'Localisation (facultatif) : ', + 'ui.terminalserver.server.registration.submitting' => 'Envoi de l\'inscription...', + 'ui.terminalserver.server.registration.success' => 'Inscription réussie !', + 'ui.terminalserver.server.registration.pending' => 'Votre compte a été créé et est en attente d\'approbation.', + 'ui.terminalserver.server.registration.pending_review' => 'Vous serez notifié dès qu\'un administrateur aura examiné votre inscription.', + 'ui.terminalserver.server.press_esc' => 'Appuyez deux fois sur ÉCHAP pour continuer...', + 'ui.terminalserver.server.banner.title' => 'Terminal BinktermPHP', + 'ui.terminalserver.server.banner.system' => 'Système : ', + 'ui.terminalserver.server.banner.location' => 'Localisation : ', + 'ui.terminalserver.server.banner.origin' => 'Origine : ', + 'ui.terminalserver.server.banner.web' => 'Web : ', + 'ui.terminalserver.server.banner.visit_web' => 'Pour passer un bon moment, visitez-nous sur le web @ {url}', + 'ui.terminalserver.server.banner.tls' => 'Connecté via TLS', + 'ui.terminalserver.server.banner.no_tls' => 'Connecté sans TLS - utilisez le port {port} pour une connexion chiffrée', + 'ui.terminalserver.server.ssh_banner.welcome' => 'Bienvenue sur {system}.', + 'ui.terminalserver.server.ssh_banner.line2' => 'Connectez-vous avec vos identifiants, ou entrez n\'importe quel nom d\'utilisateur/mot de passe', + 'ui.terminalserver.server.ssh_banner.line3' => 'pour accéder à l\'écran de connexion principal du BBS.', + 'ui.terminalserver.server.menu.title' => 'Menu principal', + 'ui.terminalserver.server.menu.select_option' => 'Sélectionnez une option :', + 'ui.terminalserver.server.menu.netmail' => 'N) Netmail ({count} messages)', + 'ui.terminalserver.server.menu.echomail' => 'E) Echomail ({count} messages)', + 'ui.terminalserver.server.menu.whos_online' => 'W) Qui est en ligne', + 'ui.terminalserver.server.menu.shoutbox' => 'S) Shoutbox', + 'ui.terminalserver.server.menu.polls' => 'P) Sondages', + 'ui.terminalserver.server.menu.doors' => 'D) Jeux de portes', + 'ui.terminalserver.server.menu.quit' => 'Q) Quitter', + 'ui.terminalserver.server.farewell' => 'Merci de votre visite, bonne journée !', + 'ui.terminalserver.server.visit_web' => 'Revenez nous rendre visite sur le web à {url}', + 'ui.terminalserver.server.whos_online.title' => 'Qui est en ligne (dernières {minutes} minutes)', + 'ui.terminalserver.server.whos_online.empty' => 'Aucun utilisateur en ligne.', + 'ui.terminalserver.server.idle.disconnect' => 'Délai d\'inactivité dépassé - déconnexion...', + 'ui.terminalserver.server.idle.warning_line' => 'Êtes-vous toujours là ? (Appuyez sur Entrée pour continuer)', + 'ui.terminalserver.server.idle.warning_key' => 'Êtes-vous toujours là ? (Appuyez sur une touche pour continuer)', + 'ui.terminalserver.server.press_any_key' => 'Appuyez sur une touche pour revenir...', + 'ui.terminalserver.server.press_continue' => 'Appuyez sur une touche pour continuer...', + 'ui.terminalserver.editor.title' => 'ÉDITEUR DE MESSAGES - MODE PLEIN ÉCRAN', + 'ui.terminalserver.editor.shortcuts' => 'Ctrl+K=Aide Ctrl+Z=Envoyer Ctrl+C=Annuler', + 'ui.terminalserver.editor.cancelled' => 'Message annulé.', + 'ui.terminalserver.editor.saved' => 'Message enregistré et prêt à être envoyé.', + 'ui.terminalserver.editor.starting_text' => 'Démarrage avec le texte cité. Entrez votre réponse ci-dessous.', + 'ui.terminalserver.editor.instructions' => 'Saisissez le texte du message. Terminez avec une ligne contenant uniquement ".". Tapez "/abort" pour annuler.', + 'ui.terminalserver.editor.help.title' => 'AIDE DE L\'ÉDITEUR DE MESSAGES', + 'ui.terminalserver.editor.help.separator' => '-------------------', + 'ui.terminalserver.editor.help.navigate' => 'Touches fléchées = Déplacer le curseur', + 'ui.terminalserver.editor.help.edit' => 'Retour arrière/Suppr = Modifier le texte', + 'ui.terminalserver.editor.help.help' => 'Ctrl+K = Aide', + 'ui.terminalserver.editor.help.start_of_line' => 'Ctrl+A = Début de ligne', + 'ui.terminalserver.editor.help.end_of_line' => 'Ctrl+E = Fin de ligne', + 'ui.terminalserver.editor.help.delete_line' => 'Ctrl+Y = Supprimer la ligne entière', + 'ui.terminalserver.editor.help.save' => 'Ctrl+Z = Enregistrer et envoyer le message', + 'ui.terminalserver.editor.help.cancel' => 'Ctrl+C = Annuler et supprimer le message', + 'ui.terminalserver.compose.to_name' => 'Destinataire : ', + 'ui.terminalserver.compose.to_address' => 'Adresse du destinataire : ', + 'ui.terminalserver.compose.subject' => 'Objet : ', + 'ui.terminalserver.compose.no_recipient' => 'Nom du destinataire requis. Message annulé.', + 'ui.terminalserver.compose.enter_message' => 'Saisissez votre message ci-dessous :', + 'ui.terminalserver.compose.select_tagline' => 'Sélectionnez une signature :', + 'ui.terminalserver.compose.no_tagline' => ' 0) Aucune', + 'ui.terminalserver.compose.tagline_default' => 'Signature n° [{default}] (Entrée pour la valeur par défaut) : ', + 'ui.terminalserver.compose.tagline_none' => 'Signature n° (Entrée pour aucune) : ', + 'ui.terminalserver.compose.message_cancelled' => 'Message annulé (vide).', + 'ui.terminalserver.echomail.no_areas' => 'Aucune zone echo disponible.', + 'ui.terminalserver.echomail.areas_header' => 'Zones echo (page {page}/{total}) :', + 'ui.terminalserver.echomail.areas_nav' => 'Entrez un #, n/p (suivant/précédent), q (quitter)', + 'ui.terminalserver.echomail.no_messages' => 'Aucun message echomail.', + 'ui.terminalserver.echomail.messages_header' => 'Echomail : {area} (page {page}/{total})', + 'ui.terminalserver.echomail.compose_title' => '=== Rédiger un echomail ===', + 'ui.terminalserver.echomail.area_label' => 'Zone : {area}', + 'ui.terminalserver.echomail.posting' => 'Publication de l\'echomail...', + 'ui.terminalserver.echomail.post_success' => '✓ Echomail publié avec succès !', + 'ui.terminalserver.echomail.post_failed' => '✗ Échec de la publication de l\'echomail : {error}', + 'ui.terminalserver.netmail.no_messages' => 'Aucun message netmail.', + 'ui.terminalserver.netmail.header' => 'Netmail (page {page}/{total}) :', + 'ui.terminalserver.netmail.compose_title' => '=== Rédiger un netmail ===', + 'ui.terminalserver.netmail.sending' => 'Envoi du netmail...', + 'ui.terminalserver.netmail.send_success' => '✓ Netmail envoyé avec succès !', + 'ui.terminalserver.netmail.send_failed' => '✗ Échec de l\'envoi du netmail : {error}', + 'ui.terminalserver.polls.disabled' => 'Le bureau de vote est désactivé.', + 'ui.terminalserver.polls.title' => 'Sondages', + 'ui.terminalserver.polls.no_polls' => 'Aucun sondage actif.', + 'ui.terminalserver.polls.detail_title' => 'Détail du sondage', + 'ui.terminalserver.polls.total_votes' => 'Total des votes : {count}', + 'ui.terminalserver.polls.enter_poll' => 'Entrez le n° du sondage ou Q pour revenir : ', + 'ui.terminalserver.polls.vote_prompt' => 'Votez avec le n° d\'option ou Q pour revenir : ', + 'ui.terminalserver.polls.voted' => 'Vote enregistré.', + 'ui.terminalserver.shoutbox.title' => 'Shoutbox', + 'ui.terminalserver.shoutbox.recent_title' => 'Shoutbox récente', + 'ui.terminalserver.shoutbox.no_messages' => 'Aucun message dans la shoutbox.', + 'ui.terminalserver.shoutbox.menu' => '[P]ublier [R]afraîchir [Q]uitter : ', + 'ui.terminalserver.shoutbox.new_shout' => 'Nouveau message (vide pour annuler) : ', + 'ui.terminalserver.shoutbox.posted' => 'Message publié.', + 'ui.terminalserver.shoutbox.post_failed' => 'Échec de la publication du message.', + 'ui.terminalserver.server.menu.files' => 'F) Fichiers', + 'ui.terminalserver.files.no_areas' => 'Aucune zone de fichiers disponible.', + 'ui.terminalserver.files.areas_header' => 'Zones de fichiers (page {page}/{total}) :', + 'ui.terminalserver.files.areas_nav' => 'Entrez un #, n/p (suivant/précédent), q (quitter)', + 'ui.terminalserver.files.area_header' => 'Fichiers : {area} (page {page}/{total})', + 'ui.terminalserver.files.no_files' => 'Aucun fichier dans cette zone.', + 'ui.terminalserver.files.files_nav' => 'T)élécharger n/p (suivant/précédent) Q)uitter', + 'ui.terminalserver.files.files_nav_upload' => 'T)élécharger E)nvoyer n/p (suivant/précédent) Q)uitter', + 'ui.terminalserver.files.files_nav_upload_only' => 'E)nvoyer n/p (suivant/précédent) Q)uitter', + 'ui.terminalserver.files.files_nav_none' => 'n/p (suivant/précédent) Q)uitter', + 'ui.terminalserver.files.transfer_unavailable' => 'ZMODEM désactivé : installez lrzsz (sz/rz) sur le serveur pour activer les transferts.', + 'ui.terminalserver.files.invalid_selection' => 'Sélection invalide.', + 'ui.terminalserver.files.download_prompt' => 'N° du fichier à télécharger (Entrée pour annuler) : ', + 'ui.terminalserver.files.download_error' => 'Fichier introuvable sur le serveur.', + 'ui.terminalserver.files.download_starting' => 'Démarrage du téléchargement ZMODEM : {name}', + 'ui.terminalserver.files.download_hint' => 'Lancez la réception ZMODEM dans votre terminal maintenant...', + 'ui.terminalserver.files.download_done' => 'Transfert terminé.', + 'ui.terminalserver.files.download_failed' => 'Le transfert a échoué ou a été annulé.', + 'ui.terminalserver.files.upload_title' => '=== Envoyer un fichier ===', + 'ui.terminalserver.files.upload_area' => 'Zone : {area}', + 'ui.terminalserver.files.upload_desc_prompt' => 'Courte description (vide pour annuler) : ', + 'ui.terminalserver.files.upload_cancelled' => 'Envoi annulé.', + 'ui.terminalserver.files.upload_starting' => 'Lancez l\'envoi ZMODEM dans votre terminal maintenant...', + 'ui.terminalserver.files.upload_failed' => 'Le transfert a échoué ou a été annulé.', + 'ui.terminalserver.files.upload_done' => 'Fichier envoyé avec succès (ID : {id}).', + 'ui.terminalserver.files.upload_error' => 'Erreur lors de l\'envoi : {error}', + 'ui.terminalserver.files.upload_duplicate' => 'Ce fichier existe déjà dans cette zone.', + 'ui.terminalserver.files.upload_readonly' => 'Cette zone est en lecture seule. Les envois ne sont pas autorisés.', + 'ui.terminalserver.files.upload_admin_only' => 'Seuls les administrateurs peuvent envoyer des fichiers dans cette zone.', + // --- Main menu: terminal settings --- + 'ui.terminalserver.server.menu.terminal_settings' => 'T) Paramètres du terminal', + + // --- Terminal settings page --- + 'ui.terminalserver.settings.title' => '=== Paramètres du terminal ===', + 'ui.terminalserver.settings.charset_label' => 'Jeu de caractères : {value}', + 'ui.terminalserver.settings.ansi_label' => 'Couleur ANSI : {value}', + 'ui.terminalserver.settings.not_set' => 'Non configuré', + 'ui.terminalserver.settings.menu_detect' => 'D) Lancer l\'assistant de détection', + 'ui.terminalserver.settings.menu_charset' => 'C) Changer le jeu de caractères manuellement', + 'ui.terminalserver.settings.menu_ansi' => 'A) Activer/désactiver la couleur ANSI', + 'ui.terminalserver.settings.menu_quit' => 'Q) Retour au menu principal', + 'ui.terminalserver.settings.saved' => 'Paramètres enregistrés.', + 'ui.terminalserver.settings.save_failed' => 'Avertissement : impossible d\'enregistrer les paramètres.', + 'ui.terminalserver.settings.invalid_choice' => 'Choix invalide.', + 'ui.terminalserver.settings.charset_prompt' => 'Sélectionnez : (U)TF-8, (C)P437, (A)SCII : ', + + // --- Terminal detection wizard --- + 'ui.terminalserver.detect.title' => '=== Configuration du terminal ===', + 'ui.terminalserver.detect.intro' => 'Le BBS va maintenant tester votre terminal pour s\'assurer que le contenu s\'affiche correctement.', + 'ui.terminalserver.detect.charset_intro' => 'Test du jeu de caractères :', + 'ui.terminalserver.detect.charset_question' => 'Les caractères ci-dessus apparaissent-ils comme des flèches, des coches et des lettres accentuées ? (O/N) : ', + 'ui.terminalserver.detect.charset_utf8' => 'Jeu de caractères UTF-8 activé.', + 'ui.terminalserver.detect.charset_cp437_intro' => 'Test de tracé de cadre CP437 :', + 'ui.terminalserver.detect.charset_cp437_question' => 'Les caractères ci-dessus apparaissent-ils comme un cadre dessiné avec des lignes et des coins ? (O/N) : ', + 'ui.terminalserver.detect.charset_cp437' => 'Jeu de caractères CP437 (DOS/ANSI) activé.', + 'ui.terminalserver.detect.charset_ascii' => 'Mode ASCII activé.', + 'ui.terminalserver.detect.ansi_intro' => 'Test des couleurs :', + 'ui.terminalserver.detect.ansi_question' => 'Les mots ci-dessus apparaissent-ils en différentes couleurs ? (O/N) : ', + 'ui.terminalserver.detect.ansi_yes' => 'Couleur ANSI activée.', + 'ui.terminalserver.detect.ansi_no' => 'Couleur ANSI désactivée.', + 'ui.terminalserver.detect.complete' => 'Configuration du terminal terminée. Paramètres enregistrés.', + 'ui.terminalserver.detect.press_enter' => 'Appuyez sur Entrée pour continuer...', + + 'ui.terminalserver.doors.no_doors' => 'Aucun jeu de portes n\'est disponible actuellement.', + 'ui.terminalserver.doors.title' => '=== Jeux de portes ===', + 'ui.terminalserver.doors.enter_choice' => 'Entrez un numéro pour jouer, ou Q pour revenir : ', + 'ui.terminalserver.doors.invalid' => 'Sélection invalide.', + 'ui.terminalserver.doors.launching' => 'Lancement de {name}...', + 'ui.terminalserver.doors.launch_error' => 'Erreur : {error}', + 'ui.terminalserver.doors.connecting' => 'Connexion au serveur de jeu...', + 'ui.terminalserver.doors.connect_failed' => 'Impossible de se connecter au pont de jeu. Le pont DOS door est-il en cours d\'exécution ?', + 'ui.terminalserver.doors.connected' => 'Connecté ! Démarrage du jeu...', + 'ui.terminalserver.doors.returned' => 'Retour depuis {name}.', + + 'ui.terminalserver.message.headers_title' => '=== En-têtes du message ===', + 'ui.terminalserver.message.no_headers' => '(Aucun en-tête de message)', +]; diff --git a/config/i18n/fr/translation_warnings.log b/config/i18n/fr/translation_warnings.log new file mode 100644 index 000000000..bf99d3429 --- /dev/null +++ b/config/i18n/fr/translation_warnings.log @@ -0,0 +1 @@ +[common] Key 'time.suffix_singular' returned empty translation diff --git a/config/i18n/hardcoded_allowlist.php b/config/i18n/hardcoded_allowlist.php new file mode 100644 index 000000000..6c95ecf13 --- /dev/null +++ b/config/i18n/hardcoded_allowlist.php @@ -0,0 +1,153 @@ + (1:234/567) +``` + +### Binkp Settings + +| Field | Default | Description | +|-------|---------|-------------| +| `port` | 24554 | TCP port for the binkp daemon | +| `timeout` | 300 | Connection timeout in seconds | +| `max_connections` | 10 | Maximum simultaneous inbound connections | +| `bind_address` | `0.0.0.0` | IP address to bind to (`0.0.0.0` = all interfaces) | +| `inbound_path` | `data/inbound` | Directory for incoming packets | +| `outbound_path` | `data/outbound` | Directory for outgoing packets | +| `preserve_processed_packets` | false | When true, moves processed packets to a `processed/` subdirectory instead of deleting them | + +### Uplink Configuration + +Each entry in the `uplinks` array defines one hub/uplink connection. + +| Field | Required | Description | +|-------|----------|-------------| +| `me` | Yes | Your FTN address as presented to this uplink | +| `address` | Yes | The uplink's FTN address | +| `hostname` | Yes | Uplink hostname or IP address | +| `port` | Yes | Uplink port (typically 24554) | +| `password` | Yes | Authentication password (shared secret) | +| `pkt_password` | No | Packet-level password (if different from session password) | +| `tic_password` | No | TIC file password for file echoes | +| `domain` | Yes | Network domain (e.g., `"fidonet"`, `"fsxnet"`, `"agoranet"`) | +| `networks` | Yes | Address patterns routed through this uplink (see below) | +| `poll_schedule` | No | Cron expression for automated polling, e.g. `"0 */4 * * *"` | +| `allow_markup` | No | Enable Markdown/StyleCodes for messages via this uplink | +| `send_domain_in_addr` | No | Include `@domain` suffix in the ADR address sent to this uplink | +| `enabled` | No | Whether uplink is active (default: `true`) | +| `default` | No | Whether this is the default uplink for unrouted messages | +| `compression` | No | Enable compression (not yet implemented) | +| `crypt` | No | Enable encryption (not yet implemented) | +| `binkp_zone` | No | DNS zone for crashmail fallback routing (e.g. `"binkp.net"`) | + +**Network Patterns** — `networks` uses wildcard patterns: +``` +"1:*/*" → all Zone 1 (FidoNet) +"21:*/*" → all Zone 21 (FSXNet) +"46:*/*" → all Zone 46 (AgoraNet) +``` + +**Multiple networks example:** +```json +"uplinks": [ + { + "me": "1:123/456.0", + "address": "1:1/23", + "domain": "fidonet", + "networks": ["1:*/*", "2:*/*", "3:*/*", "4:*/*"], + "hostname": "hub.fidonet.example.com", + "port": 24554, + "password": "fido_secret", + "poll_schedule": "*/15 * * * *", + "default": true, + "enabled": true + }, + { + "me": "21:1/999", + "address": "21:1/100", + "domain": "fsxnet", + "networks": ["21:*/*"], + "hostname": "hub.fsxnet.example.com", + "port": 24554, + "password": "fsx_secret", + "poll_schedule": "*/30 * * * *", + "enabled": true + } +] +``` + +### Security Settings + +The `security` section controls insecure (passwordless) inbound binkp sessions. + +| Field | Default | Description | +|-------|---------|-------------| +| `allow_insecure_inbound` | `false` | Allow incoming connections without password authentication | +| `insecure_inbound_receive_only` | `true` | Insecure sessions can only deliver mail, not pick up | +| `require_allowlist_for_insecure` | `false` | Only allow insecure sessions from nodes in the Admin allowlist | +| `max_insecure_sessions_per_hour` | `10` | Rate limit for insecure sessions per remote address | +| `allow_plaintext_fallback` | `true` | Allow plaintext fallback when CRAM-MD5 is available | + +Insecure sessions are typically used for receiving mail from nodes that don't have your password configured. The allowlist (Admin → Insecure Nodes) gives fine-grained control. + +### Crashmail Settings + +The `crashmail` section controls immediate direct delivery of netmail, bypassing normal hub routing. + +| Field | Default | Description | +|-------|---------|-------------| +| `enabled` | `false` | Enable crashmail direct delivery | +| `max_attempts` | `3` | Maximum delivery attempts before marking failed | +| `retry_interval_minutes` | `15` | Minutes between retry attempts | +| `use_nodelist_for_routing` | `true` | Look up destination in nodelist for hostname/port | +| `fallback_port` | `24554` | Default port if not found in nodelist | +| `allow_insecure_crash_delivery` | `false` | Allow crashmail delivery without password | + +**DNS Fallback (`binkp_zone`):** When a destination node cannot be found in the nodelist, crashmail can fall back to DNS. Set `binkp_zone` on the matching uplink to enable it: + +```json +{ + "me": "1:123/456.0", + "binkp_zone": "binkp.net" +} +``` + +The hostname is derived from the FTN address using the standard convention: +``` +f{node}.n{net}.z{zone}.{binkp_zone} + +Examples: + 1:123/456 → f456.n123.z1.binkp.net + 2:250/10 → f10.n250.z2.binkp.net +``` + +Compatible with DNS-based registries such as [binkp.net](https://binkp.net). + +--- + +## config/nodelists.json — Nodelist Sources + +Defines sources for automatic nodelist downloads. See `config/nodelists.json.example` for reference. + +```json +{ + "sources": [ + { + "name": "FidoNet", + "domain": "fidonet", + "url": "https://example.com/NODELIST.Z|DAY|", + "enabled": true + }, + { + "name": "FSXNet", + "domain": "fsxnet", + "url": "https://bbs.nz/fsxnet/FSXNET.ZIP", + "enabled": true + } + ] +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Display name for this nodelist source | +| `domain` | Yes | Network domain identifier (must match an uplink `domain`) | +| `url` | Yes | Download URL; supports date macros (see below) | +| `enabled` | No | Whether this source is active (default: `true`) | + +**URL Date Macros:** + +| Macro | Description | Example | +|-------|-------------|---------| +| `\|DAY\|` | Day of year (1–366) | `23` | +| `\|YEAR\|` | 4-digit year | `2026` | +| `\|YY\|` | 2-digit year | `26` | +| `\|MONTH\|` | 2-digit month | `01` | +| `\|DATE\|` | 2-digit day of month | `22` | + +--- + +## config/bbs.json — BBS Feature Settings + +`config/bbs.json` controls BBS-specific features: credits system, file areas, user registration, telnet/SSH settings, and more. The recommended way to edit this is through Admin → BBS Settings in the web interface. + +A documented example is provided in `config/bbs.json.example`. + +--- + +## Other Config Files + +| File | Purpose | Reference | +|------|---------|-----------| +| `config/mrc.json` | MRC multi-relay chat server connection | [docs/MRC_Chat.md](MRC_Chat.md) | +| `config/webdoors.json` | WebDoor game settings and enable/disable | [docs/WebDoors.md](WebDoors.md) | +| `config/dosdoors.json` | DOS door game drop files and node settings | [docs/DOSDoors.md](DOSDoors.md) | +| `config/nativedoors.json` | Native Linux/Windows door programs | [docs/NativeDoors.md](NativeDoors.md) | +| `config/themes.json` | Appearance system shell assignments | [docs/CUSTOMIZING.md](CUSTOMIZING.md) | +| `config/weather.json` | Weather report API key and defaults | [scripts/README_weather.md](../scripts/README_weather.md) | +| `config/lovlynet.json` | LovlyNet network registration | [docs/LovlyNet.md](LovlyNet.md) | +| `config/taglines.txt` | One tagline per line; randomly appended to messages | — | +| `config/filearea_rules.json` | Automated file area processing rules | [docs/FileAreas.md](FileAreas.md) | + +--- + +## Network Ports Reference + +| Service | Default Port | Protocol | Direction | Configured In | +|---------|-------------|----------|-----------|---------------| +| Web interface (via Apache/Caddy/Nginx) | `80`, `443` | HTTP/HTTPS | Inbound | Web server / reverse proxy | +| BinkP daemon | `24554` | TCP | In + Out | `config/binkp.json` → `binkp.port` | +| Telnet daemon (plain) | `2323` | TCP | Inbound | `.env` `TELNET_PORT` | +| Telnet daemon (TLS) | `8023` | TCP/TLS | Inbound | `.env` `TELNET_TLS_PORT` | +| SSH daemon | `2022` | SSH-2/TCP | Inbound | `.env` `SSH_PORT` | +| Gemini capsule daemon | `1965` | Gemini/TLS | Inbound | `.env` `GEMINI_PORT` | +| DOS door WebSocket bridge | `6001` | WebSocket | Inbound | `.env` `DOSDOOR_WS_PORT` | +| DOSBox bridge session range | `5000–5100` | TCP | Internal | Between bridge and emulator | +| Admin daemon (TCP fallback) | `9065` | TCP | localhost | `.env` `ADMIN_DAEMON_SOCKET` | +| PostgreSQL | `5432` | TCP | Internal | `.env` `DB_PORT` | +| MRC relay (remote) | `5000` / `5001` | TCP / TLS | Outbound | `config/mrc.json` | + +**Tips:** +- Expose only the services you actually run. +- Bind internal services (admin daemon, DOSBox bridge, PostgreSQL) to `127.0.0.1`. +- Publish user-facing services through a reverse proxy (Caddy, Nginx, Apache) with TLS. + +--- + +## Welcome & Text Files + +Customise user-facing text by creating optional files in `config/`. Examples are provided as `config/*.example`. + +| File | When shown | +|------|-----------| +| `config/terminal_welcome.txt` | Telnet/SSH BBS login screen (replaces default host:port message) | +| `config/newuser_welcome.txt` | Email sent to newly approved users | +| `config/welcome.txt` | General welcome message on the main page / login screen | + +Files are plain text; newlines are preserved as written. diff --git a/docs/CreditSystem.md b/docs/CreditSystem.md new file mode 100644 index 000000000..ea32aaf81 --- /dev/null +++ b/docs/CreditSystem.md @@ -0,0 +1,77 @@ +# Credits System + +BinktermPHP includes an integrated credits economy that rewards user participation and allows charging for certain actions. Credits can be used to encourage quality content, manage resource usage, and gamify the BBS experience. + +## Key Features + +- Configurable credit costs and rewards for various activities +- Daily login bonuses to encourage regular participation +- New user approval bonuses to welcome approved members +- Bonus rewards for longer, higher-quality content +- Transaction history and balance tracking + +## Default Credit Values + +| Activity | Amount | Type | Notes | +|----------------------------------------|--------|--------|---------------------------------------------------| +| Daily Login | +25 | Reward | Awarded once per day after 5-minute delay | +| New User Approval | +100 | Bonus | One-time reward when account is approved | +| Netmail Sent | -5 | Cost | Private messages to other users | +| Echomail Posted | +3 | Reward | Public forum posts | +| Echomail Posted (approx. 2 paragraphs) | +6 | Bonus | 2× reward for substantial posts (2+ paragraphs) | +| Crashmail Sent | -10 | Cost | Direct delivery bypassing uplink | +| Poll Creation | -15 | Cost | Creating a new poll in voting booth | + +## Configuration + +Credits are configured in `config/bbs.json` under the `credits` section. All values are customizable: + +```json +{ + "credits": { + "enabled": true, + "symbol": "CR", + "daily_amount": 25, + "daily_login_delay_minutes": 5, + "approval_bonus": 100, + "netmail_cost": 1, + "echomail_reward": 5, + "crashmail_cost": 10, + "poll_creation_cost": 15 + } +} +``` + +Settings can also be modified through the web interface at **Admin → BBS Settings → Credits System Configuration**. + +## Transaction Types + +- `payment` — User paid for a service +- `system_reward` — Automatic reward for activity +- `daily_login` — Daily login bonus +- `admin_adjustment` — Manual admin modification +- `npc_transaction` — Transaction with system/game +- `refund` — Credit refund + +## Developer API + +Extensions and WebDoors can integrate with the credits system: + +```php +// Get user's balance +$balance = UserCredit::getBalance($userId); + +// Award credits +UserCredit::credit($userId, 10, 'Completed quest', null, UserCredit::TYPE_SYSTEM_REWARD); + +// Charge credits +UserCredit::debit($userId, 5, 'Used service', null, UserCredit::TYPE_PAYMENT); + +// Get configurable costs/rewards +$cost = UserCredit::getCreditCost('action_name', $defaultValue); +$reward = UserCredit::getRewardAmount('action_name', $defaultValue); +``` + +## Disabling Credits + +Set `"enabled": false` in the credits configuration to disable the entire system. When disabled, all credit-related functionality is hidden and no transactions are recorded. diff --git a/docs/DOSDoors.md b/docs/DOSDoors.md index e44d152e3..094c7287f 100644 --- a/docs/DOSDoors.md +++ b/docs/DOSDoors.md @@ -2,6 +2,8 @@ This document was generated by AI and may contain errors. Report issues on Github. +> **See also:** [Doors.md](Doors.md) for an overview of all door types and shared multiplexing bridge setup. + ## Table of Contents - [Overview](#overview) - [System Architecture](#system-architecture) @@ -131,42 +133,38 @@ The DOS door system uses a **multiplexing bridge** architecture where a single l - Only for adventurous sysops willing to troubleshoot and debug - DOSBox is proven, reliable, and recommended for all deployments -2. **Node.js** 18.x or newer (tested with 24) - - Download from https://nodejs.org/ - - Required for the bridge server - -3. **Node.js Dependencies** (in bridge directory): - ```bash - cd scripts/dosbox-bridge - npm install ws iconv-lite pg dotenv - ``` - - `ws` - WebSocket library - - `iconv-lite` - Character encoding (CP437 ↔ UTF-8) - - `pg` - PostgreSQL client (for authentication) - - `dotenv` - Load environment variables from .env +2. **Node.js** 18.x or newer and bridge dependencies — see [Doors.md](Doors.md#prerequisites) for installation instructions. ### File Structure ``` -binktest/ +binkterm-php/ ├── dosbox-bridge/ # DOSBox configuration and DOS files │ ├── dosbox-bridge-production.conf # Headless config (default) │ ├── dosbox-bridge-test.conf # Visible window config (testing) +│ ├── maintenance.conf # Config for maintenance/admin mode +│ ├── dosdoor-maint.sh # Maintenance shell script (Linux) +│ ├── dosdoor-maint.cmd # Maintenance batch script (Windows) │ └── dos/ # DOS drive (mounted as C:) -│ └── DOORS/ # Door game installations (UPPERCASE - Linux is case-sensitive) -│ └── LORD/ # Example: Legend of the Red Dragon -│ ├── dosdoor.jsn # Door manifest (required) -│ ├── START.BAT # Launch script -│ └── ... (game files) -├── scripts/ -│ ├── dosbox-bridge/ -│ │ └── server.js # WebSocket-to-TCP bridge -│ └── cleanup_expired_dosdoor_sessions.php # Cleanup script -└── data/ - └── doorsessions/ # Per-session temp directories - └── door__node_/ # Session folder - ├── DOOR.SYS # Drop file for this session - └── dosbox.conf # Session-specific DOSBox config +│ ├── CONFIG.SYS # DOS system config +│ ├── DOORS/ # Door game installations (UPPERCASE - Linux is case-sensitive) +│ │ ├── 8WAYSL/ # 8-Way Shootout +│ │ ├── ADMIN/ # Admin utilities door +│ │ ├── BRE/ # Barren Realms Elite +│ │ ├── LORD/ # Legend of the Red Dragon +│ │ │ ├── dosdoor.jsn # Door manifest (required) +│ │ │ ├── START.BAT # Launch script +│ │ │ └── ... (game files) +│ │ └── SKEL/ # Skeleton door template +│ ├── DROPS/ # Per-node drop file directories +│ │ ├── node1/ # Drop files for node 1 (DOOR.SYS, etc.) +│ │ └── node2/ # Drop files for node 2 +│ └── TOOLS/ # DOS utility tools +│ ├── EDIT.COM # DOS text editor +│ ├── EDIT.HLP # Editor help file +│ └── README.txt +└── scripts/ + └── cleanup_expired_dosdoor_sessions.php # Cleanup script ``` --- @@ -203,13 +201,9 @@ sudo make install dosbox-x --version ``` -### 2. Install Node.js Dependencies +### 2. Install Node.js and Bridge Dependencies -From your BinktermPHP root directory: -```bash -cd scripts/dosbox-bridge -npm install ws iconv-lite pg dotenv -``` +See [Doors.md — Prerequisites](Doors.md#prerequisites) for Node.js installation and bridge dependency setup. ### 3. Configure Environment Variables @@ -289,27 +283,7 @@ php scripts/setup.php ### 5. Test the System -Start the multiplexing bridge manually: -```bash -node scripts/dosbox-bridge/multiplexing-server.js -``` - -You should see: -``` -=== DOSBox Door Bridge - Multiplexing Server === -WebSocket Port: 6001 -Bind Address: 127.0.0.1 -TCP Port Range: 5000-5100 -Disconnect Timeout: 0 minutes -Base Path: C:\devel\binktest -Database: binktest@localhost:5432/binktest - -[WS] Server listening on 127.0.0.1:6001 -[WS] Waiting for connections... - -Bridge server started successfully! -Press Ctrl+C to stop. -``` +Start the multiplexing bridge — see [Doors.md — Running the Bridge](Doors.md#running-the-bridge) for instructions. Then visit your BBS and try launching a door game. The bridge will: 1. Authenticate your WebSocket connection @@ -318,105 +292,13 @@ Then visit your BBS and try launching a door game. The bridge will: 4. Launch DOSBox which connects back to the bridge 5. Multiplex data between your browser and the door game -Press Ctrl+C to stop the bridge. - --- ## Starting the Multiplexing Bridge -The bridge is a long-running process that should be started before users can play door games. - -### Development / Testing +See [Doors.md — Running the Bridge](Doors.md#running-the-bridge) for how to start the bridge in development and production (systemd, cron, Windows NSSM). -**Windows:** -```cmd -start-door-bridge.cmd -``` - -**Linux:** -```bash -./start-door-bridge.sh -``` - -The bridge will output: -``` -=== DOSBox Door Bridge - Multiplexing Server === -WebSocket Port: 6001 -Bind Address: 127.0.0.1 -Disconnect Timeout: 0 minutes -Database: binktest@localhost:5432/binktest - -[WS] Server listening on 127.0.0.1:6001 -[WS] Waiting for connections... - -Bridge server started successfully! -Press Ctrl+C to stop. -``` - -### Production (Linux with systemd) - -For production, run the bridge as a systemd service: - -1. **Copy service file:** -```bash -sudo cp docs/dosdoor-bridge.service.example /etc/systemd/system/dosdoor-bridge.service -``` - -2. **Edit service file:** -```bash -sudo nano /etc/systemd/system/dosdoor-bridge.service -``` - -Update these fields: -- `User=` - Your application user (e.g., `binkterm`) -- `WorkingDirectory=` - Full path to binktest directory -- `ExecStart=` - Full path to multiplexing-server.js - -3. **Enable and start:** -```bash -sudo systemctl daemon-reload -sudo systemctl enable dosdoor-bridge -sudo systemctl start dosdoor-bridge -``` - -4. **Check status:** -```bash -sudo systemctl status dosdoor-bridge -sudo journalctl -u dosdoor-bridge -f # Follow logs -``` - -### Production (Linux with cron) - -Alternatively, you can start the bridge via cron using `@reboot`: - -```bash -# Edit crontab -crontab -e - -# Add line to start bridge on boot -@reboot cd /path/to/binkterm && /usr/bin/node scripts/dosbox-bridge/multiplexing-server.js >> data/logs/dosdoor-bridge.log 2>&1 -``` - -**Notes:** -- The bridge runs in the background and logs to `data/logs/dosdoor-bridge.log` -- Make sure the log directory exists: `mkdir -p /path/to/binkterm/data/logs` -- Verify the bridge started after reboot: `ps aux | grep multiplexing-server` -- To stop: `pkill -f multiplexing-server.js` -- For better process management, consider using systemd (above) or a process supervisor like PM2 - -### Production (Windows) - -On Windows, run the bridge as a service using [NSSM](https://nssm.cc/) or similar: - -```cmd -nssm install DOSBoxBridge "C:\Program Files\nodejs\node.exe" "C:\path\to\binktest\scripts\dosbox-bridge\multiplexing-server.js" -nssm set DOSBoxBridge AppDirectory "C:\path\to\binktest" -nssm start DOSBoxBridge -``` - -### Monitoring - -The bridge logs status every 60 seconds: +The bridge logs active session status every 60 seconds: ``` [STATUS] Active sessions: 3 - door_1_node1_123: 45s, WS:true, DOS:true @@ -1293,9 +1175,6 @@ php scripts/cleanup_expired_dosdoor_sessions.php # Kill all door processes kill-all-door-procs.cmd -# Start multiplexing bridge -node scripts/dosbox-bridge/multiplexing-server.js - # Check WebSocket port in use netstat -ano | findstr :6001 diff --git a/docs/Doors.md b/docs/Doors.md new file mode 100644 index 000000000..44beffea3 --- /dev/null +++ b/docs/Doors.md @@ -0,0 +1,269 @@ +# Door Games - Sysop Documentation + +BinktermPHP supports three types of door games, each suited to different use cases. + +| Type | Description | Doc | +|------|-------------|-----| +| **DOS Doors** | Classic DOS games (LORD, TradeWars, etc.) running under DOSBox-X | [DOSDoors.md](DOSDoors.md) | +| **Native Doors** | Linux/Windows binaries or scripts running via PTY | [NativeDoors.md](NativeDoors.md) | +| **WebDoors** | Browser-based HTML5/PHP games embedded in an iframe | [WebDoors.md](WebDoors.md) | + +WebDoors run entirely in the browser and require no additional server-side components. DOS Doors and Native Doors both require the **multiplexing bridge** described below. + +--- + +## Multiplexing Bridge + +DOS Doors and Native Doors share a single long-running Node.js bridge process (`scripts/dosbox-bridge/multiplexing-server.js`). The bridge: + +- Listens for WebSocket connections from browsers on a single port (default: 6001) +- Authenticates sessions against the database +- For **DOS Doors**: launches DOSBox and multiplexes TCP I/O +- For **Native Doors**: spawns the door executable via `node-pty` and multiplexes PTY I/O + +``` +[Browser] ──→ wss://bbs.example.com:6001 ──→ [Multiplexing Bridge] + (WebSocket) (Node.js Process) + ├──→ TCP ←── DOSBox → DOS Door + └──→ PTY ←─ Native Binary +``` + +Both door types use the same bridge process, the same WebSocket port, and the same session database table. You only need to run one bridge instance to support both. + +--- + +## Prerequisites + +### Node.js + +Node.js 18.x or newer is required. Tested with Node.js 24. + +- Linux: `sudo apt install nodejs` or download from https://nodejs.org/ +- Windows/macOS: Download from https://nodejs.org/ + +Verify: `node --version` + +### Bridge Dependencies + +Install Node.js dependencies from the bridge directory: + +```bash +cd scripts/dosbox-bridge +npm install +``` + +This installs: +- `ws` — WebSocket server +- `iconv-lite` — CP437 ↔ UTF-8 encoding (DOS doors) +- `pg` — PostgreSQL client for session authentication +- `node-pty` — PTY support for native doors +- `dotenv` — reads `.env` configuration + +### Database Schema + +The bridge reads session records from the database. Tables are created by `scripts/setup.php`: + +```bash +php scripts/setup.php +``` + +--- + +## File Structure + +``` +binkterm-php/ +├── scripts/ +│ └── dosbox-bridge/ +│ ├── multiplexing-server.js # WebSocket multiplexing bridge server +│ └── emulator-adapters.js # DOSBox/emulator backend adapters +└── data/ + ├── run/ + │ └── multiplexing-server.pid # PID file (daemon mode) + └── logs/ + └── multiplexing-server.log # Bridge log (daemon mode) +``` + +For door-type-specific layouts see: +- [DOSDoors.md — File Structure](DOSDoors.md#file-structure) — `dosbox-bridge/dos/`, door installations, drop file directories +- [NativeDoors.md — File Structure](NativeDoors.md#file-structure) — `native-doors/doors/`, drop files, runtime config + +--- + +## Configuration + +The bridge reads settings from your `.env` file. Shared settings relevant to both door types: + +```bash +# WebSocket port for the multiplexing bridge (default: 6001) +DOSDOOR_WS_PORT=6001 + +# WebSocket bind address (default: 127.0.0.1) +# Use 127.0.0.1 behind a reverse proxy (recommended for production) +# Use 0.0.0.0 for direct access during development +DOSDOOR_WS_BIND_HOST=127.0.0.1 + +# WebSocket URL seen by browsers (optional - auto-detected if not set) +# Set this if you are behind an SSL-terminating reverse proxy +# DOSDOOR_WS_URL=wss://bbs.example.com:6001 +# DOSDOOR_WS_URL=wss://bbs.example.com/dosdoor + +# Maximum simultaneous door sessions across all door types (default: 10) +# Each session uses one node number (1 to MAX_SESSIONS) +DOSDOOR_MAX_SESSIONS=10 + +# Comma-separated list of proxy IP addresses whose X-Forwarded-For header is trusted +# for client IP resolution in logs. Only connections arriving from one of these IPs +# will have their remote address replaced by the forwarded value. (default: 127.0.0.1) +# DOSDOOR_TRUSTED_PROXIES=127.0.0.1,10.0.0.1 +``` + +For DOS-door-specific settings (DOSBox executable, disconnect timeout, etc.) see [DOSDoors.md](DOSDoors.md#configuration). + +--- + +## Running the Bridge + +### Development / Testing + +```bash +node scripts/dosbox-bridge/multiplexing-server.js +``` + +Expected output: + +``` +=== DOSBox Door Bridge - Multiplexing Server === +WebSocket Port: 6001 +Bind Address: 127.0.0.1 +TCP Port Range: 5000-5100 +Database: binktest@localhost:5432/binktest + +[WS] Server listening on 127.0.0.1:6001 +[WS] Waiting for connections... + +Bridge server started successfully! +Press Ctrl+C to stop. +``` + +### Production — Daemon mode + +The bridge has built-in daemon support via the `--daemon` flag: + +```bash +node scripts/dosbox-bridge/multiplexing-server.js --daemon +``` + +This forks a detached background process and exits immediately, returning the shell prompt. Output: + +``` +Starting in daemon mode (PID: 12345) +PID file: /path/to/binkterm-php/data/run/multiplexing-server.pid +Log file: /path/to/binkterm-php/data/logs/multiplexing-server.log +``` + +The daemon: +- Writes its PID to `data/run/multiplexing-server.pid` +- Logs to `data/logs/multiplexing-server.log` +- Detects a stale PID file and cleans it up automatically on start +- Refuses to start a second instance if one is already running +- Responds to `SIGTERM` / `SIGINT` for clean shutdown + +**Start on boot (cron):** + +```bash +crontab -e +# Add: +@reboot cd /path/to/binkterm && /usr/bin/node scripts/dosbox-bridge/multiplexing-server.js --daemon +``` + +**Stop the daemon:** + +```bash +kill $(cat data/run/multiplexing-server.pid) +``` + +### Production — Linux (systemd) + +A service unit example is provided at `docs/dosdoor-bridge.service.example`. + +1. **Copy the service file:** + ```bash + sudo cp docs/dosdoor-bridge.service.example /etc/systemd/system/dosdoor-bridge.service + ``` + +2. **Edit the service file** and update: + - `User=` — the user that runs BinktermPHP (e.g. `binkterm`) + - `WorkingDirectory=` — full path to the BinktermPHP directory + - `ExecStart=` — full paths to `node` and `multiplexing-server.js` + +3. **Enable and start:** + ```bash + sudo systemctl daemon-reload + sudo systemctl enable dosdoor-bridge + sudo systemctl start dosdoor-bridge + ``` + +4. **Check status:** + ```bash + sudo systemctl status dosdoor-bridge + sudo journalctl -u dosdoor-bridge -f + ``` + +### Production — Windows (NSSM) + +Use [NSSM](https://nssm.cc/) to run the bridge as a Windows service: + +```cmd +nssm install DoorBridge "C:\Program Files\nodejs\node.exe" "C:\path\to\binktest\scripts\dosbox-bridge\multiplexing-server.js" +nssm set DoorBridge AppDirectory "C:\path\to\binktest" +nssm start DoorBridge +``` + +--- + +## Reverse Proxy + +If your BBS is served over HTTPS, browsers will require the WebSocket connection to also be secure (`wss://`). Two common approaches: + +**Option A — Expose the bridge on a separate port with SSL:** +Configure your reverse proxy to terminate SSL on a dedicated port (e.g. 6001) and forward to the bridge on `127.0.0.1:6001`. Set `DOSDOOR_WS_URL=wss://bbs.example.com:6001`. + +**Option B — Path-based proxy:** +Forward a path to the bridge. Set `DOSDOOR_WS_URL=wss://bbs.example.com/doorplayersocket`. + +The path `/doorplayersocket` is recommended — it is descriptive enough to be self-documenting and unlikely to conflict with any existing application routes. + +nginx example for Option B: +```nginx +location /doorplayersocket { + proxy_pass http://127.0.0.1:6001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600; +} +``` + +Apache example for Option B (requires `mod_proxy`, `mod_proxy_http`, and `mod_proxy_wstunnel`): +```apache +# Enable required modules if not already active: +# a2enmod proxy proxy_http proxy_wstunnel + +ProxyPass /doorplayersocket ws://127.0.0.1:6001/ +ProxyPassReverse /doorplayersocket ws://127.0.0.1:6001/ +``` + +If your VirtualHost uses `SSLProxyEngine`, add: +```apache +SSLProxyEngine on +``` + +--- + +## Door Type Details + +- [DOS Doors](DOSDoors.md) — Setup, DOSBox configuration, adding door games, drop file format, troubleshooting +- [Native Doors](NativeDoors.md) — Manifest format, environment variables, platform notes, test doors +- [WebDoors](WebDoors.md) — Manifest format, iframe integration, BBS API, credits system diff --git a/docs/Localization.md b/docs/Localization.md new file mode 100644 index 000000000..267c2361d --- /dev/null +++ b/docs/Localization.md @@ -0,0 +1,492 @@ +# Localization in BinktermPHP + +BinktermPHP uses a key-based localization system covering Twig templates, PHP route/controller code, and client-side JavaScript. Translation catalogs live under `config/i18n/` and are loaded on demand by locale. + +--- + +## Directory Structure + +``` +config/i18n/ +├── en/ +│ ├── common.php # UI strings (templates, JS) +│ └── errors.php # API error messages +├── es/ +│ ├── common.php +│ └── errors.php +├── overrides/ # Sysop phrase overrides (JSON, applied on top of base catalogs) +│ └── / +│ └── .json +└── hardcoded_allowlist.php # Known-OK English strings exempt from the linter +``` + +Each locale directory name becomes a supported locale identifier (e.g. `en`, `es`). New locales are added by creating a new directory with `common.php` and `errors.php` files. + +> **Note on the bundled Spanish (`es`) translation:** The Spanish catalog was generated by AI and has not been independently reviewed for accuracy. It may contain errors, awkward phrasing, or incorrect terminology. Community corrections are welcome — see the [Translation Contributor Workflow](#translation-contributor-workflow) section below. + +--- + +## Core Classes + +### `Translator` (`src/I18n/Translator.php`) + +Loads and caches catalog files, performs key lookup with fallback, and interpolates `{param}` placeholders. + +- Reads `I18N_DEFAULT_LOCALE` from `.env` (default: `en`). +- Supported locales are read from `I18N_SUPPORTED_LOCALES` (comma-separated), or auto-discovered from the `config/i18n/` directory structure. +- On a missing key, falls back to the default locale, then returns the key itself as a last resort. +- Missing keys can be logged by setting `I18N_LOG_MISSING_KEYS=true` and optionally `I18N_MISSING_KEYS_LOG_FILE`. +- After loading a base `.php` catalog, automatically merges any sysop overrides from `config/i18n/overrides//.json` (see [Language Phrase Overrides](#language-phrase-overrides)). + +### `LocaleResolver` (`src/I18n/LocaleResolver.php`) + +Determines the active locale for a request using this priority order: + +1. Explicit locale argument (e.g. from a `?locale=` query param) +2. Authenticated user's saved locale preference (`users.locale` column) +3. `binktermphp_locale` cookie +4. `Accept-Language` request header (highest `q` value wins) +5. Default locale from `.env` + +`persistLocale()` writes the resolved locale to the cookie for one year. + +--- + +## Server-Side Translation + +### Twig Templates + +The `t()` function is registered globally in `Template.php` and is available in every template. + +```twig +{# Basic usage #} +{{ t('ui.login.title', {}, locale, ['common']) }} + +{# With parameters #} +{{ t('ui.polls.create.submit', {'cost': poll_cost}, locale, ['common']) }} + +{# Errors namespace #} +{{ t('errors.auth.invalid_credentials', {}, locale, ['errors']) }} +``` + +**Arguments:** `t(key, params, locale, namespaces)` + +- `key` — dot-separated translation key +- `params` — object of `{placeholder}` substitutions +- `locale` — the `locale` Twig global (set per-request by `Template.php`) +- `namespaces` — array of catalogs to search; defaults to `['common']` + +The `locale` and `supported_locales` globals are automatically available in every template. + +### PHP (Routes / Controllers) + +Use `apiLocalizedText()` for strings returned in API responses: + +```php +apiLocalizedText('errors.auth.invalid_credentials', 'Invalid credentials'); +``` + +This resolves the current user's locale automatically. An optional `$user` array, `$params`, and `$namespace` can be passed. + +--- + +## API Error Responses + +All API errors must use the `apiError()` helper so the frontend can resolve the display text: + +```php +apiError('errors.some.key', apiLocalizedText('errors.some.key', 'English fallback'), 400); +``` + +The response payload shape is: + +```json +{ + "success": false, + "error_code": "errors.some.key", + "error": "Translated error message" +} +``` + +The `error_code` field is the translation key. The `error` field is the server-side translated string. The frontend can re-translate using the client-side catalog if needed (see below). + +Success responses that carry a human-readable message use `message_code` / `message` the same way: + +```json +{ + "success": true, + "message_code": "ui.some.success_key", + "message": "Translated success message" +} +``` + +--- + +## Client-Side Translation + +### Catalog Loading + +`Template.php` injects `window.appLocale`, `window.appDefaultLocale`, and `window.appI18nNamespaces` into every page via `base.twig`. On `DOMContentLoaded`, `app.js` fetches the catalog for all namespaces (always `['common', 'errors']`) from: + +``` +GET /api/i18n/catalog?ns=common,errors&locale= +``` + +The response merges the default locale catalog with the active locale catalog so only translated keys need to be provided for non-default locales. + +### `window.t(key, params, fallback)` + +The primary translation function available everywhere: + +```js +window.t('ui.polls.create.submit', { cost: 25 }, 'Create Poll (25 credits)') +``` + +- Looks up `key` in loaded catalogs. +- Interpolates `{param}` placeholders from `params`. +- Returns `fallback` (or the key itself) when the key is not found. + +### `uiT(key, fallback, params)` (template-local wrapper) + +Many templates define a local wrapper to handle the case where `window.t` is not yet available: + +```js +function uiT(key, fallback, params = {}) { + if (window.t) { + return window.t(key, params, fallback); + } + return fallback; +} +``` + +### `getApiErrorMessage(payload, fallback)` + +Resolves the display text for an API error payload: + +```js +.catch(error => { + showAlert('danger', getApiErrorMessage(error, 'Operation failed')); +}); +``` + +Checks `payload.error_code` first (looks it up in the client catalog), then `payload.error`, then `fallback`. + +### `getApiMessage(payload, fallback)` + +Same pattern for success messages using `message_code` / `message`. + +### Lazy Namespace Loading + +Additional namespaces can be loaded on demand: + +```js +loadI18nNamespaces(['common', 'errors']).then(function() { + // catalog is now available +}); +``` + +--- + +## Adding a New Translation Key + +1. **Add to `config/i18n/en/common.php`** (or `errors.php` for API errors): + +```php +'ui.my_feature.some_label' => 'My Label', +``` + +2. **Add the same key to `config/i18n/es/common.php`**: + +```php +'ui.my_feature.some_label' => 'Mi etiqueta', +``` + +3. **Use it in Twig**: + +```twig +{{ t('ui.my_feature.some_label', {}, locale, ['common']) }} +``` + +4. **Use it in JavaScript**: + +```js +window.t('ui.my_feature.some_label', {}, 'My Label') +``` + +5. **Run the validation scripts** before committing: + +```bash +php scripts/check_i18n_hardcoded_strings.php +php scripts/check_i18n_error_keys.php +``` + +--- + +## Key Naming Conventions + +| Prefix | Purpose | +|---|---| +| `ui..*` | Template / UI strings for a specific page | +| `ui.base.*` | Strings in the shared base layout | +| `ui.common.*` | Strings shared across many pages | +| `ui.admin..*` | Admin panel page strings | +| `errors..*` | API error messages | + +Examples: +- `ui.login.title` +- `ui.compose.echomail_guideline_identity` +- `ui.admin.bbs_settings.features.enable_webdoors` +- `errors.auth.invalid_credentials` +- `errors.polls.not_found` + +--- + +## Adding a New Locale + +1. Create `config/i18n//common.php` and `config/i18n//errors.php` returning arrays of translated keys. +2. The locale is auto-discovered from the directory name — no code changes required. +3. Optionally, pin the supported locale list explicitly via `I18N_SUPPORTED_LOCALES=en,es,fr` in `.env`. + +--- + +## Automated Catalog Generation + +The script `scripts/create_translation_catalog.php` translates the English catalogs into a new locale automatically using an AI API (OpenAI or Anthropic Claude). It is the fastest way to bootstrap a new locale and produces a complete `common.php` and `errors.php` ready for human review. + +### Requirements + +- **OpenAI**: set `OPENAI_API_KEY` in `.env` (optionally `OPENAI_API_BASE` for a custom endpoint) +- **Claude**: set `ANTHROPIC_API_KEY` in `.env` (optionally `ANTHROPIC_API_BASE`) + +### Basic Usage + +```bash +# Translate into French using whichever API key is configured +php scripts/create_translation_catalog.php --locale=fr --language="French" + +# Force a specific provider +php scripts/create_translation_catalog.php --locale=fr --language="French" --provider=claude +php scripts/create_translation_catalog.php --locale=de --language="German" --provider=openai + +# Specific model +php scripts/create_translation_catalog.php --locale=ja --language="Japanese" --model=claude-opus-4-6 + +# Overwrite an existing locale +php scripts/create_translation_catalog.php --locale=es --language="Spanish" --overwrite + +# Dry run — translate but do not write files +php scripts/create_translation_catalog.php --locale=fr --language="French" --dry-run +``` + +### Provider Auto-Detection + +The script selects the provider automatically when `--provider` is not given: + +- If only `ANTHROPIC_API_KEY` is set → **Claude** +- Otherwise → **OpenAI** + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--locale` | *(required)* | Target locale code, e.g. `fr`, `de`, `pt-BR` | +| `--language` | *(required)* | Full language name passed to the model, e.g. `French` | +| `--provider` | auto | `openai` or `claude` | +| `--model` | `gpt-4o-mini` / `claude-sonnet-4-6` | Model to use (default depends on provider) | +| `--namespaces` | `common,errors` | Which catalogs to translate | +| `--batch-size` | `150` | Translation keys per API request | +| `--timeout` | `120` | HTTP timeout in seconds per request | +| `--retries` | `3` | Retries on batch failure | +| `--pause-ms` | `0` | Milliseconds between batch requests | +| `--overwrite` | off | Overwrite existing locale files | +| `--dry-run` | off | Translate but do not write files | + +### Output + +Writes `config/i18n//common.php` and `config/i18n//errors.php`. Any keys where placeholder tokens (`{name}`, `%s`, etc.) did not survive translation are kept in English and logged to `config/i18n//translation_warnings.log`. + +### After Running + +1. Review the output files for quality — AI translations are a starting point, not a finished product. +2. Fix any entries in `translation_warnings.log`. +3. Test by setting `?locale=` in the browser and browsing key pages. +4. Commit the new locale directory. + +--- + +## Translation Contributor Workflow + +This section describes how to contribute a translation for a new language from start to finish. + +### 1. Find What Needs Translating + +Enable missing-key logging against your target locale so the application tells you what's untranslated as you browse: + +```ini +# .env +I18N_LOG_MISSING_KEYS=true +I18N_MISSING_KEYS_LOG_FILE=/path/to/binkterm/data/logs/i18n-missing.log +I18N_DEFAULT_LOCALE=en +I18N_SUPPORTED_LOCALES=en,fr # add your target locale +``` + +Then browse the site with your browser's `Accept-Language` set to the target locale (or append `?locale=fr` to any URL). Missing keys accumulate in the log file. + +Alternatively, use the English catalog as your full source of truth — every key in `config/i18n/en/common.php` and `config/i18n/en/errors.php` needs a counterpart in your locale. + +### 2. Create the Locale Directory + +```bash +mkdir config/i18n/fr +``` + +### 3. Create `common.php` + +Copy the English catalog and translate the values. Keys must stay identical — only values change. + +```php + 'Connexion', + 'ui.login.username' => 'Nom d\'utilisateur', + 'ui.login.password' => 'Mot de passe', + // ... all other keys +]; +``` + +**Tips:** +- Preserve `{placeholder}` tokens exactly — they are substituted at runtime and must not be translated or renamed. +- Keep the same key ordering as the English file to make diff reviews easier. +- You do not need to include keys whose English value is acceptable as-is; the system falls back to the default locale for any missing key. + +### 4. Create `errors.php` + +Same process for API error messages: + +```php + 'Une erreur inattendue s\'est produite', + 'errors.auth.invalid_credentials' => 'Identifiants invalides', + // ... all other keys +]; +``` + +### 5. Test Your Translation + +Set your browser's preferred language to the target locale (or use the language selector in user settings) and navigate through the interface. Key areas to check: + +- Login, registration, and password reset pages +- Compose (netmail and echomail) — including posting identity guidelines +- Admin panel settings pages +- Error messages (try submitting invalid forms) +- API responses shown in alerts/toasts + +### 6. Run the Validation Scripts + +```bash +php scripts/check_i18n_hardcoded_strings.php +php scripts/check_i18n_error_keys.php +``` + +Both must pass before submitting. They do not check translation quality but do catch missing `errors.*` keys and newly introduced hardcoded English strings. + +### 7. Submit + +Open a pull request against the `main` branch with only the new locale files (`config/i18n//`). Include a brief note in the PR description about which areas were translated and any strings intentionally left in English. + +### Notes for Translators + +- **Placeholders** like `{cost}`, `{system_name}`, `{count}` must appear verbatim in translated strings — the system replaces them at runtime. +- **HTML is not used inside catalog strings.** Do not add markup. +- **Gendered / plural forms** are not currently supported — choose a neutral phrasing where the language requires it. +- Missing keys fall back to English automatically, so a partial translation ships gracefully without breaking the interface. + +--- + +## Language Phrase Overrides + +Sysops can customize individual phrases for any locale without editing the base translation files. Overrides are layered on top of the base catalog at runtime — only the keys you define in an override file are affected; everything else falls through to the base catalog as normal. + +### Admin UI + +Navigate to **Admin → BBS Settings → Language Overrides**. Select a locale and catalog, then click **Load**. Each row shows the translation key, the current base value, and an input field for your override. Leave a field empty to use the base value. Click **Save Overrides** when done. + +### File Format + +Override files are plain JSON stored at `config/i18n/overrides//.json`: + +```json +{ + "ui.terminalserver.server.banner.title": "My BBS Telnet Service", + "ui.nav.home": "Home Base" +} +``` + +Only include the keys you want to override. Keys not present in the file are unaffected. Saving an empty set of overrides removes the file entirely. + +### How It Works + +When `Translator` loads a catalog it checks for a corresponding override file after loading the base `.php` catalog and merges any matching keys on top. The override is transparent to all callers — `t()`, `window.t()`, and API responses all see the overridden values automatically without any code changes. + +### Notes + +- Override files are written through the **admin daemon** — the web process never writes them directly. +- Keys in override files that do not exist in the base catalog are silently ignored at runtime but are still saved in the file. +- Override files are not tracked by the i18n validation scripts (`check_i18n_hardcoded_strings.php`, `check_i18n_error_keys.php`) and do not need to be committed to version control for a production installation. + +--- + +## Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `I18N_DEFAULT_LOCALE` | `en` | Fallback locale when no user preference or browser locale matches | +| `I18N_SUPPORTED_LOCALES` | *(auto)* | Comma-separated list of supported locales; auto-discovered if unset | +| `I18N_LOG_MISSING_KEYS` | `false` | Log a warning when a translation key is not found | +| `I18N_MISSING_KEYS_LOG_FILE` | *(php error log)* | Path to write missing-key log entries | + +--- + +## Validation Scripts + +Two scripts keep the catalogs consistent: + +### `scripts/check_i18n_hardcoded_strings.php` + +Scans templates and JavaScript files for user-visible English strings that should be translation keys. Strings in `config/i18n/hardcoded_allowlist.php` are exempt (e.g. API fallback strings, internal values). + +### `scripts/check_i18n_error_keys.php` + +Verifies that every `error_code` used in `apiError()` calls throughout routes and controllers exists in `config/i18n/en/errors.php`. + +Both scripts exit non-zero on failure and are run in CI via `.github/workflows/i18n-error-keys.yml`. + +--- + +## Important: Timing of Client-Side Translations + +The i18n catalog is fetched asynchronously on page load. The load sequence is: + +1. Twig renders the page server-side with the correct locale (always correct) +2. `DOMContentLoaded` fires +3. `loadUserSettings()` fetches `/api/user/settings` (async) +4. On completion, `loadI18nNamespaces()` fetches `/api/i18n/catalog` (async) +5. Only after step 4 does `window.t()` return translated strings + +There is no mechanism that queues or defers `window.t()` calls made before step 4 completes. Any JavaScript that runs during steps 2–4 and tries to set visible text via `window.t()` or `uiT()` will receive the English fallback string regardless of user locale. + +**The primary protection is server-side rendering.** Twig handles the initial render correctly, so JavaScript should not re-render already-translated text during initialization. + +**Rule:** When a UI element is already rendered by Twig server-side, do not overwrite it from JavaScript during page initialization. Only use `window.t()` / `uiT()` to update text in response to user interactions (dropdown changes, button clicks, etc.) — by which point the catalog is reliably loaded. + +If you must translate a string from JS during init (no server-rendered equivalent), defer it until the catalog is ready: + +```js +loadI18nNamespaces(['common']).then(function() { + $('#myElement').text(window.t('ui.my_feature.label', {}, 'My Label')); +}); +``` + diff --git a/docs/NativeDoors.md b/docs/NativeDoors.md index 99be9d628..2ecfe4261 100644 --- a/docs/NativeDoors.md +++ b/docs/NativeDoors.md @@ -1,7 +1,31 @@ # Native Doors +> **See also:** [Doors.md](Doors.md) for an overview of all door types and shared multiplexing bridge setup. + Native doors are BBS door programs that run as native Linux binaries or Windows executables, launched directly via PTY (pseudo-terminal). Unlike DOS doors, they require no emulator — the program runs as a regular system process with full ANSI/VT100 terminal support. +## Multiplexing Bridge Setup + +Native doors use the same multiplexing bridge as DOS doors. Before native doors will work, the bridge must be installed and running. + +**Quick start:** + +```bash +# Install bridge dependencies (includes node-pty for native door PTY support) +cd scripts/dosbox-bridge +npm install + +# Start the bridge (interactive) +node multiplexing-server.js + +# Or run as a background daemon +node multiplexing-server.js --daemon +``` + +For full setup instructions including production service configuration, environment variables, and reverse proxy setup, see [Doors.md](Doors.md). + +--- + ## How It Works 1. A user clicks **Launch** on a native door from the `/games` page. @@ -10,20 +34,25 @@ Native doors are BBS door programs that run as native Linux binaries or Windows 4. A DOOR.SYS drop file is written to `native-doors/drops/NODE{n}/DOOR.SYS` and user data is injected as environment variables. 5. When the door exits (or the user disconnects), the PTY is killed and the session is cleaned up. -## Directory Structure +## File Structure ``` -native-doors/ - doors/ ← install doors here - mydoor/ - nativedoor.json ← required manifest - mydoor.sh ← executable (or binary, .bat, etc.) - icon.png ← optional icon (64×64) - drops/ ← generated at runtime, do not edit - NODE1/ - DOOR.SYS - NODE2/ - DOOR.SYS +binkterm-php/ +├── native-doors/ +│ ├── doors/ # Install doors here +│ │ ├── linuxdoortest/ # Bundled test door (Linux) +│ │ ├── windoortest/ # Bundled test door (Windows) +│ │ └── mydoor/ # Example custom door +│ │ ├── nativedoor.json # Door manifest (required) +│ │ ├── mydoor.sh # Executable (or binary, .bat, etc.) +│ │ └── icon.png # Optional icon (64×64 PNG) +│ └── drops/ # Generated at runtime — do not edit +│ ├── NODE1/ +│ │ └── DOOR.SYS +│ └── NODE2/ +│ └── DOOR.SYS +└── config/ + └── nativedoors.json # Runtime config (managed by admin panel) ``` Each door lives in its own subdirectory under `native-doors/doors/`. The directory name is the door's ID — it is used in URLs and the database, so it must be lowercase with no spaces (e.g. `lord`, `mygame`, `linuxdoortest`). diff --git a/docs/SSHServer.md b/docs/SSHServer.md new file mode 100644 index 000000000..d2511aaae --- /dev/null +++ b/docs/SSHServer.md @@ -0,0 +1,220 @@ +# SSH Server + +BinktermPHP includes a built-in pure-PHP SSH-2 server that provides the same +BBS terminal experience as the Telnet daemon over an encrypted connection. No +external SSH daemon (OpenSSH, Dropbear, etc.) is required. + +SSH is one access method for the shared BinktermPHP Terminal Server. The +post-login feature set (netmail, echomail, file areas, doors, polls, shoutbox, +editor behavior, and menu flow) is documented in: + +- [BinktermPHP Terminal Server](TerminalServer.md) + +This document focuses on SSH-specific transport, daemon setup, and +troubleshooting. + +## Features + +- **SSH-2 protocol** — pure PHP implementation using only `ext-openssl` and + `ext-gmp`; no additional Composer dependencies +- **Password authentication** — credentials verified against the BBS API, + same as Telnet and the web interface +- **Direct login** — correct SSH credentials skip the BBS login screen and + land directly on the main menu +- **Login-screen fallback** — failed SSH auth (after all attempts) drops the + user to the normal BBS login/register screen rather than disconnecting +- **Auto-generated host key** — a 3072-bit RSA host key is created on first + run and stored in `data/ssh/ssh_host_rsa_key` +- **Terminal size** — PTY dimensions negotiated via `pty-req` are passed + through to the BBS session +- **Multi-connection** — forks per connection on Linux/macOS; single-connection + on Windows (no `pcntl_fork`) +- **Daemon mode** — supports `--daemon` flag and PID file management + +## Requirements + +- PHP 8.1+ +- PHP extensions: `curl`, `openssl`, `gmp`, `pcntl` (Linux/macOS for multiple + concurrent connections) +- BinktermPHP web API reachable (defaults to `SITE_URL` from `.env`) + +## Starting the Daemon + +```bash +php ssh/ssh_daemon.php +``` + +The daemon listens on `0.0.0.0:2022` by default. + +## Command Line Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--host=ADDR` | `0.0.0.0` | IP address to bind to | +| `--port=PORT` | `2022` | TCP port to listen on | +| `--api-base=URL` | `SITE_URL` | Base URL for BBS API requests | +| `--debug` | off | Verbose logging to console | +| `--daemon` | off | Run as a background daemon | +| `--pid-file=FILE` | `data/run/sshd.pid` | Path to write the PID file | +| `--insecure` | off | Skip SSL certificate verification for API calls | + +## Environment Variables (`.env`) + +| Variable | Default | Description | +|----------|---------|-------------| +| `SSH_BIND_HOST` | `0.0.0.0` | Bind address | +| `SSH_PORT` | `2022` | Listening port | + +Command-line arguments take precedence over `.env` values. + +## Running as a Service + +### Systemd + +```ini +[Unit] +Description=BinktermPHP SSH Daemon +After=network.target + +[Service] +Type=simple +User=yourusername +Group=yourusername +WorkingDirectory=/path/to/binktest +ExecStart=/usr/bin/php /path/to/binktest/ssh/ssh_daemon.php +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable binkterm-ssh +sudo systemctl start binkterm-ssh +``` + +### Cron (`@reboot`) + +```bash +@reboot /usr/bin/php /path/to/binktest/ssh/ssh_daemon.php --daemon +``` + +## Connecting + +### PuTTY + +1. Host Name: `your-bbs-hostname` +2. Port: `2022` +3. Connection type: **SSH** + +### OpenSSH client + +```bash +ssh -p 2022 your-bbs-hostname +``` + +### SyncTERM + +1. Add new connection +2. Connection Type: **SSH** +3. Address: `your-bbs-hostname` +4. Port: `2022` + +### ZOC + +1. New session → Connection type: **SSH2** +2. Host: `your-bbs-hostname`, Port: `2022` + +## Authentication Behaviour + +| Scenario | Result | +|----------|--------| +| Correct username + password | SSH auth succeeds → logged in, main menu shown immediately | +| Wrong password (all attempts) | SSH channel still opened → BBS login screen shown | +| Protocol error / client disconnect | Connection closed | + +## Host Key + +On first start the daemon generates a 3072-bit RSA private key using +`ext-openssl` and stores it at `data/ssh/ssh_host_rsa_key` (mode `0600`). +Subsequent starts reuse the same key so clients do not see a host-key-changed +warning. + +A bundled `config/ssh_openssl.cnf` is used for key generation so the process +works on Windows without requiring a system-wide OpenSSL installation or the +`OPENSSL_CONF` environment variable. + +To replace the host key, delete `data/ssh/ssh_host_rsa_key` and restart the +daemon. + +## Supported Algorithms + +| Category | Algorithm | +|----------|-----------| +| Key exchange | `diffie-hellman-group14-sha256` | +| Host key | `rsa-sha2-256` | +| Encryption (both directions) | `aes128-ctr` | +| MAC (both directions) | `hmac-sha2-256` | +| Compression | `none` | +| Auth method | `password` | + +These choices favour maximum compatibility with current SSH clients (PuTTY, +OpenSSH, SyncTERM, ZOC) while using only standard PHP crypto primitives. + +## Architecture + +``` +ssh_daemon.php — entry point, argument parsing +ssh/src/ + SshServer.php — accept loop, per-connection fork, daemon/PID support + SshSession.php — SSH-2 wire protocol (KEX, auth, channel setup, crypto) + SshStreamWrapper.php — PHP stream wrapper used on Windows (no pcntl_fork) +config/ssh_openssl.cnf — bundled OpenSSL config for host key generation +data/ssh/ — runtime: host key +data/run/sshd.pid — PID file (daemon mode) +data/logs/sshd.log — log file +``` + +On Linux/macOS the server forks twice per connection: one process runs the SSH +bridge (decrypts/encrypts) and a second runs `BbsSession` on a plain socket +pair. On Windows (no `pcntl_fork`) a PHP stream wrapper transparently handles +SSH crypto inline so `BbsSession` runs in the same process. + +`BbsSession` is shared with the Telnet daemon — the SSH server passes +`$isSsh = true` to skip Telnet negotiation and the ESC anti-bot challenge. + +## Security Considerations + +- The SSH layer provides transport encryption; credentials never travel in + plaintext over the network +- Host key fingerprint should be communicated to users out-of-band (e.g. on + your BBS website) so they can verify on first connect +- The daemon listens on all interfaces (`0.0.0.0`) by default; use + `--host=127.0.0.1` or firewall rules to restrict access if needed +- Use `--insecure` only in development; production should use proper TLS + certificates for the BBS API + +## Troubleshooting + +**Cannot connect** +- Check the daemon is running: `ps aux | grep ssh_daemon` +- Check the port is listening: `ss -tlnp | grep 2022` +- Verify firewall rules allow the port + +**Host key warning on reconnect** +- The host key changed (e.g. `data/ssh/` was deleted). Clients must accept the + new fingerprint or clear the old entry from `~/.ssh/known_hosts`. + +**"Failed to generate SSH host key" on startup** +- Ensure `ext-openssl` is enabled in `php.ini` +- On Windows, verify `config/ssh_openssl.cnf` is present + +**PHP Fatal: pcntl not available (Windows)** +- Expected on Windows. The daemon handles one connection at a time via the + stream wrapper fallback. For multi-connection support use Linux/macOS. + +## See Also + +- [Telnet Daemon](../telnet/README.md) — unencrypted / TLS telnet alternative diff --git a/docs/TerminalServer.md b/docs/TerminalServer.md new file mode 100644 index 000000000..9bde548c4 --- /dev/null +++ b/docs/TerminalServer.md @@ -0,0 +1,84 @@ +# BinktermPHP Terminal Server + +The BinktermPHP Terminal Server is the shared text-mode BBS experience used by +both the Telnet and SSH daemons. Once users are logged in, they interact with +the same menus, handlers, and API-backed features regardless of transport. + +Access methods: +- Telnet daemon: [telnet/README.md](../telnet/README.md) +- SSH daemon: [SSHServer.md](SSHServer.md) + +## Core Functionality + +- Netmail browsing, reading, composing, replying, and sending +- Echomail browsing, threaded reading, composing, replying, and sending +- File areas browsing with ZMODEM downloads and uploads +- Polls (view/vote/create where enabled) +- Shoutbox (view/post where enabled) +- Door launcher integration (DOS doors, native doors, and configured door menu) +- Who's Online display +- Full-screen message editor with cursor navigation and line editing controls +- ANSI color and screen-aware rendering +- Per-user localization (same i18n flow used by web/API) + +## Shared Session Model + +Both transport daemons run the same `BbsSession` flow after connection setup: + +1. Transport handshake and authentication entry +2. Login/register path (or direct-auth for valid SSH credentials) +3. Main menu and feature handlers (Netmail, Echomail, Files, Doors, etc.) +4. Logout and session cleanup + +Because the feature handlers are shared, behavior and capabilities remain +consistent across Telnet and SSH. + +## Transport-Specific Notes + +- Telnet: + - Uses telnet option negotiation and optional TLS listener + - Includes telnet-specific anti-bot/login flow behavior + +- SSH: + - Uses encrypted SSH-2 transport and host-key authentication model + - Supports direct login when SSH credentials validate successfully + +These differences are transport-layer concerns only; terminal features after +login are the same. + +## ZMODEM Requirements (Non-Windows) + +On non-Windows hosts, file transfer support uses external `sz`/`rz` binaries +from the `lrzsz` package, falling back to the built-in PHP ZMODEM implementation +when the binaries are not found. + +- **Default behavior:** terminal file transfers are disabled by default. +- Install `lrzsz` to enable ZMODEM download/upload in file areas. +- If `sz`/`rz` are not found, the built-in PHP implementation is used automatically. +- To enable terminal file transfers, set the following in `.env`: + +```ini +TERMINAL_FILE_TRANSFERS=true +``` + +When enabled, ensure `sz` and `rz` are present (typically via `lrzsz`) and +available in `PATH`, or specify their paths explicitly: + +```ini +TELNET_SZ_BIN=/usr/bin/sz +TELNET_RZ_BIN=/usr/bin/rz +``` + +### Forcing the built-in PHP ZMODEM implementation + +To bypass external `sz`/`rz` binaries and always use the built-in PHP ZMODEM +implementation (useful for testing or if external binaries are unreliable): + +```ini +TELNET_ZMODEM_FORCE_PHP=true +``` + +## Related Documentation + +- [Telnet Daemon](../telnet/README.md) +- [SSH Server](SSHServer.md) diff --git a/docs/UPGRADING_1.8.4.md b/docs/UPGRADING_1.8.4.md index 7a919062a..a18f14063 100644 --- a/docs/UPGRADING_1.8.4.md +++ b/docs/UPGRADING_1.8.4.md @@ -28,6 +28,7 @@ Make sure you've made a backup of your database and files before upgrading. **Bug Fixes** - Compose: sidebar panel can now be collapsed sideways to give the editor more width, with state persisted across page loads - Echo list: areas can now be opened in a new tab via right-click +- Netmail list now shows a paperclip icon next to the subject for messages that have file attachments ## Upgrade Instructions diff --git a/docs/UPGRADING_1.8.6.md b/docs/UPGRADING_1.8.6.md new file mode 100644 index 000000000..32bd88ab6 --- /dev/null +++ b/docs/UPGRADING_1.8.6.md @@ -0,0 +1,130 @@ +# Upgrading to 1.8.6 + +> **Note:** This release introduces localization (i18n) support across the +> entire application — templates, admin panel, API error responses, JavaScript +> UI, and outgoing emails. Localization touches virtually every part of the +> system. Testing has been performed, but some areas may have been missed. +> If you encounter any text that appears untranslated, displays a raw key +> (e.g. `ui.some.key`), or behaves unexpectedly after upgrading, please report +> it at **https://github.com/awehttam/binkterm-php/issues**. + +⚠️ Make sure you've made a backup of your database and files before upgrading. + +## Summary of Changes + +## Localization (i18n) Support + +- Translation catalogs now support broader UI/API coverage across web pages and admin tools. Ships with English (`en`), Spanish (`es`), and French (`fr`). See `docs/Localization.md` for a full technical reference and translation contributor workflow. +- **Note:** The Spanish (`es`) and French (`fr`) translations were generated by AI and have not been independently reviewed for accuracy. They may contain errors, awkward phrasing, or incorrect terminology. Community corrections are welcome via pull request. +- API responses are now expected to use `error_code` / `message_code` (with optional params), so clients can localize consistently per user locale. +- JavaScript translations use lazy catalog loading (`/api/i18n/catalog`). Pages that render text dynamically must initialize after user settings + i18n catalogs are loaded to avoid English fallback text. +- The **telnet and SSH daemons** (through the shared `BbsSession` class) now support localization. All user-facing strings in the telnet server, shell menus, message editor, echomail/netmail browsers, polls, shoutbox, and door launcher are translated via the `terminalserver` catalog namespace (`config/i18n//terminalserver.php`). The daemon defaults to the system locale (`I18N_DEFAULT_LOCALE`) pre-login and switches to the user's saved locale immediately after a successful login. +- `scripts/create_translation_catalog.php` now supports **Anthropic Claude** in addition to OpenAI for automated locale generation. Provider is auto-detected from the presence of `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in `.env`, or set explicitly with `--provider=claude|openai`. Default Claude model is `claude-sonnet-4-6`; default OpenAI model is `gpt-4o-mini`. See `docs/Localization.md` for full usage. +- New CI checks enforce i18n quality: + - `php scripts/check_i18n_error_keys.php` validates error key coverage. + - `php scripts/check_i18n_hardcoded_strings.php` blocks new hardcoded UI strings not in the allowlist. + +### BinktermPHP Terminal Server + +The BinktermPHP Terminal Server provides a BBS-style interactive terminal accessible over two protocols: + +- **Telnet** (`telnet_daemon.php`) — default port `2323`; TLS available on port `8023` +- **SSH** (`ssh/ssh_daemon.php`) — pure-PHP SSH-2 daemon; default port `8022` + +Both access methods share the same session logic (`BbsSession`) and deliver identical BBS features: menus, messaging, file areas, doors, polls, shoutbox, and more. + +#### SSH Access +- New **SSH-2 server** (`ssh/ssh_daemon.php`) — pure-PHP SSH daemon using only `ext-openssl` and `ext-gmp`, no new Composer dependencies. Default port `8022` (configurable via `SSH_PORT` in `.env`). Correct SSH credentials skip the BBS login screen; failed auth drops to the login/register screen instead of disconnecting. Host key is auto-generated on first run at `data/ssh/ssh_host_rsa_key`. See `docs/SSHServer.md` for full documentation. + +#### Telnet Access +- TLS encryption (experimental): enabled by default on port 8023 with an auto-generated self-signed certificate stored in `data/telnet/`. Set `TELNET_TLS=false` in `.env` to disable, or provide your own certificate via `TELNET_TLS_CERT` and `TELNET_TLS_KEY`. Use `--no-tls` on the command line to disable for a single run. + +#### Terminal Features +- New **File Areas** section in the BBS terminal (`F` from the main menu) +- Z-Modem file transfer support has been introduced. Both a native internal Z-Modem implementation and support for lrzsz are available. Native internal is presently recommended. +- Terminal netmail reader now supports **downloading file attachments via ZMODEM** (`Z` in the message viewer when attachments exist). +- Terminal mail browser position is now persisted in `users_meta` using `terminal_*` keys. Netmail restores the last page + selected message. Echomail restores the echoarea list page and per-area message position (page + selected message). +- New API endpoints for terminal state persistence: + - `GET /api/user/terminal-mail-state` + - `POST /api/user/terminal-mail-state` +- Optional debug toggle to force unique outbound attachment filenames during terminal ZMODEM sends: + - `TELNET_ZMODEM_DEBUG_UNIQUE_NAMES=true` +- The message reader now supports **Page Up / Page Down** keys for scrolling through long messages a full screen at a time (in addition to the existing Up/Down line-by-line scrolling). +- The message reader now renders **LSC-001 MARKUP kludge** formatted messages with ANSI terminal formatting. Markdown messages display headings, bold, italic, code blocks, bullet lists, block quotes, and horizontal rules using ANSI escape sequences. StyleCodes messages display bold, italic, underline, and inverse video. Unrecognized formats fall back to plain text. Quoted lines (`> `) are always rendered as plain dim text regardless of the declared markup format. +- Markdown strikethrough (`~~text~~`) now renders as dim `-text-` in the terminal and `text` in the web message reader. +- In the message list, you can now type a message number to jump directly to it. The selection highlight updates live as digits are typed; press Enter to open. +- The message reader now displays headers in a **styled box** instead of plain `---` separators. The box uses charset-appropriate line-drawing characters (UTF-8, CP437, or ASCII) and renders with a dark blue background and gray border on ANSI terminals. The subject line is bold; the date and secondary fields are dimmed for visual hierarchy. +- **Scroll optimization**: Scrolling through a message no longer clears and redraws the entire screen. Only the body rows are repainted in-place using cursor positioning, eliminating flicker on every keypress.- The terminal capability detection wizard now correctly detects **ASCII-only terminals**. When UTF-8 is not supported, the wizard shows a CP437 box-drawing test; terminals that cannot render CP437 are set to ASCII mode instead of defaulting to CP437. +- **Automatic ANSI detection on telnet connect**: ANSI color capability is now detected at connection time via the TELNET TTYPE negotiation (RFC 1091). The server sends `TTYPE SEND` only after the client acknowledges `DO TTYPE` (proper RFC sequence), then uses the terminal-type string to enable color automatically. Clients reporting `DUMB` or sending no TTYPE are served plain ASCII with no color escape sequences. The previous ESC[6n DSR/CPR probe has been removed — it caused SyncTerm and similar clients to pause display rendering until a key was pressed. Saved user terminal settings continue to override the auto-detected value after login. +- **Telnet connect hang fix**: Added `stream_set_write_buffer($conn, 0)` on accepted sockets to disable PHP's userspace write buffer. Previously, banner text and prompts could sit in an 8 KB buffer and not reach the client until a read operation flushed it — appearing as a blank screen on connect. + +### File Areas +- **ClamAV improvements**: Files can now be manually scanned for viruses by admins from the file details modal using the new **Virus Scan** button. New `.env` option: + - `CLAMAV_ALLOW_INFECTED=true` — accept infected files rather than rejecting them; scan result is still recorded +- **Virus detection error**: When an upload is rejected due to virus detection, the UI now shows a specific "File rejected: virus detected" message instead of the generic upload failure. The rejection is also logged to the server log. See [docs/AntiVirus.md](AntiVirus.md) for full setup and configuration instructions. +- The file areas sidebar now has a **search/filter box** to quickly find areas by tag or description. +- The file areas sidebar list is now scrollable with a fixed height, matching the echomail reader style. +- The page and sidebar heading has been renamed from "Files" to "File Areas". +- **TIC file encoding fix**: TIC files and `FILE_ID.DIZ` contents containing CP437 or ISO-8859-1 characters no longer cause a PostgreSQL encoding error. Text is converted to UTF-8 before database insertion. +- **TIC bundle extraction fix**: Files distributed via TIC that are compressed inside a FidoNet day-of-week bundle (e.g. `.FR0`) are now correctly extracted and made available for TIC processing instead of being silently discarded. +- **Inbound processing lock**: `process_packets.php` now uses a file lock to prevent multiple concurrent instances. +- **Unprocessed files**: After packet and TIC processing, any unrecognized files remaining in `data/inbound/` are moved to `data/inbound/unprocessed/` for manual review instead of accumulating indefinitely. +- File owners and admins can now edit a file through the web interface. The **Edit** button appears in the file detail modal for users who have permission. The edit dialog allows changing the filename (which renames the file on disk), the short description, and the long description in a single operation. +- Admins can **move a file to a different file area** from the same edit dialog. A "Move to Area" dropdown is shown to admins only; selecting a different area moves the physical file on disk to the new area's storage directory and updates the database record. +### Native Doors +- Anonymous (guest) access: sysops can now allow unauthenticated users to launch specific native doors by setting `allow_anonymous: true` and `guest_max_sessions: N` in `config/nativedoors.json`. Requires migration v1.10.17.2 (run via `setup.php`). +- New native door: **PubTerm (Public Terminal)** — connects anonymous users to the BBS via telnet. Disabled by default; enable in Admin → Native Doors. Configure target host/port via `PUBTERM_HOST` and `PUBTERM_PORT` in `.env` (defaults to `127.0.0.1:2323`). Linux uses `telnet -E -K`; Windows uses PuTTY `plink` (install via `winget install PuTTY.PuTTY`, or set `PUBTERM_PLINK_BIN` in `.env`). When enabled, a "Connect via Telnet" button appears on the login page. +- New **Guest Doors** page (`/guest-doors`) listing all anonymous-accessible native doors. Enable via Admin → BBS Settings → Enable Guest Doors Page. When enabled, a Guest Doors link appears in the navigation for logged-out users. +- Manifests now support a `platform` field in `requirements` (e.g. `["linux", "windows"]`). The admin UI shows a warning badge if a door's platform requirements don't match the server OS. +- Manifests now support `launch_command_windows` for platform-specific launch commands on Windows. +- Documentation reorganised: new `docs/Doors.md` entry point covers all door types and shared multiplexing bridge setup (including `--daemon` mode); `docs/DOSDoors.md` and `docs/NativeDoors.md` updated to reference it. + +### DOS Door Multiplexing Server +- **SIGHUP config reload**: send `kill -HUP $(cat data/run/multiplexing-server.pid)` to reload `.env` values without restarting. The following settings reload live: `DOSDOOR_DISCONNECT_TIMEOUT`, `DOSDOOR_DEBUG_KEEP_FILES`, `DOSDOOR_CARRIER_LOSS_TIMEOUT`. Settings that require a full restart: `DOSDOOR_WS_PORT`, `DOSDOOR_WS_BIND_HOST`, `DOSDOOR_TRUSTED_PROXIES`, `DB_*`. +- Trusted proxy support: resolves the real client IP from the `X-Forwarded-For` header when the connection originates from a trusted proxy. Set `DOSDOOR_TRUSTED_PROXIES` in `.env` to a comma-separated list of proxy IPs (default: `127.0.0.1`). Connections from unlisted addresses always use the raw socket IP. +- Log lines are now prefixed with `[sessionId|username|ip]` for every session-scoped event, including emulator adapter output (DOSBox, DOSEMU, Native), DB updates, and drop file writes. This makes it straightforward to correlate all activity for a specific user or connection in the log. + +### TIC / File Areas (FidoNet) +- Fixed a PostgreSQL encoding error (`Character not in repertoire`) that occurred when processing TIC files whose `FILE_ID.DIZ` or `Desc`/`LDesc` fields contained CP437 or ISO-8859-1 characters. Descriptions are now converted to valid UTF-8 before database insertion, matching the encoding handling already applied to message packets. + +### Web Echomail Reader +- **ANSI / plain text toggle**: Press `A` while reading a message to switch between ANSI/preformatted rendering and plain text mode. A badge is shown when plain text mode is active. The mode resets to ANSI when opening a new message. +- **Keyboard shortcuts help**: Press `?` or `H` while the message reader is open to display an overlay listing all available keyboard shortcuts. Press either key again to dismiss. + +### Web Netmail Reader +- **Keyboard shortcuts help**: Press `?` or `H` while the message reader is open to display an overlay listing all available keyboard shortcuts. Press either key again to dismiss. + +### Echomail / Netmail +- Fixed multi-level echomail quoting: when replying to a message containing already-quoted lines (e.g. `RW>> text`), the bumped quote now consistently carries a leading space (` RW>>> text`) matching the FSC-0032 quoting style used for first-level quotes. +- Fixed netmail replies using plain `>` quoting instead of FSC-0032 initials style. Netmail replies now quote identically to echomail replies. +- Fixed 30–45 second delay when sending echomail or netmail. The immediate outbound poll triggered after sending was blocking the HTTP response on non-PHP-FPM setups (Apache mod_php, nginx without FPM). The admin daemon now spawns the poll in the background so the response returns as soon as the message is saved. **Requires admin daemon restart** — see upgrade instructions below. +- **Netmail attachments now supported for local delivery.** Sending a file attachment to a user on the local system (including the sysop) stores the file directly into the recipient's private file area. Previously this returned an error. Crashmail is not required for local attachment delivery. +- Fixed locally sent netmail (e.g. messages to Sysop) not appearing in the sender's All view — only in Sent. The message record is now owned by the sender so it appears in both views; the sysop still sees it in their inbox via address matching. +- Fixed malformed or misaddressed echomail (e.g. newsletter feeds without `AREA:`/`SEEN-BY`/`PATH` kludges) being incorrectly delivered to the sysop's netmail inbox. The sysop catch-all fallback in address routing has been removed. Undeliverable messages are now dropped with a detailed log entry (from, to, subject, date, MSGID) and the original `.pkt` packet file is preserved to `data/undeliverable/` for manual inspection. + +### Admin / Sysop Tools +- New **Language Overrides** editor in Admin → BBS Settings → Language Overrides. Sysops can customize individual phrases for any locale and catalog without editing the base translation files. Overrides are stored as JSON in `config/i18n/overrides//.json` and are applied transparently on top of the base catalog at runtime. +- PWA manifest: added app shortcuts for Doors (`/games`) and Files (`/files`). + +### Web / PWA +- Fixed service worker caching: static assets (CSS, JS, fonts) are now served from the SW cache on every navigation with no redundant network requests. Switched from stale-while-revalidate to cache-first strategy; theme stylesheets and FontAwesome fonts are pre-cached at install time. The `sw.js` script now has a dedicated `Cache-Control: no-cache` header in `.htaccess` per the Service Worker spec recommendation. +- Fixed Markdown blockquotes (`>`) not rendering in the UPGRADING doc viewer. Blockquotes now display with a left border accent at normal body font size. + +## Upgrade Instructions + +### From Git + +```bash +git pull +php scripts/setup.php +scripts/restart_daemons.sh +``` + +### Using the Installer + +```bash +wget https://raw.githubusercontent.com/awehttam/binkterm-php-installer/main/binkterm-installer.phar +php binkterm-installer.phar +scripts/restart_daemons.sh +``` + diff --git a/docs/proposals/Translations.md b/docs/proposals/Translations.md new file mode 100644 index 000000000..3e5520a84 --- /dev/null +++ b/docs/proposals/Translations.md @@ -0,0 +1,272 @@ +# Translation Support Plan + +> Draft notice: This proposal is a draft generated by AI and may not have been reviewed for accuracy. + +## Goal +Add multi-language support (i18n/l10n) to the web application without a destabilizing rewrite, while preserving current behavior for English users. + +## Current State Summary +- Translation framework is installed and wired into Twig/PHP/JS. +- Catalogs are namespaced and loaded from `config/i18n//.php` (currently `common.php`, `errors.php`). +- API error responses now generally use structured error payloads with `error_code` + `error`. +- Client-side translation helper (`window.t`) and lazy catalog loading (`/api/i18n/catalog`) are active. +- Hardcoded user-facing strings still exist in some templates/pages, but core shared/user flows have substantial coverage now. + +## Scope +- In scope: + - Web UI (Twig templates, inline JS, `public_html/js/*.js`). + - API response localization strategy. + - Date/time/number formatting consistency by locale. + - Locale selection, persistence, and fallback. + - Translation key management, extraction, QA, and CI checks. +- Out of scope (initial rollout): + - Translating user-generated content (messages, posts, ads). + - Auto-translation services. + - BinkP protocol payload text. + - ~~Telnet/BinkP protocol payload text~~ — **Telnet daemon localization was added to scope and completed in 1.8.6.** All user-facing strings in the telnet server (shell menus, message editor, echomail/netmail browsers, polls, shoutbox, door launcher) are translated via the `terminalserver` catalog namespace. + +## Guiding Principles +- Preserve API stability while introducing localization safely. +- Prefer stable message/error codes over localized server text in APIs. +- Keep English as fallback for all missing keys. +- Ship incrementally by page area, not by “convert everything at once”. + +## Proposed Architecture +1. Locale Resolution +- Resolution order: + 1. User setting (`user_settings.locale`) when authenticated. + 2. Cookie/session locale for anonymous users. + 3. `Accept-Language` best match. + 4. Default locale (`en`). +- Persist locale choice in user settings and anonymous cookie. + +2. Translation Source of Truth +- Store catalogs in `config/i18n/.php` (associative arrays) or JSON files. +- Naming convention for keys: + - `nav.login`, `nav.logout` + - `dashboard.loading_shouts` + - `errors.auth.invalid_credentials` + - `admin.users.not_found` + +3. Server Translator Service +- Add `src/I18n/Translator.php`: + - `translate($key, array $params = [], ?string $locale = null): string` + - fallback chain locale -> base locale -> `en` -> key. +- Add Twig function/filter: + - `t('key')` and optional parameter interpolation. + +4. Frontend Translator +- Expose active locale and subset dictionaries via: + - global `window.i18n` + - or `/api/i18n/catalog?ns=...`. +- Add JS helper: + - `t(key, params)` for UI strings. +- Move repeated strings from inline JS to keys. + +5. API Message Strategy +- Replace direct English API error text with: + - machine code (`error_code`) + - optional default English message for backward compatibility window. +- Frontend maps `error_code` -> localized text. +- Keep transitional compatibility: + - return both fields for one release cycle: `{ error_code, error }`. + +6. Formatting Standardization +- Centralize date/time formatting in JS helper using user locale + timezone. +- Replace hardcoded `en-US` and direct string literals in relative-time functions with keyed translations. + +## Data Model Changes +1. Migration +- Add `locale` column to `user_settings` (default `en`). +- Optional: keep existing `date_format` temporarily; map to locale where possible. + +2. Backward Compatibility +- If no `locale` set, infer from current `date_format` where feasible: + - `en-US` -> `en-US` + - `en-GB` -> `en-GB` + - etc. +- Fall back to `en`. + +## Implementation Phases +### Status Update (March 6, 2026) — Shipped in 1.8.6 +- Phase 0 (Foundation): **Completed** + - Translator + locale resolver added and wired through Twig `t()`. + - Locale persistence/resolution path implemented. + - JS i18n helper and lazy namespace loading endpoint implemented. +- Phase 1 (Shared Shell/UI Chrome): **Completed (with one intentional deferral)** + - `templates/base.twig`, `templates/shells/web/base.twig`, and `templates/shells/bbs-menu/base.twig` are localized. + - `templates/old.base.twig` is intentionally deferred by project decision. +- Phase 2 (High-Traffic User Pages): **Completed** + - Localized pages include: dashboard, netmail, echomail, compose, settings, login, register, forgot/reset password, profile, user profile, about, 404, create poll, shoutbox, polls, shared message, and files. + - `public_html/js/netmail.js` and `public_html/js/echomail.js` fully migrated to translation keys. +- Phase 3 (API Error Code Migration): **Completed** + - `apiError(error_code, error, ...)` pattern is in active use across all major endpoints. + - Frontend consumers use `getApiErrorMessage(...)` broadly. + - Legacy plain-text error responses normalized. +- Phase 4 (Admin Surface): **Completed** + - All high-use admin templates migrated to translation keys: users, dashboard, binkp, economy, polls, doors, file areas, chat/admin tooling, and configuration pages. +- Phase 5 (Hardening and Cleanup): **In Progress (advanced)** + - Validation scripts in place and passing: + - `scripts/check_i18n_hardcoded_strings.php` + - `scripts/check_i18n_error_keys.php` + - Validation snapshot: `Detected hardcoded UI strings: 0`, `New violations: 0`, `Missing keys: 0`. + - Playwright tests verify `es` catalog API structure and key parity with `en`. + - Manual visual QA conducted against a checklist in both `en` and `es` on core user and admin flows. + - Remaining: no automated Playwright smoke pass runs with the user locale set to `es` — secondary locale coverage is manual only. +- Telnet Daemon Localization (added to scope): **Completed** + - All user-facing strings in the telnet server translated via the `terminalserver` catalog namespace. + - Daemon defaults to system locale pre-login, switches to user's saved locale after login. + - Ships with `en` and `es` catalogs (`config/i18n//terminalserver.php`). + +## Phase 0: Foundation (No User-Visible Language Changes) +- Add translator service and locale resolver. +- Wire translator into `Template` and Twig (`t()` helper). +- Add `locale` setting support in backend and settings API. +- Add JS i18n helper and dictionary loading. +- Add `lang` variable to layout templates (``). +- Acceptance: + - App behavior unchanged in English. + - Locale can be set and read end-to-end. + +## Phase 1: Shared Shell/UI Chrome +- Convert common templates first: + - `templates/base.twig` + - `templates/shells/web/base.twig` + - `templates/shells/bbs-menu/base.twig` + - `templates/old.base.twig` +- Convert global app JS status/error labels. +- Acceptance: + - Navigation/footer/common modals translated. + - No missing key placeholders in shared layout. + +## Phase 2: High-Traffic User Pages +- Convert: + - `dashboard.twig`, `netmail.twig`, `echomail.twig`, `compose.twig`, `settings.twig`, `login/register` flows. +- Replace inline alert/confirm/loading strings with keys. +- Acceptance: + - Core message workflows localized. + - Date/time text uses locale consistently. + +## Phase 3: API Error Code Migration +- Introduce `error_code` for major API endpoints: + - auth, messaging, polls, shoutbox, files, admin. +- Frontend consumes codes; fallback to legacy `error` during transition. +- Acceptance: + - No frontend dependency on raw English `error` strings. + +## Phase 4: Admin Surface +- Convert admin templates and admin JS panels incrementally. +- Prioritize high-use pages (users, dashboard, binkp config, stats). +- Acceptance: + - Admin core workflows localized. + +## Phase 5: Hardening and Cleanup +- Remove deprecated English fallback fields in APIs (after compatibility window). +- Add lint/check scripts: + - detect hardcoded English in Twig/JS (allowlist exceptions). + - detect missing translation keys. +- Add snapshot/integration tests for at least `en` + one secondary locale. + +## Work Breakdown (Concrete File Targets) +1. Core +- `src/Template.php` (inject locale and `t()` support) +- `src/MessageHandler.php` (persist/read locale in user settings) +- new `src/I18n/Translator.php` +- new `src/I18n/LocaleResolver.php` + +2. Routes/API +- `routes/api-routes.php` (error code rollout) +- `routes/admin-routes.php` (error code rollout) +- `routes/door-routes.php`, `src/Auth.php` (auth/error consistency) +- `routes/web-routes.php` (manifest strings and locale-aware metadata) + +3. Templates +- Shared bases first, then user pages, then admin pages. + +4. Frontend JS +- `public_html/js/app.js` +- `public_html/js/netmail.js` +- `public_html/js/echomail.js` +- page-local inline scripts in Twig templates. + +## Risks and Mitigations +1. Risk: Massive key churn and merge conflicts. +- Mitigation: namespace keys by feature and convert by phase. + +2. Risk: API clients break if `error` format changes. +- Mitigation: transition period with both `error_code` and `error`. + +3. Risk: Missing keys shipped to production. +- Mitigation: build-time key validation + runtime fallback logging. + +4. Risk: UI overflow/layout regressions in longer languages. +- Mitigation: targeted visual QA on nav, buttons, modals, tables. + +## Testing Strategy +- Unit tests: + - translator fallback and interpolation. + - locale resolver precedence. +- Integration tests: + - login/settings locale persistence. + - key pages render in `en` and secondary locale. +- API tests: + - verify `error_code` contract and fallback. +- Manual QA: + - dashboard, compose, message listing, admin user actions. + +## Definition of Done (Project-Level) +- Locale selectable and persisted. +- Shared UI + core user flows localized. +- API uses stable error codes (with migration complete). +- No critical hardcoded English in in-scope pages. +- CI checks for missing keys/hardcoded strings active. + +## Effort Estimate +- Foundation + shared UI: medium. +- Core user pages + JS conversion: medium-high. +- API error-code migration + admin conversion + test hardening: high. +- Overall: high effort, low-to-medium operational risk if phased as above. + +## Rollout Recommendation +1. Ship `en` + one additional locale first. +2. Enable locale switch behind feature flag for internal testing. +3. Expand locale coverage after API code migration stabilizes. + +## Phase 6: System Scripts and Daemons + +CLI scripts and daemons in `scripts/` generally fall into three categories for i18n purposes: + +**User-facing daemons** (output seen directly by end users — highest priority): +- `src/TelnetServer/` — **Completed in 1.8.6** via `terminalserver` catalog namespace. + +**Interactive sysop tools** (output seen by the person running the command): +- `scripts/user-manager.php` — interactive user management CLI +- `scripts/binkp_status.php` — status display +- `scripts/who.php` — who's online +- `scripts/admin_client.php` — admin CLI client + +**Background daemons and cron scripts** (output goes to log files, sysop-facing only): +- `scripts/binkp_server.php`, `scripts/binkp_scheduler.php`, `scripts/binkp_poll.php` +- `scripts/admin_daemon.php`, `scripts/mrc_daemon.php`, `scripts/gemini_daemon.php` +- `scripts/process_packets.php`, `scripts/echomail_maintenance.php`, and other maintenance scripts +- `scripts/setup.php`, `scripts/upgrade.php` + +**Current Status: Not started.** +Background daemon log output is sysop/developer-facing and is low priority for translation. Interactive sysop tools are moderate priority. No script outside the telnet daemon currently uses the i18n `Translator` class. + +## Phase 7: Legacy API Error Field Retirement + +API responses currently return both `error_code` (the stable translation key) and `error` (a plain English fallback string) for backward compatibility. Once all frontend consumers have been confirmed to use `error_code` exclusively via `getApiErrorMessage(...)`, the plain `error` field can be retired. + +**Current Status: Deferred — compatibility window still open.** + +This is intentionally deferred until there is confidence that no external integrations or older client versions rely on the plain `error` text. Suggested criteria before retiring: +- At least one full release cycle has passed since `error_code` became universal. +- A grep confirms no frontend code matches on raw English error strings. +- Release notes call out the removal explicitly. + +## Immediate Next Steps (From Current Status) +1. Keep CI guardrails green (`check_i18n_hardcoded_strings.php`, `check_i18n_error_keys.php`) on every PR adding new UI or API work. +2. Add a Playwright smoke pass that sets the user locale to `es` and verifies core pages render without raw i18n keys (completes Phase 5). +3. Legacy `error` field retirement is tracked in Phase 7 — no action needed until the compatibility window is judged closed. + diff --git a/native-doors/doors/pubterm/icon.svg b/native-doors/doors/pubterm/icon.svg new file mode 100644 index 000000000..296bbec66 --- /dev/null +++ b/native-doors/doors/pubterm/icon.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + REVERSE POLARITY BBS + GAMES MENU + + + + + + + [ DOORS ] + TYPE + + + + + + 1) Legend of the Red Dragon + RPG + + 2) TradeWars 2002 + SIM + + 3) Barren Realms Elite + STR + + 4) Usurper + RPG + + 5) Global War + STR + + 6) The Pit + ARE + + 7) Exit to Main Menu + SYS + + + + + + ENTER CHOICE: + _ + [ANSI OK] + + + + + + + + + + + + + + diff --git a/native-doors/doors/pubterm/nativedoor.json b/native-doors/doors/pubterm/nativedoor.json new file mode 100644 index 000000000..cb7a70039 --- /dev/null +++ b/native-doors/doors/pubterm/nativedoor.json @@ -0,0 +1,38 @@ +{ + "type": "nativedoor", + "version": "1.0", + "game": { + "name": "Public Terminal", + "short_name": "PUBTERM", + "author": "BinktermPHP", + "version": "1.0", + "release_year": 2026, + "description": "Connect to the BBS via telnet.", + "genre": ["Terminal"], + "players": "Single-player", + "icon": "icon.svg", + "screenshot": null + }, + "door": { + "executable": "pubterm.sh", + "launch_command": "/bin/bash pubterm.sh", + "launch_command_windows": "${PUBTERM_PLINK_BIN:-plink} -telnet ${PUBTERM_HOST:-127.0.0.1} ${PUBTERM_PORT:-2323}", + "dropfile_format": "DOOR.SYS", + "output_encoding": "utf8", + "max_nodes": 5, + "ansi_required": true, + "time_per_day": 60 + }, + "requirements": { + "admin_only": false, + "platform": ["linux", "windows"] + }, + "config": { + "enabled": false, + "credit_cost": 0, + "max_time_minutes": 60, + "max_sessions": 5, + "allow_anonymous": true, + "guest_max_sessions": 5 + } +} diff --git a/native-doors/doors/pubterm/pubterm.sh b/native-doors/doors/pubterm/pubterm.sh new file mode 100644 index 000000000..1e9fc2ba5 --- /dev/null +++ b/native-doors/doors/pubterm/pubterm.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# PubTerm - Public Terminal: telnet connection to the BBS +# Launched as a native door via the BinktermPHP multiplexing bridge. +# +# Flags: +# -E Disable the escape character (prevents user from breaking to telnet prompt) +# -K No automatic login (disables .netrc / TELNET_USER authentication) + +TELNET_HOST="${PUBTERM_HOST:-127.0.0.1}" +TELNET_PORT="${PUBTERM_PORT:-2323}" + +# Check that telnet is installed +if ! command -v telnet &>/dev/null; then + printf '\033[1;31m' + echo "" + echo " *** PUBTERM CONFIGURATION ERROR ***" + echo "" + printf '\033[0m' + echo " The 'telnet' command is not installed on this system." + echo " PubTerm requires the telnet client to connect to the BBS." + echo "" + echo " To install it:" + echo "" + echo " Debian/Ubuntu: sudo apt install telnet" + echo " RHEL/Rocky: sudo dnf install telnet" + echo " Alpine: apk add busybox-extras" + echo "" + echo " If telnet is installed at a non-standard path, set:" + echo " PUBTERM_TELNET_BIN=/path/to/telnet in your .env file" + echo "" + echo " Contact your sysop to resolve this issue." + echo "" + echo " Press any key to exit." + read -r -n 1 -s + exit 1 +fi + +TELNET_BIN="${PUBTERM_TELNET_BIN:-telnet}" + +exec "$TELNET_BIN" -E -K "$TELNET_HOST" "$TELNET_PORT" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..766f33985 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "binkterm-php-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "binkterm-php-tests", + "devDependencies": { + "@playwright/test": "^1.48.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..88e3706a8 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "binkterm-php-tests", + "private": true, + "scripts": { + "test": "playwright test", + "test:smoke": "playwright test tests/playwright/smoke.test.js", + "test:api": "playwright test tests/playwright/api-smoke.test.js", + "test:auth": "playwright test tests/playwright/auth-flow.test.js" + }, + "devDependencies": { + "@playwright/test": "^1.48.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..d18383c3e --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,52 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); +const fs = require('fs'); +const path = require('path'); + +// Load test env file +const envFile = path.join(__dirname, 'tests', '.env.test'); +if (fs.existsSync(envFile)) { + fs.readFileSync(envFile, 'utf-8').split('\n').forEach(line => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return; + const eq = trimmed.indexOf('='); + if (eq === -1) return; + const key = trimmed.slice(0, eq).trim(); + const val = trimmed.slice(eq + 1).trim(); + if (key) process.env[key] = val; + }); +} + +const STORAGE_PATH = path.join(__dirname, 'tests', 'playwright', '.auth-state.json'); + +module.exports = defineConfig({ + testDir: './tests/playwright', + timeout: 30000, + retries: 0, + workers: 1, // Sequential to avoid session conflicts (logout test would invalidate shared cookie) + reporter: [['list'], ['html', { outputFolder: 'tests/playwright-report', open: 'never' }]], + + use: { + baseURL: process.env.TEST_URL || 'http://localhost:1244', + screenshot: 'only-on-failure', + video: 'off', + }, + + projects: [ + // 1. Login once and save cookies + { + name: 'setup', + testMatch: /auth\.setup\.js/, + }, + // 2. Run all tests using saved session + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: STORAGE_PATH, + }, + dependencies: ['setup'], + testIgnore: /auth\.setup\.js/, + }, + ], +}); diff --git a/public_html/.htaccess b/public_html/.htaccess index 1a400280e..b70558703 100644 --- a/public_html/.htaccess +++ b/public_html/.htaccess @@ -1,7 +1,14 @@ RewriteEngine On -# Cache-Control headers - force browsers to revalidate using ETags +# Cache-Control headers + # Service Worker script: spec-recommended no-cache so browsers always check + # for updates via conditional request, but can use ETags for 304 responses. + # Must be declared BEFORE the generic .js rule so it takes precedence. + + Header set Cache-Control "no-cache" + + # CSS/JS: Must check with server before using cached copy Header set Cache-Control "max-age=0, must-revalidate" diff --git a/public_html/css/amber.css b/public_html/css/amber.css index e6012f910..208c1a7f5 100644 --- a/public_html/css/amber.css +++ b/public_html/css/amber.css @@ -1108,10 +1108,6 @@ input:focus, textarea:focus { background-color: rgba(255, 176, 0, 0.1) !important; } -.thread-reply { - position: relative; -} - .thread-level-1 { border-left: 2px solid var(--term-amber-dim) !important; background-color: rgba(204, 136, 0, 0.05) !important; @@ -1132,9 +1128,6 @@ input:focus, textarea:focus { background-color: rgba(255, 51, 51, 0.05) !important; } -.thread-reply::before { - background-color: var(--term-amber-dark) !important; -} .thread-root:hover { background-color: rgba(255, 176, 0, 0.15) !important; diff --git a/public_html/css/cyberpunk.css b/public_html/css/cyberpunk.css index 3ca3710da..19ab30eda 100644 --- a/public_html/css/cyberpunk.css +++ b/public_html/css/cyberpunk.css @@ -1181,10 +1181,6 @@ input:focus, textarea:focus { background-color: rgba(255, 42, 109, 0.1) !important; } -.thread-reply { - position: relative; -} - .thread-level-1 { border-left: 2px solid var(--cyber-cyan) !important; background-color: rgba(5, 217, 232, 0.05) !important; @@ -1205,10 +1201,6 @@ input:focus, textarea:focus { background-color: rgba(204, 32, 87, 0.05) !important; } -.thread-reply::before { - background-color: var(--cyber-purple-dim) !important; -} - .thread-root:hover { background-color: rgba(255, 42, 109, 0.2) !important; } diff --git a/public_html/css/greenterm.css b/public_html/css/greenterm.css index 306ccd48e..15b078ee0 100644 --- a/public_html/css/greenterm.css +++ b/public_html/css/greenterm.css @@ -1108,10 +1108,6 @@ input:focus, textarea:focus { background-color: rgba(51, 255, 51, 0.1) !important; } -.thread-reply { - position: relative; -} - .thread-level-1 { border-left: 2px solid var(--term-green-dim) !important; background-color: rgba(34, 170, 34, 0.05) !important; @@ -1132,9 +1128,6 @@ input:focus, textarea:focus { background-color: rgba(255, 51, 51, 0.05) !important; } -.thread-reply::before { - background-color: var(--term-green-dark) !important; -} .thread-root:hover { background-color: rgba(51, 255, 51, 0.15) !important; diff --git a/public_html/guest-door-player.php b/public_html/guest-door-player.php new file mode 100644 index 000000000..3aa859e44 --- /dev/null +++ b/public_html/guest-door-player.php @@ -0,0 +1,448 @@ + + + + + + + Public Terminal + + + + +
+
Public Terminal
+
+ Status: Disconnected +
+ +
+
+ + + + + diff --git a/public_html/img/logo-square.svg b/public_html/img/logo-square.svg new file mode 100644 index 000000000..17bf7de13 --- /dev/null +++ b/public_html/img/logo-square.svg @@ -0,0 +1,60 @@ + + BinktermPHP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BinktermPHP + + + diff --git a/public_html/img/logo.svg b/public_html/img/logo.svg new file mode 100644 index 000000000..db6f193c9 --- /dev/null +++ b/public_html/img/logo.svg @@ -0,0 +1,60 @@ + + BinktermPHP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BinktermPHP + + + + + + diff --git a/public_html/js/app.js b/public_html/js/app.js index b5b285e37..a4a009233 100644 --- a/public_html/js/app.js +++ b/public_html/js/app.js @@ -98,7 +98,7 @@ function parseEchomailMessage(messageText, storedKludgeLines = null, storedBotto } // Smart text processing for mobile-friendly rendering -function formatMessageText(messageText, searchTerms = []) { +function formatMessageText(messageText, searchTerms = [], forcePlain = false) { if (!messageText || messageText.trim() === '') { return ''; } @@ -120,7 +120,7 @@ function formatMessageText(messageText, searchTerms = []) { const linesWithLeadingSpaces = lines.filter(line => /^\s{5,}\S/.test(line)).length; const hasLeadingSpaceArt = linesWithLeadingSpaces >= 3 && linesWithLeadingSpaces >= (nonEmptyLines * 0.5); - const shouldRenderAnsiArt = hasCursorAnsi || (hasColorCodes && nonEmptyLines >= 4 && maxLineLength >= 30) || (hasLeadingSpaceArt && nonEmptyLines >= 4 && maxLineLength >= 30); + const shouldRenderAnsiArt = !forcePlain && (hasCursorAnsi || (hasColorCodes && nonEmptyLines >= 4 && maxLineLength >= 30) || (hasLeadingSpaceArt && nonEmptyLines >= 4 && maxLineLength >= 30)); const ansiLineStyle = hasColorCodes ? ' style="white-space: pre;"' : ''; // Check if this is ANSI art (cursor positioning or dense ANSI text) @@ -393,7 +393,13 @@ function formatKludgeLinesWithSeparator(topKludges, bottomKludges) { output += bottomKludges.map(line => formatSingleKludgeLine(line)).join('\n'); } - return output || 'No kludge lines found'; + if (output) { + return output; + } + if (window.t) { + return window.t('ui.common.no_kludge_lines_found', {}, 'No kludge lines found'); + } + return 'No kludge lines found'; } function toggleKludgeLines() { @@ -404,17 +410,146 @@ function toggleKludgeLines() { if (container.is(':visible')) { container.slideUp(); icon.removeClass('fas fa-eye').addClass('fas fa-eye-slash'); - text.text('Show Kludge Lines'); + text.text(window.t ? window.t('ui.common.show_kludge_lines', {}, 'Show Kludge Lines') : 'Show Kludge Lines'); } else { container.slideDown(); icon.removeClass('fas fa-eye-slash').addClass('fas fa-eye'); - text.text('Hide Kludge Lines'); + text.text(window.t ? window.t('ui.common.hide_kludge_lines', {}, 'Hide Kludge Lines') : 'Hide Kludge Lines'); } } // Global user settings object window.userSettings = {}; +// Lightweight i18n client state with lazy namespace loading. +window.i18n = window.i18n || { + locale: window.appLocale || 'en', + defaultLocale: window.appDefaultLocale || 'en', + catalogs: {}, + loadedNamespaces: {} +}; + +function i18nInterpolate(template, params = {}) { + let output = String(template || ''); + Object.keys(params || {}).forEach(function(key) { + const token = new RegExp(`\\{${key}\\}`, 'g'); + output = output.replace(token, String(params[key])); + }); + return output; +} + +function i18nLookup(key) { + const catalogs = window.i18n?.catalogs || {}; + for (const ns of Object.keys(catalogs)) { + if (Object.prototype.hasOwnProperty.call(catalogs[ns], key)) { + return catalogs[ns][key]; + } + } + return null; +} + +function t(key, params = {}, fallback = '') { + const value = i18nLookup(key); + if (typeof value === 'string') { + return i18nInterpolate(value, params); + } + if (fallback) { + return i18nInterpolate(fallback, params); + } + return key; +} + +window.t = t; + +function getApiErrorMessage(payload, fallback = 'An unexpected error occurred.') { + if (!payload || typeof payload !== 'object') { + return fallback; + } + if (payload.error_code) { + const params = payload.error_params && typeof payload.error_params === 'object' + ? payload.error_params + : {}; + return t(payload.error_code, params, payload.error || fallback); + } + if (payload.error) { + return String(payload.error); + } + return fallback; +} + +window.getApiErrorMessage = getApiErrorMessage; + +function getApiMessage(payload, fallback = '') { + if (!payload || typeof payload !== 'object') { + return fallback; + } + if (payload.message_code) { + const params = payload.message_params && typeof payload.message_params === 'object' + ? payload.message_params + : {}; + return t(payload.message_code, params, payload.message || fallback); + } + if (payload.message) { + return String(payload.message); + } + return fallback; +} + +window.getApiMessage = getApiMessage; + +function mergeCatalogs(catalogs) { + if (!catalogs || typeof catalogs !== 'object') { + return; + } + Object.keys(catalogs).forEach(function(ns) { + if (!window.i18n.catalogs[ns]) { + window.i18n.catalogs[ns] = {}; + } + Object.assign(window.i18n.catalogs[ns], catalogs[ns] || {}); + window.i18n.loadedNamespaces[ns] = true; + }); +} + +function loadI18nNamespaces(namespaces = []) { + const normalized = (namespaces || []) + .map(ns => String(ns || '').trim()) + .filter(ns => ns.length > 0); + if (normalized.length === 0) { + return Promise.resolve(); + } + + const missing = normalized.filter(ns => !window.i18n.loadedNamespaces[ns]); + if (missing.length === 0) { + return Promise.resolve(); + } + + const nsParam = encodeURIComponent(missing.join(',')); + const localeParam = encodeURIComponent(window.i18n.locale || window.appLocale || 'en'); + + return fetch(`/api/i18n/catalog?ns=${nsParam}&locale=${localeParam}`) + .then(function(response) { + if (!response.ok) { + throw new Error('i18n catalog load failed'); + } + return response.json(); + }) + .then(function(payload) { + if (!payload || !payload.success) { + throw new Error('invalid i18n payload'); + } + if (payload.locale) { + window.i18n.locale = payload.locale; + } + if (payload.default_locale) { + window.i18n.defaultLocale = payload.default_locale; + } + mergeCatalogs(payload.catalogs || {}); + }) + .catch(function() { + // Non-fatal in Phase 0: app keeps English literals as fallback. + }); +} + $(document).ready(function() { // Load user settings on page load loadUserSettings(); @@ -464,7 +599,11 @@ $(document).ready(function() { // Unified user settings management function loadUserSettings() { - return new Promise(function(resolve, reject) { + if (window.__userSettingsPromise) { + return window.__userSettingsPromise; + } + + window.__userSettingsPromise = new Promise(function(resolve, reject) { $.get('/api/user/settings') .done(function(response) { if (response.success && response.settings) { @@ -476,10 +615,14 @@ function loadUserSettings() { window.userSettings = response; console.log('Loaded user settings (legacy format):', window.userSettings); } - - // Apply font settings after loading - applyFontSettings(); - resolve(window.userSettings); + + window.i18n.locale = window.userSettings.locale || window.appLocale || window.i18n.locale || 'en'; + + loadI18nNamespaces(window.appI18nNamespaces || ['common']).finally(function() { + // Apply font settings after loading + applyFontSettings(); + resolve(window.userSettings); + }); }) .fail(function() { console.log('Failed to load user settings, using defaults'); @@ -491,16 +634,22 @@ function loadUserSettings() { quote_coloring: true, default_sort: 'date_desc', timezone: 'America/Los_Angeles', + locale: window.appLocale || 'en', font_family: 'Courier New, Monaco, Consolas, monospace', font_size: 16, signature_text: '' }; - - // Apply font settings after loading defaults - applyFontSettings(); - resolve(window.userSettings); + + window.i18n.locale = window.userSettings.locale || window.appLocale || 'en'; + loadI18nNamespaces(window.appI18nNamespaces || ['common']).finally(function() { + // Apply font settings after loading defaults + applyFontSettings(); + resolve(window.userSettings); + }); }); }); + + return window.__userSettingsPromise; } function saveUserSetting(key, value) { @@ -638,26 +787,34 @@ function formatDate(dateString) { const absDays = Math.abs(diffDays); const absHours = Math.abs(diffHours); if (absDays === 0 && absHours === 0) { - return 'Soon'; + return t('time.soon', {}, 'Soon'); } else if (absDays === 0) { - return `In ${absHours} hour${absHours !== 1 ? 's' : ''}`; + return t('time.in_hours', { + count: absHours, + suffix: absHours !== 1 ? t('time.suffix_plural', {}, 's') : t('time.suffix_singular', {}, '') + }, `In ${absHours} hour${absHours !== 1 ? 's' : ''}`); } else if (absDays === 1) { - return 'Tomorrow'; + return t('time.tomorrow', {}, 'Tomorrow'); } else { - return `In ${absDays} days`; + return t('time.in_days', { count: absDays }, `In ${absDays} days`); } } if (diffDays === 0) { if (diffHours === 0) { const diffMins = Math.floor(diffMs / (1000 * 60)); - return diffMins <= 1 ? 'Just now' : `${diffMins} minutes ago`; + return diffMins <= 1 + ? t('time.just_now', {}, 'Just now') + : t('time.minutes_ago', { count: diffMins }, `${diffMins} minutes ago`); } - return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`; + return t('time.hours_ago', { + count: diffHours, + suffix: diffHours !== 1 ? t('time.suffix_plural', {}, 's') : t('time.suffix_singular', {}, '') + }, `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`); } else if (diffDays === 1) { - return 'Yesterday'; + return t('time.yesterday', {}, 'Yesterday'); } else if (diffDays < 7) { - return `${diffDays} days ago`; + return t('time.days_ago', { count: diffDays }, `${diffDays} days ago`); } else { // Use user's preferred date format for older dates const userDateFormat = window.userSettings?.date_format || 'en-US'; @@ -711,7 +868,7 @@ function loadMessages(type, area = null, page = 1) { updatePagination(data.pagination); }) .fail(function() { - showError('Failed to load messages'); + showError(t('errors.failed_load_messages', {}, 'Failed to load messages')); }); } @@ -720,7 +877,7 @@ function displayMessages(messages, type) { let html = ''; if (messages.length === 0) { - html = '
No messages found
'; + html = `
${t('messages.none_found', {}, 'No messages found')}
`; } else { messages.forEach(function(msg) { html += ` @@ -733,7 +890,7 @@ function displayMessages(messages, type) { ${type === 'echomail' ? formatFullDate(msg.date_written) : formatDate(msg.date_written)} -
${escapeHtml(msg.subject || '(No Subject)')}
+
${escapeHtml(msg.subject || t('messages.no_subject', {}, '(No Subject)'))}
${msg.to_name ? `To: ${escapeHtml(msg.to_name)}` : ''} ${!msg.is_read && type === 'netmail' ? 'NEW' : ''} @@ -821,7 +978,7 @@ function showLoading(container) { $(container).html(`
- Loading... + ${window.t ? window.t('ui.common.loading', {}, 'Loading...') : 'Loading...'}
`); } diff --git a/public_html/js/chat-page.js b/public_html/js/chat-page.js index 434a8ec9e..c6edabc0a 100644 --- a/public_html/js/chat-page.js +++ b/public_html/js/chat-page.js @@ -18,6 +18,22 @@ }; let dbPromise = null; + function uiT(key, fallback, params = {}) { + if (window.t) { + return window.t(key, params, fallback); + } + return fallback; + } + + function apiError(payload, fallback) { + if (window.getApiErrorMessage) { + return window.getApiErrorMessage(payload, fallback); + } + if (payload && payload.error) { + return String(payload.error); + } + return fallback; + } function openDb() { if (dbPromise) return dbPromise; @@ -161,7 +177,7 @@ if (state.users.length === 0) { const empty = document.createElement('div'); empty.className = 'chat-empty text-muted'; - empty.textContent = 'No one online'; + empty.textContent = uiT('ui.chat.no_one_online', 'No one online'); list.appendChild(empty); return; } @@ -192,11 +208,11 @@ const metaEl = document.getElementById('chatThreadMeta'); if (state.active.type === 'room') { const room = state.rooms.find(r => r.id === state.active.id); - titleEl.textContent = room ? room.name : 'Room'; + titleEl.textContent = room ? room.name : uiT('ui.chat.room', 'Room'); metaEl.textContent = room && room.description ? room.description : ''; } else { const user = state.users.find(u => u.user_id === state.active.id); - titleEl.textContent = user ? user.username : 'Direct'; + titleEl.textContent = user ? user.username : uiT('ui.chat.direct', 'Direct'); metaEl.textContent = user && user.location ? user.location : ''; } } @@ -265,7 +281,7 @@ const header = document.createElement('div'); header.className = 'chat-message-header'; const authorClass = window.currentUserIsAdmin ? 'chat-message-author admin-action' : 'chat-message-author'; - header.innerHTML = `${escapeHtml(msg.from_username || 'System')} + header.innerHTML = `${escapeHtml(msg.from_username || uiT('ui.chat.system', 'System'))} ${formatTimestamp(msg.created_at)}`; const body = document.createElement('div'); @@ -381,10 +397,10 @@ appendMessage(data.local_message); } if (!data.success) { - alert(data.error || 'Failed to send message'); + alert(apiError(data, uiT('ui.chat.send_failed', 'Failed to send message'))); } }).catch(() => { - alert('Failed to send message'); + alert(uiT('ui.chat.send_failed', 'Failed to send message')); }); } @@ -553,7 +569,9 @@ if (!button || !currentTarget) return; const action = button.dataset.action; - if (!confirm(`${action === 'ban' ? 'Ban' : 'Kick'} this user from the room?`)) { + if (!confirm(action === 'ban' + ? uiT('ui.chat.confirm_ban', 'Ban this user from the room?') + : uiT('ui.chat.confirm_kick', 'Kick this user from the room?'))) { hideMenu(); return; } @@ -568,10 +586,10 @@ }) }).then(res => res.json()).then(data => { if (!data.success) { - alert(data.error || 'Moderation failed'); + alert(apiError(data, uiT('ui.chat.moderation_failed', 'Moderation failed'))); } }).catch(() => { - alert('Moderation failed'); + alert(uiT('ui.chat.moderation_failed', 'Moderation failed')); }).finally(() => { hideMenu(); }); diff --git a/public_html/js/echomail.js b/public_html/js/echomail.js index b55adc72d..4f6ec3bb0 100644 --- a/public_html/js/echomail.js +++ b/public_html/js/echomail.js @@ -9,6 +9,9 @@ let currentMessages = []; let currentMessageIndex = -1; let currentSearchTerms = []; let currentMessageData = null; +let currentParsedMessage = null; +let ansiRenderingEnabled = true; +let keyboardHelpVisible = false; let allEchoareas = []; let echoareaSearchQuery = ''; let searchResultCounts = null; @@ -16,9 +19,26 @@ let searchFilterCounts = null; let originalFilterCounts = null; let isSearchActive = false; +function apiError(payload, fallback) { + if (window.getApiErrorMessage) { + return window.getApiErrorMessage(payload, fallback); + } + if (payload && payload.error) { + return String(payload.error); + } + return fallback; +} + +function uiT(key, fallback, params = {}) { + if (window.t) { + return window.t(key, params, fallback); + } + return fallback; +} + // Date display configuration: 'written' or 'received' -// TODO: Add user toggle in settings -const USE_DATE_FIELD = 'received'; // related to ECHOMAIL_DATE_FIELD in backend +// Sourced from server-side ECHOMAIL_ORDER_DATE env configuration. +const USE_DATE_FIELD = (window.echomailDateField === 'written') ? 'written' : 'received'; $(document).ready(function() { loadEchomailSettings().then(function() { @@ -59,6 +79,7 @@ $(document).ready(function() { history.back(); } modalClosedByBackButton = false; + hideKeyboardHelp(); }); // Add keyboard navigation for message modal @@ -87,6 +108,17 @@ $(document).ready(function() { e.preventDefault(); toggleModalFullscreen(); break; + case 'a': + case 'A': + e.preventDefault(); + toggleAnsiRendering(); + break; + case '?': + case 'h': + case 'H': + e.preventDefault(); + toggleKeyboardHelp(); + break; } } }); @@ -118,8 +150,8 @@ function loadEchoareas() { applyEchoareaFilter(); }) .fail(function() { - $('#echoareasList').html('
Failed to load echo areas
'); - $('#mobileEchoareasList').html('
Failed to load echo areas
'); + $('#echoareasList').html(`
${uiT('ui.echoareas.load_failed', 'Failed to load echo areas')}
`); + $('#mobileEchoareasList').html(`
${uiT('ui.echoareas.load_failed', 'Failed to load echo areas')}
`); }); } @@ -152,8 +184,8 @@ function displayEchoareas(echoareas) {
-
All Messages
- View all echo areas +
${uiT('ui.common.all_messages', 'All Messages')}
+ ${uiT('ui.echomail.view_all_echo_areas', 'View all echo areas')}
All
@@ -170,7 +202,7 @@ function displayEchoareas(echoareas) { // Use search count if search is active let countDisplay; if (isSearchActive && area.search_count !== undefined) { - countDisplay = `${area.search_count} found`; + countDisplay = `${uiT('ui.echomail.search_found_count', '{count} found', { count: area.search_count })}`; } else { countDisplay = `${unreadCount}/${totalCount}`; } @@ -188,7 +220,7 @@ function displayEchoareas(echoareas) { `; }); } else { - html = '
No echo areas available
'; + html = `
${uiT('ui.echomail.no_echoareas_available', 'No echo areas available')}
`; } container.html(html); @@ -204,8 +236,8 @@ function displayMobileEchoareas(echoareas) {
-
All Messages
- View all echo areas +
${uiT('ui.common.all_messages', 'All Messages')}
+ ${uiT('ui.echomail.view_all_echo_areas', 'View all echo areas')}
All
@@ -222,7 +254,7 @@ function displayMobileEchoareas(echoareas) { // Use search count if search is active let countDisplay; if (isSearchActive && area.search_count !== undefined) { - countDisplay = `${area.search_count} found`; + countDisplay = `${uiT('ui.echomail.search_found_count', '{count} found', { count: area.search_count })}`; } else { countDisplay = `${unreadCount}/${totalCount}`; } @@ -243,7 +275,7 @@ function displayMobileEchoareas(echoareas) { // Wrap in list-group html = '
' + html + '
'; } else { - html = '
No echo areas available
'; + html = `
${uiT('ui.echomail.no_echoareas_available', 'No echo areas available')}
`; } container.html(html); @@ -323,7 +355,7 @@ function loadMessages() { loadStats(); }) .fail(function() { - $('#messagesContainer').html('
Failed to load messages
'); + $('#messagesContainer').html(`
${uiT('errors.failed_load_messages', 'Failed to load messages')}
`); }); } @@ -335,11 +367,11 @@ function loadDrafts() { // Clear pagination for drafts $('#pagination').empty(); } else { - $('#messagesContainer').html('
Failed to load drafts
'); + $('#messagesContainer').html(`
${uiT('errors.messages.drafts.list_failed', 'Failed to load drafts')}
`); } }) .fail(function() { - $('#messagesContainer').html('
Failed to load drafts
'); + $('#messagesContainer').html(`
${uiT('errors.messages.drafts.list_failed', 'Failed to load drafts')}
`); }); } @@ -348,7 +380,7 @@ function displayDrafts(drafts) { let html = ''; if (drafts.length === 0) { - html = '
No drafts found
'; + html = `
${uiT('ui.echomail.no_drafts_found', 'No drafts found')}
`; } else { // Create table structure html = ` @@ -356,32 +388,32 @@ function displayDrafts(drafts) { - - - + + + `; drafts.forEach(function(draft) { - const displayTarget = draft.to_name || 'All'; - const displayArea = draft.echoarea || 'No area'; + const displayTarget = draft.to_name || uiT('ui.common.all', 'All'); + const displayArea = draft.echoarea || uiT('ui.echomail.no_area', 'No area'); html += ` - - + + + @@ -433,19 +465,19 @@ function displayMessages(messages, isThreaded = false) { messages.forEach(function(msg) { // Check if message is addressed to current user const isToCurrentUser = msg.to_name && window.currentUserRealName && msg.to_name === window.currentUserRealName; - const toInfo = msg.to_name && msg.to_name !== 'All' ? - ` (to: ${escapeHtml(msg.to_name)})` : ''; + const toInfo = msg.to_name && msg.to_name !== uiT('ui.common.all', 'All') ? + ` (${uiT('ui.echomail.to_prefix', 'to:')} ${escapeHtml(msg.to_name)})` : ''; const isRead = msg.is_read == 1; const isShared = msg.is_shared == 1; const isSaved = msg.is_saved == 1; const readClass = isRead ? 'read' : 'unread'; - const readIcon = isRead ? '' : ''; - const shareIcon = isShared ? '' : ''; + const readIcon = isRead ? `` : ``; + const shareIcon = isShared ? `` : ''; const saveIcon = ``; @@ -453,11 +485,13 @@ function displayMessages(messages, isThreaded = false) { const threadLevel = msg.thread_level || 0; const replyCount = msg.reply_count || 0; const isThreadRoot = msg.is_thread_root || false; - const threadIcon = threadLevel > 0 ? '' : ''; - const replyCountBadge = isThreadRoot && replyCount > 0 ? ` ${replyCount}` : ''; + const threadIcon = threadLevel > 0 ? `` : ''; + const replyCountBadge = isThreadRoot && replyCount > 0 ? ` ${replyCount}` : ''; - // Add thread-specific CSS classes - const threadClasses = isThreaded ? `thread-level-${threadLevel} ${isThreadRoot ? 'thread-root' : 'thread-reply'}` : ''; + // Add thread-specific CSS classes; indent up to 2 levels (0.5rem each) + const threadClasses = isThreaded ? `thread-level-${Math.min(threadLevel, 9)} ${isThreadRoot ? 'thread-root' : 'thread-reply'}` : ''; + const indentRem = isThreaded && threadLevel > 0 ? Math.min(threadLevel, 2) * 0.75 : 0; + const threadIndent = indentRem > 0 ? `padding-left: ${indentRem}rem;` : ''; html += ` @@ -466,18 +500,18 @@ function displayMessages(messages, isThreaded = false) { - - + `; }); @@ -502,7 +536,7 @@ function updatePagination(pagination) { // Previous button if (pagination.page > 1) { - html += `
  • Previous
  • `; + html += `
  • ${uiT('ui.common.previous', 'Previous')}
  • `; } // Page numbers (show max 5 pages) @@ -520,7 +554,7 @@ function updatePagination(pagination) { // Next button if (pagination.page < pagination.pages) { - html += `
  • Next
  • `; + html += `
  • ${uiT('ui.common.next', 'Next')}
  • `; } html += ''; @@ -546,7 +580,7 @@ function sortMessages(sortBy) { function refreshMessages() { loadMessages(); - showSuccess('Messages refreshed'); + showSuccess(uiT('ui.echomail.messages_refreshed', 'Messages refreshed')); } function toggleThreading() { @@ -556,9 +590,9 @@ function toggleThreading() { // Update toggle text const toggleText = $('#threadingToggleText'); if (threadedView) { - toggleText.text('Show Flat'); + toggleText.text(uiT('ui.common.threading.show_flat', 'Show Flat')); } else { - toggleText.text('Show Threaded'); + toggleText.text(uiT('ui.common.threading.show_threaded', 'Show Threaded')); } // Save preference @@ -681,7 +715,7 @@ function viewMessage(messageId) { $('#messageContent').html(`
    - Loading message... + ${uiT('ui.common.loading_message', 'Loading message...')}
    `); @@ -703,7 +737,7 @@ function viewMessage(messageId) { $('#messageModal .modal-body').scrollTop(0); }) .fail(function() { - $('#messageContent').html('
    Failed to load message
    '); + $('#messageContent').html(`
    ${uiT('errors.messages.echomail.get_failed', 'Failed to load message')}
    `); }); } @@ -718,6 +752,45 @@ function displayMessageContent(message) { checkAndDisplayEchomailMessage(message, parsedMessage); } +function toggleKeyboardHelp() { + keyboardHelpVisible = !keyboardHelpVisible; + $('#keyboardHelpOverlay').toggle(keyboardHelpVisible); +} + +function hideKeyboardHelp() { + keyboardHelpVisible = false; + $('#keyboardHelpOverlay').hide(); +} + +function toggleAnsiRendering() { + if (!currentMessageData || !currentParsedMessage) return; + + ansiRenderingEnabled = !ansiRenderingEnabled; + + const body = currentParsedMessage.messageBody; + let bodyHtml; + if (ansiRenderingEnabled && currentMessageData.markup_html) { + bodyHtml = currentMessageData.markup_html; + } else { + bodyHtml = formatMessageText(body, currentSearchTerms, !ansiRenderingEnabled); + } + + // Replace only the body content, keeping the badge + const container = document.getElementById('messageTextContainer'); + if (!container) return; + + // Update badge visibility + const badge = document.getElementById('ansiRenderBadge'); + if (badge) badge.style.display = ansiRenderingEnabled ? 'none' : ''; + + // Replace everything after the badge with the new body + const existing = container.querySelectorAll(':scope > :not(#ansiRenderBadge)'); + existing.forEach(el => el.remove()); + const tmp = document.createElement('div'); + tmp.innerHTML = bodyHtml; + while (tmp.firstChild) container.appendChild(tmp.firstChild); +} + function downloadCurrentMessage() { if (!currentMessageId || !currentMessageData) { return; @@ -753,10 +826,13 @@ function checkAndDisplayEchomailMessage(message, parsedMessage) { } function renderEchomailMessageContent(message, parsedMessage, isInAddressBook) { + currentParsedMessage = parsedMessage; + ansiRenderingEnabled = true; + hideKeyboardHelp(); let addressBookButton; if (isInAddressBook) { addressBookButton = ` - `; @@ -766,7 +842,7 @@ function renderEchomailMessageContent(message, parsedMessage, isInAddressBook) { const replyToName = message.replyto_name || message.from_name; addressBookButton = ` - `; @@ -780,23 +856,23 @@ function renderEchomailMessageContent(message, parsedMessage, isInAddressBook) {
    - From: ${escapeHtml(message.from_name)} + ${uiT('ui.common.from_label', 'From:')} ${escapeHtml(message.from_name)} ${formatFidonetAddress(message.from_address)} ${addressBookButton}
    - To: ${escapeHtml(message.to_name || 'All')} + ${uiT('ui.common.to_label', 'To:')} ${escapeHtml(message.to_name || uiT('ui.common.all', 'All'))}
    - Area: ${escapeHtml(message.echoarea)}${message.domain ? '@' + escapeHtml(message.domain) : ''} + ${uiT('ui.common.area_label', 'Area:')} ${escapeHtml(message.echoarea)}${message.domain ? '@' + escapeHtml(message.domain) : ''}
    - Date: ${formatFullDate(message.date_written)} + ${uiT('ui.common.date_label', 'Date:')} ${formatFullDate(message.date_written)}
    - Subject: ${escapeHtml(message.subject || '(No Subject)')} + ${uiT('ui.common.subject_label', 'Subject:')} ${escapeHtml(message.subject || uiT('messages.no_subject', '(No Subject)'))}
    @@ -804,18 +880,18 @@ function renderEchomailMessageContent(message, parsedMessage, isInAddressBook) { - ${message.is_shared == 1 ? '' : ''} + title="${message.is_saved == 1 ? uiT('ui.common.remove_from_saved', 'Remove from saved') : uiT('ui.common.save_for_later', 'Save for later')}"> + ${message.is_shared == 1 ? `` : ''}
    -
    Kludge Lines
    +
    ${uiT('ui.common.kludge_lines', 'Kludge Lines')}
    -
    +
    + ${bodyHtml}
    ${message.origin_line ? `
    ${escapeHtml(message.origin_line)}
    ` : ''} @@ -884,7 +963,7 @@ function searchMessages() { } if (query.length < 2) { - showError('Please enter at least 2 characters to search'); + showError(uiT('errors.messages.search.query_too_short', 'Please enter at least 2 characters to search')); return; } @@ -933,7 +1012,7 @@ function searchMessages() { $('#mobileSearchCollapse').collapse('hide'); }) .fail(function() { - showError('Search failed'); + showError(uiT('ui.echomail.search.failed', 'Search failed')); }); } @@ -971,7 +1050,7 @@ function showClearSearchButton() { if ($('#clearSearchBtn').length === 0) { const clearBtn = ` `; $('#searchInput').after(clearBtn); @@ -981,7 +1060,7 @@ function showClearSearchButton() { if ($('#mobileClearSearchBtn').length === 0) { const mobileClearBtn = ` `; $('#mobileSearchInput').after(mobileClearBtn); @@ -1036,17 +1115,17 @@ function toggleSaveMessage(messageId, messageType, isSaved) { if (isSaved) { // Message was unsaved icon.removeClass('text-warning').addClass('text-muted'); - icon.attr('title', 'Save for later'); + icon.attr('title', uiT('ui.common.save_for_later', 'Save for later')); icon.attr('data-saved', 'false'); icon.attr('onclick', `toggleSaveMessage(${messageId}, '${messageType}', false)`); - showSuccess('Message removed from saved items'); + showSuccess(uiT('ui.echomail.saved_items.removed', 'Message removed from saved items')); } else { // Message was saved icon.removeClass('text-muted').addClass('text-warning'); - icon.attr('title', 'Remove from saved'); + icon.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); icon.attr('data-saved', 'true'); icon.attr('onclick', `toggleSaveMessage(${messageId}, '${messageType}', true)`); - showSuccess('Message saved for later'); + showSuccess(uiT('ui.echomail.saved_items.saved', 'Message saved for later')); } // If we're viewing saved messages, remove the message from view @@ -1054,12 +1133,19 @@ function toggleSaveMessage(messageId, messageType, isSaved) { loadMessages(); } } else { - showError(response.message || 'Failed to update save status'); + showError(window.getApiErrorMessage + ? window.getApiErrorMessage(response, uiT('ui.echomail.save_status.update_failed', 'Failed to update save status')) + : (response.message || uiT('ui.echomail.save_status.update_failed', 'Failed to update save status'))); } }, error: function(xhr) { - const response = JSON.parse(xhr.responseText || '{}'); - showError(response.error || 'Failed to update save status'); + let response = {}; + try { + response = JSON.parse(xhr.responseText || '{}'); + } catch (e) { + response = xhr.responseJSON || {}; + } + showError(apiError(response, uiT('ui.echomail.save_status.update_failed', 'Failed to update save status'))); } }); } @@ -1077,12 +1163,12 @@ function updateModalSaveButton(message) { // Message is saved - show "Saved" button that will unsave when clicked saveBtn.removeClass('btn-outline-warning').addClass('btn-warning'); saveIcon.removeClass('fa-bookmark').addClass('fa-bookmark'); - saveText.text('Saved'); - saveBtn.attr('title', 'Remove from saved'); + saveText.text(uiT('ui.common.saved_short', 'Saved')); + saveBtn.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); // Update header icon headerIcon.removeClass('text-muted').addClass('text-warning'); - headerIcon.attr('title', 'Remove from saved'); + headerIcon.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); // Click handlers to UNSAVE (isSaved = true) saveBtn.off('click').on('click', function() { @@ -1095,12 +1181,12 @@ function updateModalSaveButton(message) { // Message is not saved - show "Save" button that will save when clicked saveBtn.removeClass('btn-warning').addClass('btn-outline-warning'); saveIcon.removeClass('fa-bookmark').addClass('fa-bookmark'); - saveText.text('Save'); - saveBtn.attr('title', 'Save for later'); + saveText.text(uiT('ui.common.save', 'Save')); + saveBtn.attr('title', uiT('ui.common.save_for_later', 'Save for later')); // Update header icon headerIcon.removeClass('text-warning').addClass('text-muted'); - headerIcon.attr('title', 'Save for later'); + headerIcon.attr('title', uiT('ui.common.save_for_later', 'Save for later')); // Click handlers to SAVE (isSaved = false) saveBtn.off('click').on('click', function() { @@ -1131,14 +1217,14 @@ function toggleSaveMessageModal(messageId, messageType, isSaved) { if (isSaved) { // Message was unsaved saveBtn.removeClass('btn-warning').addClass('btn-outline-warning'); - saveText.text('Save'); - saveBtn.attr('title', 'Save for later'); + saveText.text(uiT('ui.common.save', 'Save')); + saveBtn.attr('title', uiT('ui.common.save_for_later', 'Save for later')); // Update header icon headerIcon.removeClass('text-warning').addClass('text-muted'); - headerIcon.attr('title', 'Save for later'); + headerIcon.attr('title', uiT('ui.common.save_for_later', 'Save for later')); - showSuccess('Message removed from saved items'); + showSuccess(uiT('ui.echomail.saved_items.removed', 'Message removed from saved items')); // Update click handlers for next time saveBtn.off('click').on('click', function() { @@ -1150,14 +1236,14 @@ function toggleSaveMessageModal(messageId, messageType, isSaved) { } else { // Message was saved saveBtn.removeClass('btn-outline-warning').addClass('btn-warning'); - saveText.text('Saved'); - saveBtn.attr('title', 'Remove from saved'); + saveText.text(uiT('ui.common.saved_short', 'Saved')); + saveBtn.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); // Update header icon headerIcon.removeClass('text-muted').addClass('text-warning'); - headerIcon.attr('title', 'Remove from saved'); + headerIcon.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); - showSuccess('Message saved for later'); + showSuccess(uiT('ui.echomail.saved_items.saved', 'Message saved for later')); // Update click handlers for next time saveBtn.off('click').on('click', function() { @@ -1173,12 +1259,12 @@ function toggleSaveMessageModal(messageId, messageType, isSaved) { if (icon.length) { if (isSaved) { icon.removeClass('text-warning').addClass('text-muted'); - icon.attr('title', 'Save for later'); + icon.attr('title', uiT('ui.common.save_for_later', 'Save for later')); icon.attr('data-saved', 'false'); icon.attr('onclick', `toggleSaveMessage(${messageId}, '${messageType}', false)`); } else { icon.removeClass('text-muted').addClass('text-warning'); - icon.attr('title', 'Remove from saved'); + icon.attr('title', uiT('ui.common.remove_from_saved', 'Remove from saved')); icon.attr('data-saved', 'true'); icon.attr('onclick', `toggleSaveMessage(${messageId}, '${messageType}', true)`); } @@ -1189,12 +1275,19 @@ function toggleSaveMessageModal(messageId, messageType, isSaved) { loadMessages(); } } else { - showError(response.message || 'Failed to update save status'); + showError(window.getApiErrorMessage + ? window.getApiErrorMessage(response, uiT('ui.echomail.save_status.update_failed', 'Failed to update save status')) + : (response.message || uiT('ui.echomail.save_status.update_failed', 'Failed to update save status'))); } }, error: function(xhr) { - const response = JSON.parse(xhr.responseText || '{}'); - showError(response.error || 'Failed to update save status'); + let response = {}; + try { + response = JSON.parse(xhr.responseText || '{}'); + } catch (e) { + response = xhr.responseJSON || {}; + } + showError(apiError(response, uiT('ui.echomail.save_status.update_failed', 'Failed to update save status'))); } }); } @@ -1236,10 +1329,10 @@ function loadStats() { .fail(function(xhr, status, error) { console.error('Echomail stats loading failed:', xhr.status, status, error); console.error('Response text:', xhr.responseText); - $('#totalMessages').text('Error'); - $('#unreadMessages').text('Error'); - $('#recentMessages').text('Error'); - $('#totalAreas').text('Error'); + $('#totalMessages').text(uiT('ui.common.error', 'Error')); + $('#unreadMessages').text(uiT('ui.common.error', 'Error')); + $('#recentMessages').text(uiT('ui.common.error', 'Error')); + $('#totalAreas').text(uiT('ui.common.error', 'Error')); }); } @@ -1256,14 +1349,14 @@ function toggleSelectMode() { if (selectMode) { // Enable select mode - btn.html(' Cancel'); + btn.html(` ${uiT('ui.common.cancel', 'Cancel')}`); btn.removeClass('btn-outline-secondary').addClass('btn-outline-warning'); checkboxColumn.removeClass('d-none'); checkboxCells.removeClass('d-none'); bulkActions.removeClass('d-none'); } else { // Disable select mode - btn.html(' Select'); + btn.html(` ${uiT('ui.common.select', 'Select')}`); btn.removeClass('btn-outline-warning').addClass('btn-outline-secondary'); checkboxColumn.addClass('d-none'); checkboxCells.addClass('d-none'); @@ -1340,14 +1433,14 @@ function clearSelection() { function markSelectedAsRead() { if (selectedMessages.size === 0) { - showError('No messages selected'); + showError(uiT('ui.messages.none_selected', 'No messages selected')); return; } const messageIds = Array.from(selectedMessages); const markBtn = $('#bulkActions .btn-outline-primary'); const originalText = markBtn.html(); - markBtn.prop('disabled', true).html(' Marking...'); + markBtn.prop('disabled', true).html(` ${uiT('ui.echomail.bulk_marking', 'Marking...')}`); $.ajax({ url: '/api/messages/echomail/read', @@ -1356,19 +1449,22 @@ function markSelectedAsRead() { contentType: 'application/json', success: function(response) { if (response.success) { - showSuccess(response.message); + const markedCount = response.marked || messageIds.length; + showSuccess(window.getApiMessage + ? window.getApiMessage(response, uiT('ui.echomail.bulk_mark_read_success', 'Marked {count} message(s) as read', { count: markedCount })) + : (response.message || uiT('ui.echomail.bulk_mark_read_success', 'Marked {count} message(s) as read', { count: markedCount }))); clearSelection(); loadMessages(); loadStats(); } else { - showError(response.error || 'Failed to mark messages as read'); + showError(apiError(response, uiT('errors.messages.echomail.bulk_read.failed', 'Failed to mark messages as read'))); } }, error: function(xhr) { - let errorMessage = 'Failed to mark messages as read'; + let errorMessage = uiT('ui.echomail.bulk_mark_read_failed', 'Failed to mark messages as read'); try { const response = JSON.parse(xhr.responseText); - errorMessage = response.error || errorMessage; + errorMessage = apiError(response, errorMessage); } catch (e) { // Use default error message } @@ -1382,17 +1478,21 @@ function markSelectedAsRead() { function deleteSelectedMessages() { if (!window.isAdmin) { - showError('Admin privileges required to delete echomail messages'); + showError(uiT('errors.messages.echomail.bulk_delete.admin_required', 'Admin privileges required to delete echomail messages')); return; } if (selectedMessages.size === 0) { - showError('No messages selected'); + showError(uiT('ui.messages.none_selected', 'No messages selected')); return; } const count = selectedMessages.size; - const confirmMessage = `Are you sure you want to delete ${count} selected message${count > 1 ? 's' : ''} for everyone?`; + const confirmMessage = uiT( + 'ui.echomail.bulk_delete.confirm', + 'Are you sure you want to delete {count} selected message(s) for everyone?', + { count } + ); if (!confirm(confirmMessage)) { return; @@ -1404,7 +1504,7 @@ function deleteSelectedMessages() { // Show loading state const deleteBtn = $('#bulkActions .btn-outline-danger'); const originalText = deleteBtn.html(); - deleteBtn.prop('disabled', true).html(' Deleting...'); + deleteBtn.prop('disabled', true).html(` ${uiT('ui.echomail.bulk_deleting', 'Deleting...')}`); $.ajax({ url: '/api/messages/echomail/delete', @@ -1413,19 +1513,22 @@ function deleteSelectedMessages() { contentType: 'application/json', success: function(response) { if (response.success) { - showSuccess(response.message); + const deletedCount = response.deleted || messageIds.length; + showSuccess(window.getApiMessage + ? window.getApiMessage(response, uiT('ui.echomail.bulk_delete.success', 'Deleted {count} message(s)', { count: deletedCount })) + : (response.message || uiT('ui.echomail.bulk_delete.success', 'Deleted {count} message(s)', { count: deletedCount }))); clearSelection(); loadMessages(); // Reload messages loadStats(); // Update statistics } else { - showError(response.error || 'Failed to delete messages'); + showError(apiError(response, uiT('ui.echomail.bulk_delete.failed', 'Failed to delete messages'))); } }, error: function(xhr) { - let errorMessage = 'Failed to delete messages'; + let errorMessage = uiT('ui.echomail.bulk_delete.failed', 'Failed to delete messages'); try { const response = JSON.parse(xhr.responseText); - errorMessage = response.error || errorMessage; + errorMessage = apiError(response, errorMessage); } catch (e) { // Use default error message } @@ -1461,7 +1564,7 @@ function updateMobileAccordionText(selectedArea) { const displayTag = selectedArea.includes('@') ? selectedArea.split('@')[0] : selectedArea; textSpan.text(`Viewing: ${displayTag}`); } else { - textSpan.text('Viewing: All Messages'); + textSpan.text(uiT('ui.echomail.viewing_all_messages', 'Viewing: All Messages')); } } } @@ -1486,7 +1589,7 @@ function updateNavigationButtons() { function updateModalTitle(subject) { const position = currentMessages.length > 0 ? `${currentMessageIndex + 1} of ${currentMessages.length}` : ''; - const titleText = subject || '(No Subject)'; + const titleText = subject || uiT('messages.no_subject', '(No Subject)'); if (position) { $('#messageSubject').html(`${escapeHtml(titleText)} (${position})`); @@ -1646,7 +1749,7 @@ function navigateMessage(direction) { $('#messageContent').html(`
    - Loading message... + ${uiT('ui.common.loading_message', 'Loading message...')}
    `); @@ -1665,7 +1768,7 @@ function navigateMessage(direction) { $('#messageModal .modal-body').scrollTop(0); }) .fail(function() { - $('#messageContent').html('
    Failed to load message
    '); + $('#messageContent').html(`
    ${uiT('errors.messages.echomail.get_failed', 'Failed to load message')}
    `); }); } @@ -1747,7 +1850,7 @@ function showShareDialog(messageId) { $('#shareModal').modal('show'); }) .fail(function() { - showError('Failed to check existing shares'); + showError(uiT('ui.echomail.shares.check_failed', 'Failed to check existing shares')); }); } @@ -1779,20 +1882,20 @@ function createShare() { $('#revokeShareBtn').removeClass('d-none'); if (data.existing) { - showSuccess('Using existing share link'); + showSuccess(uiT('ui.echomail.shares.using_existing', 'Using existing share link')); } else { - showSuccess('Share link created successfully!'); + showSuccess(uiT('ui.echomail.shares.created_success', 'Share link created successfully!')); } } else { - $('#shareErrorMessage').text(data.error || 'Failed to create share link'); + $('#shareErrorMessage').text(apiError(data, uiT('errors.messages.share_create_failed', 'Failed to create share link'))); $('#shareError').removeClass('d-none'); } }, error: function(xhr) { - let errorMessage = 'Failed to create share link'; + let errorMessage = uiT('errors.messages.share_create_failed', 'Failed to create share link'); try { const response = JSON.parse(xhr.responseText); - errorMessage = response.error || errorMessage; + errorMessage = apiError(response, errorMessage); } catch (e) { // Use default error message } @@ -1815,20 +1918,20 @@ function generateFriendlyUrl() { if (data.success) { $('#shareUrl').val(data.share_url); $('#friendlyUrlBtn').addClass('d-none'); - showSuccess('Friendly URL generated!'); + showSuccess(uiT('ui.echomail.shares.friendly_url_generated', 'Friendly URL generated!')); } else { - showError(data.error || 'Failed to generate friendly URL'); + showError(apiError(data, uiT('ui.echomail.shares.friendly_url_failed', 'Failed to generate friendly URL'))); btn.prop('disabled', false).html(originalHtml); } }) .fail(function() { - showError('Failed to generate friendly URL'); + showError(uiT('ui.echomail.shares.friendly_url_failed', 'Failed to generate friendly URL')); btn.prop('disabled', false).html(originalHtml); }); } function revokeShare() { - if (!confirm('Are you sure you want to revoke this share link? It will no longer be accessible to others.')) { + if (!confirm(uiT('ui.echomail.shares.revoke_confirm', 'Are you sure you want to revoke this share link? It will no longer be accessible to others.'))) { return; } @@ -1844,13 +1947,13 @@ function revokeShare() { $('#shareResult').addClass('d-none'); $('#createShareBtn').removeClass('d-none'); $('#revokeShareBtn').addClass('d-none'); - showSuccess('Share link revoked'); + showSuccess(uiT('ui.echomail.shares.revoked', 'Share link revoked')); } else { - showError(data.error || 'Failed to revoke share link'); + showError(apiError(data, uiT('errors.messages.share_revoke_failed', 'Failed to revoke share link'))); } }, error: function() { - showError('Failed to revoke share link'); + showError(uiT('errors.messages.share_revoke_failed', 'Failed to revoke share link')); }, complete: function() { btn.prop('disabled', false).html(originalText); @@ -1863,7 +1966,7 @@ function copyShareUrl() { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(shareUrl).then(function() { - showSuccess('Share URL copied to clipboard!'); + showSuccess(uiT('ui.echomail.shares.url_copied', 'Share URL copied to clipboard!')); // Briefly highlight the input field $('#shareUrl').select(); @@ -1893,12 +1996,12 @@ function fallbackCopyTextToClipboard(text) { try { const successful = document.execCommand('copy'); if (successful) { - showSuccess('Share URL copied to clipboard!'); + showSuccess(uiT('ui.echomail.shares.url_copied', 'Share URL copied to clipboard!')); } else { - showError('Copy to clipboard failed. Please copy manually.'); + showError(uiT('ui.common.copy_failed_manual', 'Copy to clipboard failed. Please copy manually.')); } } catch (err) { - showError('Copy to clipboard not supported. Please copy manually.'); + showError(uiT('ui.common.copy_not_supported_manual', 'Copy to clipboard not supported. Please copy manually.')); } document.body.removeChild(textArea); @@ -1944,9 +2047,9 @@ function saveToAddressBook(fromName, fromAddress, originalFromName, originalFrom // Already exists - show "already saved" state button.removeClass('btn-outline-success').addClass('btn-outline-secondary') .html(' ') - .attr('title', 'Already in address book') + .attr('title', uiT('ui.common.already_in_address_book', 'Already in address book')) .prop('disabled', true); - showError('This contact is already in your address book'); + showError(uiT('ui.address_book.already_exists', 'This contact is already in your address book')); return; } @@ -1979,11 +2082,11 @@ function saveToAddressBook(fromName, fromAddress, originalFromName, originalFrom if (response.success) { // Show success state button.removeClass('btn-outline-success').addClass('btn-success') - .html(' Saved') - .attr('title', 'Saved to address book') + .html(` ${uiT('ui.common.saved_short', 'Saved')}`) + .attr('title', uiT('ui.address_book.saved_to_address_book', 'Saved to address book')) .prop('disabled', true); - showSuccess(`${fromName} added to address book`); + showSuccess(uiT('ui.address_book.sender_added', `${fromName} added to address book`, { name: fromName })); // Refresh address book in sidebar if it exists (for netmail page) if (typeof loadAddressBook === 'function') { @@ -1993,31 +2096,30 @@ function saveToAddressBook(fromName, fromAddress, originalFromName, originalFrom // Reset button on error button.removeClass('btn-outline-success').addClass('btn-outline-danger') .html(originalHtml) - .attr('title', 'Error - click to retry') + .attr('title', uiT('ui.common.error_click_retry', 'Error - click to retry')) .prop('disabled', false); - showError(response.error || 'Failed to save to address book'); + showError(apiError(response, uiT('errors.address_book.create_failed', 'Failed to save to address book'))); } }, error: function(xhr) { // Reset button on error button.removeClass('btn-outline-success').addClass('btn-outline-danger') .html(originalHtml) - .attr('title', 'Error - click to retry') + .attr('title', uiT('ui.common.error_click_retry', 'Error - click to retry')) .prop('disabled', false); - const response = xhr.responseJSON; - showError(response && response.error ? response.error : 'Failed to save to address book'); + showError(apiError(xhr.responseJSON, uiT('errors.address_book.create_failed', 'Failed to save to address book'))); } }); } else { // Reset button on error button.html(originalHtml).attr('title', originalTitle).prop('disabled', false); - showError('Failed to check existing contacts'); + showError(uiT('ui.address_book.check_existing_failed', 'Failed to check existing contacts')); } }) .fail(function() { // Reset button on error button.html(originalHtml).attr('title', originalTitle).prop('disabled', false); - showError('Failed to check existing contacts'); + showError(uiT('ui.address_book.check_existing_failed', 'Failed to check existing contacts')); }); } @@ -2058,7 +2160,7 @@ function continueDraft(draftId) { } function deleteDraftConfirm(draftId) { - if (confirm('Are you sure you want to delete this draft? This cannot be undone.')) { + if (confirm(uiT('ui.drafts.delete_confirm', 'Are you sure you want to delete this draft? This cannot be undone.'))) { deleteDraft(draftId); } } @@ -2071,13 +2173,16 @@ function deleteDraft(draftId) { if (response.success) { // Reload drafts to show updated list loadDrafts(); - showSuccess('Draft deleted successfully'); + const successMessage = window.getApiMessage + ? window.getApiMessage(response, uiT('ui.drafts.deleted_success', 'Draft deleted successfully')) + : uiT('ui.drafts.deleted_success', 'Draft deleted successfully'); + showSuccess(successMessage); } else { - showError('Failed to delete draft'); + showError(uiT('errors.messages.drafts.delete_failed', 'Failed to delete draft')); } }, error: function() { - showError('Failed to delete draft'); + showError(uiT('errors.messages.drafts.delete_failed', 'Failed to delete draft')); } }); } diff --git a/public_html/js/netmail.js b/public_html/js/netmail.js index 5609e59ca..5931213d4 100644 --- a/public_html/js/netmail.js +++ b/public_html/js/netmail.js @@ -10,6 +10,24 @@ let userSettings = {}; let currentSearchTerms = []; let selectMode = false; let selectedMessages = new Set(); +let keyboardHelpVisible = false; + +function apiError(payload, fallback) { + if (window.getApiErrorMessage) { + return window.getApiErrorMessage(payload, fallback); + } + if (payload && payload.error) { + return String(payload.error); + } + return fallback; +} + +function uiT(key, fallback, params = {}) { + if (window.t) { + return window.t(key, params, fallback); + } + return fallback; +} $(document).ready(function() { loadNetmailSettings().then(function() { @@ -53,6 +71,12 @@ $(document).ready(function() { e.preventDefault(); downloadCurrentMessage(); break; + case '?': + case 'h': + case 'H': + e.preventDefault(); + toggleKeyboardHelp(); + break; } } }); @@ -64,6 +88,7 @@ $(document).ready(function() { history.back(); } modalClosedByBackButton = false; + hideKeyboardHelp(); }); // Filter change handler @@ -118,7 +143,7 @@ function loadMessages() { updatePagination(data.pagination); }) .fail(function() { - $('#messagesContainer').html('
    Failed to load messages
    '); + $('#messagesContainer').html(`
    ${uiT('errors.failed_load_messages', 'Failed to load messages')}
    `); }); } @@ -130,11 +155,11 @@ function loadDrafts() { // Clear pagination for drafts $('#pagination').empty(); } else { - $('#messagesContainer').html('
    Failed to load drafts
    '); + $('#messagesContainer').html(`
    ${uiT('errors.messages.drafts.list_failed', 'Failed to load drafts')}
    `); } }) .fail(function() { - $('#messagesContainer').html('
    Failed to load drafts
    '); + $('#messagesContainer').html(`
    ${uiT('errors.messages.drafts.list_failed', 'Failed to load drafts')}
    `); }); } @@ -143,7 +168,7 @@ function displayDrafts(drafts) { let html = ''; if (drafts.length === 0) { - html = '
    No drafts found
    '; + html = `
    ${uiT('ui.netmail.no_drafts_found', 'No drafts found')}
    `; } else { // Create table structure html = ` @@ -151,16 +176,16 @@ function displayDrafts(drafts) {
    To / Echo AreaSubjectLast Updated${uiT('ui.echomail.to_echo_area', 'To / Echo Area')}${uiT('ui.common.subject_label_short', 'Subject')}${uiT('ui.echomail.last_updated', 'Last Updated')}
    -
    To: ${escapeHtml(displayTarget)}
    -
    Area: ${escapeHtml(displayArea)}
    +
    ${uiT('ui.common.to_label', 'To:')} ${escapeHtml(displayTarget)}
    +
    ${uiT('ui.common.area_label', 'Area:')} ${escapeHtml(displayArea)}
    - ${escapeHtml(draft.subject || '(No Subject)')} + ${escapeHtml(draft.subject || uiT('messages.no_subject', '(No Subject)'))} ${draft.message_text ? `
    ${escapeHtml(draft.message_text.substring(0, 100))}${draft.message_text.length > 100 ? '...' : ''}` : ''}
    ${formatFullDate(draft.updated_at)}
    -
    @@ -408,7 +440,7 @@ function displayMessages(messages, isThreaded = false) { currentMessages = messages; if (messages.length === 0) { - html = '
    No messages found
    '; + html = `
    ${uiT('messages.none_found', 'No messages found')}
    `; } else { // Create table structure html = ` @@ -421,9 +453,9 @@ function displayMessages(messages, isThreaded = false) { -
    FromSubjectReceived${uiT('ui.common.from', 'From')}${uiT('ui.common.subject_label_short', 'Subject')}${uiT('ui.netmail.received', 'Received')}
    - ${threadIcon}${readIcon}${shareIcon}${saveIcon}${escapeHtml(msg.from_name)} + + ${threadIcon}${readIcon}${shareIcon}${saveIcon}${escapeHtml(msg.from_name)} ${!currentEchoarea ? `
    ${msg.echoarea} ${msg.echoarea_domain ? `${msg.echoarea_domain}` : ''}
    ` : ''} - ${isRead ? '' : ''}${escapeHtml(msg.subject || '(No Subject)')}${isRead ? '' : ''}${replyCountBadge} + ${isRead ? '' : ''}${escapeHtml(msg.subject || uiT('messages.no_subject', '(No Subject)'))}${isRead ? '' : ''}${replyCountBadge} ${toInfo ? `
    ${toInfo}` : ''}
    ${formatDate(USE_DATE_FIELD === 'written' ? msg.date_written : msg.date_received)}${formatDate(USE_DATE_FIELD === 'written' ? msg.date_written : msg.date_received)}
    - - - + + + `; drafts.forEach(function(draft) { - const displayTo = draft.to_name || 'Unknown'; + const displayTo = draft.to_name || uiT('ui.common.unknown', 'Unknown'); const displayAddress = draft.to_address || ''; html += ` @@ -170,13 +195,13 @@ function displayDrafts(drafts) { ${displayAddress ? `
    ${escapeHtml(displayAddress)}
    ` : ''} - - - - - - + + + + + + @@ -216,14 +232,14 @@ function displayFeeds(feeds) { const isActive = feed.active; const rowClass = isActive ? '' : 'table-secondary'; const statusIcon = isActive ? - '' : - ''; + `` : + ``; const lastCheck = feed.last_check ? new Date(feed.last_check).toLocaleString() : - 'Never'; + `${uiT('ui.admin.auto_feed.never', 'Never')}`; - const feedName = escapeHtml(feed.feed_name || 'Unnamed Feed'); + const feedName = escapeHtml(feed.feed_name || uiT('ui.admin.auto_feed.unnamed_feed', 'Unnamed Feed')); const feedUrl = escapeHtml(feed.feed_url); const truncatedUrl = feedUrl.length > 50 ? feedUrl.substring(0, 47) + '...' : feedUrl; @@ -233,14 +249,14 @@ function displayFeeds(feeds) {
    ToSubjectLast Updated${uiT('ui.netmail.to', 'To')}${uiT('ui.common.subject_label_short', 'Subject')}${uiT('ui.netmail.last_updated', 'Last Updated')}
    - ${escapeHtml(draft.subject || '(No Subject)')} + ${escapeHtml(draft.subject || uiT('messages.no_subject', '(No Subject)'))} ${draft.message_text ? `
    ${escapeHtml(draft.message_text.substring(0, 100))}${draft.message_text.length > 100 ? '...' : ''}` : ''}
    ${formatFullDate(draft.updated_at)}
    -
    @@ -203,7 +228,7 @@ function displayMessages(messages, isThreaded = false) { let html = ''; if (messages.length === 0) { - html = '
    No messages found
    '; + html = `
    ${uiT('messages.none_found', 'No messages found')}
    `; } else { html = ` @@ -214,10 +239,10 @@ function displayMessages(messages, isThreaded = false) { - - - - + + + + @@ -234,30 +259,32 @@ function displayMessages(messages, isThreaded = false) { const threadLevel = msg.thread_level || 0; const replyCount = msg.reply_count || 0; const isThreadRoot = msg.is_thread_root || false; - const threadIndent = threadLevel > 0 ? `style="text-indent: ${threadLevel * 0.5}rem;"` : ''; - const threadIcon = threadLevel > 0 ? '' : ''; - const replyCountBadge = isThreadRoot && replyCount > 0 ? ` ${replyCount}` : ''; - - // Add thread-specific CSS classes + // Indent up to 2 levels (0.75rem each); deeper nesting shown via border-left color + const indentRem = Math.min(threadLevel, 2) * 0.75; + const threadIndent = threadLevel > 0 ? `style="padding-left: ${indentRem}rem;"` : ''; + const threadIcon = threadLevel > 0 ? `` : ''; + const replyCountBadge = isThreadRoot && replyCount > 0 ? ` ${replyCount}` : ''; + const threadLevelClass = threadLevel > 0 ? ` thread-reply thread-level-${Math.min(threadLevel, 9)}` : ''; html += ` - + - @@ -294,7 +321,7 @@ function updatePagination(pagination) { // Previous button if (pagination.page > 1) { - html += `
  • Previous
  • `; + html += `
  • ${uiT('ui.common.previous', 'Previous')}
  • `; } // Page numbers @@ -305,7 +332,7 @@ function updatePagination(pagination) { // Next button if (pagination.page < pagination.pages) { - html += `
  • Next
  • `; + html += `
  • ${uiT('ui.common.next', 'Next')}
  • `; } html += ''; @@ -373,7 +400,7 @@ function viewMessage(messageId) { $('#messageContent').html(`
    - Loading message... + ${uiT('ui.common.loading_message', 'Loading message...')}
    `); @@ -385,7 +412,7 @@ function viewMessage(messageId) { displayMessageContent(data); }) .fail(function() { - $('#messageContent').html('
    Failed to load message
    '); + $('#messageContent').html(`
    ${uiT('errors.messages.netmail.get_failed', 'Failed to load message')}
    `); }); } @@ -394,7 +421,7 @@ function displayMessageContent(message) { const currentUserId = window.currentUser ? window.currentUser.id : null; const isSent = (message.user_id && currentUserId && message.user_id == currentUserId); - $('#messageSubject').text(message.subject || '(No Subject)'); + $('#messageSubject').text(message.subject || uiT('messages.no_subject', '(No Subject)')); // Parse message to separate kludge lines from body (use stored kludge_lines if available) const parsedMessage = parseNetmailMessage(message.message_text || '', message.kludge_lines || null, message.bottom_kludges || null); @@ -432,7 +459,7 @@ function renderMessageContent(message, parsedMessage, isSent, isInAddressBook) { let addressBookButton; if (isInAddressBook) { addressBookButton = ` - `; @@ -440,7 +467,7 @@ function renderMessageContent(message, parsedMessage, isSent, isInAddressBook) { const replyToAddress = message.replyto_address || message.reply_address || message.original_author_address || message.from_address; const replyToName = message.replyto_name || message.from_name; addressBookButton = ` - `; @@ -454,30 +481,30 @@ function renderMessageContent(message, parsedMessage, isSent, isInAddressBook) {
    - From: ${escapeHtml(message.from_name)} + ${uiT('ui.common.from_label', 'From:')} ${escapeHtml(message.from_name)} ${formatFidonetAddress(message.from_address)} ${addressBookButton}
    - To: ${escapeHtml(message.to_name)} + ${uiT('ui.common.to_label', 'To:')} ${escapeHtml(message.to_name)} ${formatFidonetAddress(message.to_address)}
    - Date: ${formatFullDate(message.date_received)} + ${uiT('ui.common.date_label', 'Date:')} ${formatFullDate(message.date_received)}
    - Subject: ${escapeHtml(message.subject || '(No Subject)')} + ${uiT('ui.common.subject_label', 'Subject:')} ${escapeHtml(message.subject || uiT('messages.no_subject', '(No Subject)'))}
    ${message.received_insecure ? `
    - - Received Insecurely + + ${uiT('ui.netmail.received_insecurely', 'Received Insecurely')} - This message was not authenticated + ${uiT('ui.netmail.not_authenticated', 'This message was not authenticated')}
    ` : ''} @@ -486,10 +513,10 @@ function renderMessageContent(message, parsedMessage, isSent, isInAddressBook) { ${parsedMessage.kludgeLines.length > 0 ? `
    -
    Kludge Lines
    +
    ${uiT('ui.common.kludge_lines', 'Kludge Lines')}
    From/ToSubjectAddressReceived${uiT('ui.netmail.from_to', 'From/To')}${uiT('ui.common.subject_label_short', 'Subject')}${uiT('ui.common.address', 'Address')}${uiT('ui.netmail.received', 'Received')}
    - ${isUnread ? '' : ''}${threadIcon}${escapeHtml(isSent ? 'To: ' + msg.to_name : msg.from_name)} + ${isUnread ? `` : ``}${threadIcon}${escapeHtml(isSent ? `${uiT('ui.common.to_label', 'To:')} ` + msg.to_name : msg.from_name)}
    - ${isUnread ? '' : ''}${escapeHtml(msg.subject || '(No Subject)')}${isUnread ? '' : ''}${replyCountBadge} + + ${isUnread ? '' : ''}${escapeHtml(msg.subject || uiT('messages.no_subject', '(No Subject)'))}${isUnread ? '' : ''}${replyCountBadge} + ${msg.has_attachment ? ` ` : ''}
    - NETMAIL - ${isUnread ? 'NEW' : ''} - ${msg.received_insecure ? '' : ''} + ${uiT('ui.netmail.badge_netmail', 'NETMAIL')} + ${isUnread ? `${uiT('ui.netmail.badge_new', 'NEW')}` : ''} + ${msg.received_insecure ? `` : ''}
    @@ -268,7 +295,7 @@ function displayMessages(messages, isThreaded = false) { ${formatDate(msg.date_received)} -
    - +
    CategoryEvents
    {{ t('ui.admin.activity_stats.category') }}{{ t('ui.admin.activity_stats.events') }}
    @@ -79,10 +79,10 @@
    -
    Daily Activity (last 30 days)
    +
    {{ t('ui.admin.activity_stats.daily_activity_last_30') }}
    - +
    DateEvents
    {{ t('ui.admin.activity_stats.date') }}{{ t('ui.admin.activity_stats.events') }}
    @@ -96,10 +96,10 @@
    -
    Most Viewed Echoareas
    +
    {{ t('ui.admin.activity_stats.most_viewed_echoareas') }}
    - +
    AreaViews
    {{ t('ui.admin.activity_stats.area') }}{{ t('ui.admin.activity_stats.views') }}
    @@ -107,10 +107,10 @@
    -
    Most Active Echoareas (posts)
    +
    {{ t('ui.admin.activity_stats.most_active_echoareas') }}
    - +
    AreaPosts
    {{ t('ui.admin.activity_stats.area') }}{{ t('ui.admin.activity_stats.posts') }}
    @@ -124,10 +124,10 @@
    -
    Most Played WebDoors
    +
    {{ t('ui.admin.activity_stats.most_played_webdoors') }}
    - +
    GameSessions
    {{ t('ui.admin.activity_stats.game') }}{{ t('ui.admin.activity_stats.sessions') }}
    @@ -135,10 +135,10 @@
    -
    Most Played DOS Doors
    +
    {{ t('ui.admin.activity_stats.most_played_dos_doors') }}
    - +
    DoorSessions
    {{ t('ui.admin.activity_stats.door') }}{{ t('ui.admin.activity_stats.sessions') }}
    @@ -152,10 +152,10 @@
    -
    Top Downloaded Files
    +
    {{ t('ui.admin.activity_stats.top_downloaded_files') }}
    - +
    FileDownloads
    {{ t('ui.admin.activity_stats.file') }}{{ t('ui.admin.activity_stats.downloads') }}
    @@ -163,10 +163,10 @@
    -
    Most Browsed File Areas
    +
    {{ t('ui.admin.activity_stats.most_browsed_file_areas') }}
    - +
    AreaViews
    {{ t('ui.admin.activity_stats.area') }}{{ t('ui.admin.activity_stats.views') }}
    @@ -180,10 +180,10 @@
    -
    Most Searched Nodelist Queries
    +
    {{ t('ui.admin.activity_stats.most_searched_nodelist_queries') }}
    - +
    QuerySearches
    {{ t('ui.admin.activity_stats.query') }}{{ t('ui.admin.activity_stats.searches') }}
    @@ -191,10 +191,10 @@
    -
    Most Viewed Nodes
    +
    {{ t('ui.admin.activity_stats.most_viewed_nodes') }}
    - +
    NodeViews
    {{ t('ui.admin.activity_stats.node') }}{{ t('ui.admin.activity_stats.views') }}
    @@ -206,10 +206,10 @@
    -
    Most Active Users
    +
    {{ t('ui.admin.activity_stats.most_active_users') }}
    - +
    UserEvents
    {{ t('ui.admin.activity_stats.user') }}{{ t('ui.admin.activity_stats.events') }}
    @@ -219,10 +219,10 @@
    -
    Activity by Hour of Day (UTC)
    +
    {{ t('ui.admin.activity_stats.activity_by_hour_of_day') }} ({{ t('ui.admin.activity_stats.utc', {}, locale, ['common']) }})
    - +
    Hour (UTC)Events
    {{ t('ui.admin.activity_stats.hour') }} ({{ t('ui.admin.activity_stats.utc', {}, locale, ['common']) }}){{ t('ui.admin.activity_stats.events') }}
    @@ -237,43 +237,53 @@ {% endblock %} + + + diff --git a/templates/admin/ads.twig b/templates/admin/ads.twig index 06e484c51..6df4ea6bb 100644 --- a/templates/admin/ads.twig +++ b/templates/admin/ads.twig @@ -1,40 +1,40 @@ {% extends "base.twig" %} -{% block title %}Advertisements{% endblock %} +{% block title %}{{ t('ui.admin.ads.page_title') }}{% endblock %} {% block content %}
    -

    Advertisements

    +

    {{ t('ui.admin.ads.heading') }}

    - Upload ANSI ads (`.ans`) to the bbs_ads/ directory. Ads are displayed randomly on the dashboard. + {{ t('ui.admin.ads.info_text_prefix') }} bbs_ads/ {{ t('ui.admin.ads.info_text_suffix') }}
    -
    Upload New Advertisement
    +
    {{ t('ui.admin.ads.upload_new') }}
    - +
    - - -
    Only letters, numbers, dot, dash, underscore.
    + + +
    {{ t('ui.admin.ads.save_as_help') }}
    @@ -44,9 +44,9 @@
    -
    Current Advertisements
    +
    {{ t('ui.admin.ads.current_advertisements') }}
    @@ -54,15 +54,15 @@ - - - - + + + + - +
    NameSizeUpdatedActions{{ t('ui.common.name') }}{{ t('ui.admin.ads.size') }}{{ t('ui.admin.ads.updated') }}{{ t('ui.admin.ads.actions') }}
    Loading ads...{{ t('ui.admin.ads.loading_ads') }}
    @@ -74,8 +74,17 @@ {% block scripts %} {% endblock %} + diff --git a/templates/admin/auto_feed.twig b/templates/admin/auto_feed.twig index 330f0f630..19039b861 100644 --- a/templates/admin/auto_feed.twig +++ b/templates/admin/auto_feed.twig @@ -1,16 +1,16 @@ {% extends "base.twig" %} -{% block title %}Auto Feed - {{ parent() }}{% endblock %} +{% block title %}{{ t('ui.admin.auto_feed.page_title') }} - {{ parent() }}{% endblock %} {% block content %}

    - Auto Feed + {{ t('ui.admin.auto_feed.heading') }}

    @@ -18,13 +18,13 @@
    -
    RSS/Atom Feeds
    +
    {{ t('ui.admin.auto_feed.rss_atom_feeds') }}
    - Loading feeds... + {{ t('ui.admin.auto_feed.loading_feeds') }}
    @@ -34,17 +34,17 @@
    -
    Statistics
    +
    {{ t('ui.admin.auto_feed.statistics') }}
    -
    Total Feeds:
    +
    {{ t('ui.admin.auto_feed.total_feeds') }}:
    -
    -
    Active:
    +
    {{ t('ui.admin.auto_feed.active_label') }}:
    -
    -
    Articles Posted:
    +
    {{ t('ui.admin.auto_feed.articles_posted') }}:
    -
    @@ -52,14 +52,14 @@
    -
    About
    +
    {{ t('ui.admin.auto_feed.about_title') }}

    - Auto Feed monitors RSS/Atom feeds and automatically posts new articles to specified echoareas. + {{ t('ui.admin.auto_feed.about_text') }}

    - Run php scripts/rss_poster.php via cron to check feeds periodically. + {{ t('ui.admin.auto_feed.cron_hint_prefix') }} php scripts/rss_poster.php {{ t('ui.admin.auto_feed.cron_hint_suffix') }}

    @@ -71,7 +71,7 @@
    Name/URLEcho AreaPost AsArticlesLast CheckActions${uiT('ui.admin.auto_feed.name_url', 'Name/URL')}${uiT('ui.admin.auto_feed.echo_area', 'Echo Area')}${uiT('ui.admin.auto_feed.post_as', 'Post As')}${uiT('ui.admin.auto_feed.articles', 'Articles')}${uiT('ui.admin.auto_feed.last_check', 'Last Check')}${uiT('ui.admin.auto_feed.actions', 'Actions')}
    ${feedName}
    ${truncatedUrl} - ${feed.last_error ? '
    Error' : ''} + ${feed.last_error ? `
    ${uiT('ui.common.error', 'Error')}` : ''}
    - ${escapeHtml(feed.echoarea_tag || 'Unknown')}
    - @ ${escapeHtml(feed.echoarea_domain || 'Unknown')} + ${escapeHtml(feed.echoarea_tag || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}
    + @ ${escapeHtml(feed.echoarea_domain || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}
    - ${escapeHtml(feed.username || 'User #' + feed.post_as_user_id)} + ${escapeHtml(feed.username || (uiT('ui.admin.auto_feed.user_prefix', 'User #') + feed.post_as_user_id))} ${feed.articles_posted || 0} @@ -250,13 +266,13 @@ function displayFeeds(feeds) {
    - - -
    @@ -277,7 +293,7 @@ function displayFeeds(feeds) { function showAddFeedModal() { currentFeedId = null; - $('#feedModalTitle').text('Add Feed'); + $('#feedModalTitle').text(uiT('ui.admin.auto_feed.add_feed', 'Add Feed')); $('#feedForm')[0].reset(); $('#feedId').val(''); $('#feedActive').prop('checked', true); @@ -287,7 +303,7 @@ function showAddFeedModal() { function editFeed(id) { currentFeedId = id; - $('#feedModalTitle').text('Edit Feed'); + $('#feedModalTitle').text(uiT('ui.admin.auto_feed.edit_feed', 'Edit Feed')); $.get(`/admin/api/auto-feed/feeds/${id}`) .done(function(data) { @@ -302,7 +318,7 @@ function editFeed(id) { $('#feedModal').modal('show'); }) .fail(function() { - showError('Failed to load feed details'); + showError(uiT('ui.admin.auto_feed.load_details_failed', 'Failed to load feed details')); }); } @@ -330,14 +346,20 @@ function saveFeed() { method: method, contentType: 'application/json', data: JSON.stringify(formData), - success: function() { + success: function(response) { $('#feedModal').modal('hide'); - showSuccess(`Feed ${currentFeedId ? 'updated' : 'created'} successfully`); + const defaultMessage = currentFeedId + ? uiT('ui.admin.auto_feed.updated_success', 'Feed updated successfully') + : uiT('ui.admin.auto_feed.created_success', 'Feed created successfully'); + const successMessage = window.getApiMessage + ? window.getApiMessage(response, defaultMessage) + : defaultMessage; + showSuccess(successMessage); loadFeeds(); loadStats(); }, error: function(xhr) { - const error = xhr.responseJSON ? xhr.responseJSON.error : 'Operation failed'; + const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.operation_failed', 'Operation failed')); showError(error); } }); @@ -357,30 +379,36 @@ function deleteFeed() { $.ajax({ url: `/admin/api/auto-feed/feeds/${currentFeedId}`, method: 'DELETE', - success: function() { + success: function(response) { $('#deleteModal').modal('hide'); - showSuccess('Feed deleted successfully'); + const successMessage = window.getApiMessage + ? window.getApiMessage(response, uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully')) + : uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully'); + showSuccess(successMessage); loadFeeds(); loadStats(); }, error: function(xhr) { - const error = xhr.responseJSON ? xhr.responseJSON.error : 'Delete failed'; + const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.delete_failed', 'Delete failed')); showError(error); } }); } function checkFeed(id) { - showInfo('Checking feed...'); + showInfo(uiT('ui.admin.auto_feed.checking_feed', 'Checking feed...')); $.post(`/admin/api/auto-feed/check/${id}`) .done(function(data) { - showSuccess(`Feed checked: ${data.articles_posted} new article(s) posted`); + const successMessage = window.getApiMessage + ? window.getApiMessage(data, uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted })) + : uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted }); + showSuccess(successMessage); loadFeeds(); loadStats(); }) .fail(function(xhr) { - const error = xhr.responseJSON ? xhr.responseJSON.error : 'Check failed'; + const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.check_failed', 'Check failed')); showError(error); }); } @@ -411,7 +439,7 @@ function loadEchoareas() { .done(function(data) { const select = $('#feedEchoarea'); select.empty(); - select.append(''); + select.append(``); data.echoareas.forEach(function(area) { const label = `${area.tag} @ ${area.domain}${area.description ? ' - ' + area.description : ''}`; @@ -419,5 +447,15 @@ function loadEchoareas() { }); }); } + +function apiError(payload, fallback) { + if (window.getApiErrorMessage) { + return window.getApiErrorMessage(payload, fallback); + } + if (payload && payload.error) { + return String(payload.error); + } + return fallback; +} {% endblock %} diff --git a/templates/admin/bbs_settings.twig b/templates/admin/bbs_settings.twig index 43dff44dd..1db086d57 100644 --- a/templates/admin/bbs_settings.twig +++ b/templates/admin/bbs_settings.twig @@ -1,37 +1,37 @@ {% extends "base.twig" %} -{% block title %}BBS Settings{% endblock %} +{% block title %}{{ t('ui.admin.bbs_settings.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %}
    -

    BBS Settings

    +

    {{ t('ui.admin.bbs_settings.heading', {}, locale, ['common']) }}

    -
    System Settings
    +
    {{ t('ui.admin.bbs_settings.system.title', {}, locale, ['common']) }}
    - +
    - + - Select an admin user. + {{ t('ui.admin.bbs_settings.system.select_admin_user', {}, locale, ['common']) }}
    - +
    - +
    - +
    - +
    - +
    @@ -54,56 +54,61 @@
    -
    BBS Features
    +
    {{ t('ui.admin.bbs_settings.features.title', {}, locale, ['common']) }}
    - + {% if not webdoors_active %} - (inactive) - Activate + ({{ t('ui.admin.bbs_settings.features.inactive', {}, locale, ['common']) }}) + {{ t('ui.admin.bbs_settings.features.activate', {}, locale, ['common']) }} {% endif %}
    - +
    - +
    - +
    - +
    -
    +
    - + +
    +
    + + +
    {{ t('ui.admin.bbs_settings.features.guest_doors_page_help', {}, locale, ['common']) }}
    - + - Default interface for viewing echomail. Users can override this in their settings. + {{ t('ui.admin.bbs_settings.features.default_echo_help', {}, locale, ['common']) }}

    - + - Maximum number of additional areas a user can cross-post to (2-20). + {{ t('ui.admin.bbs_settings.features.max_cross_post_help', {}, locale, ['common']) }}
    - +
    @@ -111,7 +116,7 @@
    -
    Credits System Configuration
    +
    {{ t('ui.admin.bbs_settings.credits.title', {}, locale, ['common']) }}
    @@ -119,90 +124,90 @@
    - +
    - + - Example: $, USD (max 5 characters). Leave blank for no symbol. + {{ t('ui.admin.bbs_settings.credits.currency_symbol_help', {}, locale, ['common']) }}
    - + - Amount of credits awarded to users on daily login. + {{ t('ui.admin.bbs_settings.credits.daily_login_bonus_help', {}, locale, ['common']) }}
    - + - Minutes after login before daily bonus is awarded. + {{ t('ui.admin.bbs_settings.credits.daily_login_delay_help', {}, locale, ['common']) }}
    - + - Credits awarded when a pending user is approved. + {{ t('ui.admin.bbs_settings.credits.new_user_approval_bonus_help', {}, locale, ['common']) }}
    - + - One-time bonus awarded when user logs in 14+ days after account creation. + {{ t('ui.admin.bbs_settings.credits.new_user_14_day_bonus_help', {}, locale, ['common']) }}
    - + - Credits charged when sending a netmail message. + {{ t('ui.admin.bbs_settings.credits.netmail_cost_help', {}, locale, ['common']) }}
    - + - Credits earned when posting an echomail message. Doubled when >= 1200 characters long. + {{ t('ui.admin.bbs_settings.credits.echomail_reward_help', {}, locale, ['common']) }}
    - + - Credits charged when sending direct/crashmail delivery. + {{ t('ui.admin.bbs_settings.credits.crashmail_cost_help', {}, locale, ['common']) }}
    - + - Credits charged when creating a new poll. + {{ t('ui.admin.bbs_settings.credits.poll_creation_cost_help', {}, locale, ['common']) }}
    - + - Percentage of credit transfers taken as fee (0.05 = 5%). Distributed to sysops. + {{ t('ui.admin.bbs_settings.credits.transfer_fee_help', {}, locale, ['common']) }}

    -
    Referral System
    +
    {{ t('ui.admin.bbs_settings.credits.referral_system', {}, locale, ['common']) }}
    - +
    - + - Credits awarded when a referred user is approved by admin. + {{ t('ui.admin.bbs_settings.credits.referral_bonus_help', {}, locale, ['common']) }}
    - +
    -
    Taglines
    +
    {{ t('ui.admin.bbs_settings.taglines.title', {}, locale, ['common']) }}
    - - - These appear as selectable taglines when users compose messages. + + + {{ t('ui.admin.bbs_settings.taglines.help', {}, locale, ['common']) }}
    - +
    @@ -213,11 +218,20 @@ {% block scripts %} {% endblock %} diff --git a/templates/admin/binkp_config.twig b/templates/admin/binkp_config.twig index 5e3660b32..eebfe7e12 100644 --- a/templates/admin/binkp_config.twig +++ b/templates/admin/binkp_config.twig @@ -1,16 +1,16 @@ {% extends "base.twig" %} -{% block title %}Binkp Configuration{% endblock %} +{% block title %}{{ t('ui.admin.binkp_config.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %}
    -

    Binkp Configuration

    +

    {{ t('ui.admin.binkp_config.heading', {}, locale, ['common']) }}

    - After saving, restart the daemons to apply changes. + {{ t('ui.admin.binkp_config.restart_notice', {}, locale, ['common']) }}
    @@ -19,32 +19,32 @@
    -
    System
    +
    {{ t('ui.admin.binkp_config.system.title', {}, locale, ['common']) }}
    - +
    - +
    - + - Select an admin user. + {{ t('ui.admin.binkp_config.system.select_admin_user', {}, locale, ['common']) }}
    - +
    - +
    - +
    - +
    @@ -61,28 +61,28 @@
    -
    Binkp
    +
    {{ t('ui.admin.binkp_config.binkp.title', {}, locale, ['common']) }}
    - +
    - +
    - +
    - +
    - +
    @@ -90,28 +90,28 @@
    -
    Security
    +
    {{ t('ui.admin.binkp_config.security.title', {}, locale, ['common']) }}
    - +
    - +
    - +
    - +
    - +
    @@ -119,32 +119,32 @@
    -
    Crashmail
    +
    {{ t('ui.admin.binkp_config.crashmail.title', {}, locale, ['common']) }}
    - +
    - +
    - +
    - +
    - +
    - +
    @@ -153,26 +153,26 @@
    -
    Uplinks
    - +
    {{ t('ui.admin.binkp_config.uplinks.title', {}, locale, ['common']) }}
    +
    - - - - - - - - - - - - + + + + + + + + + + + + @@ -182,9 +182,9 @@
    - - +
    @@ -193,91 +193,91 @@ @@ -289,13 +289,22 @@ let binkpConfig = null; let uplinkModal; +function uiT(key, fallback, params = {}) { + if (window.t) { + return window.t(key, params, fallback); + } + return fallback; +} + document.addEventListener('DOMContentLoaded', function() { uplinkModal = new bootstrap.Modal(document.getElementById('uplinkModal')); document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(element) { new bootstrap.Tooltip(element); }); - loadBinkpConfig(); - loadAdminUsers('systemSysop'); + loadI18nNamespaces(['common', 'errors']).then(function() { + loadBinkpConfig(); + loadAdminUsers('systemSysop'); + }); }); function loadBinkpConfig() { @@ -303,14 +312,14 @@ function loadBinkpConfig() { .then(response => response.json()) .then(data => { if (!data.success) { - throw new Error(data.error || 'Failed to load config'); + throw new Error(apiError(data, uiT('ui.admin.binkp_config.load_failed', 'Failed to load config'))); } binkpConfig = data.config || {}; populateForm(); renderUplinks(); }) .catch(error => { - showBinkpAlert(error.message || 'Failed to load config', 'danger'); + showBinkpAlert(error.message || uiT('ui.admin.binkp_config.load_failed', 'Failed to load config'), 'danger'); }); } @@ -389,23 +398,23 @@ function gatherConfig() { function validateConfig(config) { if (!config.system.name || !config.system.address || !config.system.sysop) { - return 'System name, address, and sysop are required.'; + return uiT('ui.admin.binkp_config.validation.system_required', 'System name, address, and sysop are required.'); } if (!Number.isInteger(config.binkp.port) || config.binkp.port < 1 || config.binkp.port > 65535) { - return 'Binkp port must be between 1 and 65535.'; + return uiT('ui.admin.binkp_config.validation.port_range', 'Binkp port must be between 1 and 65535.'); } if (!Number.isInteger(config.binkp.timeout) || config.binkp.timeout < 1) { - return 'Binkp timeout must be a positive number.'; + return uiT('ui.admin.binkp_config.validation.timeout_positive', 'Binkp timeout must be a positive number.'); } if (!Number.isInteger(config.binkp.max_connections) || config.binkp.max_connections < 1) { - return 'Max connections must be a positive number.'; + return uiT('ui.admin.binkp_config.validation.max_connections_positive', 'Max connections must be a positive number.'); } return null; } function saveBinkpConfig() { if (!binkpConfig) { - showBinkpAlert('Config not loaded.', 'danger'); + showBinkpAlert(uiT('ui.admin.binkp_config.config_not_loaded', 'Config not loaded.'), 'danger'); return; } const config = gatherConfig(); @@ -423,23 +432,23 @@ function saveBinkpConfig() { .then(response => response.json()) .then(data => { if (!data.success) { - throw new Error(data.error || 'Failed to save config'); + throw new Error(apiError(data, uiT('ui.admin.binkp_config.save_failed', 'Failed to save config'))); } binkpConfig = data.config || config; renderUplinks(); - showBinkpAlert('Configuration saved.', 'success'); + showBinkpAlert(apiMessage(data, uiT('ui.admin.binkp_config.configuration_saved', 'Configuration saved.')), 'success'); }) .catch(error => { - showBinkpAlert(error.message || 'Failed to save config', 'danger'); + showBinkpAlert(error.message || uiT('ui.admin.binkp_config.save_failed', 'Failed to save config'), 'danger'); }); } function reloadBinkpConfig() { - if (!confirm('Reload binkp daemon configuration? This will send SIGHUP to the daemon.')) { + if (!confirm(uiT('ui.admin.binkp_config.reload_confirm', 'Reload binkp daemon configuration? This will send SIGHUP to the daemon.'))) { return; } - showBinkpAlert('Reloading configuration...', 'info'); + showBinkpAlert(uiT('ui.admin.binkp_config.reloading', 'Reloading configuration...'), 'info'); fetch('/admin/api/binkp-reload', { method: 'POST', @@ -448,12 +457,15 @@ function reloadBinkpConfig() { .then(response => response.json()) .then(data => { if (!data.success) { - throw new Error(data.error || 'Failed to reload config'); + throw new Error(apiError(data, uiT('ui.admin.binkp_config.reload_failed', 'Failed to reload config'))); } - showBinkpAlert('Configuration reloaded successfully. Daemon has picked up changes.', 'success'); + showBinkpAlert( + apiMessage(data, uiT('ui.admin.binkp_config.reloaded_success', 'Configuration reloaded successfully. Daemon has picked up changes.')), + 'success' + ); }) .catch(error => { - showBinkpAlert(error.message || 'Failed to reload config', 'danger'); + showBinkpAlert(error.message || uiT('ui.admin.binkp_config.reload_failed', 'Failed to reload config'), 'danger'); }); } @@ -462,7 +474,7 @@ function renderUplinks() { tbody.innerHTML = ''; const uplinks = binkpConfig.uplinks || []; if (uplinks.length === 0) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; return; } uplinks.forEach((uplink, index) => { @@ -474,24 +486,44 @@ function renderUplinks() { - - - - - + + + + + `; tbody.appendChild(row); }); } +function apiError(payload, fallback) { + if (window.getApiErrorMessage) { + return window.getApiErrorMessage(payload, fallback); + } + if (payload && payload.error) { + return String(payload.error); + } + return fallback; +} + +function apiMessage(payload, fallback) { + if (window.getApiMessage) { + return window.getApiMessage(payload, fallback); + } + if (payload && payload.message) { + return String(payload.message); + } + return fallback; +} + function openUplinkModal(index = null) { document.getElementById('uplinkModalAlert').innerHTML = ''; if (index === null) { - document.getElementById('uplinkModalLabel').textContent = 'Add Uplink'; + document.getElementById('uplinkModalLabel').textContent = uiT('ui.admin.binkp_config.uplinks.add_uplink', 'Add Uplink'); document.getElementById('uplinkIndex').value = ''; document.getElementById('uplinkMe').value = ''; document.getElementById('uplinkAddress').value = ''; @@ -515,7 +547,7 @@ function openUplinkModal(index = null) { if (!uplink) { return; } - document.getElementById('uplinkModalLabel').textContent = 'Edit Uplink'; + document.getElementById('uplinkModalLabel').textContent = uiT('ui.admin.binkp_config.uplinks.edit_uplink', 'Edit Uplink'); document.getElementById('uplinkIndex').value = index; document.getElementById('uplinkMe').value = uplink.me || ''; document.getElementById('uplinkAddress').value = uplink.address || ''; @@ -586,7 +618,7 @@ function saveUplink() { } function deleteUplink(index) { - if (!confirm('Remove this uplink?')) { + if (!confirm(uiT('ui.admin.binkp_config.remove_uplink_confirm', 'Remove this uplink?'))) { return; } binkpConfig.uplinks.splice(index, 1); @@ -595,10 +627,10 @@ function deleteUplink(index) { function validateUplink(uplink) { if (!uplink.hostname) { - return 'Uplink hostname is required.'; + return uiT('ui.admin.binkp_config.validation.uplink_hostname_required', 'Uplink hostname is required.'); } if (!Number.isInteger(uplink.port) || uplink.port < 1 || uplink.port > 65535) { - return 'Uplink port must be between 1 and 65535.'; + return uiT('ui.admin.binkp_config.validation.uplink_port_range', 'Uplink port must be between 1 and 65535.'); } return null; } @@ -661,7 +693,7 @@ function syncSysopSelect(selectId) { if (!exists) { const option = document.createElement('option'); option.value = current; - option.textContent = `${current} (current)`; + option.textContent = `${current} (${uiT('ui.admin.binkp_config.current', 'current')})`; select.insertBefore(option, select.firstChild); } select.value = current; diff --git a/templates/admin/binkp_sessions.twig b/templates/admin/binkp_sessions.twig index b0e5e0e38..d1b9c4cf6 100644 --- a/templates/admin/binkp_sessions.twig +++ b/templates/admin/binkp_sessions.twig @@ -1,17 +1,17 @@ {% extends "base.twig" %} -{% block title %}Binkp Sessions - Admin{% endblock %} +{% block title %}{{ t('ui.admin.binkp_sessions.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %}
    -

    Binkp Session Log

    +

    {{ t('ui.admin.binkp_sessions.heading', {}, locale, ['common']) }}

    @@ -21,7 +21,7 @@
    -
    Total (24h)
    +
    {{ t('ui.admin.binkp_sessions.total_24h', {}, locale, ['common']) }}
    -
    @@ -29,7 +29,7 @@
    -
    Secure
    +
    {{ t('ui.admin.binkp_sessions.secure', {}, locale, ['common']) }}
    -
    @@ -37,7 +37,7 @@
    -
    Insecure
    +
    {{ t('ui.admin.binkp_sessions.insecure', {}, locale, ['common']) }}
    -
    @@ -45,7 +45,7 @@
    -
    Crash Out
    +
    {{ t('ui.admin.binkp_sessions.crash_out', {}, locale, ['common']) }}
    -
    @@ -53,7 +53,7 @@
    -
    Successful
    +
    {{ t('ui.admin.binkp_sessions.successful', {}, locale, ['common']) }}
    -
    @@ -61,7 +61,7 @@
    -
    Failed
    +
    {{ t('ui.admin.binkp_sessions.failed', {}, locale, ['common']) }}
    -
    @@ -71,35 +71,35 @@
    -
    Filter Sessions
    +
    {{ t('ui.admin.binkp_sessions.filter_sessions', {}, locale, ['common']) }}
    - +
    - +
    - - + +
    @@ -109,27 +109,27 @@
    -
    Recent Sessions
    +
    {{ t('ui.admin.binkp_sessions.recent_sessions', {}, locale, ['common']) }}
    MeUplinkDomainHostnamePortScheduleMarkdownPosting NameADR @DomainEnabledDefaultActions{{ t('ui.admin.binkp_config.uplinks.table.me', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.uplink', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.domain', {}, locale, ['common']) }}{{ t('ui.common.hostname', {}, locale, ['common']) }}{{ t('ui.common.port', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.schedule', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.markdown', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.posting_name', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.adr_domain', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.enabled', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.default', {}, locale, ['common']) }}{{ t('ui.admin.binkp_config.uplinks.table.actions', {}, locale, ['common']) }}
    No uplinks configured.
    ' + escapeHtml(uiT('ui.admin.binkp_config.uplinks.none_configured', 'No uplinks configured.')) + '
    ${escapeHtml(uplink.hostname || '')} ${uplink.port ?? ''} ${escapeHtml(uplink.poll_schedule || '')}${uplink.allow_markup ? 'Yes' : 'No'}${(uplink.posting_name_policy || 'real_name') === 'username' ? 'Username' : 'Real Name'}${uplink.send_domain_in_addr ? 'Yes' : 'No'}${uplink.enabled ? 'Yes' : 'No'}${uplink.default ? 'Yes' : 'No'}${uplink.allow_markup ? uiT('ui.common.yes', 'Yes') : uiT('ui.common.no', 'No')}${(uplink.posting_name_policy || 'real_name') === 'username' ? uiT('ui.admin.binkp_config.uplinks.username', 'Username') : uiT('ui.admin.binkp_config.uplinks.real_name', 'Real Name')}${uplink.send_domain_in_addr ? uiT('ui.common.yes', 'Yes') : uiT('ui.common.no', 'No')}${uplink.enabled ? uiT('ui.common.yes', 'Yes') : uiT('ui.common.no', 'No')}${uplink.default ? uiT('ui.common.yes', 'Yes') : uiT('ui.common.no', 'No')} - - + +
    - - - - - - - - - + + + + + + + + + - +
    TimeRemote AddressIPTypeDirectionMsgs InMsgs OutStatusDuration{{ t('ui.admin.binkp_sessions.time', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.remote_address', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.ip', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.type', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.direction', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.msgs_in', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.msgs_out', {}, locale, ['common']) }}{{ t('ui.common.status', {}, locale, ['common']) }}{{ t('ui.admin.binkp_sessions.duration', {}, locale, ['common']) }}
    Loading...{{ t('ui.common.loading', {}, locale, ['common']) }}
    @@ -147,9 +147,18 @@ {% endblock %} + diff --git a/templates/admin/economy.twig b/templates/admin/economy.twig index 17a7b846f..7746016a3 100644 --- a/templates/admin/economy.twig +++ b/templates/admin/economy.twig @@ -1,21 +1,21 @@ {% extends "base.twig" %} -{% block title %}Economy Viewer{% endblock %} +{% block title %}{{ t('ui.admin.economy.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %}
    -

    Economy Viewer

    +

    {{ t('ui.admin.economy.heading', {}, locale, ['common']) }}

    - + - Dashboard + {{ t('ui.admin.economy.dashboard', {}, locale, ['common']) }}
    @@ -24,9 +24,9 @@
    - Loading... + {{ t('ui.common.loading', {}, locale, ['common']) }}
    -

    Loading economy statistics...

    +

    {{ t('ui.admin.economy.loading_statistics', {}, locale, ['common']) }}