mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 05:11:46 +00:00
chore: move i18n to shared package (#1066)
* chore: move i18n to shared package * chore: move server translations to shared package and apply linter and prettier on entire shared package
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 455 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB |
@@ -1,524 +0,0 @@
|
||||
<img width="5292" height="1404" alt="Release 2 9 0 (2)" src="https://github.com/user-attachments/assets/6ff67226-3535-444e-991f-0bc0352e22e7" />
|
||||
|
||||
# TREK 3.0.0
|
||||
|
||||
<video src="https://github.com/mauriceboe/trek-media/raw/main/.github/assets/TREK1.mp4" controls width="100%"></video>
|
||||
|
||||
> **The biggest TREK release to date.** A new Journey addon turns your trips into rich travel journals. Mapbox GL joins Leaflet as a first-class renderer. MCP gets a full OAuth 2.1 authorization server. Offline-first PWA, self-service password reset, and a dashboard redesigned from the ground up. Fifteen languages, top to bottom.
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Photos moved from Trip Planner to Journey
|
||||
|
||||
In previous versions, Immich and Synology Photos were integrated directly into the Trip Planner via a "Photos" tab. **This tab has been removed.** Photos are now part of the new **Journey addon**, which is purpose-built for documenting your travels with stories, photos, and maps.
|
||||
|
||||
**What this means for you:**
|
||||
- **No photos are lost.** The previous integration was read-only — TREK never uploaded to or deleted from your Immich/Synology library. Your photos remain untouched in your photo provider.
|
||||
- **Previously linked trip photos are no longer displayed in the Trip Planner.** To view and organize your travel photos, enable the Journey addon (Settings > Addons) and create a Journey linked to your trip.
|
||||
- **Journey brings a much richer photo experience:** upload photos directly to TREK, browse and import from Immich/Synology with duplicate detection, reorder photos, view EXIF metadata, and export everything as a PDF photo book.
|
||||
|
||||
### New Immich API Key Permissions Required
|
||||
|
||||
Journey introduces **photo upload sync** — when you upload a photo to a Journey entry, TREK can optionally sync it to your Immich library. This requires an additional Immich API permission that was not needed before.
|
||||
|
||||
**Previous versions required:**
|
||||
| Permission | Used for |
|
||||
|---|---|
|
||||
| `user.read` | Connection test |
|
||||
| `asset.read` | Browse photos by date, search |
|
||||
| `asset.view` | Stream thumbnails |
|
||||
| `asset.download` | Stream originals |
|
||||
| `album.read` | List and browse albums |
|
||||
| `timeline.read` | Browse timeline buckets |
|
||||
|
||||
**New in 3.0.0 — additionally required:**
|
||||
| Permission | Used for |
|
||||
|---|---|
|
||||
| `asset.upload` | Sync uploaded Journey photos to Immich |
|
||||
|
||||
> **How to update your Immich API key:** Go to your Immich instance > User Settings > API Keys. Edit your existing TREK key (or create a new one) and ensure `asset.upload` is enabled in addition to the existing permissions. If you don't plan to use Journey's upload sync, the old key will continue to work — the upload simply won't sync to Immich.
|
||||
|
||||
**No changes needed for Synology Photos** — Synology uses session-based authentication which inherits the user's full permissions.
|
||||
|
||||
### OIDC_ONLY deprecated
|
||||
|
||||
The `OIDC_ONLY` environment variable is deprecated. Replace with `DISABLE_LOCAL_LOGIN=true` + `DISABLE_LOCAL_REGISTRATION=true` for equivalent behavior. The old variable still works but will be removed in a future release.
|
||||
|
||||
---
|
||||
|
||||
<img width="5292" height="1404" alt="Release 2 9 0 (3)" src="https://github.com/user-attachments/assets/76976c02-dd81-49ab-83f5-e2221d6b018b" />
|
||||
|
||||
## Journey Addon — Travel Journal
|
||||
|
||||
The headline feature of 3.0.0. Journey is a new global addon that transforms your trips into magazine-style travel stories.
|
||||
|
||||
### Core
|
||||
- **5-table schema** — journeys, entries, photos, trips, contributors with full relational integrity
|
||||
- **Trip-to-Journey sync engine** — link one or more trips to a journey; skeleton entries and photos are synced automatically
|
||||
- **Timeline, Gallery, and Map views** — browse entries chronologically, as a photo grid, or on an interactive map with SVG pin markers
|
||||
- **Entry editor** — markdown toolbar, custom date picker, location search (Nominatim/Google Maps), mood (Amazing/Good/Neutral/Rough), weather (Sunny to Snowy), and Pros & Cons sections
|
||||
- **Entry reorder** — move-up / move-down arrows on each entry (desktop), skipped on skeleton suggestions
|
||||
- **Hide skeletons toggle** — per-contributor setting to focus on the written entries only
|
||||
|
||||
### Photos
|
||||
- **Immich & Synology browser** — browse by trip dates, custom range, or album with duplicate detection
|
||||
- **Photo upload** — direct upload with drag-and-drop, reorder (Make 1st), and delete
|
||||
- **EXIF metadata** — displayed in lightbox for Immich photos
|
||||
- **Thumbnail to original fallback** — seamless resolution upgrade everywhere
|
||||
- **HEIC rendering fix** — serve fullsize thumbnail for original to fix HEIC rendering on non-Safari browsers
|
||||
- **Contributor photo access** — invited contributors can view all journey photos even without their own Immich/Synology connection (owner credentials are used for the proxy)
|
||||
- **Safari gallery picker fix** — repaired grid layout collapse on Safari (#717)
|
||||
|
||||
### Sharing & Export
|
||||
- **Public share links** — token-based access with language picker, no login required
|
||||
- **Public photo proxy** — validates share token instead of auth for photo streaming
|
||||
- **Thumbnail size in public gallery** — grid loads thumbnails instead of originals, lightbox keeps originals (cuts bandwidth on shared links significantly)
|
||||
- **PDF photo book export** — Polarsteps-inspired layout with cover, day chapters, photo grids, and stories
|
||||
|
||||
### Collaboration
|
||||
- **Contributors** — invite users as editors or viewers
|
||||
- **Trip linking/unlinking** — manage synced trips from Journey Settings and Desktop Sidebar
|
||||
- **Cover image** — upload or pick from journey photos
|
||||
|
||||
### Frontend
|
||||
- **JourneyPage** — frontpage with hero card, active journey stats, trip suggestions ("Trip just ended — turn it into a Journey")
|
||||
- **JourneyDetailPage** — full timeline/gallery/map with inline entry editing
|
||||
- **JourneyPublicPage** — public share view with language picker and read-only timeline
|
||||
|
||||
---
|
||||
|
||||
## Mapbox GL as a First-Class Renderer
|
||||
|
||||
Leaflet gets a sibling. Users can now switch the trip planner map to **Mapbox GL JS** for a proper 3D globe, terrain, and 3D buildings.
|
||||
|
||||
- **Settings toggle** — choose between Leaflet and Mapbox GL in Settings > Map
|
||||
- **Globe projection** — smooth rotating globe when zoomed out, mercator when zoomed in
|
||||
- **3D terrain and buildings** — enabled on Standard and Satellite styles, with custom 3D buildings in dark/light mode
|
||||
- **Trip route, GPX geometries, place markers** — full feature parity with the Leaflet renderer
|
||||
- **Transport reservations overlay** — great-circle arcs for flights/cruises, straight lines for trains/cars, clickable endpoint badges with IATA codes, rotating mid-arc stats label for flights. Honours the per-booking "show route" toggle in DayPlanSidebar
|
||||
- **Auto-fit on load** — planner map zooms to the trip's places on initial render
|
||||
- **Booking route label toggle** — separate setting to hide IATA labels on endpoint markers
|
||||
- **Infrastructure** — WebAssembly allowed in CSP for Mapbox GL's 3D engine, PWA precache limit raised so the mapbox-gl bundle builds, Mapbox endpoints allowed in `connect-src` / `img-src`
|
||||
|
||||
---
|
||||
|
||||
## MCP: OAuth 2.1 & Granular Scopes
|
||||
|
||||
MCP authentication has been completely rebuilt around the OAuth 2.1 specification.
|
||||
|
||||
- **OAuth 2.1 authorization server** — full PKCE flow with authorization codes, access tokens, refresh tokens, and token rotation with replay detection
|
||||
- **Granular scopes** — 24 scopes across 11 groups (trips, places, atlas, packing, todos, budget, reservations, collab, notifications, vacay, geo/weather) with per-scope read/write/delete control
|
||||
- **Dynamic Client Registration (DCR)** — RFC 7591 endpoint at `POST /oauth/register`, with strict redirect_uri validation (HTTPS / loopback / reverse-DNS private-use schemes only; rejects `javascript:` / `data:` / `file:` / etc.)
|
||||
- **RFC 9728 Protected Resource Metadata** — `/.well-known/oauth-protected-resource` exposes the MCP endpoint's auth requirements for client auto-discovery
|
||||
- **RFC 8707 audience binding** — tokens are audience-bound to `<app_url>/mcp` by default and validated on every MCP request
|
||||
- **Consent screen** — user-facing scope selection with grouped permission display
|
||||
- **Admin panel** — OAuth sessions management in MCP Access panel with collapsible scope lists
|
||||
- **Per-client rate limiting** — configurable rate limits per OAuth client
|
||||
- **Addon gating** — MCP tools are only registered when their corresponding addon is enabled
|
||||
- **Compound tools** — single-call multi-step workflows (e.g. create day with places in one tool call, fetch full trip context) to reduce MCP round-trips
|
||||
- **Surface alignment** — MCP tool schemas and responses kept in sync with the current app state (fewer drifted fields, correct enum sets)
|
||||
- **Static token deprecation** — existing MCP tokens still work but surface deprecation notices; migration path to OAuth is documented
|
||||
- **Collab sub-feature gating** — MCP tools for chat/notes/polls respect the admin-level collab sub-feature toggles
|
||||
|
||||
---
|
||||
|
||||
## Self-Service Password Reset
|
||||
|
||||
Users can now reset their own password without admin intervention.
|
||||
|
||||
- **Email-based flow** — `/forgot-password` issues a single-use reset token delivered via SMTP (or logged to the server console if SMTP is not configured)
|
||||
- **MFA-aware** — if the user has MFA enabled, the reset endpoint additionally verifies a TOTP code or backup code before rotating the password
|
||||
- **Session invalidation** — resetting the password bumps `users.password_version`, which kicks every existing JWT, MCP static token, and OAuth bearer token for that user out in one shot
|
||||
- **Server-side URL building** — the reset link is built from `APP_URL` / `ALLOWED_ORIGINS`, not from request headers, so a spoofed `Host` / `Origin` cannot redirect the link to an attacker-controlled domain
|
||||
- **Rate limiting + audit** — per-IP rate limit on `/forgot-password`, all requests audited (including "no such user" so abuse is visible)
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Redesign
|
||||
|
||||
The dashboard has been rebuilt with a mobile-first design language.
|
||||
|
||||
### Mobile
|
||||
- **Greeting header** — "Good morning, {username}" with notification bell and avatar
|
||||
- **Spotlight hero card** — the next upcoming or ongoing trip as a full-width hero with cover image, progress bar (for live trips), stats grid, and frosted-glass action buttons
|
||||
- **Quick Actions** — New Trip, Currency Converter, Timezone as icon cards
|
||||
- **Trip cards** — cover image with title overlay, status badge (In X days / Starts today / Ongoing / Completed), bottom stats (starts, duration, places, buddies)
|
||||
|
||||
### Desktop
|
||||
- **Unified header toolbar** — the dashboard, planner, vacay, and journey now share the same toolbar style
|
||||
- **Unified card design** — desktop grid cards now match the mobile card style (cover + title overlay + stats)
|
||||
- **Hero card** — SpotlightCard with progress bar for ongoing trips, countdown for upcoming, stats grid
|
||||
- **Hover actions** — edit/copy/archive/delete buttons appear on hover as frosted-glass icons
|
||||
- **Status badges** — CircleCheck icon for completed trips, Clock for upcoming, pulsing dot for ongoing
|
||||
|
||||
### Both
|
||||
- **BottomNav profile sheet** — slide-up sheet with user info, settings, admin, and logout
|
||||
- **Dark mode** — full dark mode support across all new components
|
||||
- **Shared PageSidebar** — Settings and Admin pages share a single sidebar component for layout consistency
|
||||
|
||||
---
|
||||
|
||||
## PWA Offline Mode
|
||||
|
||||
TREK now works offline as a Progressive Web App with full data synchronization.
|
||||
|
||||
- **IndexedDB (Dexie) storage** — trips, places, assignments, categories, tags, accommodations, reservations, budget items, packing items, files, and trip members cached locally
|
||||
- **Offline mutation queue** — changes made offline are queued with monotonic timestamps and replayed on reconnect (FIFO)
|
||||
- **Offline dashboard** — trip list loaded from Dexie when network is unavailable
|
||||
- **Offline trip planner** — full planner functionality with cached data
|
||||
- **Repo layer** — all data access routed through repository layer that falls back to offline storage
|
||||
- **Offline banner** — visible indicator with safe-area-inset support for iOS PWA
|
||||
- **Idempotency keys** — prevents duplicate mutations on replay, scoped by `(key, user_id, method, path)` so the same key on different endpoints can't leak cached bodies
|
||||
- **Offline document downloads** — document downloads work from the PWA cache when the network is unavailable
|
||||
|
||||
---
|
||||
|
||||
## Transport Reservations: Multi-Day + Map Visualization
|
||||
|
||||
- **Multi-day transport reservations** — flights, trains, cruises, car rentals can span multiple days with a dedicated modal and automatic route segmentation across the affected days (#384, #587)
|
||||
- **Map visualization** — transport endpoints render on both Leaflet and Mapbox GL maps as clickable badges with IATA codes, great-circle arcs for flights/cruises, straight lines for trains/cars, and a rotating mid-arc stats label (IATA → IATA · distance · duration) on flights
|
||||
- **Per-booking route toggle** — each booking in DayPlanSidebar has a "Show booking routes" button; connections only render when toggled on
|
||||
- **Check-in time ranges** — hotel bookings now support a check-in window (e.g. "15:00 -- 22:00") with a new `check_in_end` field (#366)
|
||||
- **Cascaded delete** — deleting a reservation now cleans up related budget items, file links, and trip_items
|
||||
|
||||
---
|
||||
|
||||
## Reservations Redesign
|
||||
|
||||
The reservations panel has been completely redesigned with a modern, unified layout.
|
||||
|
||||
- **Unified toolbar** — title, type filter pills with count badges, and add button in one row with muted background
|
||||
- **Type filters** — multi-select filter buttons (Flight, Hotel, Restaurant, etc.) with per-type count badges, persisted in sessionStorage
|
||||
- **Responsive grid** — auto-fill layout with max 3 columns that fills full width
|
||||
- **Card redesign** — status + type badge in header, labeled fields in rounded boxes, hover shadow
|
||||
- **Mobile responsive** — filters hidden on mobile, booking code on separate row, weekday hidden in dates, reduced padding
|
||||
|
||||
---
|
||||
|
||||
## Apple Wallet pkpass Support
|
||||
|
||||
- **.pkpass MIME type** — server correctly serves `application/vnd.apple.pkpass` with the right Content-Type
|
||||
- **Upload + download** — .pkpass files can be attached to bookings or places and opened directly in Apple Wallet on iOS
|
||||
|
||||
---
|
||||
|
||||
## Todo Due-Date Reminders
|
||||
|
||||
- **Scheduler** — a new background scheduler scans todos with upcoming due dates and sends one reminder per item (default lead: 3 days)
|
||||
- **No spam** — `todo_items.reminded_at` prevents re-sending a reminder for the same item on subsequent scheduler runs
|
||||
- **Notification channel aware** — reminders respect the user's notification channel preferences (email, webhook, ntfy)
|
||||
|
||||
---
|
||||
|
||||
## Collab Sub-Feature Toggles
|
||||
|
||||
Individual collab sections can now be toggled on/off from the admin addons page (#604).
|
||||
|
||||
- **Admin UI** — sub-toggles for Chat, Notes, Polls, and What's Next under the Collab addon, with icons matching the collab panel tabs
|
||||
- **Dynamic desktop layout** — Chat always stays at fixed 380px width; remaining active panels share space equally
|
||||
- **Mobile** — disabled tabs are hidden from the tab bar
|
||||
- **API** — GET/PUT /admin/collab-features endpoints stored in app_settings
|
||||
|
||||
---
|
||||
|
||||
## Place Import: KMZ/KML + Naver Maps + Selective GPX
|
||||
|
||||
Three ways to import places into your trips.
|
||||
|
||||
### KMZ/KML Import
|
||||
- **Unified file import modal** — drag-and-drop or file picker for KML, KMZ, and GPX files
|
||||
- **KMZ unpacking** — extracts KML from ZIP archive with 50MB decompressed size limit
|
||||
- **Folder-to-category mapping** — KML folders are automatically matched to TREK categories
|
||||
- **Place deduplication** — skips places that already exist in the trip (by name + coordinates)
|
||||
|
||||
### Naver Maps List Import
|
||||
- **Always enabled** — no longer requires addon toggle, available alongside Google Maps list import
|
||||
- **Shortlink resolution** — resolves naver.me shortlinks to full list URLs
|
||||
- **Pagination support** — handles large Naver Maps lists with automatic pagination
|
||||
|
||||
### Selective GPX/KML Element Import
|
||||
- **Pick what to import** — import modal now lets you choose individual waypoints / tracks / folders instead of an all-or-nothing dump
|
||||
- **Performance** — larger files (thousands of points) parse and render without freezing the UI
|
||||
|
||||
---
|
||||
|
||||
## Search Autocomplete
|
||||
|
||||
- **Real-time suggestions** — autocomplete suggestions appear as you type in the place search field
|
||||
- **Google Places API** — primary autocomplete provider with location bias
|
||||
- **Nominatim fallback** — free fallback when Google API key is not configured
|
||||
- **Bounding box bias** — search results biased to the current map viewport
|
||||
|
||||
---
|
||||
|
||||
## ntfy Notification Channel
|
||||
|
||||
- **ntfy as first-class channel** — push notifications via any ntfy server (self-hosted or ntfy.sh)
|
||||
- **Admin configuration** — server URL and topic configuration in admin panel with clear token button
|
||||
- **Per-user opt-in** — users can enable/disable ntfy in their notification preferences
|
||||
- **Full i18n** — ntfy strings translated in all 15 languages
|
||||
|
||||
---
|
||||
|
||||
## Login & Language
|
||||
|
||||
- **Language dropdown on login page** — users can select their preferred language before logging in
|
||||
- **Browser auto-detection** — language is automatically detected from browser settings on first visit
|
||||
- **DEFAULT_LANGUAGE env var** — configurable default language for the instance, documented across all deployment configs (Docker, Helm, Synology)
|
||||
|
||||
---
|
||||
|
||||
## Granular Auth Toggles
|
||||
|
||||
- **OIDC_ONLY replaced** — split into `DISABLE_LOCAL_LOGIN`, `DISABLE_LOCAL_REGISTRATION`, and `DISABLE_PASSWORD_CHANGE` for fine-grained control over authentication methods
|
||||
- Allows mixed setups (e.g., OIDC + local admin account, or OIDC-only with no local registration)
|
||||
|
||||
---
|
||||
|
||||
## Synology Photos: OTP, SSL Skip & Session Management
|
||||
|
||||
- **OTP support** — one-time password field for 2FA-enabled Synology NAS
|
||||
- **Skip SSL verification** — toggle for self-signed certificates
|
||||
- **Device ID persistence** — prevents repeated 2FA prompts
|
||||
- **Session-cleared notification** — routed through unified notification system
|
||||
- **Provider URL hint** — contextual help text for Synology URL format
|
||||
- **Thumbnail size bump** — default thumbnail size raised from `sm` (240 px) to `m` (320 px) so grids no longer look pixelated on retina
|
||||
- **Passphrase support** — shared-album links with passphrases work from the browse UI (#689)
|
||||
|
||||
---
|
||||
|
||||
## Atlas Improvements
|
||||
|
||||
- **Scoped region matching** — region name matching is now scoped by country to prevent cross-country false matches
|
||||
- **Expanded country lookup tables** — more countries and regions recognized correctly, including A3 fallback for invalid ISO_A2 codes
|
||||
- **Nominatim rate limiting** — shared throttle prevents 429 errors, background region fill, fetch timeout
|
||||
- **Stadia Maps fix** — resolved 401 errors on journey and atlas maps
|
||||
|
||||
---
|
||||
|
||||
## i18n: Full 15-Language Coverage
|
||||
|
||||
- **Indonesian added** — complete translation with full parity to English, bringing the total to 15 languages (EN, DE, FR, ES, IT, NL, PL, RU, ZH, ZH-TW, BR, CS, HU, AR, ID)
|
||||
- **Comprehensive audit** — every key translated natively, no English fallbacks
|
||||
- **OAuth scope labels** — all 24 scopes have localized names and descriptions
|
||||
- **Journey addon** — complete coverage for all journal, editor, sharing, and PDF export strings
|
||||
- **Mapbox GL settings** — localized labels for renderer toggle, style picker, 3D / quality switches
|
||||
- **Ellipsis standardization** — all ellipsis characters normalized to three dots (...)
|
||||
|
||||
---
|
||||
|
||||
## Vacay Improvements
|
||||
|
||||
- **Trip indicator dots** — small blue dots on calendar days where trips are scheduled
|
||||
- **Configurable week start** — choose Monday or Sunday as first day of the week (#224)
|
||||
- **Holiday overlap** — vacations can now be placed on public holidays
|
||||
- **Today marker** — visual indicator for the current day in the calendar
|
||||
- **Unified toolbar** — same header style as planner/dashboard/journey
|
||||
- **Bottom padding fix** — toolbar no longer overlaps the last row (#533)
|
||||
|
||||
---
|
||||
|
||||
## iCal Export Improvements
|
||||
|
||||
- **Day activities and notes** — iCal export now includes daily activities and notes, not just the trip dates (#375)
|
||||
|
||||
---
|
||||
|
||||
## Budget Improvements
|
||||
|
||||
- **Drag-and-drop reorder** — budget categories and individual items can be reordered via drag-and-drop (#479)
|
||||
- **Category legend redesign** — prevents overflow on small screens (#564)
|
||||
- **Comma decimal support** — pasting numbers with comma separators works correctly
|
||||
- **Table alignment fix** — budget data rows and the "New Entry" row now share column widths (#759)
|
||||
|
||||
---
|
||||
|
||||
## Packing List Improvements
|
||||
|
||||
- **Bulk import + template apply without full reload** — new items appear in place instead of triggering the trip loading screen (#760)
|
||||
- **Reservation link cleanup** — packing items linked to deleted reservations stay in the list without the dangling reference
|
||||
- **Bag tracking** — keep track of which items live in which bag, with optional weight tracking and per-bag totals
|
||||
|
||||
---
|
||||
|
||||
## Planner & UX Improvements
|
||||
|
||||
- **Emil-style polish pass** — consistent transitions/animations across cards, hover states, and drawer sheets; shared components for toolbars and section headers
|
||||
- **Planner drag-and-drop jank fix** — dragging places across days is smooth again on long trips
|
||||
- **Unified toolbar header** — dashboard, planner, vacay, and journey share a single toolbar style for visual consistency
|
||||
- **Places sidebar polish** — filter counts, compact select UI, tooltip component, "No Category" / "Uncategorized" filter (#607)
|
||||
- **Dayplan toolbar polish** — cleaner alignment, weather archive fallback for past trips
|
||||
- **Unplanned filter sync** — unplanned filter properly syncs with map markers (#385)
|
||||
- **Place notes** — notes textarea in place edit form with proper display in inspector (#596)
|
||||
- **Place deduplication** — Google Maps list re-import skips existing places (#543)
|
||||
- **File download button** — all file views now include a download button
|
||||
- **Note modal** — no longer closes on outside click (#480)
|
||||
- **Google Maps links** — use place name + google_place_id for accurate links (#554)
|
||||
- **Packing list menu** — no longer cut off by overflow (#557)
|
||||
- **Trip date change** — preserving day content when date range changes
|
||||
- **PDF export** — render restaurant, event, tour, and other reservation types
|
||||
|
||||
---
|
||||
|
||||
## Admin Panel Improvements
|
||||
|
||||
- **Collab sub-feature toggles** — individual toggles for Chat, Notes, Polls, What's Next
|
||||
- **Photo provider icons** — Immich and Synology Photos SVG brand icons in addon manager
|
||||
- **Bag tracking icon** — Luggage icon for the bag tracking sub-toggle
|
||||
- **Naver List Import** — now always enabled, removed from addon toggles
|
||||
- **Shared PageSidebar** — admin pages use the same sidebar layout as Settings
|
||||
|
||||
---
|
||||
|
||||
## Mobile Improvements
|
||||
|
||||
- **Bottom nav fix** — prevent clipping of scrollable content and dialogs
|
||||
- **Journey mobile** — compact add-entry button, scrollable settings dialog, iOS PWA fixes, drop hero / inline tab-bar, eager map tiles, trimmed picker labels
|
||||
- **Dashboard mobile** — spotlight trip in hero, smaller badges, check icon for completed
|
||||
- **Bottom nav dark mode** — consistent dark mode styling
|
||||
- **Safe area support** — proper insets for iOS PWA
|
||||
|
||||
---
|
||||
|
||||
## Documentation & Wiki
|
||||
|
||||
- **Full GitHub Wiki** — 74 pages covering setup, deployment, addon docs, troubleshooting, API reference, and MCP
|
||||
- **CI sync workflow** — `./wiki/**` in the main repo is auto-synced to the GitHub Wiki on push to `main`
|
||||
- **README redesign** — Apple-style hero with animated video, feature tiles, and a screenshot gallery; hero video hosted externally so the repo stays lightweight
|
||||
- **MCP compound tools doc** — `MCP.md` documents the compound / multi-step tools
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
Fifth-pass internal audit. Critical + High + Medium findings addressed in one bundled PR:
|
||||
|
||||
- **JWT password_version gate** — a single `verifyJwtAndLoadUser` helper is now used by every auth surface (web session, MCP bearer, file download token, photo route, MFA policy). A password reset bumps `password_version` and invalidates every outstanding session/token for the user in one shot.
|
||||
- **MFA policy via cookie** — `require_mfa` now applies to cookie-authenticated SPA sessions too (previously only the `Authorization` header was checked, so the whole SPA bypassed it).
|
||||
- **OIDC id_token verification** — full JWKS-based signature verification (iss, aud, exp, nbf) plus `userinfo.sub == id_token.sub` cross-check. `kid` match is strict — no fallback to an arbitrary key.
|
||||
- **OIDC invite redemption** — invite-token increment and user INSERT run in a single `db.transaction`; concurrent callbacks cannot double-redeem a single-use invite.
|
||||
- **OAuth 2.1 DCR** — redirect_uri allowlist rejects `javascript:` / `data:` / `vbscript:` / `file:` / `blob:` / `about:` / `chrome:` and requires private-use schemes to be reverse-DNS (RFC 8252 §7.1).
|
||||
- **OAuth audience binding** — `audience` defaults to the MCP endpoint when no `resource` parameter is sent, so new tokens always carry the correct audience claim.
|
||||
- **HSTS on in production** — `NODE_ENV=production` is enough to enable HSTS (previously required `FORCE_HTTPS=true`). `includeSubDomains` stays off by default to avoid breaking apex-domain setups; opt in with `HSTS_INCLUDE_SUBDOMAINS=true`.
|
||||
- **Cookie Secure behind proxies** — `trek_session` Secure flag is now derived from `req.secure` (Express's `trust proxy`-aware field), so instances behind Traefik / Caddy / Cloudflare Tunnel get Secure cookies without `FORCE_HTTPS`.
|
||||
- **Share-token expiry** — public share tokens default to 90-day TTL. Existing tokens stay NULL (no expiry) so already-distributed links keep working.
|
||||
- **Photo route scoping** — share tokens can only unlock photos that belong to the same trip as the token.
|
||||
- **Bcrypt MFA backup codes** — backup codes are now bcrypt-hashed at rest. Legacy SHA-256 codes keep working until the user regenerates.
|
||||
- **Demo-mode guards** — single `DEMO_EMAILS` registry fixes the drift where `demoUploadBlock` only matched the pre-rename `demo@nomad.app` string.
|
||||
- **Filesystem safety** — `permanentDeleteFile` / `emptyTrash` / avatar cleanup use async `fs.promises.rm({ force: true })` and only drop the DB row when the on-disk unlink actually succeeded.
|
||||
- **Idempotency store hardening** — key length capped at 128 chars, response bodies over 256 KiB not cached, primary key widened to `(key, user_id, method, path)` so the same key on a different endpoint does not replay an unrelated response.
|
||||
- **Permissions cache invalidation** — `restoreFromZip` now drops the permissions cache after a DB swap.
|
||||
- **Reset-URL source** — password-reset email URL is built from server-side `APP_URL` / `ALLOWED_ORIGINS`, never from request headers.
|
||||
- **Critical DB indexes** — added `trips(user_id)`, `trips(created_at DESC)`, `photos(day_id/place_id)`, `reservations(day_id)`, `share_tokens(token)` and conditional `day_accommodations` / `notifications` indexes.
|
||||
|
||||
Upstream CVEs patched:
|
||||
|
||||
- **hono** 4.12.9 to 4.12.12 — directory traversal (CVE-2026-39407, CVE-2026-39408), HTTP response splitting, improper input validation (CVE-2026-39410), IP restriction bypass (CVE-2026-39409)
|
||||
- **@hono/node-server** 1.19.11 to 1.19.13 — directory traversal (CVE-2026-39406)
|
||||
- **nodemailer** 8.0.4 to 8.0.5 — CRLF injection
|
||||
|
||||
---
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed OIDC-only mode login/logout loop (#491)
|
||||
- Fixed dayplan duplicate reservation display, date off-by-one, and missing day_id on edit
|
||||
- Fixed booking date handling and file auth bugs
|
||||
- Fixed dayplan time-based auto-sort for places and free reorder for untimed
|
||||
- Fixed streaming response end on client disconnect during asset pipe
|
||||
- Fixed per-day transport positions for multi-day reservations
|
||||
- Fixed stale budget category reset when category no longer exists
|
||||
- Fixed trip redirect to plan tab when active tab addon is disabled
|
||||
- Fixed reservation price/budget field visibility when budget addon disabled
|
||||
- Fixed HEIC photo rendering on non-Safari browsers
|
||||
- Fixed CSP path matching for paths ending in /
|
||||
- Fixed avatar URLs in notifications, admin panel, and budget
|
||||
- Fixed budget member avatars lost after updating item fields
|
||||
- Fixed budget table column alignment broken by `display: flex` on `<td>` (#759)
|
||||
- Fixed collab notes line break preservation (#608)
|
||||
- Fixed weather archive date handling for future trips (#599)
|
||||
- Fixed duplicate skeleton entries for multi-day places (#606)
|
||||
- Fixed ghost Gallery / `[Trip Photos]` entries in journal timeline and public share (#764)
|
||||
- Fixed journey reorder arrows rendering on skeleton suggestions (#763)
|
||||
- Fixed journey map OSM tile warning (#627)
|
||||
- Fixed journey gallery picker grid collapse on Safari (#717)
|
||||
- Fixed content divider placement in journal entries (#624)
|
||||
- Fixed local photos wrong provider label (#625)
|
||||
- Fixed Synology pagination and album scroll leak (#644)
|
||||
- Fixed Stadia Maps 401 on journey and atlas maps (#640)
|
||||
- Fixed Nominatim User-Agent and error diagnostics
|
||||
- Fixed map tooltips, journey creation, and contributor avatars
|
||||
- Fixed notifications SMTP error surfacing, webhook button label, backup timestamp (#537)
|
||||
- Fixed stale accommodation_id on reservation update (#522)
|
||||
- Fixed hardcoded Immich in toast — now uses provider_name
|
||||
- Fixed MCP safeBroadcast recursive call bug
|
||||
- Fixed MCP Zod v4 `z.record()` API compatibility in transport tool schemas
|
||||
- Fixed Vite module preload polyfill CSP inline script violation
|
||||
- Fixed PWA offline session redirect and file download auth (#505, #541)
|
||||
- Fixed `FORCE_HTTPS` redirect applying to `/api/health`, breaking container health-checks
|
||||
- Fixed journey bugs reported by @roel-de-vries (#722–#736)
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- **Prerelease workflow** — automated prerelease pipeline with major version support, version propagation, and race/orphan tag protection
|
||||
- **Helm chart** — moved to `charts/trek/`, published via helm-publisher action to `gh-pages`, `appVersion` used as default image tag
|
||||
- **Docker** — workflow improvements, tag management cleanup, `server/data/airports.json` properly included in image after assets refactor
|
||||
- **CI** — contributor workflow automation, `npm audit` removal from install steps, manual trigger for prerelease, client test job added alongside server tests with split coverage artifacts
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- **Backend** — expanded to ~87% coverage with comprehensive tests for OAuth, MCP tools, addon gating, services, and session management
|
||||
- **Frontend** — expanded to ~82% coverage with tests for dashboard, planner, settings, admin panels, and component interactions
|
||||
- **Journey** — 89.5% new code coverage
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed to this release:
|
||||
|
||||
- @mauriceboe
|
||||
- @jubnl
|
||||
- @gravitysc
|
||||
- @luojiyin1987
|
||||
- @marco783
|
||||
- @isaiastavares
|
||||
- @tiquis0290
|
||||
- @xenocent
|
||||
- @gfrcsd
|
||||
- @roel-de-vries
|
||||
|
||||
---
|
||||
|
||||
## Stats
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Commits | 500+ |
|
||||
| Merged PRs | 130+ |
|
||||
| Files changed | 700+ |
|
||||
| Lines added | 120,000+ |
|
||||
| Contributors | 12+ |
|
||||
|
||||
---
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
docker pull mauriceboe/trek:3.0.0
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Migrations run automatically on startup. No manual steps required.
|
||||
|
||||
**Checklist:**
|
||||
1. Update your Immich API key to include `asset.upload` (optional, only needed for Journey upload sync)
|
||||
2. If using `OIDC_ONLY`, migrate to `DISABLE_LOCAL_LOGIN` + `DISABLE_LOCAL_REGISTRATION`
|
||||
3. Enable the Journey addon in Settings > Addons to start using the travel journal
|
||||
4. Try the Mapbox GL renderer in Settings > Map if you want 3D terrain and a proper globe view (requires a free Mapbox access token)
|
||||
@@ -1,405 +0,0 @@
|
||||
|
||||
<img width="5292" height="1404" alt="Release 2 9 0 (2)" src="https://github.com/user-attachments/assets/6ff67226-3535-444e-991f-0bc0352e22e7" />
|
||||
|
||||
# TREK 3.0.0
|
||||
|
||||
> **This is the biggest TREK release to date.** Journey turns your trips into rich travel journals. MCP gets full OAuth 2.1 security. The dashboard has been redesigned for mobile-first. And every corner of the app now speaks 15 languages natively.
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### Photos moved from Trip Planner to Journey
|
||||
|
||||
In previous versions, Immich and Synology Photos were integrated directly into the Trip Planner via a "Photos" tab. **This tab has been removed.** Photos are now part of the new **Journey addon**, which is purpose-built for documenting your travels with stories, photos, and maps.
|
||||
|
||||
**What this means for you:**
|
||||
- **No photos are lost.** The previous integration was read-only — TREK never uploaded to or deleted from your Immich/Synology library. Your photos remain untouched in your photo provider.
|
||||
- **Previously linked trip photos are no longer displayed in the Trip Planner.** To view and organize your travel photos, enable the Journey addon (Settings > Addons) and create a Journey linked to your trip.
|
||||
- **Journey brings a much richer photo experience:** upload photos directly to TREK, browse and import from Immich/Synology with duplicate detection, reorder photos, view EXIF metadata, and export everything as a PDF photo book.
|
||||
|
||||
### New Immich API Key Permissions Required
|
||||
|
||||
Journey introduces **photo upload sync** — when you upload a photo to a Journey entry, TREK can optionally sync it to your Immich library. This requires an additional Immich API permission that was not needed before.
|
||||
|
||||
**Previous versions required:**
|
||||
| Permission | Used for |
|
||||
|---|---|
|
||||
| `user.read` | Connection test |
|
||||
| `asset.read` | Browse photos by date, search |
|
||||
| `asset.view` | Stream thumbnails |
|
||||
| `asset.download` | Stream originals |
|
||||
| `album.read` | List and browse albums |
|
||||
| `timeline.read` | Browse timeline buckets |
|
||||
|
||||
**New in 3.0.0 — additionally required:**
|
||||
| Permission | Used for |
|
||||
|---|---|
|
||||
| `asset.upload` | Sync uploaded Journey photos to Immich |
|
||||
|
||||
> **How to update your Immich API key:** Go to your Immich instance > User Settings > API Keys. Edit your existing TREK key (or create a new one) and ensure `asset.upload` is enabled in addition to the existing permissions. If you don't plan to use Journey's upload sync, the old key will continue to work — the upload simply won't sync to Immich.
|
||||
|
||||
**No changes needed for Synology Photos** — Synology uses session-based authentication which inherits the user's full permissions.
|
||||
|
||||
### OIDC_ONLY deprecated
|
||||
|
||||
The `OIDC_ONLY` environment variable is deprecated. Replace with `DISABLE_LOCAL_LOGIN=true` + `DISABLE_LOCAL_REGISTRATION=true` for equivalent behavior. The old variable still works but will be removed in a future release.
|
||||
|
||||
---
|
||||
<img width="5292" height="1404" alt="Release 2 9 0 (3)" src="https://github.com/user-attachments/assets/76976c02-dd81-49ab-83f5-e2221d6b018b" />
|
||||
|
||||
## Journey Addon — Travel Journal
|
||||
|
||||
The headline feature of 3.0.0. Journey is a new global addon that transforms your trips into magazine-style travel stories.
|
||||
|
||||
### Core
|
||||
- **5-table schema** — journeys, entries, photos, trips, contributors with full relational integrity
|
||||
- **Trip-to-Journey sync engine** — link one or more trips to a journey; skeleton entries and photos are synced automatically
|
||||
- **Timeline, Gallery, and Map views** — browse entries chronologically, as a photo grid, or on an interactive map with SVG pin markers
|
||||
- **Entry editor** — markdown toolbar, custom date picker, location search (Nominatim/Google Maps), mood (Amazing/Good/Neutral/Rough), weather (Sunny to Snowy), and Pros & Cons sections
|
||||
|
||||
### Photos
|
||||
- **Immich & Synology browser** — browse by trip dates, custom range, or album with duplicate detection
|
||||
- **Photo upload** — direct upload with drag-and-drop, reorder (Make 1st), and delete
|
||||
- **EXIF metadata** — displayed in lightbox for Immich photos
|
||||
- **Thumbnail to original fallback** — seamless resolution upgrade everywhere
|
||||
- **HEIC rendering fix** — serve fullsize thumbnail for original to fix HEIC rendering on non-Safari browsers
|
||||
- **Contributor photo access** — invited contributors can view all journey photos even without their own Immich/Synology connection (owner credentials are used for the proxy)
|
||||
|
||||
### Sharing & Export
|
||||
- **Public share links** — token-based access with language picker, no login required
|
||||
- **Public photo proxy** — validates share token instead of auth for photo streaming
|
||||
- **PDF photo book export** — Polarsteps-inspired layout with cover, day chapters, photo grids, and stories
|
||||
|
||||
### Collaboration
|
||||
- **Contributors** — invite users as editors or viewers
|
||||
- **Trip linking/unlinking** — manage synced trips from Journey Settings and Desktop Sidebar
|
||||
- **Cover image** — upload or pick from journey photos
|
||||
|
||||
### Frontend
|
||||
- **JourneyPage** — frontpage with hero card, active journey stats, trip suggestions ("Trip just ended — turn it into a Journey")
|
||||
- **JourneyDetailPage** — full timeline/gallery/map with inline entry editing
|
||||
- **JourneyPublicPage** — public share view with language picker and read-only timeline
|
||||
|
||||
---
|
||||
|
||||
## MCP: OAuth 2.1 & Granular Scopes
|
||||
|
||||
MCP authentication has been completely rebuilt around the OAuth 2.1 specification.
|
||||
|
||||
- **OAuth 2.1 authorization server** — full PKCE flow with authorization codes, access tokens, refresh tokens, and token rotation with replay detection
|
||||
- **Granular scopes** — 24 scopes across 11 groups (trips, places, atlas, packing, todos, budget, reservations, collab, notifications, vacay, geo/weather) with per-scope read/write/delete control
|
||||
- **Dynamic Client Registration (DCR)** — RFC 7591 endpoint at POST /oauth/register for browser-initiated and public clients
|
||||
- **Consent screen** — user-facing scope selection with grouped permission display
|
||||
- **Admin panel** — OAuth sessions management in MCP Access panel with collapsible scope lists
|
||||
- **Per-client rate limiting** — configurable rate limits per OAuth client
|
||||
- **Addon gating** — MCP tools are only registered when their corresponding addon is enabled
|
||||
- **Static token deprecation** — existing MCP tokens still work but surface deprecation notices; migration path to OAuth is documented
|
||||
- **Security hardening** — Critical + High + Medium findings addressed (token storage, PKCE enforcement, scope validation)
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Redesign
|
||||
|
||||
The dashboard has been rebuilt with a mobile-first design language.
|
||||
|
||||
### Mobile
|
||||
- **Greeting header** — "Good morning, {username}" with notification bell and avatar
|
||||
- **Spotlight hero card** — the next upcoming or ongoing trip as a full-width hero with cover image, progress bar (for live trips), stats grid, and frosted-glass action buttons
|
||||
- **Quick Actions** — New Trip, Currency Converter, Timezone as icon cards
|
||||
- **Trip cards** — cover image with title overlay, status badge (In X days / Starts today / Ongoing / Completed), bottom stats (starts, duration, places, buddies)
|
||||
|
||||
### Desktop
|
||||
- **Unified card design** — desktop grid cards now match the mobile card style (cover + title overlay + stats)
|
||||
- **Hero card** — SpotlightCard with progress bar for ongoing trips, countdown for upcoming, stats grid
|
||||
- **Hover actions** — edit/copy/archive/delete buttons appear on hover as frosted-glass icons
|
||||
- **Status badges** — CircleCheck icon for completed trips, Clock for upcoming, pulsing dot for ongoing
|
||||
|
||||
### Both
|
||||
- **BottomNav profile sheet** — slide-up sheet with user info, settings, admin, and logout
|
||||
- **Dark mode** — full dark mode support across all new components
|
||||
|
||||
---
|
||||
|
||||
## PWA Offline Mode
|
||||
|
||||
TREK now works offline as a Progressive Web App with full data synchronization.
|
||||
|
||||
- **IndexedDB (Dexie) storage** — trips, places, assignments, categories, tags, accommodations, reservations, budget items, packing items, files, and trip members cached locally
|
||||
- **Offline mutation queue** — changes made offline are queued with monotonic timestamps and replayed on reconnect (FIFO)
|
||||
- **Offline dashboard** — trip list loaded from Dexie when network is unavailable
|
||||
- **Offline trip planner** — full planner functionality with cached data
|
||||
- **Repo layer** — all data access routed through repository layer that falls back to offline storage
|
||||
- **Offline banner** — visible indicator with safe-area-inset support for iOS PWA
|
||||
- **Idempotency keys** — prevents duplicate mutations on replay (Migration 100)
|
||||
|
||||
---
|
||||
|
||||
## Reservations Redesign
|
||||
|
||||
The reservations panel has been completely redesigned with a modern, unified layout.
|
||||
|
||||
- **Unified toolbar** — title, type filter pills with count badges, and add button in one row with muted background
|
||||
- **Type filters** — multi-select filter buttons (Flight, Hotel, Restaurant, etc.) with per-type count badges, persisted in sessionStorage
|
||||
- **Responsive grid** — auto-fill layout with max 3 columns that fills full width
|
||||
- **Card redesign** — status + type badge in header, labeled fields in rounded boxes, hover shadow
|
||||
- **Check-in time ranges** — hotel bookings now support a check-in window (e.g. "15:00 -- 22:00") with a new check_in_end field (#366)
|
||||
- **Mobile responsive** — filters hidden on mobile, booking code on separate row, weekday hidden in dates, reduced padding
|
||||
|
||||
---
|
||||
|
||||
## Collab Sub-Feature Toggles
|
||||
|
||||
Individual collab sections can now be toggled on/off from the admin addons page (#604).
|
||||
|
||||
- **Admin UI** — sub-toggles for Chat, Notes, Polls, and What's Next under the Collab addon, with icons matching the collab panel tabs
|
||||
- **Dynamic desktop layout** — Chat always stays at fixed 380px width; remaining active panels share space equally
|
||||
- **Mobile** — disabled tabs are hidden from the tab bar
|
||||
- **API** — GET/PUT /admin/collab-features endpoints stored in app_settings
|
||||
|
||||
---
|
||||
|
||||
## Place Import: KMZ/KML & Naver Maps
|
||||
|
||||
Two new ways to import places into your trips.
|
||||
|
||||
### KMZ/KML Import
|
||||
- **Unified file import modal** — drag-and-drop or file picker for KML, KMZ, and GPX files
|
||||
- **KMZ unpacking** — extracts KML from ZIP archive with 50MB decompressed size limit
|
||||
- **Folder-to-category mapping** — KML folders are automatically matched to TREK categories
|
||||
- **Place deduplication** — skips places that already exist in the trip (by name + coordinates)
|
||||
|
||||
### Naver Maps List Import
|
||||
- **Always enabled** — no longer requires addon toggle, available alongside Google Maps list import
|
||||
- **Shortlink resolution** — resolves naver.me shortlinks to full list URLs
|
||||
- **Pagination support** — handles large Naver Maps lists with automatic pagination
|
||||
|
||||
---
|
||||
|
||||
## Search Autocomplete
|
||||
|
||||
- **Real-time suggestions** — autocomplete suggestions appear as you type in the place search field
|
||||
- **Google Places API** — primary autocomplete provider with location bias
|
||||
- **Nominatim fallback** — free fallback when Google API key is not configured
|
||||
- **Bounding box bias** — search results biased to the current map viewport
|
||||
|
||||
---
|
||||
|
||||
## ntfy Notification Channel
|
||||
|
||||
- **ntfy as first-class channel** — push notifications via any ntfy server (self-hosted or ntfy.sh)
|
||||
- **Admin configuration** — server URL and topic configuration in admin panel with clear token button
|
||||
- **Per-user opt-in** — users can enable/disable ntfy in their notification preferences
|
||||
- **Full i18n** — ntfy strings translated in all 15 languages
|
||||
|
||||
---
|
||||
|
||||
## Login & Language
|
||||
|
||||
- **Language dropdown on login page** — users can select their preferred language before logging in
|
||||
- **Browser auto-detection** — language is automatically detected from browser settings on first visit
|
||||
- **DEFAULT_LANGUAGE env var** — configurable default language for the instance, documented across all deployment configs (Docker, Helm, Synology)
|
||||
|
||||
---
|
||||
|
||||
## Granular Auth Toggles
|
||||
|
||||
- **OIDC_ONLY replaced** — split into DISABLE_LOCAL_LOGIN, DISABLE_LOCAL_REGISTRATION, and DISABLE_PASSWORD_CHANGE for fine-grained control over authentication methods
|
||||
- Allows mixed setups (e.g., OIDC + local admin account, or OIDC-only with no local registration)
|
||||
|
||||
---
|
||||
|
||||
## Synology Photos: OTP, SSL Skip & Session Management
|
||||
|
||||
- **OTP support** — one-time password field for 2FA-enabled Synology NAS
|
||||
- **Skip SSL verification** — toggle for self-signed certificates
|
||||
- **Device ID persistence** — prevents repeated 2FA prompts
|
||||
- **Session-cleared notification** — routed through unified notification system
|
||||
- **Provider URL hint** — contextual help text for Synology URL format
|
||||
|
||||
---
|
||||
|
||||
## Atlas Improvements
|
||||
|
||||
- **Scoped region matching** — region name matching is now scoped by country to prevent cross-country false matches
|
||||
- **Expanded country lookup tables** — more countries and regions recognized correctly, including A3 fallback for invalid ISO_A2 codes
|
||||
- **Nominatim rate limiting** — shared throttle prevents 429 errors, background region fill, fetch timeout
|
||||
- **Stadia Maps fix** — resolved 401 errors on journey and atlas maps
|
||||
|
||||
---
|
||||
|
||||
## i18n: Full 15-Language Coverage
|
||||
|
||||
- **Indonesian added** — complete translation with full parity to English, bringing the total to 15 languages (EN, DE, FR, ES, IT, NL, PL, RU, ZH, ZH-TW, BR, CS, HU, AR, ID)
|
||||
- **Comprehensive audit** — every key translated natively, no English fallbacks
|
||||
- **OAuth scope labels** — all 24 scopes have localized names and descriptions
|
||||
- **Journey addon** — complete coverage for all journal, editor, sharing, and PDF export strings
|
||||
- **Ellipsis standardization** — all ellipsis characters normalized to three dots (...)
|
||||
|
||||
---
|
||||
|
||||
## Vacay Improvements
|
||||
|
||||
- **Trip indicator dots** — small blue dots on calendar days where trips are scheduled
|
||||
- **Configurable week start** — choose Monday or Sunday as first day of the week (#224)
|
||||
- **Holiday overlap** — vacations can now be placed on public holidays
|
||||
- **Today marker** — visual indicator for the current day in the calendar
|
||||
- **Bottom padding fix** — toolbar no longer overlaps the last row (#533)
|
||||
|
||||
---
|
||||
|
||||
## iCal Export Improvements
|
||||
|
||||
- **Day activities and notes** — iCal export now includes daily activities and notes, not just the trip dates (#375)
|
||||
|
||||
---
|
||||
|
||||
## Budget Improvements
|
||||
|
||||
- **Drag-and-drop reorder** — budget categories and individual items can be reordered via drag-and-drop (#479)
|
||||
- **Category legend redesign** — prevents overflow on small screens (#564)
|
||||
- **Comma decimal support** — pasting numbers with comma separators works correctly
|
||||
|
||||
---
|
||||
|
||||
## Planner & UX Improvements
|
||||
|
||||
- **Collapsible day detail panel** — day detail panel can be collapsed/expanded in the planner
|
||||
- **Uncategorized filter** — "No Category" option in category dropdown to find places without a category (#607)
|
||||
- **Map multi-category filter** — filter syncs with map view for uncategorized places
|
||||
- **Unplanned filter sync** — unplanned filter properly syncs with map markers (#385)
|
||||
- **Place notes** — notes textarea in place edit form with proper display in inspector (#596)
|
||||
- **Place deduplication** — Google Maps list re-import skips existing places (#543)
|
||||
- **File download button** — all file views now include a download button
|
||||
- **Note modal** — no longer closes on outside click (#480)
|
||||
- **Google Maps links** — use place name + google_place_id for accurate links (#554)
|
||||
- **Packing list menu** — no longer cut off by overflow (#557)
|
||||
- **Trip date change** — preserving day content when date range changes
|
||||
- **PDF export** — render restaurant, event, tour, and other reservation types
|
||||
|
||||
---
|
||||
|
||||
## Admin Panel Improvements
|
||||
|
||||
- **Collab sub-feature toggles** — individual toggles for Chat, Notes, Polls, What's Next
|
||||
- **Photo provider icons** — Immich and Synology Photos SVG brand icons in addon manager
|
||||
- **Bag tracking icon** — Luggage icon for the bag tracking sub-toggle
|
||||
- **Naver List Import** — now always enabled, removed from addon toggles
|
||||
|
||||
---
|
||||
|
||||
## Mobile Improvements
|
||||
|
||||
- **Bottom nav fix** — prevent clipping of scrollable content and dialogs
|
||||
- **Journey mobile** — compact add-entry button, scrollable settings dialog, iOS PWA fixes
|
||||
- **Dashboard mobile** — spotlight trip in hero, smaller badges, check icon for completed
|
||||
- **Bottom nav dark mode** — consistent dark mode styling
|
||||
- **Safe area support** — proper insets for iOS PWA
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- **Backend** — expanded to ~87% coverage with comprehensive tests for OAuth, MCP tools, addon gating, services, and session management
|
||||
- **Frontend** — expanded to ~82% coverage with tests for dashboard, planner, settings, admin panels, and component interactions
|
||||
- **Journey** — 89.5% new code coverage
|
||||
- **CI** — client test job added alongside server tests with split coverage artifacts
|
||||
|
||||
---
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed OIDC-only mode login/logout loop (#491)
|
||||
- Fixed dayplan duplicate reservation display, date off-by-one, and missing day_id on edit
|
||||
- Fixed booking date handling and file auth bugs
|
||||
- Fixed dayplan time-based auto-sort for places and free reorder for untimed
|
||||
- Fixed streaming response end on client disconnect during asset pipe
|
||||
- Fixed per-day transport positions for multi-day reservations
|
||||
- Fixed stale budget category reset when category no longer exists
|
||||
- Fixed trip redirect to plan tab when active tab addon is disabled
|
||||
- Fixed reservation price/budget field visibility when budget addon disabled
|
||||
- Fixed HEIC photo rendering on non-Safari browsers
|
||||
- Fixed CSP path matching for paths ending in /
|
||||
- Fixed avatar URLs in notifications, admin panel, and budget
|
||||
- Fixed budget member avatars lost after updating item fields
|
||||
- Fixed collab notes line break preservation (#608)
|
||||
- Fixed weather archive date handling for future trips (#599)
|
||||
- Fixed duplicate skeleton entries for multi-day places (#606)
|
||||
- Fixed ghost Gallery entries in journal timeline and public share
|
||||
- Fixed journey map OSM tile warning (#627)
|
||||
- Fixed content divider placement in journal entries (#624)
|
||||
- Fixed local photos wrong provider label (#625)
|
||||
- Fixed Synology pagination and album scroll leak (#644)
|
||||
- Fixed Stadia Maps 401 on journey and atlas maps (#640)
|
||||
- Fixed Nominatim User-Agent and error diagnostics
|
||||
- Fixed map tooltips, journey creation, and contributor avatars
|
||||
- Fixed notifications SMTP error surfacing, webhook button label, backup timestamp (#537)
|
||||
- Fixed stale accommodation_id on reservation update (#522)
|
||||
- Fixed hardcoded Immich in toast — now uses provider_name
|
||||
- Fixed MCP safeBroadcast recursive call bug
|
||||
- Fixed Vite module preload polyfill CSP inline script violation
|
||||
- Fixed PWA offline session redirect and file download auth (#505, #541)
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- **hono** 4.12.9 to 4.12.12 — fixes directory traversal (CVE-2026-39407, CVE-2026-39408), HTTP response splitting, improper input validation (CVE-2026-39410), and IP restriction bypass (CVE-2026-39409)
|
||||
- **@hono/node-server** 1.19.11 to 1.19.13 — fixes directory traversal (CVE-2026-39406)
|
||||
- **nodemailer** 8.0.4 to 8.0.5 — fixes CRLF injection
|
||||
- **OAuth 2.1 hardening** — token storage, PKCE enforcement, scope intersection validation
|
||||
- **Google Maps regex** — replaced too-permissive regex with safer utility function
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- **Prerelease workflow** — automated prerelease pipeline with major version support, version propagation, and race/orphan tag protection
|
||||
- **Helm chart** — moved to charts/trek/, published via helm-publisher action to gh-pages, appVersion used as default image tag
|
||||
- **Docker** — workflow improvements, tag management cleanup
|
||||
- **CI** — contributor workflow automation, npm audit removal from install steps, manual trigger for prerelease
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed to this release:
|
||||
|
||||
- @mauriceboe
|
||||
- @jubnl
|
||||
- @gravitysc
|
||||
- @luojiyin1987
|
||||
- @marco783
|
||||
- @isaiastavares
|
||||
- @tiquis0290
|
||||
- @xenocent
|
||||
- @gfrcsd
|
||||
|
||||
---
|
||||
|
||||
## Stats
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Commits | 280+ |
|
||||
| Merged PRs | 49 |
|
||||
| Files changed | 500+ |
|
||||
| Lines added | 108,000+ |
|
||||
| Contributors | 12 |
|
||||
|
||||
---
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
docker pull mauriceboe/trek:3.0.0
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Migrations run automatically on startup. No manual steps required.
|
||||
|
||||
**Checklist:**
|
||||
1. Update your Immich API key to include `asset.upload` (optional, only needed for Journey upload sync)
|
||||
2. If using `OIDC_ONLY`, migrate to `DISABLE_LOCAL_LOGIN` + `DISABLE_LOCAL_REGISTRATION`
|
||||
3. Enable the Journey addon in Settings > Addons to start using the travel journal
|
||||
|
||||
+22
-20
@@ -2,31 +2,33 @@ import axios, { AxiosInstance } from 'axios'
|
||||
import type { WeatherResult } from '@trek/shared'
|
||||
import { getSocketId } from './websocket'
|
||||
import { isReachable, probeNow } from '../sync/connectivity'
|
||||
import en from '../i18n/translations/en'
|
||||
import br from '../i18n/translations/br'
|
||||
import de from '../i18n/translations/de'
|
||||
import es from '../i18n/translations/es'
|
||||
import fr from '../i18n/translations/fr'
|
||||
import it from '../i18n/translations/it'
|
||||
import nl from '../i18n/translations/nl'
|
||||
import pl from '../i18n/translations/pl'
|
||||
import cs from '../i18n/translations/cs'
|
||||
import hu from '../i18n/translations/hu'
|
||||
import ru from '../i18n/translations/ru'
|
||||
import zh from '../i18n/translations/zh'
|
||||
import zhTw from '../i18n/translations/zhTw'
|
||||
import ar from '../i18n/translations/ar'
|
||||
|
||||
const rateLimitTranslations: Record<string, Record<string, string | unknown>> = {
|
||||
en, br, de, es, fr, it, nl, pl, cs, hu, ru, zh, 'zh-TW': zhTw, ar,
|
||||
const RATE_LIMIT_MESSAGES: Record<string, string> = {
|
||||
en: 'Too many attempts. Please try again later.',
|
||||
de: 'Zu viele Versuche. Bitte versuchen Sie es später erneut.',
|
||||
es: 'Demasiados intentos. Inténtelo de nuevo más tarde.',
|
||||
fr: 'Trop de tentatives. Veuillez réessayer plus tard.',
|
||||
hu: 'Túl sok próbálkozás. Kérjük, próbálja újra később.',
|
||||
nl: 'Te veel pogingen. Probeer het later opnieuw.',
|
||||
br: 'Muitas tentativas. Tente novamente mais tarde.',
|
||||
cs: 'Příliš mnoho pokusů. Zkuste to prosím znovu.',
|
||||
pl: 'Zbyt wiele prób. Spróbuj ponownie później.',
|
||||
ru: 'Слишком много попыток. Попробуйте позже.',
|
||||
zh: '尝试次数过多,请稍后再试。',
|
||||
'zh-TW': '嘗試次數過多,請稍後再試。',
|
||||
it: 'Troppi tentativi. Riprova più tardi.',
|
||||
tr: 'Çok fazla deneme. Lütfen daha sonra tekrar deneyin.',
|
||||
ar: 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.',
|
||||
id: 'Terlalu banyak percobaan. Coba lagi nanti.',
|
||||
ja: '試行回数が多すぎます。時間をおいて再度お試しください。',
|
||||
ko: '시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.',
|
||||
uk: 'Занадто багато спроб. Спробуйте пізніше.',
|
||||
}
|
||||
|
||||
function translateRateLimit(): string {
|
||||
const fallback = 'Too many attempts. Please try again later.'
|
||||
const fallback = RATE_LIMIT_MESSAGES['en']!
|
||||
try {
|
||||
const lang = localStorage.getItem('app_language') || 'en'
|
||||
const table = rateLimitTranslations[lang] || rateLimitTranslations.en
|
||||
return (table['common.tooManyAttempts'] as string) || (rateLimitTranslations.en['common.tooManyAttempts'] as string) || fallback
|
||||
return RATE_LIMIT_MESSAGES[lang] ?? fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -102,19 +102,19 @@ describe('BottomNav', () => {
|
||||
expect(screen.queryByText('testuser')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BOTTOMNAV-010: Trips label translates when language is fr', () => {
|
||||
it('FE-COMP-BOTTOMNAV-010: Trips label translates when language is fr', async () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'fr' }) });
|
||||
render(<BottomNav />);
|
||||
expect(screen.getByText('Mes voyages')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Mes voyages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BOTTOMNAV-011: Profile label translates when language is fr', () => {
|
||||
it('FE-COMP-BOTTOMNAV-011: Profile label translates when language is fr', async () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'fr' }) });
|
||||
render(<BottomNav />);
|
||||
expect(screen.getByText('Profil')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Profil')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BOTTOMNAV-012: addon labels translate when language is fr', () => {
|
||||
it('FE-COMP-BOTTOMNAV-012: addon labels translate when language is fr', async () => {
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ language: 'fr' }) });
|
||||
seedStore(useAddonStore, {
|
||||
addons: [
|
||||
@@ -124,9 +124,9 @@ describe('BottomNav', () => {
|
||||
],
|
||||
});
|
||||
render(<BottomNav />);
|
||||
expect(screen.getByText('Vacances')).toBeInTheDocument();
|
||||
expect(screen.getByText('Atlas')).toBeInTheDocument();
|
||||
expect(screen.getByText('Journal de voyage')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Vacances')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Atlas')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Journal de voyage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-BOTTOMNAV-013: unknown addon id is not rendered', () => {
|
||||
|
||||
@@ -1,56 +1,47 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, ReactNode } from 'react'
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState, ReactNode } from 'react'
|
||||
import { useSettingsStore } from '../store/settingsStore'
|
||||
import de from './translations/de'
|
||||
import en from './translations/en'
|
||||
import es from './translations/es'
|
||||
import fr from './translations/fr'
|
||||
import hu from './translations/hu'
|
||||
import it from './translations/it'
|
||||
import tr from './translations/tr'
|
||||
import ru from './translations/ru'
|
||||
import zh from './translations/zh'
|
||||
import zhTw from './translations/zhTw'
|
||||
import nl from './translations/nl'
|
||||
import id from './translations/id'
|
||||
import ar from './translations/ar'
|
||||
import br from './translations/br'
|
||||
import cs from './translations/cs'
|
||||
import pl from './translations/pl'
|
||||
import ja from './translations/ja'
|
||||
import ko from './translations/ko'
|
||||
import uk from './translations/uk'
|
||||
import { SUPPORTED_LANGUAGES, SupportedLanguageCode } from './supportedLanguages'
|
||||
import en from '@trek/shared/i18n/en'
|
||||
import type { SupportedLanguageCode } from '@trek/shared'
|
||||
import {
|
||||
SUPPORTED_LANGUAGES,
|
||||
getLocaleForLanguage,
|
||||
getIntlLanguage,
|
||||
isRtlLanguage,
|
||||
} from '@trek/shared'
|
||||
import type { TranslationStrings } from '@trek/shared/i18n'
|
||||
|
||||
export { SUPPORTED_LANGUAGES }
|
||||
|
||||
type TranslationStrings = Record<string, string | { name: string; category: string }[]>
|
||||
|
||||
// Keyed by SupportedLanguageCode so TypeScript enforces all languages have a translation.
|
||||
const translations: Record<SupportedLanguageCode, TranslationStrings> = {
|
||||
de, en, es, fr, hu, it, tr, ru, zh, 'zh-TW': zhTw, nl, id, ar, br, cs, pl, ja, ko, uk,
|
||||
// One explicit dynamic import per locale — Vite code-splits a separate chunk per locale.
|
||||
// Only the active locale is fetched; en is always available synchronously as the fallback.
|
||||
const localeLoaders: Record<SupportedLanguageCode, () => Promise<{ default: TranslationStrings }>> = {
|
||||
en: () => Promise.resolve({ default: en }),
|
||||
de: () => import('@trek/shared/i18n/de'),
|
||||
es: () => import('@trek/shared/i18n/es'),
|
||||
fr: () => import('@trek/shared/i18n/fr'),
|
||||
hu: () => import('@trek/shared/i18n/hu'),
|
||||
it: () => import('@trek/shared/i18n/it'),
|
||||
tr: () => import('@trek/shared/i18n/tr'),
|
||||
ru: () => import('@trek/shared/i18n/ru'),
|
||||
zh: () => import('@trek/shared/i18n/zh'),
|
||||
'zh-TW': () => import('@trek/shared/i18n/zh-TW'),
|
||||
nl: () => import('@trek/shared/i18n/nl'),
|
||||
id: () => import('@trek/shared/i18n/id'),
|
||||
ar: () => import('@trek/shared/i18n/ar'),
|
||||
br: () => import('@trek/shared/i18n/br'),
|
||||
cs: () => import('@trek/shared/i18n/cs'),
|
||||
pl: () => import('@trek/shared/i18n/pl'),
|
||||
ja: () => import('@trek/shared/i18n/ja'),
|
||||
ko: () => import('@trek/shared/i18n/ko'),
|
||||
uk: () => import('@trek/shared/i18n/uk'),
|
||||
}
|
||||
|
||||
// Derived from SUPPORTED_LANGUAGES — add new languages there, not here.
|
||||
const LOCALES: Record<string, string> = Object.fromEntries(
|
||||
SUPPORTED_LANGUAGES.map(l => [l.value, l.locale])
|
||||
)
|
||||
const RTL_LANGUAGES = new Set(['ar'])
|
||||
// Re-export pure helpers that live in shared so downstream consumers can import them
|
||||
// through this module without changing their import path.
|
||||
export { getLocaleForLanguage, getIntlLanguage, isRtlLanguage }
|
||||
|
||||
export function getLocaleForLanguage(language: string): string {
|
||||
return LOCALES[language] || LOCALES.en
|
||||
}
|
||||
|
||||
export function getIntlLanguage(language: string): string {
|
||||
if (language === 'br') return 'pt-BR'
|
||||
return ['de', 'es', 'fr', 'hu', 'it', 'tr', 'ru', 'zh', 'zh-TW', 'nl', 'ar', 'cs', 'pl', 'id', 'ja', 'ko', 'uk'].includes(language) ? language : 'en'
|
||||
}
|
||||
|
||||
export function isRtlLanguage(language: string): boolean {
|
||||
return RTL_LANGUAGES.has(language)
|
||||
}
|
||||
|
||||
// Detects the user's preferred language from the browser/OS settings and maps
|
||||
// it to one of the supported language codes. Returns null if no match is found.
|
||||
// Detects the user's preferred language from browser/OS settings.
|
||||
// Returns null if no supported language matches.
|
||||
export function detectBrowserLanguage(): string | null {
|
||||
if (typeof navigator === 'undefined') return null
|
||||
const browserLangs = navigator.languages?.length
|
||||
@@ -59,17 +50,14 @@ export function detectBrowserLanguage(): string | null {
|
||||
const supported = SUPPORTED_LANGUAGES.map(l => l.value)
|
||||
|
||||
for (const lang of browserLangs) {
|
||||
// Exact match (e.g. 'de', 'zh-TW') — case-insensitive
|
||||
const exactMatch = supported.find(s => s.toLowerCase() === lang.toLowerCase())
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
// pt-BR has no exact match (our code is 'br', not 'pt-BR'), so map it explicitly.
|
||||
// pt-PT and bare 'pt' are NOT mapped — they fall through to null and let the
|
||||
// server default or 'en' fallback apply instead.
|
||||
// pt-BR has no exact match (our code is 'br'), so map it explicitly.
|
||||
// pt-PT and bare 'pt' are NOT mapped — they fall through to null.
|
||||
if (lang.toLowerCase() === 'pt-br') return 'br'
|
||||
|
||||
// Prefix match (e.g. 'de-AT' → 'de', 'zh-CN' → 'zh') — case-insensitive
|
||||
const prefix = lang.split('-')[0].toLowerCase()
|
||||
const prefix = lang.split('-')[0]?.toLowerCase()
|
||||
const prefixMatch = supported.find(s => s.toLowerCase() === prefix)
|
||||
if (prefixMatch) return prefixMatch
|
||||
}
|
||||
@@ -91,18 +79,27 @@ interface TranslationProviderProps {
|
||||
|
||||
export function TranslationProvider({ children }: TranslationProviderProps) {
|
||||
const language = useSettingsStore((s) => s.settings.language) || 'en'
|
||||
const [strings, setStrings] = useState<TranslationStrings>(en)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = language
|
||||
document.documentElement.dir = isRtlLanguage(language) ? 'rtl' : 'ltr'
|
||||
}, [language])
|
||||
|
||||
const value = useMemo((): TranslationContextValue => {
|
||||
const strings = translations[language] || translations.en
|
||||
const fallback = translations.en
|
||||
useEffect(() => {
|
||||
const loader = localeLoaders[language as SupportedLanguageCode]
|
||||
if (!loader) return
|
||||
|
||||
let cancelled = false
|
||||
loader().then(mod => {
|
||||
if (!cancelled) setStrings(mod.default)
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [language])
|
||||
|
||||
const value = useMemo((): TranslationContextValue => {
|
||||
function t(key: string, params?: Record<string, string | number>): string {
|
||||
let val: string = (strings[key] ?? fallback[key] ?? key) as string
|
||||
let val: string = (strings[key] ?? en[key] ?? key) as string
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
val = val.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))
|
||||
@@ -112,7 +109,7 @@ export function TranslationProvider({ children }: TranslationProviderProps) {
|
||||
}
|
||||
|
||||
return { t, language, locale: getLocaleForLanguage(language) }
|
||||
}, [language])
|
||||
}, [strings, language])
|
||||
|
||||
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>
|
||||
}
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
{ value: 'de', label: 'Deutsch', locale: 'de-DE' },
|
||||
{ value: 'en', label: 'English', locale: 'en-US' },
|
||||
{ value: 'es', label: 'Español', locale: 'es-ES' },
|
||||
{ value: 'fr', label: 'Français', locale: 'fr-FR' },
|
||||
{ value: 'hu', label: 'Magyar', locale: 'hu-HU' },
|
||||
{ value: 'nl', label: 'Nederlands', locale: 'nl-NL' },
|
||||
{ value: 'br', label: 'Português (Brasil)', locale: 'pt-BR' },
|
||||
{ value: 'cs', label: 'Česky', locale: 'cs-CZ' },
|
||||
{ value: 'pl', label: 'Polski', locale: 'pl-PL' },
|
||||
{ value: 'ru', label: 'Русский', locale: 'ru-RU' },
|
||||
{ value: 'zh', label: '简体中文', locale: 'zh-CN' },
|
||||
{ value: 'zh-TW', label: '繁體中文', locale: 'zh-TW' },
|
||||
{ value: 'it', label: 'Italiano', locale: 'it-IT' },
|
||||
{ value: 'tr', label: 'Türkçe', locale: 'tr-TR' },
|
||||
{ value: 'ar', label: 'العربية', locale: 'ar-SA' },
|
||||
{ value: 'id', label: 'Bahasa Indonesia', locale: 'id-ID' },
|
||||
{ value: 'ja', label: '日本語', locale: 'ja-JP' },
|
||||
{ value: 'ko', label: '한국어', locale: 'ko-KR' },
|
||||
{ value: 'uk', label: 'Українська', locale: 'uk-UA' },
|
||||
] as const
|
||||
|
||||
export type SupportedLanguageCode = typeof SUPPORTED_LANGUAGES[number]['value']
|
||||
|
||||
export const SUPPORTED_LANGUAGE_CODES: string[] = SUPPORTED_LANGUAGES.map(l => l.value)
|
||||
// Canonical language registry now lives in @trek/shared. Re-exported here so
|
||||
// existing imports of './supportedLanguages' continue to work unchanged.
|
||||
export { SUPPORTED_LANGUAGES, SUPPORTED_LANGUAGE_CODES } from '@trek/shared'
|
||||
export type { SupportedLanguageCode } from '@trek/shared'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Generated
+307
@@ -5971,6 +5971,275 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
|
||||
"integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/type-utils": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.60.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz",
|
||||
"integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
|
||||
"integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.60.0",
|
||||
"@typescript-eslint/types": "^8.60.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
|
||||
"integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz",
|
||||
"integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
|
||||
"integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
|
||||
"integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.60.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/visitor-keys": "8.60.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
|
||||
"integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.60.0",
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
|
||||
"integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.60.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||
@@ -16782,6 +17051,19 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
@@ -17140,6 +17422,30 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.60.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz",
|
||||
"integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.60.0",
|
||||
"@typescript-eslint/parser": "8.60.0",
|
||||
"@typescript-eslint/typescript-estree": "8.60.0",
|
||||
"@typescript-eslint/utils": "8.60.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tz-lookup": {
|
||||
"version": "6.1.25",
|
||||
"resolved": "https://registry.npmjs.org/tz-lookup/-/tz-lookup-6.1.25.tgz",
|
||||
@@ -19485,6 +19791,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Extracts client locale files into per-namespace files under shared/src/i18n/{locale}/.
|
||||
* Run with: npx tsx scripts/migrate-i18n.mts
|
||||
*
|
||||
* Safe to re-run — locale dirs are cleaned first. Hand-authored files
|
||||
* (types.ts, languages.ts, index.ts) in shared/src/i18n/ are never touched.
|
||||
*/
|
||||
import { mkdir, rm, writeFile } from 'fs/promises'
|
||||
import { dirname, join } from 'path'
|
||||
import { fileURLToPath, pathToFileURL } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = join(__dirname, '..')
|
||||
const TRANSLATIONS_DIR = join(ROOT, 'client/src/i18n/translations')
|
||||
const I18N_OUT = join(ROOT, 'shared/src/i18n')
|
||||
|
||||
// Maps locale code → source filename (without .ts) in client/src/i18n/translations/
|
||||
const LOCALE_FILE_MAP: Record<string, string> = {
|
||||
de: 'de', en: 'en', es: 'es', fr: 'fr', hu: 'hu',
|
||||
it: 'it', tr: 'tr', ru: 'ru', zh: 'zh', 'zh-TW': 'zhTw',
|
||||
nl: 'nl', id: 'id', ar: 'ar', br: 'br', cs: 'cs',
|
||||
pl: 'pl', ja: 'ja', ko: 'ko', uk: 'uk',
|
||||
}
|
||||
|
||||
type TranslationValue = string | { name: string; category: string }[]
|
||||
type LocaleStrings = Record<string, TranslationValue>
|
||||
|
||||
async function loadLocale(code: string): Promise<LocaleStrings> {
|
||||
const filename = LOCALE_FILE_MAP[code]
|
||||
if (!filename) throw new Error(`Unknown locale code: ${code}`)
|
||||
const file = join(TRANSLATIONS_DIR, `${filename}.ts`)
|
||||
const mod = await import(pathToFileURL(file).href)
|
||||
return mod.default as LocaleStrings
|
||||
}
|
||||
|
||||
function serializeValue(value: TranslationValue, innerIndent: string): string {
|
||||
if (Array.isArray(value)) {
|
||||
// Pretty-print the array then re-indent each line after the first
|
||||
const lines = JSON.stringify(value, null, 2).split('\n')
|
||||
return lines.map((l, i) => (i === 0 ? l : innerIndent + l)).join('\n')
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
async function writeLocaleDir(code: string, strings: LocaleStrings): Promise<void> {
|
||||
const outDir = join(I18N_OUT, code)
|
||||
await mkdir(outDir, { recursive: true })
|
||||
|
||||
// Group keys by top-level namespace prefix (everything before the first dot)
|
||||
const namespaces = new Map<string, Array<[string, TranslationValue]>>()
|
||||
for (const [key, value] of Object.entries(strings)) {
|
||||
const ns = key.split('.')[0] ?? key
|
||||
if (!namespaces.has(ns)) namespaces.set(ns, [])
|
||||
namespaces.get(ns)!.push([key, value])
|
||||
}
|
||||
|
||||
// Write one file per namespace
|
||||
for (const [ns, entries] of namespaces) {
|
||||
const lines: string[] = [
|
||||
`import type { TranslationStrings } from '../types'`,
|
||||
``,
|
||||
`const ${ns}: TranslationStrings = {`,
|
||||
...entries.map(([k, v]) => ` ${JSON.stringify(k)}: ${serializeValue(v, ' ')},`),
|
||||
`}`,
|
||||
`export default ${ns}`,
|
||||
]
|
||||
await writeFile(join(outDir, `${ns}.ts`), lines.join('\n') + '\n')
|
||||
}
|
||||
|
||||
// Write index.ts that merges all namespace files into a single locale object
|
||||
const nsNames = [...namespaces.keys()]
|
||||
const indexLines: string[] = [
|
||||
...nsNames.map(ns => `import ${ns} from './${ns}'`),
|
||||
``,
|
||||
`const locale = {`,
|
||||
...nsNames.map(ns => ` ...${ns},`),
|
||||
`}`,
|
||||
`export default locale`,
|
||||
]
|
||||
await writeFile(join(outDir, 'index.ts'), indexLines.join('\n') + '\n')
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('Loading English base...')
|
||||
const en = await loadLocale('en')
|
||||
const codes = Object.keys(LOCALE_FILE_MAP)
|
||||
|
||||
// Clean existing locale dirs; leave hand-authored files (types.ts, languages.ts, index.ts) alone
|
||||
await Promise.all(codes.map(code => rm(join(I18N_OUT, code), { recursive: true, force: true })))
|
||||
|
||||
for (const code of codes) {
|
||||
process.stdout.write(`Processing ${code}...`)
|
||||
let strings = await loadLocale(code)
|
||||
|
||||
if (code === 'ar') {
|
||||
// ar.ts spreads en — keep only keys that ar actually translates (value differs from en)
|
||||
const pruned: LocaleStrings = {}
|
||||
for (const [key, val] of Object.entries(strings)) {
|
||||
if (JSON.stringify(val) !== JSON.stringify(en[key])) {
|
||||
pruned[key] = val
|
||||
}
|
||||
}
|
||||
strings = pruned
|
||||
console.log(` ${Object.keys(strings).length} own keys (pruned from ${Object.keys(en).length} en total)`)
|
||||
} else {
|
||||
const nsCount = new Set(Object.keys(strings).map(k => k.split('.')[0])).size
|
||||
console.log(` ${Object.keys(strings).length} keys, ${nsCount} namespaces`)
|
||||
}
|
||||
|
||||
await writeLocaleDir(code, strings)
|
||||
}
|
||||
|
||||
console.log('\nDone! Run: cd shared && npm run build')
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1) })
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { SUPPORTED_LANGUAGE_CODES as SUPPORTED_LANG_CODES } from '@trek/shared';
|
||||
|
||||
const dataDir = path.resolve(__dirname, '../data');
|
||||
|
||||
@@ -101,10 +102,6 @@ export const ENCRYPTION_KEY = _encryptionKey;
|
||||
|
||||
// DEFAULT_LANGUAGE sets the language shown on the login page before the user
|
||||
// selects one. Only applies when the user has no saved language preference.
|
||||
// Supported values: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
|
||||
// Must stay in sync with client/src/i18n/supportedLanguages.ts (canonical source).
|
||||
// Kept duplicated here because server and client are separate npm packages.
|
||||
const SUPPORTED_LANG_CODES = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar'];
|
||||
const rawDefaultLang = process.env.DEFAULT_LANGUAGE?.toLowerCase() || 'en';
|
||||
if (!SUPPORTED_LANG_CODES.includes(rawDefaultLang)) {
|
||||
console.warn(`DEFAULT_LANGUAGE="${rawDefaultLang}" is not supported. Falling back to "en". Supported: ${SUPPORTED_LANG_CODES.join(', ')}`);
|
||||
|
||||
@@ -7,6 +7,13 @@ import { checkSsrf, createPinnedDispatcher } from '../utils/ssrfGuard';
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
import type { NotifEventType } from './notificationPreferencesService';
|
||||
import { EMAIL_I18N as I18N, EVENT_TEXTS, PASSWORD_RESET_I18N } from '@trek/shared/i18n/externalNotifications';
|
||||
import type { EmailStrings, EventText, PasswordResetStrings, NotificationEventKey } from '@trek/shared/i18n/externalNotifications';
|
||||
|
||||
// Compile-time guard: shared NotificationEventKey and server NotifEventType must stay in sync.
|
||||
type _EvtFwd = NotifEventType extends NotificationEventKey ? true : never
|
||||
type _EvtBwd = NotificationEventKey extends NotifEventType ? true : never
|
||||
const _eventKeyDriftGuard: [_EvtFwd, _EvtBwd] = [true, true]
|
||||
|
||||
interface SmtpConfig {
|
||||
host: string;
|
||||
@@ -103,208 +110,9 @@ export function getAdminWebhookUrl(): string | null {
|
||||
return value ? decrypt_api_key(value) : null;
|
||||
}
|
||||
|
||||
// ── Email i18n strings ─────────────────────────────────────────────────────
|
||||
// ── Email i18n strings — imported from @trek/shared/i18n/externalNotifications ──
|
||||
|
||||
interface EmailStrings { footer: string; manage: string; madeWith: string; openTrek: string }
|
||||
|
||||
const I18N: Record<string, EmailStrings> = {
|
||||
en: { footer: 'You received this because you have notifications enabled in TREK.', manage: 'Manage preferences in Settings', madeWith: 'Made with', openTrek: 'Open TREK' },
|
||||
de: { footer: 'Du erhältst diese E-Mail, weil du Benachrichtigungen in TREK aktiviert hast.', manage: 'Einstellungen verwalten', madeWith: 'Made with', openTrek: 'TREK öffnen' },
|
||||
fr: { footer: 'Vous recevez cet e-mail car les notifications sont activées dans TREK.', manage: 'Gérer les préférences', madeWith: 'Made with', openTrek: 'Ouvrir TREK' },
|
||||
es: { footer: 'Recibiste esto porque tienes las notificaciones activadas en TREK.', manage: 'Gestionar preferencias', madeWith: 'Made with', openTrek: 'Abrir TREK' },
|
||||
nl: { footer: 'Je ontvangt dit omdat je meldingen hebt ingeschakeld in TREK.', manage: 'Voorkeuren beheren', madeWith: 'Made with', openTrek: 'TREK openen' },
|
||||
ru: { footer: 'Вы получили это, потому что у вас включены уведомления в TREK.', manage: 'Управление настройками', madeWith: 'Made with', openTrek: 'Открыть TREK' },
|
||||
zh: { footer: '您收到此邮件是因为您在 TREK 中启用了通知。', manage: '管理偏好设置', madeWith: 'Made with', openTrek: '打开 TREK' },
|
||||
'zh-TW': { footer: '您收到這封郵件是因為您在 TREK 中啟用了通知。', manage: '管理偏好設定', madeWith: 'Made with', openTrek: '開啟 TREK' },
|
||||
ar: { footer: 'تلقيت هذا لأنك قمت بتفعيل الإشعارات في TREK.', manage: 'إدارة التفضيلات', madeWith: 'Made with', openTrek: 'فتح TREK' },
|
||||
id: { footer: 'Anda menerima ini karena Anda telah mengaktifkan notifikasi di TREK.', manage: 'Kelola preferensi di Pengaturan', madeWith: 'Dibuat dengan', openTrek: 'Buka TREK' },
|
||||
};
|
||||
|
||||
// Translated notification texts per event type
|
||||
interface EventText { title: string; body: string }
|
||||
type EventTextFn = (params: Record<string, string>) => EventText
|
||||
|
||||
const EVENT_TEXTS: Record<string, Record<NotifEventType, EventTextFn>> = {
|
||||
en: {
|
||||
trip_invite: p => ({ title: `Trip invite: "${p.trip}"`, body: `${p.actor} invited ${p.invitee || 'a member'} to the trip "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `New booking: ${p.booking}`, body: `${p.actor} added a new ${p.type} "${p.booking}" to "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Trip reminder: ${p.trip}`, body: `Your trip "${p.trip}" is coming up soon!` }),
|
||||
todo_due: p => ({ title: `To-do due: ${p.todo}`, body: `"${p.todo}" in "${p.trip}" is due on ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Vacay Fusion Invite', body: `${p.actor} invited you to fuse vacation plans. Open TREK to accept or decline.` }),
|
||||
photos_shared: p => ({ title: `${p.count} photos shared`, body: `${p.actor} shared ${p.count} photo(s) in "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `New message in "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Packing: ${p.category}`, body: `${p.actor} assigned you to the "${p.category}" packing category in "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'New TREK version available', body: `TREK ${p.version} is now available. Visit the admin panel to update.` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology session cleared', body: 'Your Synology account or URL changed. You have been logged out of Synology Photos.' }),
|
||||
},
|
||||
de: {
|
||||
trip_invite: p => ({ title: `Einladung zu "${p.trip}"`, body: `${p.actor} hat ${p.invitee || 'ein Mitglied'} zur Reise "${p.trip}" eingeladen.` }),
|
||||
booking_change: p => ({ title: `Neue Buchung: ${p.booking}`, body: `${p.actor} hat eine neue Buchung "${p.booking}" (${p.type}) zu "${p.trip}" hinzugefügt.` }),
|
||||
trip_reminder: p => ({ title: `Reiseerinnerung: ${p.trip}`, body: `Deine Reise "${p.trip}" steht bald an!` }),
|
||||
todo_due: p => ({ title: `Aufgabe fällig: ${p.todo}`, body: `"${p.todo}" in "${p.trip}" ist am ${p.due} fällig.` }),
|
||||
vacay_invite: p => ({ title: 'Vacay Fusion-Einladung', body: `${p.actor} hat dich eingeladen, Urlaubspläne zu fusionieren. Öffne TREK um anzunehmen oder abzulehnen.` }),
|
||||
photos_shared: p => ({ title: `${p.count} Fotos geteilt`, body: `${p.actor} hat ${p.count} Foto(s) in "${p.trip}" geteilt.` }),
|
||||
collab_message: p => ({ title: `Neue Nachricht in "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Packliste: ${p.category}`, body: `${p.actor} hat dich der Kategorie "${p.category}" in der Packliste von "${p.trip}" zugewiesen.` }),
|
||||
version_available: p => ({ title: 'Neue TREK-Version verfügbar', body: `TREK ${p.version} ist jetzt verfügbar. Besuche das Admin-Panel zum Aktualisieren.` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology-Sitzung beendet', body: 'Dein Synology-Konto oder die URL hat sich geändert. Du wurdest von Synology Photos abgemeldet.' }),
|
||||
},
|
||||
fr: {
|
||||
trip_invite: p => ({ title: `Invitation à "${p.trip}"`, body: `${p.actor} a invité ${p.invitee || 'un membre'} au voyage "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nouvelle réservation : ${p.booking}`, body: `${p.actor} a ajouté une réservation "${p.booking}" (${p.type}) à "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Rappel de voyage : ${p.trip}`, body: `Votre voyage "${p.trip}" approche !` }),
|
||||
todo_due: p => ({ title: `Tâche à échéance : ${p.todo}`, body: `"${p.todo}" dans "${p.trip}" est due le ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Invitation Vacay Fusion', body: `${p.actor} vous invite à fusionner les plans de vacances. Ouvrez TREK pour accepter ou refuser.` }),
|
||||
photos_shared: p => ({ title: `${p.count} photos partagées`, body: `${p.actor} a partagé ${p.count} photo(s) dans "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nouveau message dans "${p.trip}"`, body: `${p.actor} : ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Bagages : ${p.category}`, body: `${p.actor} vous a assigné à la catégorie "${p.category}" dans "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nouvelle version TREK disponible', body: `TREK ${p.version} est maintenant disponible. Rendez-vous dans le panneau d'administration pour mettre à jour.` }),
|
||||
synology_session_cleared: () => ({ title: 'Session Synology effacée', body: 'Votre compte ou URL Synology a changé. Vous avez été déconnecté de Synology Photos.' }),
|
||||
},
|
||||
es: {
|
||||
trip_invite: p => ({ title: `Invitación a "${p.trip}"`, body: `${p.actor} invitó a ${p.invitee || 'un miembro'} al viaje "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nueva reserva: ${p.booking}`, body: `${p.actor} añadió una reserva "${p.booking}" (${p.type}) a "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Recordatorio: ${p.trip}`, body: `¡Tu viaje "${p.trip}" se acerca!` }),
|
||||
todo_due: p => ({ title: `Tarea pendiente: ${p.todo}`, body: `"${p.todo}" en "${p.trip}" vence el ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Invitación Vacay Fusion', body: `${p.actor} te invitó a fusionar planes de vacaciones. Abre TREK para aceptar o rechazar.` }),
|
||||
photos_shared: p => ({ title: `${p.count} fotos compartidas`, body: `${p.actor} compartió ${p.count} foto(s) en "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nuevo mensaje en "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Equipaje: ${p.category}`, body: `${p.actor} te asignó a la categoría "${p.category}" en "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nueva versión de TREK disponible', body: `TREK ${p.version} ya está disponible. Visita el panel de administración para actualizar.` }),
|
||||
synology_session_cleared: () => ({ title: 'Sesión de Synology cerrada', body: 'Tu cuenta o URL de Synology ha cambiado. Has cerrado sesión en Synology Photos.' }),
|
||||
},
|
||||
nl: {
|
||||
trip_invite: p => ({ title: `Uitnodiging voor "${p.trip}"`, body: `${p.actor} heeft ${p.invitee || 'een lid'} uitgenodigd voor de reis "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nieuwe boeking: ${p.booking}`, body: `${p.actor} heeft een boeking "${p.booking}" (${p.type}) toegevoegd aan "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Reisherinnering: ${p.trip}`, body: `Je reis "${p.trip}" komt eraan!` }),
|
||||
todo_due: p => ({ title: `Taak verloopt: ${p.todo}`, body: `"${p.todo}" in "${p.trip}" verloopt op ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Vacay Fusion uitnodiging', body: `${p.actor} nodigt je uit om vakantieplannen te fuseren. Open TREK om te accepteren of af te wijzen.` }),
|
||||
photos_shared: p => ({ title: `${p.count} foto's gedeeld`, body: `${p.actor} heeft ${p.count} foto('s) gedeeld in "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nieuw bericht in "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Paklijst: ${p.category}`, body: `${p.actor} heeft je toegewezen aan de categorie "${p.category}" in "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nieuwe TREK-versie beschikbaar', body: `TREK ${p.version} is nu beschikbaar. Bezoek het beheerderspaneel om bij te werken.` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology-sessie gewist', body: 'Je Synology-account of URL is gewijzigd. Je bent uitgelogd bij Synology Photos.' }),
|
||||
},
|
||||
ru: {
|
||||
trip_invite: p => ({ title: `Приглашение в "${p.trip}"`, body: `${p.actor} пригласил ${p.invitee || 'участника'} в поездку "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Новое бронирование: ${p.booking}`, body: `${p.actor} добавил бронирование "${p.booking}" (${p.type}) в "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Напоминание: ${p.trip}`, body: `Ваша поездка "${p.trip}" скоро начнётся!` }),
|
||||
todo_due: p => ({ title: `Задача к сроку: ${p.todo}`, body: `"${p.todo}" в поездке "${p.trip}" — срок ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Приглашение Vacay Fusion', body: `${p.actor} приглашает вас объединить планы отпуска. Откройте TREK для подтверждения.` }),
|
||||
photos_shared: p => ({ title: `${p.count} фото`, body: `${p.actor} поделился ${p.count} фото в "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Новое сообщение в "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Список вещей: ${p.category}`, body: `${p.actor} назначил вас в категорию "${p.category}" в "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Доступна новая версия TREK', body: `TREK ${p.version} теперь доступен. Перейдите в панель администратора для обновления.` }),
|
||||
synology_session_cleared: () => ({ title: 'Сессия Synology сброшена', body: 'Ваш аккаунт или URL Synology изменился. Вы вышли из Synology Photos.' }),
|
||||
},
|
||||
zh: {
|
||||
trip_invite: p => ({ title: `邀请加入"${p.trip}"`, body: `${p.actor} 邀请了 ${p.invitee || '成员'} 加入旅行"${p.trip}"。` }),
|
||||
booking_change: p => ({ title: `新预订:${p.booking}`, body: `${p.actor} 在"${p.trip}"中添加了预订"${p.booking}"(${p.type})。` }),
|
||||
trip_reminder: p => ({ title: `旅行提醒:${p.trip}`, body: `你的旅行"${p.trip}"即将开始!` }),
|
||||
todo_due: p => ({ title: `待办事项即将到期:${p.todo}`, body: `"${p.trip}" 中的"${p.todo}"将于 ${p.due} 到期。` }),
|
||||
vacay_invite: p => ({ title: 'Vacay 融合邀请', body: `${p.actor} 邀请你合并假期计划。打开 TREK 接受或拒绝。` }),
|
||||
photos_shared: p => ({ title: `${p.count} 张照片已分享`, body: `${p.actor} 在"${p.trip}"中分享了 ${p.count} 张照片。` }),
|
||||
collab_message: p => ({ title: `"${p.trip}"中的新消息`, body: `${p.actor}:${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `行李清单:${p.category}`, body: `${p.actor} 将你分配到"${p.trip}"中的"${p.category}"类别。` }),
|
||||
version_available: p => ({ title: '新版 TREK 可用', body: `TREK ${p.version} 现已可用。请前往管理面板进行更新。` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology 会话已清除', body: '您的 Synology 账户或 URL 已更改,您已退出 Synology Photos。' }),
|
||||
},
|
||||
'zh-TW': {
|
||||
trip_invite: p => ({ title: `邀請加入「${p.trip}」`, body: `${p.actor} 邀請了 ${p.invitee || '成員'} 加入行程「${p.trip}」。` }),
|
||||
booking_change: p => ({ title: `新預訂:${p.booking}`, body: `${p.actor} 在「${p.trip}」中新增了預訂「${p.booking}」(${p.type})。` }),
|
||||
trip_reminder: p => ({ title: `行程提醒:${p.trip}`, body: `您的行程「${p.trip}」即將開始!` }),
|
||||
todo_due: p => ({ title: `待辦事項即將到期:${p.todo}`, body: `「${p.trip}」中的「${p.todo}」將於 ${p.due} 到期。` }),
|
||||
vacay_invite: p => ({ title: 'Vacay 融合邀請', body: `${p.actor} 邀請您合併假期計畫。開啟 TREK 以接受或拒絕。` }),
|
||||
photos_shared: p => ({ title: `已分享 ${p.count} 張照片`, body: `${p.actor} 在「${p.trip}」中分享了 ${p.count} 張照片。` }),
|
||||
collab_message: p => ({ title: `「${p.trip}」中的新訊息`, body: `${p.actor}:${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `打包清單:${p.category}`, body: `${p.actor} 已將您指派到「${p.trip}」中的「${p.category}」分類。` }),
|
||||
version_available: p => ({ title: '新版 TREK 可用', body: `TREK ${p.version} 現已可用。請前往管理面板進行更新。` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology 工作階段已清除', body: '您的 Synology 帳戶或 URL 已變更,您已登出 Synology Photos。' }),
|
||||
},
|
||||
ar: {
|
||||
trip_invite: p => ({ title: `دعوة إلى "${p.trip}"`, body: `${p.actor} دعا ${p.invitee || 'عضو'} إلى الرحلة "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `حجز جديد: ${p.booking}`, body: `${p.actor} أضاف حجز "${p.booking}" (${p.type}) إلى "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `تذكير: ${p.trip}`, body: `رحلتك "${p.trip}" تقترب!` }),
|
||||
todo_due: p => ({ title: `مهمة مستحقة: ${p.todo}`, body: `"${p.todo}" في "${p.trip}" مستحقة في ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'دعوة دمج الإجازة', body: `${p.actor} يدعوك لدمج خطط الإجازة. افتح TREK للقبول أو الرفض.` }),
|
||||
photos_shared: p => ({ title: `${p.count} صور مشتركة`, body: `${p.actor} شارك ${p.count} صورة في "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `رسالة جديدة في "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `قائمة التعبئة: ${p.category}`, body: `${p.actor} عيّنك في فئة "${p.category}" في "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'إصدار TREK جديد متاح', body: `TREK ${p.version} متاح الآن. تفضل بزيارة لوحة الإدارة للتحديث.` }),
|
||||
synology_session_cleared: () => ({ title: 'تمت إعادة تعيين جلسة Synology', body: 'تغيّر حسابك أو رابط Synology. تم تسجيل خروجك من Synology Photos.' }),
|
||||
},
|
||||
br: {
|
||||
trip_invite: p => ({ title: `Convite para "${p.trip}"`, body: `${p.actor} convidou ${p.invitee || 'um membro'} para a viagem "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nova reserva: ${p.booking}`, body: `${p.actor} adicionou uma reserva "${p.booking}" (${p.type}) em "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Lembrete: ${p.trip}`, body: `Sua viagem "${p.trip}" está chegando!` }),
|
||||
todo_due: p => ({ title: `Tarefa com vencimento: ${p.todo}`, body: `"${p.todo}" em "${p.trip}" vence em ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Convite Vacay Fusion', body: `${p.actor} convidou você para fundir planos de férias. Abra o TREK para aceitar ou recusar.` }),
|
||||
photos_shared: p => ({ title: `${p.count} fotos compartilhadas`, body: `${p.actor} compartilhou ${p.count} foto(s) em "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nova mensagem em "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Bagagem: ${p.category}`, body: `${p.actor} atribuiu você à categoria "${p.category}" em "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nova versão do TREK disponível', body: `O TREK ${p.version} está disponível. Acesse o painel de administração para atualizar.` }),
|
||||
synology_session_cleared: () => ({ title: 'Sessão Synology encerrada', body: 'Sua conta ou URL do Synology foi alterada. Você foi desconectado do Synology Photos.' }),
|
||||
},
|
||||
cs: {
|
||||
trip_invite: p => ({ title: `Pozvánka do "${p.trip}"`, body: `${p.actor} pozval ${p.invitee || 'člena'} na výlet "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nová rezervace: ${p.booking}`, body: `${p.actor} přidal rezervaci "${p.booking}" (${p.type}) k "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Připomínka výletu: ${p.trip}`, body: `Váš výlet "${p.trip}" se blíží!` }),
|
||||
todo_due: p => ({ title: `Úkol se blíží: ${p.todo}`, body: `"${p.todo}" ve výletě "${p.trip}" má termín ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Pozvánka Vacay Fusion', body: `${p.actor} vás pozval ke spojení dovolenkových plánů. Otevřete TREK pro přijetí nebo odmítnutí.` }),
|
||||
photos_shared: p => ({ title: `${p.count} sdílených fotek`, body: `${p.actor} sdílel ${p.count} foto v "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nová zpráva v "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Balení: ${p.category}`, body: `${p.actor} vás přiřadil do kategorie "${p.category}" v "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nová verze TREK dostupná', body: `TREK ${p.version} je nyní dostupný. Navštivte administrátorský panel pro aktualizaci.` }),
|
||||
synology_session_cleared: () => ({ title: 'Relace Synology byla zrušena', body: 'Váš účet nebo URL Synology se změnil. Byli jste odhlášeni ze Synology Photos.' }),
|
||||
},
|
||||
hu: {
|
||||
trip_invite: p => ({ title: `Meghívó a(z) "${p.trip}" utazásra`, body: `${p.actor} meghívta ${p.invitee || 'egy tagot'} a(z) "${p.trip}" utazásra.` }),
|
||||
booking_change: p => ({ title: `Új foglalás: ${p.booking}`, body: `${p.actor} hozzáadott egy "${p.booking}" (${p.type}) foglalást a(z) "${p.trip}" utazáshoz.` }),
|
||||
trip_reminder: p => ({ title: `Utazás emlékeztető: ${p.trip}`, body: `A(z) "${p.trip}" utazás hamarosan kezdődik!` }),
|
||||
todo_due: p => ({ title: `Teendő esedékes: ${p.todo}`, body: `"${p.todo}" (${p.trip}) határideje: ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Vacay Fusion meghívó', body: `${p.actor} meghívott a nyaralási tervek összevonásához. Nyissa meg a TREK-et az elfogadáshoz vagy elutasításhoz.` }),
|
||||
photos_shared: p => ({ title: `${p.count} fotó megosztva`, body: `${p.actor} ${p.count} fotót osztott meg a(z) "${p.trip}" utazásban.` }),
|
||||
collab_message: p => ({ title: `Új üzenet a(z) "${p.trip}" utazásban`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Csomagolás: ${p.category}`, body: `${p.actor} hozzárendelte Önt a "${p.category}" csomagolási kategóriához a(z) "${p.trip}" utazásban.` }),
|
||||
version_available: p => ({ title: 'Új TREK verzió érhető el', body: `A TREK ${p.version} elérhető. Látogasson el az adminisztrációs panelre a frissítéshez.` }),
|
||||
synology_session_cleared: () => ({ title: 'Synology munkamenet törölve', body: 'A Synology fiókja vagy URL-je megváltozott. Kijelentkeztek a Synology Photos-ból.' }),
|
||||
},
|
||||
it: {
|
||||
trip_invite: p => ({ title: `Invito a "${p.trip}"`, body: `${p.actor} ha invitato ${p.invitee || 'un membro'} al viaggio "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nuova prenotazione: ${p.booking}`, body: `${p.actor} ha aggiunto una prenotazione "${p.booking}" (${p.type}) a "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Promemoria viaggio: ${p.trip}`, body: `Il tuo viaggio "${p.trip}" si avvicina!` }),
|
||||
todo_due: p => ({ title: `Attività in scadenza: ${p.todo}`, body: `"${p.todo}" in "${p.trip}" scade il ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Invito Vacay Fusion', body: `${p.actor} ti ha invitato a fondere i piani vacanza. Apri TREK per accettare o rifiutare.` }),
|
||||
photos_shared: p => ({ title: `${p.count} foto condivise`, body: `${p.actor} ha condiviso ${p.count} foto in "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nuovo messaggio in "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Bagagli: ${p.category}`, body: `${p.actor} ti ha assegnato alla categoria "${p.category}" in "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nuova versione TREK disponibile', body: `TREK ${p.version} è ora disponibile. Visita il pannello di amministrazione per aggiornare.` }),
|
||||
synology_session_cleared: () => ({ title: 'Sessione Synology rimossa', body: 'Il tuo account o URL Synology è cambiato. Sei stato disconnesso da Synology Photos.' }),
|
||||
},
|
||||
pl: {
|
||||
trip_invite: p => ({ title: `Zaproszenie do "${p.trip}"`, body: `${p.actor} zaprosił ${p.invitee || 'członka'} do podróży "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Nowa rezerwacja: ${p.booking}`, body: `${p.actor} dodał rezerwację "${p.booking}" (${p.type}) do "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Przypomnienie o podróży: ${p.trip}`, body: `Twoja podróż "${p.trip}" zbliża się!` }),
|
||||
todo_due: p => ({ title: `Zadanie z terminem: ${p.todo}`, body: `"${p.todo}" w "${p.trip}" — termin ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Zaproszenie Vacay Fusion', body: `${p.actor} zaprosił Cię do połączenia planów urlopowych. Otwórz TREK, aby zaakceptować lub odrzucić.` }),
|
||||
photos_shared: p => ({ title: `${p.count} zdjęć udostępnionych`, body: `${p.actor} udostępnił ${p.count} zdjęcie/zdjęcia w "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Nowa wiadomość w "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Pakowanie: ${p.category}`, body: `${p.actor} przypisał Cię do kategorii "${p.category}" w "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Nowa wersja TREK dostępna', body: `TREK ${p.version} jest teraz dostępny. Odwiedź panel administracyjny, aby zaktualizować.` }),
|
||||
synology_session_cleared: () => ({ title: 'Sesja Synology wyczyszczona', body: 'Twoje konto lub URL Synology uległo zmianie. Zostałeś wylogowany z Synology Photos.' }),
|
||||
},
|
||||
id: {
|
||||
trip_invite: p => ({ title: `Undangan perjalanan: "${p.trip}"`, body: `${p.actor} mengundang ${p.invitee || 'seorang anggota'} ke perjalanan "${p.trip}".` }),
|
||||
booking_change: p => ({ title: `Pemesanan baru: ${p.booking}`, body: `${p.actor} menambahkan "${p.booking}" (${p.type}) baru ke "${p.trip}".` }),
|
||||
trip_reminder: p => ({ title: `Pengingat perjalanan: ${p.trip}`, body: `Perjalanan Anda "${p.trip}" akan segera tiba!` }),
|
||||
todo_due: p => ({ title: `Tugas jatuh tempo: ${p.todo}`, body: `"${p.todo}" di "${p.trip}" jatuh tempo pada ${p.due}.` }),
|
||||
vacay_invite: p => ({ title: 'Undangan Penggabungan Vacay', body: `${p.actor} mengundang Anda untuk menggabungkan rencana liburan. Buka TREK untuk menerima atau menolak.` }),
|
||||
photos_shared: p => ({ title: `${p.count} foto dibagikan`, body: `${p.actor} membagikan ${p.count} foto di "${p.trip}".` }),
|
||||
collab_message: p => ({ title: `Pesan baru di "${p.trip}"`, body: `${p.actor}: ${p.preview}` }),
|
||||
packing_tagged: p => ({ title: `Pengepakan: ${p.category}`, body: `${p.actor} menugaskan Anda ke kategori "${p.category}" di "${p.trip}".` }),
|
||||
version_available: p => ({ title: 'Versi TREK baru tersedia', body: `TREK ${p.version} sekarang tersedia. Kunjungi panel admin untuk memperbarui.` }),
|
||||
},
|
||||
};
|
||||
// EVENT_TEXTS imported from @trek/shared/i18n/externalNotifications
|
||||
|
||||
// Get localized event text
|
||||
export function getEventText(lang: string, event: NotifEventType, params: Record<string, string>): EventText {
|
||||
@@ -362,24 +170,7 @@ export function buildEmailHtml(subject: string, body: string, lang: string, navi
|
||||
|
||||
// ── Password reset email ───────────────────────────────────────────────────
|
||||
|
||||
interface PasswordResetStrings { subject: string; greeting: string; body: string; ctaIntro: string; expiry: string; ignore: string }
|
||||
|
||||
const PASSWORD_RESET_I18N: Record<string, PasswordResetStrings> = {
|
||||
en: { subject: 'Reset your password', greeting: 'Hi', body: 'We received a request to reset the password for your TREK account. Click the button below to set a new password.', ctaIntro: 'Reset password', expiry: 'This link expires in 60 minutes.', ignore: "If you didn't request this, you can safely ignore this email — your password won't change." },
|
||||
de: { subject: 'Passwort zurücksetzen', greeting: 'Hallo', body: 'Wir haben eine Anfrage erhalten, das Passwort für dein TREK-Konto zurückzusetzen. Klicke auf den Button unten, um ein neues Passwort festzulegen.', ctaIntro: 'Passwort zurücksetzen', expiry: 'Dieser Link ist 60 Minuten gültig.', ignore: 'Wenn du das nicht warst, ignoriere diese E-Mail — dein Passwort bleibt unverändert.' },
|
||||
fr: { subject: 'Réinitialisez votre mot de passe', greeting: 'Bonjour', body: 'Nous avons reçu une demande de réinitialisation du mot de passe de votre compte TREK. Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe.', ctaIntro: 'Réinitialiser le mot de passe', expiry: 'Ce lien expire dans 60 minutes.', ignore: "Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail — votre mot de passe ne changera pas." },
|
||||
es: { subject: 'Restablecer tu contraseña', greeting: 'Hola', body: 'Recibimos una solicitud para restablecer la contraseña de tu cuenta de TREK. Haz clic en el botón de abajo para establecer una nueva contraseña.', ctaIntro: 'Restablecer contraseña', expiry: 'Este enlace caduca en 60 minutos.', ignore: 'Si no solicitaste esto, puedes ignorar este correo — tu contraseña no cambiará.' },
|
||||
it: { subject: 'Reimposta la tua password', greeting: 'Ciao', body: 'Abbiamo ricevuto una richiesta di reimpostazione della password per il tuo account TREK. Clicca il pulsante qui sotto per impostare una nuova password.', ctaIntro: 'Reimposta password', expiry: 'Questo link scade tra 60 minuti.', ignore: 'Se non hai richiesto questa operazione, ignora questa email — la tua password non cambierà.' },
|
||||
nl: { subject: 'Reset je wachtwoord', greeting: 'Hallo', body: 'We hebben een verzoek ontvangen om het wachtwoord voor je TREK-account te resetten. Klik op de knop hieronder om een nieuw wachtwoord in te stellen.', ctaIntro: 'Wachtwoord resetten', expiry: 'Deze link verloopt over 60 minuten.', ignore: 'Als jij dit niet hebt aangevraagd, kun je deze e-mail negeren — je wachtwoord blijft ongewijzigd.' },
|
||||
ru: { subject: 'Сброс пароля', greeting: 'Здравствуйте', body: 'Мы получили запрос на сброс пароля вашего аккаунта TREK. Нажмите кнопку ниже, чтобы установить новый пароль.', ctaIntro: 'Сбросить пароль', expiry: 'Ссылка действительна 60 минут.', ignore: 'Если вы не запрашивали сброс — просто проигнорируйте это письмо, пароль останется прежним.' },
|
||||
zh: { subject: '重置您的密码', greeting: '您好', body: '我们收到了重置您的 TREK 账户密码的请求。点击下方按钮设置新密码。', ctaIntro: '重置密码', expiry: '此链接将在 60 分钟后失效。', ignore: '如果这不是您本人的请求,可以忽略本邮件 — 您的密码不会改变。' },
|
||||
'zh-TW': { subject: '重設您的密碼', greeting: '您好', body: '我們收到了重設您 TREK 帳號密碼的請求。點擊下方按鈕以設定新密碼。', ctaIntro: '重設密碼', expiry: '此連結將於 60 分鐘後失效。', ignore: '若非您本人發起的請求,請忽略此郵件 — 您的密碼不會變更。' },
|
||||
hu: { subject: 'Jelszó visszaállítása', greeting: 'Szia', body: 'Kérést kaptunk a TREK-fiókod jelszavának visszaállítására. Kattints az alábbi gombra az új jelszó beállításához.', ctaIntro: 'Jelszó visszaállítása', expiry: 'Ez a link 60 perc után lejár.', ignore: 'Ha nem te kérted ezt, nyugodtan hagyd figyelmen kívül ezt az e-mailt — a jelszavad változatlan marad.' },
|
||||
ar: { subject: 'إعادة تعيين كلمة المرور', greeting: 'مرحبا', body: 'تلقينا طلبًا لإعادة تعيين كلمة المرور لحسابك في TREK. انقر على الزر أدناه لتعيين كلمة مرور جديدة.', ctaIntro: 'إعادة تعيين كلمة المرور', expiry: 'تنتهي صلاحية هذا الرابط خلال 60 دقيقة.', ignore: 'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.' },
|
||||
br: { subject: 'Redefinir sua senha', greeting: 'Olá', body: 'Recebemos um pedido para redefinir a senha da sua conta TREK. Clique no botão abaixo para definir uma nova senha.', ctaIntro: 'Redefinir senha', expiry: 'Este link expira em 60 minutos.', ignore: 'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.' },
|
||||
cs: { subject: 'Obnovení hesla', greeting: 'Ahoj', body: 'Obdrželi jsme žádost o obnovení hesla k tvému účtu TREK. Klikni na tlačítko níže a nastav nové heslo.', ctaIntro: 'Obnovit heslo', expiry: 'Odkaz vyprší za 60 minut.', ignore: 'Pokud jsi o obnovení nežádal/a, tento e-mail ignoruj — heslo zůstane beze změny.' },
|
||||
pl: { subject: 'Zresetuj hasło', greeting: 'Cześć', body: 'Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta TREK. Kliknij przycisk poniżej, aby ustawić nowe hasło.', ctaIntro: 'Zresetuj hasło', expiry: 'Link wygaśnie za 60 minut.', ignore: 'Jeśli to nie Ty, zignoruj tę wiadomość — Twoje hasło pozostanie bez zmian.' },
|
||||
};
|
||||
// PASSWORD_RESET_I18N imported from @trek/shared/i18n/externalNotifications
|
||||
|
||||
function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings, recipient: string, resetUrl: string, lang: string): string {
|
||||
const safeGreeting = escapeHtml(`${strings.greeting}, ${recipient}`);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js';
|
||||
|
||||
import gitignore from 'eslint-config-flat-gitignore';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import eslintPluginPrettier from 'eslint-plugin-prettier';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
gitignore({ strict: false }),
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
plugins: {
|
||||
prettier: eslintPluginPrettier,
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['node_modules'],
|
||||
},
|
||||
);
|
||||
+22
-1
@@ -12,6 +12,26 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./i18n": {
|
||||
"types": "./dist/i18n/index.d.ts",
|
||||
"import": "./dist/i18n/index.js",
|
||||
"require": "./dist/i18n/index.cjs"
|
||||
},
|
||||
"./i18n/*": {
|
||||
"types": "./dist/i18n/*/index.d.ts",
|
||||
"import": "./dist/i18n/*/index.js",
|
||||
"require": "./dist/i18n/*/index.cjs"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"i18n": [
|
||||
"./dist/i18n/index.d.ts"
|
||||
],
|
||||
"i18n/*": [
|
||||
"./dist/i18n/*/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -38,6 +58,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"prettier": "3.8.3",
|
||||
"prettier-plugin-organize-imports": "^4.3.0"
|
||||
"prettier-plugin-organize-imports": "^4.3.0",
|
||||
"typescript-eslint": "^8.58.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { idSchema, idParamSchema, nonEmptyString, isoDateTime } from './primitives.schema';
|
||||
import { paginationQuerySchema } from './pagination.schema';
|
||||
import {
|
||||
idSchema,
|
||||
idParamSchema,
|
||||
nonEmptyString,
|
||||
isoDateTime,
|
||||
} from './primitives.schema';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('@trek/shared primitives', () => {
|
||||
it('idSchema accepts positive integers, rejects others', () => {
|
||||
@@ -29,11 +35,16 @@ describe('@trek/shared primitives', () => {
|
||||
describe('@trek/shared pagination', () => {
|
||||
it('applies defaults and coerces', () => {
|
||||
expect(paginationQuerySchema.parse({})).toEqual({ page: 1, perPage: 50 });
|
||||
expect(paginationQuerySchema.parse({ page: '2', perPage: '10' })).toEqual({ page: 2, perPage: 10 });
|
||||
expect(paginationQuerySchema.parse({ page: '2', perPage: '10' })).toEqual({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('enforces bounds', () => {
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 0 }).success).toBe(false);
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe(false);
|
||||
expect(paginationQuerySchema.safeParse({ perPage: 999 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const admin: TranslationStrings = {
|
||||
'admin.notifications.title': 'الإشعارات',
|
||||
'admin.notifications.hint':
|
||||
'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.',
|
||||
'admin.notifications.none': 'معطّل',
|
||||
'admin.notifications.email': 'البريد الإلكتروني (SMTP)',
|
||||
'admin.ntfy.hint':
|
||||
'تسمح للمستخدمين بإعداد موضوعات ntfy الخاصة لتلقي إشعارات الدفع. قم بتعيين الخادم الافتراضي أدناه لملء إعدادات المستخدم مسبقًا.',
|
||||
'admin.notifications.save': 'حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.saved': 'تم حفظ إعدادات الإشعارات',
|
||||
'admin.notifications.testWebhook': 'إرسال webhook تجريبي',
|
||||
'admin.notifications.testWebhookSuccess': 'تم إرسال webhook التجريبي بنجاح',
|
||||
'admin.notifications.testWebhookFailed': 'فشل إرسال webhook التجريبي',
|
||||
'admin.notifications.testNtfy': 'إرسال Ntfy تجريبي',
|
||||
'admin.notifications.testNtfySuccess': 'تم إرسال Ntfy التجريبي بنجاح',
|
||||
'admin.notifications.testNtfyFailed': 'فشل إرسال Ntfy التجريبي',
|
||||
'admin.notifications.inappPanel.hint':
|
||||
'الإشعارات داخل التطبيق نشطة دائمًا ولا يمكن تعطيلها بشكل عام.',
|
||||
'admin.notifications.adminWebhookPanel.title': 'Webhook المسؤول',
|
||||
'admin.notifications.adminWebhookPanel.hint':
|
||||
'يُستخدم هذا الـ Webhook حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن Webhooks المستخدمين ويُرسل تلقائيًا عند تعيين رابط URL.',
|
||||
'admin.notifications.adminWebhookPanel.saved': 'تم حفظ رابط Webhook المسؤول',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess':
|
||||
'تم إرسال Webhook الاختباري بنجاح',
|
||||
'admin.notifications.adminWebhookPanel.testFailed':
|
||||
'فشل إرسال Webhook الاختباري',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint':
|
||||
'يُرسل Webhook المسؤول تلقائيًا عند تعيين رابط URL',
|
||||
'admin.notifications.adminNtfyPanel.title': 'Ntfy المسؤول',
|
||||
'admin.notifications.adminNtfyPanel.hint':
|
||||
'يُستخدم موضوع Ntfy هذا حصريًا لإشعارات المسؤول (مثل تنبيهات الإصدارات). وهو مستقل عن مواضيع المستخدمين ويُرسل دائمًا عند تهيئته.',
|
||||
'admin.notifications.adminNtfyPanel.serverLabel': 'عنوان URL خادم Ntfy',
|
||||
'admin.notifications.adminNtfyPanel.serverHint':
|
||||
'يُستخدم أيضًا كخادم افتراضي لإشعارات ntfy للمستخدمين. اتركه فارغًا لاستخدام ntfy.sh. يمكن للمستخدمين تغييره في إعداداتهم الخاصة.',
|
||||
'admin.notifications.adminNtfyPanel.topicLabel': 'موضوع المسؤول',
|
||||
'admin.notifications.adminNtfyPanel.tokenLabel': 'رمز الوصول (اختياري)',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared': 'تم مسح رمز وصول المسؤول',
|
||||
'admin.notifications.adminNtfyPanel.saved': 'تم حفظ إعدادات Ntfy للمسؤول',
|
||||
'admin.notifications.adminNtfyPanel.test': 'إرسال Ntfy تجريبي',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess':
|
||||
'تم إرسال Ntfy التجريبي بنجاح',
|
||||
'admin.notifications.adminNtfyPanel.testFailed': 'فشل إرسال Ntfy التجريبي',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint':
|
||||
'يُرسل Ntfy للمسؤول دائمًا عند تهيئة موضوع',
|
||||
'admin.notifications.adminNotificationsHint':
|
||||
'حدد القنوات التي تُسلّم إشعارات المسؤول (مثل تنبيهات الإصدارات). يُرسل الـ Webhook تلقائيًا عند تعيين رابط URL لـ Webhook المسؤول.',
|
||||
'admin.notifications.tripReminders.title': 'تذكيرات الرحلات',
|
||||
'admin.notifications.tripReminders.hint':
|
||||
'إرسال تذكير قبل بدء الرحلة (يتطلب تعيين أيام التذكير على الرحلة).',
|
||||
'admin.notifications.tripReminders.enabled': 'تم تفعيل تذكيرات الرحلات',
|
||||
'admin.notifications.tripReminders.disabled': 'تم تعطيل تذكيرات الرحلات',
|
||||
'admin.smtp.title': 'البريد والإشعارات',
|
||||
'admin.smtp.hint': 'تكوين SMTP لإرسال إشعارات البريد الإلكتروني.',
|
||||
'admin.smtp.testButton': 'إرسال بريد تجريبي',
|
||||
'admin.webhook.hint':
|
||||
'إرسال الإشعارات إلى webhook خارجي (Discord، Slack، إلخ).',
|
||||
'admin.smtp.testSuccess': 'تم إرسال البريد التجريبي بنجاح',
|
||||
'admin.smtp.testFailed': 'فشل إرسال البريد التجريبي',
|
||||
'admin.title': 'الإدارة',
|
||||
'admin.subtitle': 'إدارة المستخدمين وإعدادات النظام',
|
||||
'admin.tabs.users': 'المستخدمون',
|
||||
'admin.tabs.categories': 'الفئات',
|
||||
'admin.tabs.backup': 'النسخ الاحتياطي',
|
||||
'admin.tabs.notifications': 'الإشعارات',
|
||||
'admin.tabs.audit': 'تدقيق',
|
||||
'admin.stats.users': 'المستخدمون',
|
||||
'admin.stats.trips': 'الرحلات',
|
||||
'admin.stats.places': 'الأماكن',
|
||||
'admin.stats.photos': 'الصور',
|
||||
'admin.stats.files': 'الملفات',
|
||||
'admin.table.user': 'المستخدم',
|
||||
'admin.table.email': 'البريد الإلكتروني',
|
||||
'admin.table.role': 'الدور',
|
||||
'admin.table.created': 'تم الإنشاء',
|
||||
'admin.table.lastLogin': 'آخر تسجيل دخول',
|
||||
'admin.table.actions': 'الإجراءات',
|
||||
'admin.you': '(أنت)',
|
||||
'admin.editUser': 'تعديل المستخدم',
|
||||
'admin.newPassword': 'كلمة مرور جديدة',
|
||||
'admin.newPasswordHint': 'اتركه فارغًا للاحتفاظ بالحالية',
|
||||
'admin.deleteUser': 'حذف المستخدم "{name}"؟ سيتم حذف جميع الرحلات نهائيًا.',
|
||||
'admin.deleteUserTitle': 'حذف المستخدم',
|
||||
'admin.newPasswordPlaceholder': 'أدخل كلمة مرور جديدة…',
|
||||
'admin.toast.loadError': 'فشل تحميل بيانات الإدارة',
|
||||
'admin.toast.userUpdated': 'تم تحديث المستخدم',
|
||||
'admin.toast.updateError': 'فشل التحديث',
|
||||
'admin.toast.userDeleted': 'تم حذف المستخدم',
|
||||
'admin.toast.deleteError': 'فشل الحذف',
|
||||
'admin.toast.cannotDeleteSelf': 'لا يمكنك حذف حسابك الخاص',
|
||||
'admin.toast.userCreated': 'تم إنشاء المستخدم',
|
||||
'admin.toast.createError': 'فشل إنشاء المستخدم',
|
||||
'admin.toast.fieldsRequired':
|
||||
'اسم المستخدم والبريد الإلكتروني وكلمة المرور مطلوبة',
|
||||
'admin.createUser': 'إنشاء مستخدم',
|
||||
'admin.invite.title': 'روابط الدعوة',
|
||||
'admin.invite.subtitle': 'إنشاء روابط تسجيل للاستخدام المحدود',
|
||||
'admin.invite.create': 'إنشاء رابط',
|
||||
'admin.invite.createAndCopy': 'إنشاء ونسخ',
|
||||
'admin.invite.empty': 'لم يتم إنشاء روابط دعوة بعد',
|
||||
'admin.invite.maxUses': 'الحد الأقصى للاستخدام',
|
||||
'admin.invite.expiry': 'تنتهي بعد',
|
||||
'admin.invite.uses': 'مستخدم',
|
||||
'admin.invite.expiresAt': 'تنتهي في',
|
||||
'admin.invite.createdBy': 'بواسطة',
|
||||
'admin.invite.active': 'نشط',
|
||||
'admin.invite.expired': 'منتهي',
|
||||
'admin.invite.usedUp': 'مستنفد',
|
||||
'admin.invite.copied': 'تم نسخ رابط الدعوة',
|
||||
'admin.invite.copyLink': 'نسخ الرابط',
|
||||
'admin.invite.deleted': 'تم حذف رابط الدعوة',
|
||||
'admin.invite.createError': 'فشل إنشاء رابط الدعوة',
|
||||
'admin.invite.deleteError': 'فشل حذف رابط الدعوة',
|
||||
'admin.tabs.settings': 'الإعدادات',
|
||||
'admin.allowRegistration': 'السماح بالتسجيل',
|
||||
'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم',
|
||||
'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)',
|
||||
'admin.requireMfaHint':
|
||||
'يجب على المستخدمين الذين لا يملكون 2FA إكمال الإعداد في الإعدادات قبل استخدام التطبيق.',
|
||||
'admin.apiKeys': 'مفاتيح API',
|
||||
'admin.apiKeysHint':
|
||||
'اختياري. يُفعّل بيانات الأماكن الموسعة مثل الصور والطقس.',
|
||||
'admin.mapsKey': 'مفتاح Google Maps API',
|
||||
'admin.mapsKeyHint':
|
||||
'مطلوب للبحث عن الأماكن. احصل عليه من console.cloud.google.com',
|
||||
'admin.mapsKeyHintLong':
|
||||
'بدون مفتاح API، يُستخدم OpenStreetMap للبحث. مع مفتاح Google يمكن تحميل الصور والتقييمات وساعات العمل أيضًا. احصل عليه من console.cloud.google.com.',
|
||||
'admin.recommended': 'مُوصى به',
|
||||
'admin.weatherKey': 'مفتاح OpenWeatherMap API',
|
||||
'admin.weatherKeyHint': 'لبيانات الطقس. مجاني من openweathermap.org',
|
||||
'admin.validateKey': 'اختبار',
|
||||
'admin.keyValid': 'متصل',
|
||||
'admin.keyInvalid': 'غير صالح',
|
||||
'admin.keySaved': 'تم حفظ مفاتيح API',
|
||||
'admin.oidcTitle': 'تسجيل الدخول الموحد (OIDC)',
|
||||
'admin.oidcSubtitle':
|
||||
'السماح بتسجيل الدخول عبر مزودين خارجيين مثل Google أو Apple أو Authentik أو Keycloak.',
|
||||
'admin.oidcDisplayName': 'الاسم المعروض',
|
||||
'admin.oidcIssuer': 'عنوان URL للمُصدر',
|
||||
'admin.oidcIssuerHint':
|
||||
'عنوان OpenID Connect Issuer URL للمزود. مثال: https://accounts.google.com',
|
||||
'admin.oidcSaved': 'تم حفظ إعدادات OIDC',
|
||||
'admin.oidcOnlyMode': 'تعطيل المصادقة بكلمة المرور',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'عند التفعيل، يُسمح فقط بتسجيل الدخول عبر SSO. سيتم حظر تسجيل الدخول والتسجيل بكلمة المرور.',
|
||||
'admin.fileTypes': 'أنواع الملفات المسموح بها',
|
||||
'admin.fileTypesHint': 'حدد أنواع الملفات التي يمكن للمستخدمين رفعها.',
|
||||
'admin.fileTypesFormat':
|
||||
'امتدادات مفصولة بفواصل (مثل jpg,png,pdf,doc). استخدم * للسماح بجميع الأنواع.',
|
||||
'admin.fileTypesSaved': 'تم حفظ إعدادات أنواع الملفات',
|
||||
'admin.placesPhotos.title': 'صور الأماكن',
|
||||
'admin.placesPhotos.subtitle':
|
||||
'جلب الصور من Google Places API. عطّلها للحفاظ على حصة API. صور Wikimedia غير متأثرة.',
|
||||
'admin.placesAutocomplete.title': 'الإكمال التلقائي للأماكن',
|
||||
'admin.placesAutocomplete.subtitle':
|
||||
'استخدام Google Places API لاقتراحات البحث. عطّلها للحفاظ على حصة API.',
|
||||
'admin.placesDetails.title': 'تفاصيل الأماكن',
|
||||
'admin.placesDetails.subtitle':
|
||||
'جلب معلومات تفصيلية عن الأماكن (الساعات، التقييم، الموقع) من Google Places API. عطّلها للحفاظ على حصة API.',
|
||||
'admin.bagTracking.title': 'تتبع الأمتعة',
|
||||
'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر',
|
||||
'admin.collab.chat.title': 'الدردشة',
|
||||
'admin.collab.chat.subtitle': 'المراسلة في الوقت الفعلي للتعاون',
|
||||
'admin.collab.notes.title': 'الملاحظات',
|
||||
'admin.collab.notes.subtitle': 'ملاحظات ومستندات مشتركة',
|
||||
'admin.collab.polls.title': 'الاستطلاعات',
|
||||
'admin.collab.polls.subtitle': 'استطلاعات وتصويت جماعي',
|
||||
'admin.collab.whatsnext.title': 'ما التالي',
|
||||
'admin.collab.whatsnext.subtitle': 'اقتراحات الأنشطة والخطوات التالية',
|
||||
'admin.tabs.config': 'التخصيص',
|
||||
'admin.tabs.defaults': 'الإعدادات الافتراضية',
|
||||
'admin.defaultSettings.title': 'إعدادات المستخدم الافتراضية',
|
||||
'admin.defaultSettings.description':
|
||||
'تعيين الإعدادات الافتراضية على مستوى النظام. سيرى المستخدمون الذين لم يغيروا إعدادًا هذه القيم. تحظى تغييراتهم دائمًا بالأولوية.',
|
||||
'admin.defaultSettings.saved': 'تم حفظ الإعداد الافتراضي',
|
||||
'admin.defaultSettings.reset': 'إعادة التعيين إلى الإعداد الافتراضي المدمج',
|
||||
'admin.defaultSettings.resetToBuiltIn': 'إعادة تعيين',
|
||||
'admin.tabs.templates': 'قوالب التعبئة',
|
||||
'admin.packingTemplates.title': 'قوالب التعبئة',
|
||||
'admin.packingTemplates.subtitle': 'إنشاء قوائم تعبئة قابلة لإعادة الاستخدام',
|
||||
'admin.packingTemplates.create': 'قالب جديد',
|
||||
'admin.packingTemplates.namePlaceholder': 'اسم القالب (مثال: عطلة شاطئية)',
|
||||
'admin.packingTemplates.empty': 'لم يتم إنشاء قوالب بعد',
|
||||
'admin.packingTemplates.items': 'عناصر',
|
||||
'admin.packingTemplates.categories': 'فئات',
|
||||
'admin.packingTemplates.itemName': 'اسم العنصر',
|
||||
'admin.packingTemplates.itemCategory': 'الفئة',
|
||||
'admin.packingTemplates.categoryName': 'اسم الفئة (مثال: ملابس)',
|
||||
'admin.packingTemplates.addCategory': 'إضافة فئة',
|
||||
'admin.packingTemplates.created': 'تم إنشاء القالب',
|
||||
'admin.packingTemplates.deleted': 'تم حذف القالب',
|
||||
'admin.packingTemplates.loadError': 'فشل تحميل القوالب',
|
||||
'admin.packingTemplates.createError': 'فشل إنشاء القالب',
|
||||
'admin.packingTemplates.deleteError': 'فشل حذف القالب',
|
||||
'admin.packingTemplates.saveError': 'فشل الحفظ',
|
||||
'admin.tabs.addons': 'الإضافات',
|
||||
'admin.addons.title': 'الإضافات',
|
||||
'admin.addons.subtitle': 'فعّل أو عطّل الميزات لتخصيص تجربة TREK.',
|
||||
'admin.addons.catalog.packing.name': 'القوائم',
|
||||
'admin.addons.catalog.packing.description': 'قوائم التعبئة والمهام لرحلاتك',
|
||||
'admin.addons.catalog.budget.name': 'الميزانية',
|
||||
'admin.addons.catalog.budget.description': 'تتبع النفقات وخطط ميزانية الرحلة',
|
||||
'admin.addons.catalog.documents.name': 'المستندات',
|
||||
'admin.addons.catalog.documents.description': 'حفظ وإدارة وثائق السفر',
|
||||
'admin.addons.catalog.vacay.name': 'الإجازة',
|
||||
'admin.addons.catalog.vacay.description': 'مخطط إجازات شخصي مع عرض تقويم',
|
||||
'admin.addons.catalog.atlas.name': 'الأطلس',
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
|
||||
'admin.addons.catalog.collab.name': 'التعاون',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'ملاحظات واستطلاعات ودردشة لحظية لتخطيط الرحلة',
|
||||
'admin.addons.catalog.memories.name': 'صور (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'شارك صور رحلتك عبر Immich',
|
||||
'admin.addons.catalog.mcp.description':
|
||||
'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
|
||||
'admin.addons.subtitleBefore': 'فعّل أو عطّل الميزات لتخصيص تجربة ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.enabled': 'مفعّل',
|
||||
'admin.addons.disabled': 'معطّل',
|
||||
'admin.addons.type.trip': 'رحلة',
|
||||
'admin.addons.type.global': 'عام',
|
||||
'admin.addons.type.integration': 'تكامل',
|
||||
'admin.addons.tripHint': 'متاح كعلامة تبويب داخل كل رحلة',
|
||||
'admin.addons.globalHint': 'متاح كقسم مستقل في التنقل الرئيسي',
|
||||
'admin.addons.integrationHint':
|
||||
'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
|
||||
'admin.addons.toast.updated': 'تم تحديث الإضافة',
|
||||
'admin.addons.toast.error': 'فشل تحديث الإضافة',
|
||||
'admin.addons.noAddons': 'لا توجد إضافات متاحة',
|
||||
'admin.weather.title': 'بيانات الطقس',
|
||||
'admin.weather.badge': 'منذ 24 مارس 2026',
|
||||
'admin.weather.description':
|
||||
'يستخدم TREK خدمة Open-Meteo كمصدر لبيانات الطقس. وهي خدمة مجانية ومفتوحة المصدر ولا تتطلب مفتاح API.',
|
||||
'admin.weather.forecast': 'توقعات 16 يومًا',
|
||||
'admin.weather.forecastDesc': 'سابقًا 5 أيام (OpenWeatherMap)',
|
||||
'admin.weather.climate': 'بيانات المناخ التاريخية',
|
||||
'admin.weather.climateDesc':
|
||||
'متوسطات آخر 85 سنة للأيام بعد توقعات الـ 16 يومًا',
|
||||
'admin.weather.requests': '10,000 طلب / يوم',
|
||||
'admin.weather.requestsDesc': 'مجاني، بدون مفتاح API',
|
||||
'admin.weather.locationHint':
|
||||
'يعتمد الطقس على أول مكان بإحداثيات في كل يوم. إذا لم يكن هناك مكان مخصص ليوم ما، يُستخدم أي مكان من قائمة الأماكن كمرجع.',
|
||||
'admin.tabs.mcpTokens': 'وصول MCP',
|
||||
'admin.mcpTokens.title': 'وصول MCP',
|
||||
'admin.mcpTokens.subtitle': 'إدارة جلسات OAuth ورموز API لجميع المستخدمين',
|
||||
'admin.mcpTokens.sectionTitle': 'رموز API',
|
||||
'admin.mcpTokens.owner': 'المالك',
|
||||
'admin.mcpTokens.tokenName': 'اسم الرمز',
|
||||
'admin.mcpTokens.created': 'تاريخ الإنشاء',
|
||||
'admin.mcpTokens.lastUsed': 'آخر استخدام',
|
||||
'admin.mcpTokens.never': 'أبداً',
|
||||
'admin.mcpTokens.empty': 'لم يتم إنشاء أي رموز MCP بعد',
|
||||
'admin.mcpTokens.deleteTitle': 'حذف الرمز',
|
||||
'admin.mcpTokens.deleteMessage':
|
||||
'سيتم إلغاء هذا الرمز فوراً. سيفقد المستخدم وصوله إلى MCP عبر هذا الرمز.',
|
||||
'admin.mcpTokens.deleteSuccess': 'تم حذف الرمز',
|
||||
'admin.mcpTokens.deleteError': 'فشل حذف الرمز',
|
||||
'admin.mcpTokens.loadError': 'فشل تحميل الرموز',
|
||||
'admin.oauthSessions.sectionTitle': 'جلسات OAuth',
|
||||
'admin.oauthSessions.clientName': 'العميل',
|
||||
'admin.oauthSessions.owner': 'المالك',
|
||||
'admin.oauthSessions.scopes': 'الصلاحيات',
|
||||
'admin.oauthSessions.created': 'تاريخ الإنشاء',
|
||||
'admin.oauthSessions.empty': 'لا توجد جلسات OAuth نشطة',
|
||||
'admin.oauthSessions.revokeTitle': 'إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeMessage':
|
||||
'سيتم إلغاء جلسة OAuth هذه فوراً. سيفقد العميل وصوله إلى MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'تم إلغاء الجلسة',
|
||||
'admin.oauthSessions.revokeError': 'فشل إلغاء الجلسة',
|
||||
'admin.oauthSessions.loadError': 'فشل تحميل جلسات OAuth',
|
||||
'admin.audit.subtitle':
|
||||
'أحداث الأمان والإدارة (النسخ الاحتياطية، المستخدمون، المصادقة الثنائية، الإعدادات).',
|
||||
'admin.audit.empty': 'لا توجد سجلات تدقيق بعد.',
|
||||
'admin.audit.refresh': 'تحديث',
|
||||
'admin.audit.loadMore': 'تحميل المزيد',
|
||||
'admin.audit.showing': 'تم تحميل {count} · الإجمالي {total}',
|
||||
'admin.audit.col.time': 'الوقت',
|
||||
'admin.audit.col.user': 'المستخدم',
|
||||
'admin.audit.col.action': 'الإجراء',
|
||||
'admin.audit.col.resource': 'المورد',
|
||||
'admin.audit.col.ip': 'عنوان IP',
|
||||
'admin.audit.col.details': 'التفاصيل',
|
||||
'admin.github.title': 'سجل الإصدارات',
|
||||
'admin.github.subtitle': 'آخر التحديثات من {repo}',
|
||||
'admin.github.latest': 'الأحدث',
|
||||
'admin.github.prerelease': 'إصدار تجريبي',
|
||||
'admin.github.showDetails': 'إظهار التفاصيل',
|
||||
'admin.github.hideDetails': 'إخفاء التفاصيل',
|
||||
'admin.github.loadMore': 'تحميل المزيد',
|
||||
'admin.github.loading': 'جارٍ التحميل...',
|
||||
'admin.github.error': 'فشل تحميل الإصدارات',
|
||||
'admin.github.by': 'بواسطة',
|
||||
'admin.github.support': 'يساعدني في تطوير TREK',
|
||||
'admin.update.available': 'يتوفر تحديث',
|
||||
'admin.update.text': 'TREK {version} متوفر. أنت تستخدم {current}.',
|
||||
'admin.update.button': 'عرض على GitHub',
|
||||
'admin.update.install': 'تثبيت التحديث',
|
||||
'admin.update.confirmTitle': 'تثبيت التحديث؟',
|
||||
'admin.update.confirmText':
|
||||
'سيتم تحديث TREK من {current} إلى {version}. سيُعاد تشغيل الخادم تلقائيًا بعد ذلك.',
|
||||
'admin.update.dataInfo':
|
||||
'جميع بياناتك (الرحلات، المستخدمون، مفاتيح API، المرفوعات، الإجازة، الأطلس، الميزانيات) ستبقى محفوظة.',
|
||||
'admin.update.warning':
|
||||
'سيكون التطبيق غير متاح لفترة وجيزة أثناء إعادة التشغيل.',
|
||||
'admin.update.confirm': 'حدّث الآن',
|
||||
'admin.update.installing': 'جارٍ التحديث…',
|
||||
'admin.update.success': 'تم تثبيت التحديث. ستتم إعادة تشغيل الخادم…',
|
||||
'admin.update.failed': 'فشل التحديث',
|
||||
'admin.update.backupHint': 'نوصي بإنشاء نسخة احتياطية قبل التحديث.',
|
||||
'admin.update.backupLink': 'الذهاب إلى النسخ الاحتياطي',
|
||||
'admin.update.howTo': 'كيفية التحديث',
|
||||
'admin.update.dockerText':
|
||||
'يعمل TREK الخاص بك في Docker. للتحديث إلى {version}، نفّذ الأوامر التالية على الخادم:',
|
||||
'admin.update.reloadHint': 'يرجى إعادة تحميل الصفحة بعد بضع ثوانٍ.',
|
||||
'admin.tabs.permissions': 'الصلاحيات',
|
||||
};
|
||||
export default admin;
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const airport: TranslationStrings = {
|
||||
'airport.searchPlaceholder': 'رمز المطار أو المدينة (مثل FRA)',
|
||||
};
|
||||
export default airport;
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const atlas: TranslationStrings = {
|
||||
'atlas.subtitle': 'بصمتك السفرية حول العالم',
|
||||
'atlas.countries': 'الدول',
|
||||
'atlas.trips': 'الرحلات',
|
||||
'atlas.places': 'الأماكن',
|
||||
'atlas.unmark': 'إزالة',
|
||||
'atlas.confirmMark': 'تعيين هذا البلد كمُزار؟',
|
||||
'atlas.confirmUnmark': 'إزالة هذا البلد من قائمة المُزارة؟',
|
||||
'atlas.confirmUnmarkRegion': 'إزالة هذه المنطقة من قائمة المُزارة؟',
|
||||
'atlas.markVisited': 'تعيين كمُزار',
|
||||
'atlas.markVisitedHint': 'إضافة هذا البلد إلى قائمة المُزارة',
|
||||
'atlas.markRegionVisitedHint': 'إضافة هذه المنطقة إلى قائمة المُزارة',
|
||||
'atlas.addToBucket': 'إضافة إلى قائمة الأمنيات',
|
||||
'atlas.addPoi': 'إضافة مكان',
|
||||
'atlas.searchCountry': 'ابحث عن دولة...',
|
||||
'atlas.bucketNamePlaceholder': 'الاسم (بلد، مدينة، مكان…)',
|
||||
'atlas.month': 'الشهر',
|
||||
'atlas.year': 'السنة',
|
||||
'atlas.addToBucketHint': 'حفظ كمكان تريد زيارته',
|
||||
'atlas.bucketWhen': 'متى تخطط للزيارة؟',
|
||||
'atlas.statsTab': 'الإحصائيات',
|
||||
'atlas.bucketTab': 'قائمة الأمنيات',
|
||||
'atlas.addBucket': 'إضافة إلى قائمة الأمنيات',
|
||||
'atlas.bucketNotesPlaceholder': 'ملاحظات (اختياري)',
|
||||
'atlas.bucketEmpty': 'قائمة أمنياتك فارغة',
|
||||
'atlas.bucketEmptyHint': 'أضف أماكن تحلم بزيارتها',
|
||||
'atlas.days': 'الأيام',
|
||||
'atlas.visitedCountries': 'الدول التي تمت زيارتها',
|
||||
'atlas.cities': 'المدن',
|
||||
'atlas.noData': 'لا توجد بيانات سفر بعد',
|
||||
'atlas.noDataHint': 'أنشئ رحلة وأضف أماكن لرؤية خريطتك العالمية',
|
||||
'atlas.lastTrip': 'آخر رحلة',
|
||||
'atlas.nextTrip': 'الرحلة القادمة',
|
||||
'atlas.daysLeft': 'يوم متبقٍ',
|
||||
'atlas.streak': 'سلسلة',
|
||||
'atlas.years': 'سنوات',
|
||||
'atlas.yearInRow': 'سنة متتالية',
|
||||
'atlas.yearsInRow': 'سنوات متتالية',
|
||||
'atlas.tripIn': 'رحلة في',
|
||||
'atlas.tripsIn': 'رحلات في',
|
||||
'atlas.since': 'منذ',
|
||||
'atlas.europe': 'أوروبا',
|
||||
'atlas.asia': 'آسيا',
|
||||
'atlas.northAmerica': 'أمريكا الشمالية',
|
||||
'atlas.southAmerica': 'أمريكا الجنوبية',
|
||||
'atlas.africa': 'أفريقيا',
|
||||
'atlas.oceania': 'أوقيانوسيا',
|
||||
'atlas.other': 'أخرى',
|
||||
'atlas.firstVisit': 'أول رحلة',
|
||||
'atlas.lastVisitLabel': 'آخر رحلة',
|
||||
'atlas.tripSingular': 'رحلة',
|
||||
'atlas.tripPlural': 'رحلات',
|
||||
'atlas.placeVisited': 'مكان تمت زيارته',
|
||||
'atlas.placesVisited': 'أماكن تمت زيارتها',
|
||||
};
|
||||
export default atlas;
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const backup: TranslationStrings = {
|
||||
'backup.title': 'النسخ الاحتياطي',
|
||||
'backup.subtitle': 'قاعدة البيانات وجميع الملفات المرفوعة',
|
||||
'backup.refresh': 'تحديث',
|
||||
'backup.upload': 'رفع نسخة احتياطية',
|
||||
'backup.uploading': 'جارٍ الرفع…',
|
||||
'backup.create': 'إنشاء نسخة',
|
||||
'backup.creating': 'جارٍ الإنشاء…',
|
||||
'backup.empty': 'لا توجد نسخ احتياطية بعد',
|
||||
'backup.createFirst': 'إنشاء أول نسخة',
|
||||
'backup.download': 'تنزيل',
|
||||
'backup.restore': 'استعادة',
|
||||
'backup.confirm.restore':
|
||||
'استعادة النسخة "{name}"؟\n\nسيتم استبدال جميع البيانات الحالية بالنسخة.',
|
||||
'backup.confirm.uploadRestore':
|
||||
'رفع واستعادة النسخة "{name}"؟\n\nسيتم الكتابة فوق جميع البيانات الحالية.',
|
||||
'backup.confirm.delete': 'حذف النسخة "{name}"؟',
|
||||
'backup.toast.loadError': 'فشل تحميل النسخ الاحتياطية',
|
||||
'backup.toast.created': 'تم إنشاء النسخة الاحتياطية بنجاح',
|
||||
'backup.toast.createError': 'فشل إنشاء النسخة',
|
||||
'backup.toast.restored': 'تمت الاستعادة. ستُعاد تحميل الصفحة…',
|
||||
'backup.toast.restoreError': 'فشلت الاستعادة',
|
||||
'backup.toast.uploadError': 'فشل الرفع',
|
||||
'backup.toast.deleted': 'تم حذف النسخة',
|
||||
'backup.toast.deleteError': 'فشل الحذف',
|
||||
'backup.toast.downloadError': 'فشل التنزيل',
|
||||
'backup.toast.settingsSaved': 'تم حفظ إعدادات النسخ الاحتياطي التلقائي',
|
||||
'backup.toast.settingsError': 'فشل حفظ الإعدادات',
|
||||
'backup.auto.title': 'النسخ الاحتياطي التلقائي',
|
||||
'backup.auto.subtitle': 'نسخ احتياطي تلقائي وفق جدول زمني',
|
||||
'backup.auto.enable': 'تفعيل النسخ التلقائي',
|
||||
'backup.auto.enableHint':
|
||||
'سيتم إنشاء نسخ احتياطية تلقائيًا وفق الجدول المختار',
|
||||
'backup.auto.interval': 'الفترة',
|
||||
'backup.auto.hour': 'التنفيذ في الساعة',
|
||||
'backup.auto.hourHint': 'التوقيت المحلي للخادم (تنسيق {format})',
|
||||
'backup.auto.dayOfWeek': 'يوم الأسبوع',
|
||||
'backup.auto.dayOfMonth': 'يوم الشهر',
|
||||
'backup.auto.dayOfMonthHint': 'محدود بين 1–28 للتوافق مع جميع الأشهر',
|
||||
'backup.auto.scheduleSummary': 'الجدول',
|
||||
'backup.auto.summaryDaily': 'كل يوم الساعة {hour}:00',
|
||||
'backup.auto.summaryWeekly': 'كل {day} الساعة {hour}:00',
|
||||
'backup.auto.summaryMonthly': 'اليوم {day} من كل شهر الساعة {hour}:00',
|
||||
'backup.auto.envLockedHint':
|
||||
'النسخ الاحتياطي التلقائي مُعدّ عبر متغيرات بيئة Docker. لتعديل الإعدادات، حدّث docker-compose.yml وأعد تشغيل الحاوية.',
|
||||
'backup.auto.copyEnv': 'نسخ متغيرات بيئة Docker',
|
||||
'backup.auto.envCopied': 'تم نسخ متغيرات بيئة Docker إلى الحافظة',
|
||||
'backup.auto.keepLabel': 'حذف النسخ القديمة بعد',
|
||||
'backup.dow.sunday': 'أحد',
|
||||
'backup.dow.monday': 'إثن',
|
||||
'backup.dow.tuesday': 'ثلا',
|
||||
'backup.dow.wednesday': 'أرب',
|
||||
'backup.dow.thursday': 'خمي',
|
||||
'backup.dow.friday': 'جمع',
|
||||
'backup.dow.saturday': 'سبت',
|
||||
'backup.interval.hourly': 'كل ساعة',
|
||||
'backup.interval.daily': 'يوميًا',
|
||||
'backup.interval.weekly': 'أسبوعيًا',
|
||||
'backup.interval.monthly': 'شهريًا',
|
||||
'backup.keep.1day': 'يوم واحد',
|
||||
'backup.keep.3days': '3 أيام',
|
||||
'backup.keep.7days': '7 أيام',
|
||||
'backup.keep.14days': '14 يومًا',
|
||||
'backup.keep.30days': '30 يومًا',
|
||||
'backup.keep.forever': 'الاحتفاظ للأبد',
|
||||
'backup.restoreConfirmTitle': 'استعادة النسخة الاحتياطية؟',
|
||||
'backup.restoreWarning':
|
||||
'سيتم استبدال جميع البيانات الحالية (الرحلات، الأماكن، المستخدمون، المرفوعات) بالنسخة نهائيًا. لا يمكن التراجع عن ذلك.',
|
||||
'backup.restoreTip':
|
||||
'نصيحة: أنشئ نسخة احتياطية للحالة الحالية قبل الاستعادة.',
|
||||
'backup.restoreConfirm': 'نعم، استعادة',
|
||||
};
|
||||
export default backup;
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const budget: TranslationStrings = {
|
||||
'budget.title': 'الميزانية',
|
||||
'budget.exportCsv': 'تصدير CSV',
|
||||
'budget.emptyTitle': 'لم يتم إنشاء ميزانية بعد',
|
||||
'budget.emptyText': 'أنشئ فئات وإدخالات لتخطيط ميزانية سفرك',
|
||||
'budget.emptyPlaceholder': 'أدخل اسم الفئة...',
|
||||
'budget.createCategory': 'إنشاء فئة',
|
||||
'budget.category': 'الفئة',
|
||||
'budget.categoryName': 'اسم الفئة',
|
||||
'budget.table.name': 'الاسم',
|
||||
'budget.table.total': 'الإجمالي',
|
||||
'budget.table.persons': 'الأشخاص',
|
||||
'budget.table.days': 'الأيام',
|
||||
'budget.table.perPerson': 'لكل شخص',
|
||||
'budget.table.perDay': 'لكل يوم',
|
||||
'budget.table.perPersonDay': 'لكل شخص / يوم',
|
||||
'budget.table.note': 'ملاحظة',
|
||||
'budget.table.date': 'التاريخ',
|
||||
'budget.newEntry': 'إدخال جديد',
|
||||
'budget.defaultEntry': 'إدخال جديد',
|
||||
'budget.defaultCategory': 'فئة جديدة',
|
||||
'budget.total': 'الإجمالي',
|
||||
'budget.totalBudget': 'إجمالي الميزانية',
|
||||
'budget.byCategory': 'حسب الفئة',
|
||||
'budget.editTooltip': 'انقر للتعديل',
|
||||
'budget.linkedToReservation': 'مرتبط بحجز — عدّل الاسم هناك',
|
||||
'budget.confirm.deleteCategory':
|
||||
'هل تريد حذف الفئة "{name}" مع {count} إدخالات؟',
|
||||
'budget.deleteCategory': 'حذف الفئة',
|
||||
'budget.perPerson': 'لكل شخص',
|
||||
'budget.paid': 'مدفوع',
|
||||
'budget.open': 'مفتوح',
|
||||
'budget.noMembers': 'لا أعضاء معينون',
|
||||
'budget.settlement': 'التسوية',
|
||||
'budget.settlementInfo':
|
||||
'انقر على صورة العضو في بند الميزانية لتحديده باللون الأخضر — وهذا يعني أنه دفع. ثم تُظهر التسوية من يدين لمن وبكم.',
|
||||
'budget.netBalances': 'الأرصدة الصافية',
|
||||
'budget.categoriesLabel': 'فئات',
|
||||
};
|
||||
export default budget;
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const categories: TranslationStrings = {
|
||||
'categories.title': 'الفئات',
|
||||
'categories.subtitle': 'إدارة فئات الأماكن',
|
||||
'categories.new': 'فئة جديدة',
|
||||
'categories.empty': 'لا توجد فئات بعد',
|
||||
'categories.namePlaceholder': 'اسم الفئة',
|
||||
'categories.icon': 'الأيقونة',
|
||||
'categories.color': 'اللون',
|
||||
'categories.customColor': 'اختيار لون مخصص',
|
||||
'categories.preview': 'معاينة',
|
||||
'categories.defaultName': 'فئة',
|
||||
'categories.update': 'تحديث',
|
||||
'categories.create': 'إنشاء',
|
||||
'categories.confirm.delete':
|
||||
'حذف الفئة؟ لن يتم حذف الأماكن التابعة لهذه الفئة.',
|
||||
'categories.toast.loadError': 'فشل تحميل الفئات',
|
||||
'categories.toast.nameRequired': 'يرجى إدخال اسم',
|
||||
'categories.toast.updated': 'تم تحديث الفئة',
|
||||
'categories.toast.created': 'تم إنشاء الفئة',
|
||||
'categories.toast.saveError': 'فشل الحفظ',
|
||||
'categories.toast.deleted': 'تم حذف الفئة',
|
||||
'categories.toast.deleteError': 'فشل الحذف',
|
||||
};
|
||||
export default categories;
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collab: TranslationStrings = {
|
||||
'collab.tabs.chat': 'الدردشة',
|
||||
'collab.tabs.notes': 'الملاحظات',
|
||||
'collab.tabs.polls': 'الاستطلاعات',
|
||||
'collab.whatsNext.title': 'ما التالي',
|
||||
'collab.whatsNext.today': 'اليوم',
|
||||
'collab.whatsNext.tomorrow': 'غدًا',
|
||||
'collab.whatsNext.empty': 'لا توجد أنشطة قادمة',
|
||||
'collab.whatsNext.until': 'إلى',
|
||||
'collab.whatsNext.emptyHint': 'ستظهر الأنشطة التي لها وقت هنا',
|
||||
'collab.chat.send': 'إرسال',
|
||||
'collab.chat.placeholder': 'اكتب رسالة...',
|
||||
'collab.chat.empty': 'ابدأ المحادثة',
|
||||
'collab.chat.emptyHint': 'تتم مشاركة الرسائل مع جميع أعضاء الرحلة',
|
||||
'collab.chat.emptyDesc': 'شارك الأفكار والخطط والتحديثات مع مجموعة السفر',
|
||||
'collab.chat.today': 'اليوم',
|
||||
'collab.chat.yesterday': 'أمس',
|
||||
'collab.chat.deletedMessage': 'حذف رسالة',
|
||||
'collab.chat.reply': 'رد',
|
||||
'collab.chat.loadMore': 'تحميل الرسائل الأقدم',
|
||||
'collab.chat.justNow': 'الآن',
|
||||
'collab.chat.minutesAgo': 'منذ {n} د',
|
||||
'collab.chat.hoursAgo': 'منذ {n} س',
|
||||
'collab.notes.title': 'الملاحظات',
|
||||
'collab.notes.new': 'ملاحظة جديدة',
|
||||
'collab.notes.empty': 'لا توجد ملاحظات بعد',
|
||||
'collab.notes.emptyHint': 'ابدأ بتسجيل الأفكار والخطط',
|
||||
'collab.notes.all': 'الكل',
|
||||
'collab.notes.titlePlaceholder': 'عنوان الملاحظة',
|
||||
'collab.notes.contentPlaceholder': 'اكتب شيئًا...',
|
||||
'collab.notes.categoryPlaceholder': 'الفئة',
|
||||
'collab.notes.newCategory': 'فئة جديدة...',
|
||||
'collab.notes.category': 'الفئة',
|
||||
'collab.notes.noCategory': 'بلا فئة',
|
||||
'collab.notes.color': 'اللون',
|
||||
'collab.notes.save': 'حفظ',
|
||||
'collab.notes.cancel': 'إلغاء',
|
||||
'collab.notes.edit': 'تعديل',
|
||||
'collab.notes.delete': 'حذف',
|
||||
'collab.notes.pin': 'تثبيت',
|
||||
'collab.notes.unpin': 'إلغاء التثبيت',
|
||||
'collab.notes.daysAgo': 'منذ {n} يوم',
|
||||
'collab.notes.categorySettings': 'إدارة الفئات',
|
||||
'collab.notes.create': 'إنشاء',
|
||||
'collab.notes.website': 'الموقع الإلكتروني',
|
||||
'collab.notes.attachFiles': 'إرفاق ملفات',
|
||||
'collab.notes.noCategoriesYet': 'لا توجد فئات بعد',
|
||||
'collab.notes.emptyDesc': 'أنشئ ملاحظة للبدء',
|
||||
'collab.polls.title': 'الاستطلاعات',
|
||||
'collab.polls.new': 'استطلاع جديد',
|
||||
'collab.polls.empty': 'لا توجد استطلاعات بعد',
|
||||
'collab.polls.emptyHint': 'اسأل المجموعة وصوّتوا معًا',
|
||||
'collab.polls.question': 'السؤال',
|
||||
'collab.polls.questionPlaceholder': 'ماذا ينبغي أن نفعل؟',
|
||||
'collab.polls.addOption': '+ إضافة خيار',
|
||||
'collab.polls.optionPlaceholder': 'الخيار {n}',
|
||||
'collab.polls.create': 'إنشاء استطلاع',
|
||||
'collab.polls.close': 'إغلاق',
|
||||
'collab.polls.closed': 'مغلق',
|
||||
'collab.polls.votes': '{n} أصوات',
|
||||
'collab.polls.vote': '{n} صوت',
|
||||
'collab.polls.multipleChoice': 'اختيار متعدد',
|
||||
'collab.polls.multiChoice': 'اختيار متعدد',
|
||||
'collab.polls.deadline': 'الموعد النهائي',
|
||||
'collab.polls.option': 'خيار',
|
||||
'collab.polls.options': 'الخيارات',
|
||||
'collab.polls.delete': 'حذف',
|
||||
'collab.polls.closedSection': 'مغلق',
|
||||
};
|
||||
export default collab;
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const common: TranslationStrings = {
|
||||
'common.save': 'حفظ',
|
||||
'common.showMore': 'عرض المزيد',
|
||||
'common.showLess': 'عرض أقل',
|
||||
'common.cancel': 'إلغاء',
|
||||
'common.clear': 'مسح',
|
||||
'common.delete': 'حذف',
|
||||
'common.edit': 'تعديل',
|
||||
'common.add': 'إضافة',
|
||||
'common.loading': 'جارٍ التحميل...',
|
||||
'common.import': 'استيراد',
|
||||
'common.select': 'تحديد',
|
||||
'common.selectAll': 'تحديد الكل',
|
||||
'common.deselectAll': 'إلغاء تحديد الكل',
|
||||
'common.error': 'خطأ',
|
||||
'common.unknownError': 'خطأ غير معروف',
|
||||
'common.tooManyAttempts': 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.',
|
||||
'common.back': 'رجوع',
|
||||
'common.all': 'الكل',
|
||||
'common.close': 'إغلاق',
|
||||
'common.open': 'فتح',
|
||||
'common.upload': 'رفع',
|
||||
'common.search': 'بحث',
|
||||
'common.confirm': 'تأكيد',
|
||||
'common.ok': 'حسنًا',
|
||||
'common.yes': 'نعم',
|
||||
'common.no': 'لا',
|
||||
'common.or': 'أو',
|
||||
'common.none': 'لا شيء',
|
||||
'common.date': 'التاريخ',
|
||||
'common.rename': 'إعادة تسمية',
|
||||
'common.discardChanges': 'تجاهل التغييرات',
|
||||
'common.discard': 'تجاهل',
|
||||
'common.name': 'الاسم',
|
||||
'common.email': 'البريد الإلكتروني',
|
||||
'common.password': 'كلمة المرور',
|
||||
'common.saving': 'جارٍ الحفظ...',
|
||||
'common.saved': 'تم الحفظ',
|
||||
'common.update': 'تحديث',
|
||||
'common.change': 'تغيير',
|
||||
'common.uploading': 'جارٍ الرفع...',
|
||||
'common.backToPlanning': 'العودة إلى التخطيط',
|
||||
'common.reset': 'إعادة تعيين',
|
||||
'common.expand': 'توسيع',
|
||||
'common.collapse': 'طي',
|
||||
'common.copy': 'نسخ',
|
||||
'common.copied': 'تم النسخ',
|
||||
};
|
||||
export default common;
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const dashboard: TranslationStrings = {
|
||||
'dashboard.title': 'رحلاتي',
|
||||
'dashboard.subtitle.loading': 'جارٍ تحميل الرحلات...',
|
||||
'dashboard.subtitle.trips': '{count} رحلة ({archived} مؤرشفة)',
|
||||
'dashboard.subtitle.empty': 'ابدأ رحلتك الأولى',
|
||||
'dashboard.subtitle.activeOne': '{count} رحلة نشطة',
|
||||
'dashboard.subtitle.activeMany': '{count} رحلات نشطة',
|
||||
'dashboard.subtitle.archivedSuffix': ' · {count} مؤرشفة',
|
||||
'dashboard.newTrip': 'رحلة جديدة',
|
||||
'dashboard.gridView': 'عرض شبكي',
|
||||
'dashboard.listView': 'عرض قائمة',
|
||||
'dashboard.currency': 'العملة',
|
||||
'dashboard.timezone': 'المناطق الزمنية',
|
||||
'dashboard.localTime': 'المحلي',
|
||||
'dashboard.timezoneCustomTitle': 'منطقة زمنية مخصصة',
|
||||
'dashboard.timezoneCustomLabelPlaceholder': 'الاسم (اختياري)',
|
||||
'dashboard.timezoneCustomTzPlaceholder': 'مثال: Asia/Riyadh',
|
||||
'dashboard.timezoneCustomAdd': 'إضافة',
|
||||
'dashboard.timezoneCustomErrorEmpty': 'أدخل معرّف منطقة زمنية',
|
||||
'dashboard.timezoneCustomErrorInvalid':
|
||||
'منطقة زمنية غير صالحة. استخدم صيغة مثل Asia/Riyadh',
|
||||
'dashboard.timezoneCustomErrorDuplicate': 'مضافة بالفعل',
|
||||
'dashboard.emptyTitle': 'لا توجد رحلات بعد',
|
||||
'dashboard.emptyText': 'أنشئ رحلتك الأولى وابدأ التخطيط',
|
||||
'dashboard.emptyButton': 'إنشاء أول رحلة',
|
||||
'dashboard.nextTrip': 'الرحلة القادمة',
|
||||
'dashboard.shared': 'مشتركة',
|
||||
'dashboard.sharedBy': 'شاركها {name}',
|
||||
'dashboard.days': 'الأيام',
|
||||
'dashboard.places': 'الأماكن',
|
||||
'dashboard.members': 'ال חברים',
|
||||
'dashboard.archive': 'أرشفة',
|
||||
'dashboard.copyTrip': 'نسخ',
|
||||
'dashboard.copySuffix': 'نسخة',
|
||||
'dashboard.restore': 'استعادة',
|
||||
'dashboard.archived': 'مؤرشفة',
|
||||
'dashboard.status.ongoing': 'جارية',
|
||||
'dashboard.status.today': 'اليوم',
|
||||
'dashboard.status.tomorrow': 'غدًا',
|
||||
'dashboard.status.past': 'منتهية',
|
||||
'dashboard.status.daysLeft': 'متبقي {count} يوم',
|
||||
'dashboard.toast.loadError': 'فشل تحميل الرحلات',
|
||||
'dashboard.toast.created': 'تم إنشاء الرحلة بنجاح',
|
||||
'dashboard.toast.createError': 'فشل إنشاء الرحلة',
|
||||
'dashboard.toast.updated': 'تم تحديث الرحلة',
|
||||
'dashboard.toast.updateError': 'فشل تحديث الرحلة',
|
||||
'dashboard.toast.deleted': 'تم حذف الرحلة',
|
||||
'dashboard.toast.deleteError': 'فشل حذف الرحلة',
|
||||
'dashboard.toast.archived': 'تمت أرشفة الرحلة',
|
||||
'dashboard.toast.archiveError': 'فشل الأرشفة',
|
||||
'dashboard.toast.restored': 'تمت استعادة الرحلة',
|
||||
'dashboard.toast.restoreError': 'فشل الاستعادة',
|
||||
'dashboard.toast.copied': 'تم نسخ الرحلة!',
|
||||
'dashboard.toast.copyError': 'فشل نسخ الرحلة',
|
||||
'dashboard.confirm.delete':
|
||||
'حذف الرحلة "{title}"؟ سيتم حذف جميع الأماكن والخطط نهائيًا.',
|
||||
'dashboard.editTrip': 'تعديل الرحلة',
|
||||
'dashboard.createTrip': 'إنشاء رحلة جديدة',
|
||||
'dashboard.tripTitle': 'العنوان',
|
||||
'dashboard.tripTitlePlaceholder': 'مثال: صيف في اليابان',
|
||||
'dashboard.tripDescription': 'الوصف',
|
||||
'dashboard.tripDescriptionPlaceholder': 'عمّ تتحدث هذه الرحلة؟',
|
||||
'dashboard.startDate': 'تاريخ البداية',
|
||||
'dashboard.endDate': 'تاريخ النهاية',
|
||||
'dashboard.dayCount': 'عدد الأيام',
|
||||
'dashboard.dayCountHint':
|
||||
'عدد الأيام المراد التخطيط لها عندما لا يتم تحديد تواريخ السفر.',
|
||||
'dashboard.noDateHint':
|
||||
'لا يوجد تاريخ محدد. سيتم إنشاء 7 أيام افتراضية ويمكنك تغيير ذلك لاحقًا.',
|
||||
'dashboard.coverImage': 'صورة الغلاف',
|
||||
'dashboard.addCoverImage': 'إضافة صورة غلاف',
|
||||
'dashboard.addMembers': 'رفاق السفر',
|
||||
'dashboard.addMember': 'إضافة عضو',
|
||||
'dashboard.coverSaved': 'تم حفظ صورة الغلاف',
|
||||
'dashboard.coverUploadError': 'فشل الرفع',
|
||||
'dashboard.coverRemoveError': 'فشل الإزالة',
|
||||
'dashboard.titleRequired': 'العنوان مطلوب',
|
||||
'dashboard.endDateError': 'يجب أن يكون تاريخ النهاية بعد البداية',
|
||||
};
|
||||
export default dashboard;
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const day: TranslationStrings = {
|
||||
'day.precipProb': 'احتمال هطول الأمطار',
|
||||
'day.precipitation': 'الهطول',
|
||||
'day.wind': 'الرياح',
|
||||
'day.sunrise': 'شروق الشمس',
|
||||
'day.sunset': 'غروب الشمس',
|
||||
'day.hourlyForecast': 'التوقعات بالساعة',
|
||||
'day.climateHint':
|
||||
'متوسطات تاريخية — التوقعات الفعلية متاحة خلال 16 يومًا من هذا التاريخ.',
|
||||
'day.noWeather': 'لا تتوفر بيانات طقس. أضف مكانًا بإحداثيات.',
|
||||
'day.overview': 'ملخص اليوم',
|
||||
'day.accommodation': 'الإقامة',
|
||||
'day.addAccommodation': 'إضافة إقامة',
|
||||
'day.hotelDayRange': 'تطبيق على الأيام',
|
||||
'day.noPlacesForHotel': 'أضف أماكن إلى رحلتك أولًا',
|
||||
'day.allDays': 'الكل',
|
||||
'day.checkIn': 'تسجيل الوصول',
|
||||
'day.checkInUntil': 'حتى',
|
||||
'day.checkOut': 'تسجيل المغادرة',
|
||||
'day.confirmation': 'التأكيد',
|
||||
'day.editAccommodation': 'تعديل الإقامة',
|
||||
'day.reservations': 'الحجوزات',
|
||||
};
|
||||
export default day;
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const dayplan: TranslationStrings = {
|
||||
'dayplan.icsTooltip': 'تصدير التقويم (ICS)',
|
||||
'dayplan.emptyDay': 'لا توجد أماكن مخططة لهذا اليوم',
|
||||
'dayplan.cannotReorderTransport':
|
||||
'لا يمكن إعادة ترتيب الحجوزات ذات الوقت الثابت',
|
||||
'dayplan.confirmRemoveTimeTitle': 'إزالة الوقت؟',
|
||||
'dayplan.confirmRemoveTimeBody':
|
||||
'هذا المكان له وقت ثابت ({time}). نقله سيزيل الوقت ويسمح بالترتيب الحر.',
|
||||
'dayplan.confirmRemoveTimeAction': 'إزالة الوقت ونقل',
|
||||
'dayplan.cannotDropOnTimed':
|
||||
'لا يمكن وضع العناصر بين الإدخالات المرتبطة بوقت',
|
||||
'dayplan.cannotBreakChronology':
|
||||
'سيؤدي هذا إلى كسر الترتيب الزمني للعناصر والحجوزات المجدولة',
|
||||
'dayplan.addNote': 'إضافة ملاحظة',
|
||||
'dayplan.editNote': 'تعديل الملاحظة',
|
||||
'dayplan.noteAdd': 'إضافة ملاحظة',
|
||||
'dayplan.noteEdit': 'تعديل الملاحظة',
|
||||
'dayplan.noteTitle': 'ملاحظة',
|
||||
'dayplan.noteSubtitle': 'ملاحظة يومية',
|
||||
'dayplan.totalCost': 'إجمالي التكلفة',
|
||||
'dayplan.days': 'الأيام',
|
||||
'dayplan.dayN': 'اليوم {n}',
|
||||
'dayplan.calculating': 'جارٍ الحساب...',
|
||||
'dayplan.route': 'المسار',
|
||||
'dayplan.optimize': 'تحسين',
|
||||
'dayplan.optimized': 'تم تحسين المسار',
|
||||
'dayplan.routeError': 'فشل حساب المسار',
|
||||
'dayplan.toast.needTwoPlaces': 'يلزم مكانان على الأقل لتحسين المسار',
|
||||
'dayplan.toast.routeOptimized': 'تم تحسين المسار',
|
||||
'dayplan.toast.noGeoPlaces': 'لم يتم العثور على أماكن بإحداثيات لحساب المسار',
|
||||
'dayplan.confirmed': 'مؤكد',
|
||||
'dayplan.pendingRes': 'قيد الانتظار',
|
||||
'dayplan.pdfTooltip': 'تصدير خطة اليوم بصيغة PDF',
|
||||
'dayplan.pdfError': 'فشل تصدير PDF',
|
||||
};
|
||||
export default dayplan;
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { NotificationLocale } from '../externalNotifications/types';
|
||||
|
||||
const ar: NotificationLocale = {
|
||||
email: {
|
||||
footer: 'تلقيت هذا لأنك قمت بتفعيل الإشعارات في TREK.',
|
||||
manage: 'إدارة التفضيلات',
|
||||
madeWith: 'Made with',
|
||||
openTrek: 'فتح TREK',
|
||||
},
|
||||
events: {
|
||||
trip_invite: (p) => ({
|
||||
title: `دعوة إلى "${p.trip}"`,
|
||||
body: `${p.actor} دعا ${p.invitee || 'عضو'} إلى الرحلة "${p.trip}".`,
|
||||
}),
|
||||
booking_change: (p) => ({
|
||||
title: `حجز جديد: ${p.booking}`,
|
||||
body: `${p.actor} أضاف حجز "${p.booking}" (${p.type}) إلى "${p.trip}".`,
|
||||
}),
|
||||
trip_reminder: (p) => ({
|
||||
title: `تذكير: ${p.trip}`,
|
||||
body: `رحلتك "${p.trip}" تقترب!`,
|
||||
}),
|
||||
todo_due: (p) => ({
|
||||
title: `مهمة مستحقة: ${p.todo}`,
|
||||
body: `"${p.todo}" في "${p.trip}" مستحقة في ${p.due}.`,
|
||||
}),
|
||||
vacay_invite: (p) => ({
|
||||
title: 'دعوة دمج الإجازة',
|
||||
body: `${p.actor} يدعوك لدمج خطط الإجازة. افتح TREK للقبول أو الرفض.`,
|
||||
}),
|
||||
photos_shared: (p) => ({
|
||||
title: `${p.count} صور مشتركة`,
|
||||
body: `${p.actor} شارك ${p.count} صورة في "${p.trip}".`,
|
||||
}),
|
||||
collab_message: (p) => ({
|
||||
title: `رسالة جديدة في "${p.trip}"`,
|
||||
body: `${p.actor}: ${p.preview}`,
|
||||
}),
|
||||
packing_tagged: (p) => ({
|
||||
title: `قائمة التعبئة: ${p.category}`,
|
||||
body: `${p.actor} عيّنك في فئة "${p.category}" في "${p.trip}".`,
|
||||
}),
|
||||
version_available: (p) => ({
|
||||
title: 'إصدار TREK جديد متاح',
|
||||
body: `TREK ${p.version} متاح الآن. تفضل بزيارة لوحة الإدارة للتحديث.`,
|
||||
}),
|
||||
synology_session_cleared: () => ({
|
||||
title: 'تمت إعادة تعيين جلسة Synology',
|
||||
body: 'تغيّر حسابك أو رابط Synology. تم تسجيل خروجك من Synology Photos.',
|
||||
}),
|
||||
},
|
||||
passwordReset: {
|
||||
subject: 'إعادة تعيين كلمة المرور',
|
||||
greeting: 'مرحبا',
|
||||
body: 'تلقينا طلبًا لإعادة تعيين كلمة المرور لحسابك في TREK. انقر على الزر أدناه لتعيين كلمة مرور جديدة.',
|
||||
ctaIntro: 'إعادة تعيين كلمة المرور',
|
||||
expiry: 'تنتهي صلاحية هذا الرابط خلال 60 دقيقة.',
|
||||
ignore:
|
||||
'إذا لم تطلب هذا، يمكنك تجاهل هذه الرسالة — لن تتغير كلمة المرور الخاصة بك.',
|
||||
},
|
||||
};
|
||||
|
||||
export default ar;
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const files: TranslationStrings = {
|
||||
'files.title': 'الملفات',
|
||||
'files.pageTitle': 'الملفات والمستندات',
|
||||
'files.subtitle': '{count} ملف لـ {trip}',
|
||||
'files.download': 'تنزيل',
|
||||
'files.openError': 'تعذر فتح الملف',
|
||||
'files.downloadPdf': 'تنزيل PDF',
|
||||
'files.count': '{count} ملفات',
|
||||
'files.countSingular': 'ملف واحد',
|
||||
'files.uploaded': 'تم رفع {count}',
|
||||
'files.uploadError': 'فشل الرفع',
|
||||
'files.dropzone': 'أسقط الملفات هنا',
|
||||
'files.dropzoneHint': 'أو انقر للتصفح',
|
||||
'files.allowedTypes':
|
||||
'صور، PDF، DOC، DOCX، XLS، XLSX، TXT، CSV · حد أقصى 50 ميغابايت',
|
||||
'files.uploading': 'جارٍ الرفع...',
|
||||
'files.filterAll': 'الكل',
|
||||
'files.filterPdf': 'ملفات PDF',
|
||||
'files.filterImages': 'الصور',
|
||||
'files.filterDocs': 'المستندات',
|
||||
'files.filterCollab': 'ملاحظات Collab',
|
||||
'files.sourceCollab': 'من ملاحظات Collab',
|
||||
'files.empty': 'لا توجد ملفات بعد',
|
||||
'files.emptyHint': 'ارفع ملفات لإرفاقها برحلتك',
|
||||
'files.openTab': 'فتح في تبويب جديد',
|
||||
'files.confirm.delete': 'هل تريد حذف هذا الملف؟',
|
||||
'files.toast.deleted': 'تم حذف الملف',
|
||||
'files.toast.deleteError': 'فشل حذف الملف',
|
||||
'files.sourcePlan': 'خطة اليوم',
|
||||
'files.sourceBooking': 'الحجز',
|
||||
'files.sourceTransport': 'النقل',
|
||||
'files.attach': 'إرفاق',
|
||||
'files.pasteHint': 'يمكنك أيضًا لصق الصور من الحافظة (Ctrl+V)',
|
||||
'files.trash': 'سلة المهملات',
|
||||
'files.trashEmpty': 'سلة المهملات فارغة',
|
||||
'files.emptyTrash': 'إفراغ السلة',
|
||||
'files.restore': 'استعادة',
|
||||
'files.star': 'تمييز',
|
||||
'files.unstar': 'إلغاء التمييز',
|
||||
'files.assign': 'إسناد',
|
||||
'files.assignTitle': 'إسناد ملف',
|
||||
'files.assignPlace': 'المكان',
|
||||
'files.assignBooking': 'الحجز',
|
||||
'files.assignTransport': 'النقل',
|
||||
'files.unassigned': 'غير مسند',
|
||||
'files.unlink': 'إزالة الرابط',
|
||||
'files.toast.trashed': 'تم النقل إلى سلة المهملات',
|
||||
'files.toast.restored': 'تمت استعادة الملف',
|
||||
'files.toast.trashEmptied': 'تم إفراغ سلة المهملات',
|
||||
'files.toast.assigned': 'تم إسناد الملف',
|
||||
'files.toast.assignError': 'فشل الإسناد',
|
||||
'files.toast.restoreError': 'فشلت الاستعادة',
|
||||
'files.confirm.permanentDelete':
|
||||
'حذف هذا الملف نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.confirm.emptyTrash':
|
||||
'حذف جميع ملفات سلة المهملات نهائيًا؟ لا يمكن التراجع عن ذلك.',
|
||||
'files.noteLabel': 'ملاحظة',
|
||||
'files.notePlaceholder': 'أضف ملاحظة...',
|
||||
};
|
||||
export default files;
|
||||
@@ -0,0 +1,86 @@
|
||||
import admin from './admin';
|
||||
import airport from './airport';
|
||||
import atlas from './atlas';
|
||||
import backup from './backup';
|
||||
import budget from './budget';
|
||||
import categories from './categories';
|
||||
import collab from './collab';
|
||||
import common from './common';
|
||||
import dashboard from './dashboard';
|
||||
import day from './day';
|
||||
import dayplan from './dayplan';
|
||||
import files from './files';
|
||||
import inspector from './inspector';
|
||||
import journey from './journey';
|
||||
import login from './login';
|
||||
import map from './map';
|
||||
import members from './members';
|
||||
import memories from './memories';
|
||||
import nav from './nav';
|
||||
import notif from './notif';
|
||||
import notifications from './notifications';
|
||||
import oauth from './oauth';
|
||||
import packing from './packing';
|
||||
import pdf from './pdf';
|
||||
import perm from './perm';
|
||||
import photos from './photos';
|
||||
import places from './places';
|
||||
import planner from './planner';
|
||||
import register from './register';
|
||||
import reservations from './reservations';
|
||||
import settings from './settings';
|
||||
import share from './share';
|
||||
import shared from './shared';
|
||||
import stats from './stats';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import transport from './transport';
|
||||
import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
...trips,
|
||||
...nav,
|
||||
...dashboard,
|
||||
...settings,
|
||||
...admin,
|
||||
...dayplan,
|
||||
...share,
|
||||
...shared,
|
||||
...login,
|
||||
...register,
|
||||
...vacay,
|
||||
...atlas,
|
||||
...trip,
|
||||
...places,
|
||||
...inspector,
|
||||
...reservations,
|
||||
...airport,
|
||||
...map,
|
||||
...budget,
|
||||
...files,
|
||||
...packing,
|
||||
...members,
|
||||
...categories,
|
||||
...backup,
|
||||
...photos,
|
||||
...pdf,
|
||||
...planner,
|
||||
...stats,
|
||||
...day,
|
||||
...memories,
|
||||
...collab,
|
||||
...perm,
|
||||
...undo,
|
||||
...notifications,
|
||||
...todo,
|
||||
...notif,
|
||||
...journey,
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
};
|
||||
export default locale;
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const inspector: TranslationStrings = {
|
||||
'inspector.opened': 'مفتوح',
|
||||
'inspector.closed': 'مغلق',
|
||||
'inspector.openingHours': 'ساعات العمل',
|
||||
'inspector.showHours': 'عرض ساعات العمل',
|
||||
'inspector.files': 'الملفات',
|
||||
'inspector.filesCount': '{count} ملفات',
|
||||
'inspector.remove': 'إزالة',
|
||||
'inspector.removeFromDay': 'إزالة من اليوم',
|
||||
'inspector.addToDay': 'إضافة إلى اليوم',
|
||||
'inspector.confirmedRes': 'حجز مؤكد',
|
||||
'inspector.pendingRes': 'حجز قيد الانتظار',
|
||||
'inspector.google': 'فتح في Google Maps',
|
||||
'inspector.website': 'فتح الموقع الإلكتروني',
|
||||
'inspector.addRes': 'حجز',
|
||||
'inspector.editRes': 'تعديل الحجز',
|
||||
'inspector.participants': 'المشاركون',
|
||||
'inspector.trackStats': 'بيانات المسار',
|
||||
};
|
||||
export default inspector;
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const journey: TranslationStrings = {
|
||||
'journey.search.placeholder': 'البحث في الرحلات…',
|
||||
'journey.search.noResults': 'لا توجد رحلات تطابق "{query}"',
|
||||
'journey.status.archived': 'مؤرشف',
|
||||
'journey.detail.backToJourney': 'العودة للمجلة',
|
||||
'journey.detail.photos': 'صور',
|
||||
'journey.detail.day': 'اليوم {number}',
|
||||
'journey.detail.places': 'أماكن',
|
||||
'journey.skeletons.show': 'إظهار الاقتراحات',
|
||||
'journey.skeletons.hide': 'إخفاء الاقتراحات',
|
||||
'journey.editor.discardChangesConfirm':
|
||||
'لديك تغييرات غير محفوظة. هل تريد تجاهلها؟',
|
||||
'journey.editor.uploadFailed': 'فشل رفع الصور',
|
||||
'journey.editor.uploadPhotos': 'رفع صور',
|
||||
'journey.editor.uploading': '...جارٍ الرفع',
|
||||
'journey.editor.uploadingProgress': 'جارٍ الرفع {done}/{total}…',
|
||||
'journey.editor.uploadPartialFailed':
|
||||
'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
|
||||
'journey.editor.fromGallery': 'من المعرض',
|
||||
'journey.editor.addAnother': 'إضافة آخر',
|
||||
'journey.editor.makeFirst': 'جعله الأول',
|
||||
'journey.editor.searching': 'جارٍ البحث...',
|
||||
'journey.share.copy': 'نسخ',
|
||||
'journey.share.copied': 'تم النسخ!',
|
||||
'journey.invite.role': 'الدور',
|
||||
'journey.invite.viewer': 'مشاهد',
|
||||
'journey.invite.editor': 'محرر',
|
||||
'journey.invite.invite': 'دعوة',
|
||||
'journey.invite.inviting': 'جارٍ الدعوة...',
|
||||
'journey.settings.endJourney': 'أرشفة الرحلة',
|
||||
'journey.settings.reopenJourney': 'استعادة الرحلة',
|
||||
'journey.settings.archived': 'تم أرشفة الرحلة',
|
||||
'journey.settings.reopened': 'تمت إعادة فتح الرحلة',
|
||||
'journey.settings.endDescription':
|
||||
'يخفي شارة البث المباشر. يمكنك إعادة الفتح في أي وقت.',
|
||||
'journey.settings.failedToDelete': 'فشل في الحذف',
|
||||
'journey.entries.deleteTitle': 'حذف الإدخال',
|
||||
'journey.photosUploaded': 'تم رفع {count} صورة',
|
||||
'journey.photosUploadFailed': 'فشل رفع بعض الصور',
|
||||
'journey.photosAdded': 'تمت إضافة {count} صورة',
|
||||
'journey.picker.tripPeriod': 'فترة الرحلة',
|
||||
'journey.picker.dateRange': 'نطاق التاريخ',
|
||||
'journey.picker.allPhotos': 'كل الصور',
|
||||
'journey.picker.albums': 'ألبومات',
|
||||
'journey.picker.selected': 'محدد',
|
||||
'journey.picker.addTo': 'إضافة إلى',
|
||||
'journey.picker.newGallery': 'معرض جديد',
|
||||
'journey.picker.selectAll': 'تحديد الكل',
|
||||
'journey.picker.deselectAll': 'إلغاء تحديد الكل',
|
||||
'journey.picker.noAlbums': 'لم يتم العثور على ألبومات',
|
||||
'journey.picker.selectDate': 'اختر تاريخ',
|
||||
'journey.picker.search': 'بحث',
|
||||
};
|
||||
export default journey;
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const login: TranslationStrings = {
|
||||
'login.error': 'فشل تسجيل الدخول. يرجى التحقق من بياناتك.',
|
||||
'login.tagline': 'رحلاتك.\nخطتك.',
|
||||
'login.description':
|
||||
'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.',
|
||||
'login.features.maps': 'خرائط تفاعلية',
|
||||
'login.features.mapsDesc': 'Google Places ومسارات وتجميع',
|
||||
'login.features.realtime': 'مزامنة فورية',
|
||||
'login.features.realtimeDesc': 'خططوا معًا عبر WebSocket',
|
||||
'login.features.budget': 'تتبع الميزانية',
|
||||
'login.features.budgetDesc': 'فئات ورسوم وتقسيم لكل شخص',
|
||||
'login.features.collab': 'تعاون',
|
||||
'login.features.collabDesc': 'عدة مستخدمين مع رحلات مشتركة',
|
||||
'login.features.packing': 'قوائم تجهيز',
|
||||
'login.features.packingDesc': 'فئات وتقدم واقتراحات',
|
||||
'login.features.bookings': 'الحجوزات',
|
||||
'login.features.bookingsDesc': 'رحلات وفنادق ومطاعم وغير ذلك',
|
||||
'login.features.files': 'المستندات',
|
||||
'login.features.filesDesc': 'رفع الملفات وإدارتها',
|
||||
'login.features.routes': 'مسارات ذكية',
|
||||
'login.features.routesDesc': 'تحسين تلقائي وتصدير إلى Google Maps',
|
||||
'login.selfHosted': 'استضافة ذاتية · مفتوح المصدر · بياناتك تبقى ملكك',
|
||||
'login.title': 'تسجيل الدخول',
|
||||
'login.subtitle': 'مرحبًا بعودتك',
|
||||
'login.signingIn': 'جارٍ تسجيل الدخول…',
|
||||
'login.signIn': 'دخول',
|
||||
'login.createAdmin': 'إنشاء حساب مسؤول',
|
||||
'login.createAdminHint': 'أعد إعداد أول حساب مسؤول لـ TREK.',
|
||||
'login.setNewPassword': 'تعيين كلمة مرور جديدة',
|
||||
'login.setNewPasswordHint': 'يجب عليك تغيير كلمة المرور قبل المتابعة.',
|
||||
'login.createAccount': 'إنشاء حساب',
|
||||
'login.createAccountHint': 'سجّل حسابًا جديدًا.',
|
||||
'login.creating': 'جارٍ الإنشاء…',
|
||||
'login.noAccount': 'ليس لديك حساب؟',
|
||||
'login.hasAccount': 'لديك حساب بالفعل؟',
|
||||
'login.register': 'تسجيل',
|
||||
'login.username': 'اسم المستخدم',
|
||||
'login.oidc.registrationDisabled': 'التسجيل معطّل. تواصل مع المسؤول.',
|
||||
'login.oidc.noEmail': 'لم يتم استلام بريد إلكتروني من المزوّد.',
|
||||
'login.oidc.tokenFailed': 'فشلت المصادقة.',
|
||||
'login.oidc.invalidState': 'جلسة غير صالحة. حاول مرة أخرى.',
|
||||
'login.demoFailed': 'فشل الدخول إلى العرض التجريبي',
|
||||
'login.oidcSignIn': 'تسجيل الدخول عبر {name}',
|
||||
'login.oidcOnly':
|
||||
'تم تعطيل المصادقة بكلمة المرور. يرجى تسجيل الدخول عبر مزود SSO.',
|
||||
'login.oidcLoggedOut': 'تم تسجيل خروجك. سجّل الدخول مجدداً عبر مزود SSO.',
|
||||
'login.demoHint': 'جرّب العرض التجريبي دون الحاجة للتسجيل',
|
||||
'login.mfaTitle': 'المصادقة الثنائية',
|
||||
'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
|
||||
'login.mfaCodeLabel': 'رمز التحقق',
|
||||
'login.mfaCodeRequired': 'أدخل الرمز من تطبيق المصادقة.',
|
||||
'login.mfaHint': 'افتح Google Authenticator أو Authy أو أي تطبيق TOTP آخر.',
|
||||
'login.mfaBack': '← العودة لتسجيل الدخول',
|
||||
'login.mfaVerify': 'تحقق',
|
||||
'login.invalidInviteLink': 'رابط الدعوة غير صالح أو منتهي الصلاحية',
|
||||
'login.oidcFailed': 'فشل تسجيل الدخول عبر OIDC',
|
||||
'login.usernameRequired': 'اسم المستخدم مطلوب',
|
||||
'login.passwordMinLength': 'يجب أن تكون كلمة المرور 8 أحرف على الأقل',
|
||||
'login.forgotPassword': 'نسيت كلمة المرور؟',
|
||||
'login.forgotPasswordTitle': 'إعادة تعيين كلمة المرور',
|
||||
'login.forgotPasswordBody':
|
||||
'أدخل عنوان البريد الإلكتروني المسجَّل. إذا كان الحساب موجودًا، سنرسل رابط إعادة التعيين.',
|
||||
'login.forgotPasswordSubmit': 'إرسال الرابط',
|
||||
'login.forgotPasswordSentTitle': 'تحقق من بريدك',
|
||||
'login.forgotPasswordSentBody':
|
||||
'إذا كان هناك حساب مرتبط بهذا البريد، فإن الرابط في الطريق. تنتهي صلاحيته خلال 60 دقيقة.',
|
||||
'login.forgotPasswordSmtpHintOff':
|
||||
'ملاحظة: لم يقم المسؤول بتكوين SMTP، لذا سيتم كتابة رابط إعادة التعيين في وحدة تحكم الخادم بدلاً من إرساله عبر البريد الإلكتروني.',
|
||||
'login.backToLogin': 'العودة إلى تسجيل الدخول',
|
||||
'login.newPassword': 'كلمة المرور الجديدة',
|
||||
'login.confirmPassword': 'تأكيد كلمة المرور الجديدة',
|
||||
'login.passwordsDontMatch': 'كلمتا المرور غير متطابقتين',
|
||||
'login.mfaCode': 'رمز 2FA',
|
||||
'login.resetPasswordTitle': 'ضبط كلمة مرور جديدة',
|
||||
'login.resetPasswordBody':
|
||||
'اختر كلمة مرور قوية لم تستخدمها هنا من قبل. 8 أحرف على الأقل.',
|
||||
'login.resetPasswordMfaBody':
|
||||
'أدخل رمز 2FA أو رمز النسخ الاحتياطي لإتمام إعادة التعيين.',
|
||||
'login.resetPasswordSubmit': 'إعادة تعيين كلمة المرور',
|
||||
'login.resetPasswordVerify': 'تحقق وأعد التعيين',
|
||||
'login.resetPasswordSuccessTitle': 'تم تحديث كلمة المرور',
|
||||
'login.resetPasswordSuccessBody':
|
||||
'يمكنك الآن تسجيل الدخول بكلمة المرور الجديدة.',
|
||||
'login.resetPasswordInvalidLink': 'رابط إعادة تعيين غير صالح',
|
||||
'login.resetPasswordInvalidLinkBody':
|
||||
'هذا الرابط مفقود أو تالف. اطلب رابطًا جديدًا للمتابعة.',
|
||||
'login.resetPasswordFailed': 'فشلت إعادة التعيين. ربما انتهت صلاحية الرابط.',
|
||||
};
|
||||
export default login;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const map: TranslationStrings = {
|
||||
'map.connections': 'الاتصالات',
|
||||
'map.showConnections': 'عرض مسارات الحجوزات',
|
||||
'map.hideConnections': 'إخفاء مسارات الحجوزات',
|
||||
};
|
||||
export default map;
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const members: TranslationStrings = {
|
||||
'members.shareTrip': 'مشاركة الرحلة',
|
||||
'members.inviteUser': 'دعوة مستخدم',
|
||||
'members.selectUser': 'اختر مستخدمًا…',
|
||||
'members.invite': 'دعوة',
|
||||
'members.allHaveAccess': 'جميع المستخدمين لديهم صلاحية الوصول بالفعل.',
|
||||
'members.access': 'الصلاحية',
|
||||
'members.person': 'شخص',
|
||||
'members.persons': 'أشخاص',
|
||||
'members.you': 'أنت',
|
||||
'members.owner': 'المالك',
|
||||
'members.leaveTrip': 'مغادرة الرحلة',
|
||||
'members.removeAccess': 'إزالة الصلاحية',
|
||||
'members.confirmLeave': 'مغادرة الرحلة؟ ستفقد صلاحية الوصول.',
|
||||
'members.confirmRemove': 'إزالة صلاحية هذا المستخدم؟',
|
||||
'members.loadError': 'فشل تحميل الأعضاء',
|
||||
'members.added': 'تمت الإضافة',
|
||||
'members.addError': 'فشلت الإضافة',
|
||||
'members.removed': 'تمت إزالة العضو',
|
||||
'members.removeError': 'فشلت الإزالة',
|
||||
};
|
||||
export default members;
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const memories: TranslationStrings = {
|
||||
'memories.title': 'صور',
|
||||
'memories.notConnected': 'Immich غير متصل',
|
||||
'memories.notConnectedHint':
|
||||
'قم بتوصيل Immich في الإعدادات لعرض صور رحلتك هنا.',
|
||||
'memories.notConnectedMultipleHint':
|
||||
'قم بتوصيل أحد موفري الصور هؤلاء: {provider_names} في الإعدادات لتتمكن من إضافة صور إلى هذه الرحلة.',
|
||||
'memories.noDates': 'أضف تواريخ لرحلتك لتحميل الصور.',
|
||||
'memories.noPhotos': 'لم يتم العثور على صور',
|
||||
'memories.noPhotosHint': 'لم يتم العثور على صور في Immich لفترة هذه الرحلة.',
|
||||
'memories.photosFound': 'صور',
|
||||
'memories.fromOthers': 'من آخرين',
|
||||
'memories.sharePhotos': 'مشاركة الصور',
|
||||
'memories.sharing': 'مشترك',
|
||||
'memories.reviewTitle': 'مراجعة صورك',
|
||||
'memories.reviewHint': 'انقر على الصور لاستبعادها من المشاركة.',
|
||||
'memories.shareCount': 'مشاركة {count} صور',
|
||||
'memories.providerUrl': 'عنوان URL للخادم',
|
||||
'memories.providerApiKey': 'مفتاح API',
|
||||
'memories.providerUsername': 'اسم المستخدم',
|
||||
'memories.providerPassword': 'كلمة المرور',
|
||||
'memories.providerOTP': 'رمز MFA (إذا كان مفعلاً)',
|
||||
'memories.skipSSLVerification': 'تخطي التحقق من شهادة SSL',
|
||||
'memories.immichAutoUpload': 'نسخ صور الرحلة إلى Immich عند الرفع',
|
||||
'memories.providerUrlHintSynology':
|
||||
'أدرج مسار تطبيق Photos في URL، مثل https://nas:5001/photo',
|
||||
'memories.testConnection': 'اختبار الاتصال',
|
||||
'memories.testShort': 'اختبار',
|
||||
'memories.testFirst': 'اختبر الاتصال أولاً',
|
||||
'memories.connected': 'متصل',
|
||||
'memories.disconnected': 'غير متصل',
|
||||
'memories.connectionSuccess': 'تم الاتصال بـ Immich',
|
||||
'memories.connectionError': 'تعذر الاتصال بـ Immich',
|
||||
'memories.saved': 'تم حفظ إعدادات {provider_name}',
|
||||
'memories.providerDisconnectedBanner':
|
||||
'اتصالك بـ {provider_name} مفقود. أعد الاتصال في الإعدادات لعرض الصور.',
|
||||
'memories.saveError': 'تعذّر حفظ إعدادات {provider_name}',
|
||||
'memories.addPhotos': 'إضافة صور',
|
||||
'memories.linkAlbum': 'ربط ألبوم',
|
||||
'memories.selectAlbum': 'اختيار ألبوم Immich',
|
||||
'memories.selectAlbumMultiple': 'اختيار ألبوم',
|
||||
'memories.noAlbums': 'لم يتم العثور على ألبومات',
|
||||
'memories.syncAlbum': 'مزامنة الألبوم',
|
||||
'memories.unlinkAlbum': 'إلغاء الربط',
|
||||
'memories.photos': 'صور',
|
||||
'memories.selectPhotos': 'اختيار صور من Immich',
|
||||
'memories.selectPhotosMultiple': 'اختيار الصور',
|
||||
'memories.selectHint': 'انقر على الصور لتحديدها.',
|
||||
'memories.selected': 'محدد',
|
||||
'memories.addSelected': 'إضافة {count} صور',
|
||||
'memories.alreadyAdded': 'تمت الإضافة',
|
||||
'memories.private': 'خاص',
|
||||
'memories.stopSharing': 'إيقاف المشاركة',
|
||||
'memories.oldest': 'الأقدم أولاً',
|
||||
'memories.newest': 'الأحدث أولاً',
|
||||
'memories.allLocations': 'جميع المواقع',
|
||||
'memories.tripDates': 'تواريخ الرحلة',
|
||||
'memories.allPhotos': 'جميع الصور',
|
||||
'memories.confirmShareTitle': 'مشاركة مع أعضاء الرحلة؟',
|
||||
'memories.confirmShareHint':
|
||||
'{count} صور ستكون مرئية لجميع أعضاء هذه الرحلة. يمكنك جعل الصور الفردية خاصة لاحقًا.',
|
||||
'memories.confirmShareButton': 'مشاركة الصور',
|
||||
'memories.error.loadAlbums': 'فشل تحميل الألبومات',
|
||||
'memories.error.linkAlbum': 'فشل ربط الألبوم',
|
||||
'memories.error.unlinkAlbum': 'فشل إلغاء ربط الألبوم',
|
||||
'memories.error.syncAlbum': 'فشل مزامنة الألبوم',
|
||||
'memories.error.loadPhotos': 'فشل تحميل الصور',
|
||||
'memories.error.addPhotos': 'فشل إضافة الصور',
|
||||
'memories.error.removePhoto': 'فشل حذف الصورة',
|
||||
'memories.error.toggleSharing': 'فشل تحديث إعدادات المشاركة',
|
||||
'memories.saveRouteNotConfigured': 'مسار الحفظ غير مهيأ لهذا المزود',
|
||||
'memories.testRouteNotConfigured': 'مسار الاختبار غير مهيأ لهذا المزود',
|
||||
'memories.fillRequiredFields': 'يرجى ملء جميع الحقول المطلوبة',
|
||||
};
|
||||
export default memories;
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const nav: TranslationStrings = {
|
||||
'nav.trip': 'الرحلة',
|
||||
'nav.share': 'مشاركة',
|
||||
'nav.settings': 'الإعدادات',
|
||||
'nav.admin': 'الإدارة',
|
||||
'nav.logout': 'تسجيل الخروج',
|
||||
'nav.lightMode': 'الوضع الفاتح',
|
||||
'nav.darkMode': 'الوضع الداكن',
|
||||
'nav.autoMode': 'الوضع التلقائي',
|
||||
'nav.administrator': 'المسؤول',
|
||||
'nav.myTrips': 'رحلاتي',
|
||||
};
|
||||
export default nav;
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const notif: TranslationStrings = {
|
||||
'notif.test.title': '[اختبار] إشعار',
|
||||
'notif.test.simple.text': 'هذا إشعار اختبار بسيط.',
|
||||
'notif.test.boolean.text': 'هل تقبل هذا الإشعار الاختباري؟',
|
||||
'notif.test.navigate.text': 'انقر أدناه للانتقال إلى لوحة التحكم.',
|
||||
'notif.trip_invite.title': 'دعوة للرحلة',
|
||||
'notif.trip_invite.text': '{actor} دعاك إلى {trip}',
|
||||
'notif.booking_change.title': 'تم تحديث الحجز',
|
||||
'notif.booking_change.text': '{actor} حدّث حجزاً في {trip}',
|
||||
'notif.trip_reminder.title': 'تذكير بالرحلة',
|
||||
'notif.trip_reminder.text': 'رحلتك {trip} تقترب!',
|
||||
'notif.todo_due.title': 'مهمة مستحقة',
|
||||
'notif.todo_due.text': '{todo} في {trip} مستحقة في {due}',
|
||||
'notif.vacay_invite.title': 'دعوة دمج الإجازة',
|
||||
'notif.vacay_invite.text': '{actor} يدعوك لدمج خطط الإجازة',
|
||||
'notif.photos_shared.title': 'تمت مشاركة الصور',
|
||||
'notif.photos_shared.text': '{actor} شارك {count} صورة في {trip}',
|
||||
'notif.collab_message.title': 'رسالة جديدة',
|
||||
'notif.collab_message.text': '{actor} أرسل رسالة في {trip}',
|
||||
'notif.packing_tagged.title': 'مهمة التعبئة',
|
||||
'notif.packing_tagged.text': '{actor} عيّنك في {category} في {trip}',
|
||||
'notif.version_available.title': 'إصدار جديد متاح',
|
||||
'notif.version_available.text': 'TREK {version} متاح الآن',
|
||||
'notif.action.view_trip': 'عرض الرحلة',
|
||||
'notif.action.view_collab': 'عرض الرسائل',
|
||||
'notif.action.view_packing': 'عرض التعبئة',
|
||||
'notif.action.view_photos': 'عرض الصور',
|
||||
'notif.action.view_vacay': 'عرض Vacay',
|
||||
'notif.action.view_admin': 'الذهاب للإدارة',
|
||||
'notif.action.view': 'عرض',
|
||||
'notif.action.accept': 'قبول',
|
||||
'notif.action.decline': 'رفض',
|
||||
'notif.generic.title': 'إشعار',
|
||||
'notif.generic.text': 'لديك إشعار جديد',
|
||||
'notif.dev.unknown_event.title': '[DEV] حدث غير معروف',
|
||||
'notif.dev.unknown_event.text':
|
||||
'نوع الحدث "{event}" غير مسجل في EVENT_NOTIFICATION_CONFIG',
|
||||
};
|
||||
export default notif;
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const notifications: TranslationStrings = {
|
||||
'notifications.title': 'الإشعارات',
|
||||
'notifications.markAllRead': 'تحديد الكل كمقروء',
|
||||
'notifications.deleteAll': 'حذف الكل',
|
||||
'notifications.showAll': 'عرض جميع الإشعارات',
|
||||
'notifications.empty': 'لا توجد إشعارات',
|
||||
'notifications.emptyDescription': 'لقد اطلعت على كل شيء!',
|
||||
'notifications.all': 'الكل',
|
||||
'notifications.unreadOnly': 'غير مقروء',
|
||||
'notifications.markRead': 'تحديد كمقروء',
|
||||
'notifications.markUnread': 'تحديد كغير مقروء',
|
||||
'notifications.delete': 'حذف',
|
||||
'notifications.system': 'النظام',
|
||||
'notifications.synologySessionCleared.title': 'تم قطع اتصال Synology Photos',
|
||||
'notifications.synologySessionCleared.text':
|
||||
'تغير خادمك أو حسابك — انتقل إلى الإعدادات لاختبار اتصالك مرة أخرى.',
|
||||
'notifications.versionAvailable.title': 'تحديث متاح',
|
||||
'notifications.versionAvailable.text': 'TREK {version} متاح الآن.',
|
||||
'notifications.versionAvailable.button': 'عرض التفاصيل',
|
||||
'notifications.test.title': 'إشعار تجريبي من {actor}',
|
||||
'notifications.test.text': 'هذا إشعار تجريبي بسيط.',
|
||||
'notifications.test.booleanTitle': 'يطلب منك {actor} الموافقة',
|
||||
'notifications.test.booleanText': 'إشعار تجريبي يتطلب إجابة.',
|
||||
'notifications.test.accept': 'موافقة',
|
||||
'notifications.test.decline': 'رفض',
|
||||
'notifications.test.navigateTitle': 'تحقق من شيء ما',
|
||||
'notifications.test.navigateText': 'إشعار تجريبي للتنقل.',
|
||||
'notifications.test.goThere': 'اذهب إلى هناك',
|
||||
'notifications.test.adminTitle': 'إذاعة المسؤول',
|
||||
'notifications.test.adminText':
|
||||
'أرسل {actor} إشعاراً تجريبياً لجميع المسؤولين.',
|
||||
'notifications.test.tripTitle': 'نشر {actor} في رحلتك',
|
||||
'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".',
|
||||
};
|
||||
export default notifications;
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const oauth: TranslationStrings = {
|
||||
'oauth.scope.group.trips': 'الرحلات',
|
||||
'oauth.scope.group.places': 'الأماكن',
|
||||
'oauth.scope.group.packing': 'الأمتعة',
|
||||
'oauth.scope.group.todos': 'المهام',
|
||||
'oauth.scope.group.budget': 'الميزانية',
|
||||
'oauth.scope.group.reservations': 'الحجوزات',
|
||||
'oauth.scope.group.collab': 'التعاون',
|
||||
'oauth.scope.group.notifications': 'الإشعارات',
|
||||
'oauth.scope.group.vacay': 'الإجازة',
|
||||
'oauth.scope.group.weather': 'الطقس',
|
||||
'oauth.scope.group.journey': 'مذكرة السفر',
|
||||
'oauth.scope.trips:read.label': 'عرض الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:read.description':
|
||||
'قراءة الرحلات والأيام والملاحظات والأعضاء',
|
||||
'oauth.scope.trips:write.label': 'تحرير الرحلات وخطط السفر',
|
||||
'oauth.scope.trips:write.description':
|
||||
'إنشاء وتحديث الرحلات والأيام والملاحظات وإدارة الأعضاء',
|
||||
'oauth.scope.trips:delete.label': 'حذف الرحلات',
|
||||
'oauth.scope.trips:delete.description':
|
||||
'حذف الرحلات بأكملها نهائياً — هذا الإجراء لا يمكن التراجع عنه',
|
||||
'oauth.scope.trips:share.label': 'إدارة روابط المشاركة',
|
||||
'oauth.scope.trips:share.description':
|
||||
'إنشاء روابط مشاركة عامة وتحديثها وإلغاؤها',
|
||||
'oauth.scope.places:read.label': 'عرض الأماكن وبيانات الخريطة',
|
||||
'oauth.scope.places:read.description':
|
||||
'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
|
||||
'oauth.scope.places:write.label': 'إدارة الأماكن',
|
||||
'oauth.scope.places:write.description':
|
||||
'إنشاء وتحديث وحذف الأماكن والتعيينات والعلامات',
|
||||
'oauth.scope.atlas:read.label': 'عرض Atlas',
|
||||
'oauth.scope.atlas:read.description':
|
||||
'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
|
||||
'oauth.scope.atlas:write.label': 'إدارة Atlas',
|
||||
'oauth.scope.atlas:write.description':
|
||||
'تعليم الدول والمناطق كمزارة، وإدارة قائمة الأمنيات',
|
||||
'oauth.scope.packing:read.label': 'عرض قوائم الأمتعة',
|
||||
'oauth.scope.packing:read.description':
|
||||
'قراءة عناصر الأمتعة والحقائب ومُسنَدي الفئات',
|
||||
'oauth.scope.packing:write.label': 'إدارة قوائم الأمتعة',
|
||||
'oauth.scope.packing:write.description':
|
||||
'إضافة وتحديث وحذف وتبديل وإعادة ترتيب عناصر الأمتعة والحقائب',
|
||||
'oauth.scope.todos:read.label': 'عرض قوائم المهام',
|
||||
'oauth.scope.todos:read.description': 'قراءة مهام الرحلة ومُسنَدي الفئات',
|
||||
'oauth.scope.todos:write.label': 'إدارة قوائم المهام',
|
||||
'oauth.scope.todos:write.description':
|
||||
'إنشاء وتحديث وتبديل وحذف وإعادة ترتيب المهام',
|
||||
'oauth.scope.budget:read.label': 'عرض الميزانية',
|
||||
'oauth.scope.budget:read.description': 'قراءة بنود الميزانية وتفاصيل النفقات',
|
||||
'oauth.scope.budget:write.label': 'إدارة الميزانية',
|
||||
'oauth.scope.budget:write.description': 'إنشاء وتحديث وحذف بنود الميزانية',
|
||||
'oauth.scope.reservations:read.label': 'عرض الحجوزات',
|
||||
'oauth.scope.reservations:read.description': 'قراءة الحجوزات وتفاصيل الإقامة',
|
||||
'oauth.scope.reservations:write.label': 'إدارة الحجوزات',
|
||||
'oauth.scope.reservations:write.description':
|
||||
'إنشاء وتحديث وحذف وإعادة ترتيب الحجوزات',
|
||||
'oauth.scope.collab:read.label': 'عرض التعاون',
|
||||
'oauth.scope.collab:read.description':
|
||||
'قراءة ملاحظات التعاون والاستطلاعات والرسائل',
|
||||
'oauth.scope.collab:write.label': 'إدارة التعاون',
|
||||
'oauth.scope.collab:write.description':
|
||||
'إنشاء وتحديث وحذف الملاحظات والاستطلاعات والرسائل التعاونية',
|
||||
'oauth.scope.notifications:read.label': 'عرض الإشعارات',
|
||||
'oauth.scope.notifications:read.description':
|
||||
'قراءة إشعارات التطبيق وأعداد غير المقروءة',
|
||||
'oauth.scope.notifications:write.label': 'إدارة الإشعارات',
|
||||
'oauth.scope.notifications:write.description':
|
||||
'تعليم الإشعارات كمقروءة والرد عليها',
|
||||
'oauth.scope.vacay:read.label': 'عرض خطط الإجازة',
|
||||
'oauth.scope.vacay:read.description':
|
||||
'قراءة بيانات تخطيط الإجازة والإدخالات والإحصاءات',
|
||||
'oauth.scope.vacay:write.label': 'إدارة خطط الإجازة',
|
||||
'oauth.scope.vacay:write.description':
|
||||
'إنشاء وإدارة إدخالات الإجازة والعطلات وخطط الفريق',
|
||||
'oauth.scope.geo:read.label': 'الخرائط والترميز الجغرافي',
|
||||
'oauth.scope.geo:read.description':
|
||||
'البحث عن مواقع وحل عناوين الخرائط والترميز الجغرافي العكسي للإحداثيات',
|
||||
'oauth.scope.weather:read.label': 'توقعات الطقس',
|
||||
'oauth.scope.weather:read.description':
|
||||
'جلب توقعات الطقس لمواقع الرحلة وتواريخها',
|
||||
'oauth.scope.journey:read.label': 'عرض مذكرات السفر',
|
||||
'oauth.scope.journey:read.description':
|
||||
'قراءة مذكرات السفر والمدخلات وقائمة المساهمين',
|
||||
'oauth.scope.journey:write.label': 'إدارة مذكرات السفر',
|
||||
'oauth.scope.journey:write.description':
|
||||
'إنشاء مذكرات السفر وتحديثها وحذفها وإدخالاتها',
|
||||
'oauth.scope.journey:share.label': 'إدارة روابط مذكرات السفر',
|
||||
'oauth.scope.journey:share.description':
|
||||
'إنشاء روابط مشاركة عامة لمذكرات السفر وتحديثها وإلغاؤها',
|
||||
};
|
||||
export default oauth;
|
||||
@@ -0,0 +1,184 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const packing: TranslationStrings = {
|
||||
'packing.title': 'قائمة التجهيز',
|
||||
'packing.empty': 'قائمة التجهيز فارغة',
|
||||
'packing.import': 'استيراد',
|
||||
'packing.importTitle': 'استيراد قائمة التعبئة',
|
||||
'packing.importHint':
|
||||
'عنصر واحد لكل سطر. يمكن إضافة الفئة والكمية مفصولة بفاصلة أو فاصلة منقوطة أو علامة تبويب: الاسم، الفئة، الكمية',
|
||||
'packing.importPlaceholder':
|
||||
'فرشاة أسنان\nواقي شمس، نظافة\nقمصان، ملابس، 5\nجواز سفر، مستندات',
|
||||
'packing.importCsv': 'تحميل CSV/TXT',
|
||||
'packing.importAction': 'استيراد {count}',
|
||||
'packing.importSuccess': 'تم استيراد {count} عنصر',
|
||||
'packing.importError': 'فشل الاستيراد',
|
||||
'packing.importEmpty': 'لا توجد عناصر للاستيراد',
|
||||
'packing.progress': '{packed} من {total} جُهّز ({percent}%)',
|
||||
'packing.clearChecked': 'إزالة {count} محدد',
|
||||
'packing.clearCheckedShort': 'إزالة {count}',
|
||||
'packing.suggestions': 'اقتراحات',
|
||||
'packing.suggestionsTitle': 'إضافة اقتراحات',
|
||||
'packing.allSuggested': 'تمت إضافة جميع الاقتراحات',
|
||||
'packing.allPacked': 'تم تجهيز الكل!',
|
||||
'packing.addPlaceholder': 'إضافة عنصر جديد...',
|
||||
'packing.categoryPlaceholder': 'الفئة...',
|
||||
'packing.filterAll': 'الكل',
|
||||
'packing.filterOpen': 'مفتوح',
|
||||
'packing.filterDone': 'تم',
|
||||
'packing.emptyTitle': 'قائمة التجهيز فارغة',
|
||||
'packing.emptyHint': 'أضف عناصر أو استخدم الاقتراحات',
|
||||
'packing.emptyFiltered': 'لا توجد عناصر مطابقة لهذا الفلتر',
|
||||
'packing.menuRename': 'إعادة تسمية',
|
||||
'packing.menuCheckAll': 'تحديد الكل',
|
||||
'packing.menuUncheckAll': 'إلغاء تحديد الكل',
|
||||
'packing.menuDeleteCat': 'حذف الفئة',
|
||||
'packing.noMembers': 'لا أعضاء',
|
||||
'packing.addItem': 'إضافة عنصر',
|
||||
'packing.addItemPlaceholder': 'اسم العنصر...',
|
||||
'packing.addCategory': 'إضافة فئة',
|
||||
'packing.newCategoryPlaceholder': 'اسم الفئة (مثال: ملابس)',
|
||||
'packing.applyTemplate': 'تطبيق قالب',
|
||||
'packing.template': 'قالب',
|
||||
'packing.templateApplied': 'تمت إضافة {count} عنصر من القالب',
|
||||
'packing.templateError': 'فشل تطبيق القالب',
|
||||
'packing.saveAsTemplate': 'حفظ كقالب',
|
||||
'packing.templateName': 'اسم القالب',
|
||||
'packing.templateSaved': 'تم حفظ قائمة الحقائب كقالب',
|
||||
'packing.bags': 'أمتعة',
|
||||
'packing.noBag': 'غير معيّن',
|
||||
'packing.totalWeight': 'الوزن الإجمالي',
|
||||
'packing.bagName': 'الاسم...',
|
||||
'packing.addBag': 'إضافة أمتعة',
|
||||
'packing.changeCategory': 'تغيير الفئة',
|
||||
'packing.confirm.clearChecked': 'هل تريد إزالة {count} عنصر محدد؟',
|
||||
'packing.confirm.deleteCat': 'هل تريد حذف الفئة "{name}" مع {count} عنصر؟',
|
||||
'packing.defaultCategory': 'أخرى',
|
||||
'packing.toast.saveError': 'فشل الحفظ',
|
||||
'packing.toast.deleteError': 'فشل الحذف',
|
||||
'packing.toast.renameError': 'فشلت إعادة التسمية',
|
||||
'packing.toast.addError': 'فشلت الإضافة',
|
||||
'packing.suggestions.items': [
|
||||
{
|
||||
name: 'جواز السفر',
|
||||
category: 'المستندات',
|
||||
},
|
||||
{
|
||||
name: 'بطاقة الهوية',
|
||||
category: 'المستندات',
|
||||
},
|
||||
{
|
||||
name: 'تأمين السفر',
|
||||
category: 'المستندات',
|
||||
},
|
||||
{
|
||||
name: 'تذاكر الطيران',
|
||||
category: 'المستندات',
|
||||
},
|
||||
{
|
||||
name: 'بطاقة ائتمان',
|
||||
category: 'المالية',
|
||||
},
|
||||
{
|
||||
name: 'نقد',
|
||||
category: 'المالية',
|
||||
},
|
||||
{
|
||||
name: 'تأشيرة',
|
||||
category: 'المستندات',
|
||||
},
|
||||
{
|
||||
name: 'قمصان',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'بنطلونات',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'ملابس داخلية',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'جوارب',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'جاكيت',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'ملابس نوم',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'ملابس سباحة',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'معطف مطر',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'أحذية مريحة',
|
||||
category: 'الملابس',
|
||||
},
|
||||
{
|
||||
name: 'فرشاة أسنان',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'معجون أسنان',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'شامبو',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'مزيل عرق',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'واقي شمس',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'شفرة حلاقة',
|
||||
category: 'أدوات العناية',
|
||||
},
|
||||
{
|
||||
name: 'شاحن',
|
||||
category: 'الإلكترونيات',
|
||||
},
|
||||
{
|
||||
name: 'بطارية محمولة',
|
||||
category: 'الإلكترونيات',
|
||||
},
|
||||
{
|
||||
name: 'سماعات',
|
||||
category: 'الإلكترونيات',
|
||||
},
|
||||
{
|
||||
name: 'محول سفر',
|
||||
category: 'الإلكترونيات',
|
||||
},
|
||||
{
|
||||
name: 'كاميرا',
|
||||
category: 'الإلكترونيات',
|
||||
},
|
||||
{
|
||||
name: 'مسكنات ألم',
|
||||
category: 'الصحة',
|
||||
},
|
||||
{
|
||||
name: 'لاصقات جروح',
|
||||
category: 'الصحة',
|
||||
},
|
||||
{
|
||||
name: 'مطهر',
|
||||
category: 'الصحة',
|
||||
},
|
||||
],
|
||||
};
|
||||
export default packing;
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const pdf: TranslationStrings = {
|
||||
'pdf.travelPlan': 'خطة السفر',
|
||||
'pdf.planned': 'مخطط',
|
||||
'pdf.costLabel': 'التكلفة EUR',
|
||||
'pdf.preview': 'معاينة PDF',
|
||||
'pdf.saveAsPdf': 'حفظ كـ PDF',
|
||||
};
|
||||
export default pdf;
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const perm: TranslationStrings = {
|
||||
'perm.title': 'إعدادات الصلاحيات',
|
||||
'perm.subtitle': 'التحكم في من يمكنه تنفيذ الإجراءات عبر التطبيق',
|
||||
'perm.saved': 'تم حفظ إعدادات الصلاحيات',
|
||||
'perm.resetDefaults': 'إعادة التعيين إلى الافتراضي',
|
||||
'perm.customized': 'مخصص',
|
||||
'perm.level.admin': 'المسؤول فقط',
|
||||
'perm.level.tripOwner': 'مالك الرحلة',
|
||||
'perm.level.tripMember': 'أعضاء الرحلة',
|
||||
'perm.level.everybody': 'الجميع',
|
||||
'perm.cat.trip': 'إدارة الرحلات',
|
||||
'perm.cat.members': 'إدارة الأعضاء',
|
||||
'perm.cat.files': 'الملفات',
|
||||
'perm.cat.content': 'المحتوى والجدول الزمني',
|
||||
'perm.cat.extras': 'الميزانية والتعبئة والتعاون',
|
||||
'perm.action.trip_create': 'إنشاء رحلات',
|
||||
'perm.action.trip_edit': 'تعديل تفاصيل الرحلة',
|
||||
'perm.action.trip_delete': 'حذف الرحلات',
|
||||
'perm.action.trip_archive': 'أرشفة / إلغاء أرشفة الرحلات',
|
||||
'perm.action.trip_cover_upload': 'رفع صورة الغلاف',
|
||||
'perm.action.member_manage': 'إضافة / إزالة الأعضاء',
|
||||
'perm.action.file_upload': 'رفع الملفات',
|
||||
'perm.action.file_edit': 'تعديل بيانات الملف',
|
||||
'perm.action.file_delete': 'حذف الملفات',
|
||||
'perm.action.place_edit': 'إضافة / تعديل / حذف الأماكن',
|
||||
'perm.action.day_edit': 'تعديل الأيام والملاحظات والتعيينات',
|
||||
'perm.action.reservation_edit': 'إدارة الحجوزات',
|
||||
'perm.action.budget_edit': 'إدارة الميزانية',
|
||||
'perm.action.packing_edit': 'إدارة قوائم التعبئة',
|
||||
'perm.action.collab_edit': 'التعاون (ملاحظات، استطلاعات، دردشة)',
|
||||
'perm.action.share_manage': 'إدارة روابط المشاركة',
|
||||
'perm.actionHint.trip_create': 'من يمكنه إنشاء رحلات جديدة',
|
||||
'perm.actionHint.trip_edit':
|
||||
'من يمكنه تغيير اسم الرحلة والتواريخ والوصف والعملة',
|
||||
'perm.actionHint.trip_delete': 'من يمكنه حذف رحلة نهائياً',
|
||||
'perm.actionHint.trip_archive': 'من يمكنه أرشفة أو إلغاء أرشفة رحلة',
|
||||
'perm.actionHint.trip_cover_upload': 'من يمكنه رفع أو تغيير صورة الغلاف',
|
||||
'perm.actionHint.member_manage': 'من يمكنه دعوة أو إزالة أعضاء الرحلة',
|
||||
'perm.actionHint.file_upload': 'من يمكنه رفع ملفات إلى رحلة',
|
||||
'perm.actionHint.file_edit': 'من يمكنه تعديل أوصاف الملفات والروابط',
|
||||
'perm.actionHint.file_delete':
|
||||
'من يمكنه نقل الملفات إلى سلة المهملات أو حذفها نهائياً',
|
||||
'perm.actionHint.place_edit': 'من يمكنه إضافة أو تعديل أو حذف الأماكن',
|
||||
'perm.actionHint.day_edit':
|
||||
'من يمكنه تعديل الأيام وملاحظات الأيام وتعيينات الأماكن',
|
||||
'perm.actionHint.reservation_edit': 'من يمكنه إنشاء أو تعديل أو حذف الحجوزات',
|
||||
'perm.actionHint.budget_edit':
|
||||
'من يمكنه إنشاء أو تعديل أو حذف عناصر الميزانية',
|
||||
'perm.actionHint.packing_edit': 'من يمكنه إدارة عناصر التعبئة والحقائب',
|
||||
'perm.actionHint.collab_edit':
|
||||
'من يمكنه إنشاء ملاحظات واستطلاعات وإرسال رسائل',
|
||||
'perm.actionHint.share_manage': 'من يمكنه إنشاء أو حذف روابط المشاركة العامة',
|
||||
};
|
||||
export default perm;
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const photos: TranslationStrings = {
|
||||
'photos.title': 'صور',
|
||||
'photos.subtitle': '{count} صورة لـ {trip}',
|
||||
'photos.dropHere': 'أسقط الصور هنا...',
|
||||
'photos.dropHereActive': 'أسقط الصور هنا',
|
||||
'photos.captionForAll': 'تعليق (للجميع)',
|
||||
'photos.captionPlaceholder': 'تعليق اختياري...',
|
||||
'photos.addCaption': 'إضافة تعليق...',
|
||||
'photos.allDays': 'كل الأيام',
|
||||
'photos.noPhotos': 'لا توجد صور بعد',
|
||||
'photos.uploadHint': 'ارفع صور رحلتك',
|
||||
'photos.clickToSelect': 'أو انقر للاختيار',
|
||||
'photos.linkPlace': 'ربط بمكان',
|
||||
'photos.noPlace': 'بلا مكان',
|
||||
'photos.uploadN': 'رفع {n} صورة',
|
||||
'photos.linkDay': 'ربط اليوم',
|
||||
'photos.noDay': 'لا يوم',
|
||||
'photos.dayLabel': 'اليوم {number}',
|
||||
'photos.photoSelected': 'صورة محددة',
|
||||
'photos.photosSelected': 'صور محددة',
|
||||
'photos.fileTypeHint':
|
||||
'JPG, PNG, WebP · الحد الأقصى 10 ميغابايت · حتى 30 صورة',
|
||||
};
|
||||
export default photos;
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const places: TranslationStrings = {
|
||||
'places.addPlace': 'إضافة مكان/نشاط',
|
||||
'places.importFile': 'استيراد ملف',
|
||||
'places.sidebarDrop': 'أفلت للاستيراد',
|
||||
'places.importFileHint':
|
||||
'استورد ملفات .gpx أو .kml أو .kmz من أدوات مثل Google My Maps وGoogle Earth أو جهاز تتبع GPS.',
|
||||
'places.importFileDropHere': 'انقر لاختيار ملف أو اسحبه وأفلته هنا',
|
||||
'places.importFileDropActive': 'أفلت الملف للاختيار',
|
||||
'places.importFileUnsupported':
|
||||
'نوع الملف غير مدعوم. استخدم .gpx أو .kml أو .kmz.',
|
||||
'places.importFileTooLarge':
|
||||
'الملف كبير جدًا. الحد الأقصى لحجم الرفع هو {maxMb} MB.',
|
||||
'places.importFileError': 'فشل الاستيراد',
|
||||
'places.importAllSkipped': 'جميع الأماكن موجودة بالفعل في الرحلة.',
|
||||
'places.gpxImported': 'تم استيراد {count} مكان من GPX',
|
||||
'places.gpxImportTypes': 'ما الذي تريد استيراده؟',
|
||||
'places.gpxImportWaypoints': 'نقاط الطريق',
|
||||
'places.gpxImportRoutes': 'المسارات',
|
||||
'places.gpxImportTracks': 'المسارات (مع هندسة الطريق)',
|
||||
'places.gpxImportNoneSelected': 'اختر نوعاً واحداً على الأقل للاستيراد.',
|
||||
'places.kmlImportTypes': 'ما الذي تريد استيراده؟',
|
||||
'places.kmlImportPoints': 'نقاط (Placemarks)',
|
||||
'places.kmlImportPaths': 'مسارات (LineStrings)',
|
||||
'places.kmlImportNoneSelected': 'اختر نوعًا واحدًا على الأقل.',
|
||||
'places.selectionCount': '{count} محدد',
|
||||
'places.deleteSelected': 'حذف المحدد',
|
||||
'places.kmlKmzImported': 'تم استيراد {count} مكان من KMZ/KML',
|
||||
'places.urlResolved': 'تم استيراد المكان من الرابط',
|
||||
'places.importList': 'استيراد قائمة',
|
||||
'places.kmlKmzSummaryValues':
|
||||
'علامات المواضع: {total} • تم الاستيراد: {created} • تم التجاوز: {skipped}',
|
||||
'places.importGoogleList': 'قائمة Google',
|
||||
'places.importNaverList': 'قائمة Naver',
|
||||
'places.googleListHint':
|
||||
'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
|
||||
'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"',
|
||||
'places.googleListError': 'فشل استيراد قائمة Google Maps',
|
||||
'places.naverListHint':
|
||||
'الصق رابط قائمة Naver Maps مشتركة لاستيراد جميع الأماكن.',
|
||||
'places.naverListImported': 'تم استيراد {count} مكان من "{list}"',
|
||||
'places.naverListError': 'فشل استيراد قائمة Naver Maps',
|
||||
'places.viewDetails': 'عرض التفاصيل',
|
||||
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
|
||||
'places.all': 'الكل',
|
||||
'places.unplanned': 'غير مخطط',
|
||||
'places.filterTracks': 'المسارات',
|
||||
'places.search': 'ابحث عن أماكن...',
|
||||
'places.allCategories': 'كل الفئات',
|
||||
'places.categoriesSelected': 'فئات',
|
||||
'places.clearFilter': 'مسح الفلتر',
|
||||
'places.count': '{count} أماكن',
|
||||
'places.countSingular': 'مكان واحد',
|
||||
'places.allPlanned': 'تم تخطيط جميع الأماكن',
|
||||
'places.noneFound': 'لم يتم العثور على أماكن',
|
||||
'places.editPlace': 'تعديل المكان',
|
||||
'places.formName': 'الاسم',
|
||||
'places.formNamePlaceholder': 'مثال: برج إيفل',
|
||||
'places.formDescription': 'الوصف',
|
||||
'places.formDescriptionPlaceholder': 'وصف مختصر...',
|
||||
'places.formAddress': 'العنوان',
|
||||
'places.formAddressPlaceholder': 'الشارع، المدينة، البلد',
|
||||
'places.formLat': 'خط العرض (مثال: 48.8566)',
|
||||
'places.formLng': 'خط الطول (مثال: 2.3522)',
|
||||
'places.formCategory': 'الفئة',
|
||||
'places.noCategory': 'بلا فئة',
|
||||
'places.categoryNamePlaceholder': 'اسم الفئة',
|
||||
'places.formTime': 'الوقت',
|
||||
'places.startTime': 'البداية',
|
||||
'places.endTime': 'النهاية',
|
||||
'places.endTimeBeforeStart': 'وقت النهاية قبل وقت البداية',
|
||||
'places.timeCollision': 'تداخل في الوقت مع:',
|
||||
'places.formWebsite': 'الموقع الإلكتروني',
|
||||
'places.formNotes': 'ملاحظات',
|
||||
'places.formNotesPlaceholder': 'ملاحظات شخصية...',
|
||||
'places.formReservation': 'حجز',
|
||||
'places.reservationNotesPlaceholder': 'ملاحظات الحجز، رقم التأكيد...',
|
||||
'places.mapsSearchPlaceholder': 'ابحث عن أماكن...',
|
||||
'places.mapsSearchError': 'فشل البحث عن المكان.',
|
||||
'places.loadingDetails': 'جارٍ تحميل تفاصيل المكان…',
|
||||
'places.osmHint':
|
||||
'يتم البحث عبر OpenStreetMap (بدون صور أو ساعات عمل أو تقييمات). أضف مفتاح Google API في الإعدادات للحصول على جميع التفاصيل.',
|
||||
'places.osmActive':
|
||||
'البحث عبر OpenStreetMap (بدون صور أو تقييمات أو ساعات عمل). أضف مفتاح Google API في الإعدادات لبيانات موسعة.',
|
||||
'places.categoryCreateError': 'فشل إنشاء الفئة',
|
||||
'places.nameRequired': 'يرجى إدخال اسم',
|
||||
'places.saveError': 'فشل الحفظ',
|
||||
};
|
||||
export default places;
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const planner: TranslationStrings = {
|
||||
'planner.places': 'الأماكن',
|
||||
'planner.bookings': 'الحجوزات',
|
||||
'planner.packingList': 'قائمة التجهيز',
|
||||
'planner.documents': 'المستندات',
|
||||
'planner.dayPlan': 'خطة اليوم',
|
||||
'planner.reservations': 'الحجوزات',
|
||||
'planner.minTwoPlaces': 'يلزم مكانان على الأقل مع إحداثيات',
|
||||
'planner.noGeoPlaces': 'لا توجد أماكن بإحداثيات',
|
||||
'planner.routeCalculated': 'تم حساب المسار',
|
||||
'planner.routeCalcFailed': 'تعذر حساب المسار',
|
||||
'planner.routeError': 'خطأ أثناء حساب المسار',
|
||||
'planner.icsExportFailed': 'فشل تصدير ICS',
|
||||
'planner.routeOptimized': 'تم تحسين المسار',
|
||||
'planner.reservationUpdated': 'تم تحديث الحجز',
|
||||
'planner.reservationAdded': 'تمت إضافة الحجز',
|
||||
'planner.confirmDeleteReservation': 'حذف الحجز؟',
|
||||
'planner.reservationDeleted': 'تم حذف الحجز',
|
||||
'planner.days': 'الأيام',
|
||||
'planner.allPlaces': 'كل الأماكن',
|
||||
'planner.totalPlaces': 'إجمالي {n} أماكن',
|
||||
'planner.noDaysPlanned': 'لا توجد أيام مخططة بعد',
|
||||
'planner.editTrip': 'تعديل الرحلة ←',
|
||||
'planner.placeOne': 'مكان واحد',
|
||||
'planner.placeN': '{n} أماكن',
|
||||
'planner.addNote': 'إضافة ملاحظة',
|
||||
'planner.noEntries': 'لا توجد عناصر لهذا اليوم',
|
||||
'planner.addPlace': 'إضافة مكان/نشاط',
|
||||
'planner.addPlaceShort': '+ إضافة مكان/نشاط',
|
||||
'planner.resPending': 'حجز قيد الانتظار · ',
|
||||
'planner.resConfirmed': 'حجز مؤكد · ',
|
||||
'planner.notePlaceholder': 'ملاحظة…',
|
||||
'planner.noteTimePlaceholder': 'الوقت (اختياري)',
|
||||
'planner.noteExamplePlaceholder':
|
||||
'مثال: S3 الساعة 14:30 من المحطة المركزية، عبّارة من الرصيف 7، استراحة غداء…',
|
||||
'planner.totalCost': 'إجمالي التكلفة',
|
||||
'planner.searchPlaces': 'ابحث عن أماكن…',
|
||||
'planner.allCategories': 'كل الفئات',
|
||||
'planner.noPlacesFound': 'لم يتم العثور على أماكن',
|
||||
'planner.addFirstPlace': 'أضف أول مكان',
|
||||
'planner.noReservations': 'لا توجد حجوزات',
|
||||
'planner.addFirstReservation': 'أضف أول حجز',
|
||||
'planner.new': 'جديد',
|
||||
'planner.addToDay': '+ يوم',
|
||||
'planner.calculating': 'جارٍ الحساب…',
|
||||
'planner.route': 'المسار',
|
||||
'planner.optimize': 'تحسين',
|
||||
'planner.openGoogleMaps': 'فتح في Google Maps',
|
||||
'planner.selectDayHint': 'اختر يومًا من القائمة اليسرى لعرض خطة اليوم',
|
||||
'planner.noPlacesForDay': 'لا توجد أماكن لهذا اليوم بعد',
|
||||
'planner.addPlacesLink': 'إضافة أماكن ←',
|
||||
'planner.minTotal': 'دقيقة إجمالًا',
|
||||
'planner.noReservation': 'لا يوجد حجز',
|
||||
'planner.removeFromDay': 'إزالة من اليوم',
|
||||
'planner.addToThisDay': 'إضافة إلى اليوم',
|
||||
'planner.overview': 'نظرة عامة',
|
||||
'planner.noDays': 'لا توجد أيام بعد',
|
||||
'planner.editTripToAddDays': 'عدّل الرحلة لإضافة أيام',
|
||||
'planner.dayCount': '{n} أيام',
|
||||
'planner.clickToUnlock': 'انقر لفتح القفل',
|
||||
'planner.keepPosition': 'الحفاظ على الموضع أثناء تحسين المسار',
|
||||
'planner.dayDetails': 'تفاصيل اليوم',
|
||||
'planner.dayN': 'اليوم {n}',
|
||||
};
|
||||
export default planner;
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const register: TranslationStrings = {
|
||||
'register.passwordMismatch': 'كلمتا المرور غير متطابقتين',
|
||||
'register.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
|
||||
'register.failed': 'فشل التسجيل',
|
||||
'register.getStarted': 'ابدأ الآن',
|
||||
'register.subtitle': 'أنشئ حسابًا وابدأ التخطيط لرحلات أحلامك.',
|
||||
'register.feature1': 'خطط رحلات غير محدودة',
|
||||
'register.feature2': 'عرض خريطة تفاعلي',
|
||||
'register.feature3': 'إدارة الأماكن والفئات',
|
||||
'register.feature4': 'تتبع الحجوزات',
|
||||
'register.feature5': 'إنشاء قوائم تجهيز',
|
||||
'register.feature6': 'حفظ الصور والملفات',
|
||||
'register.createAccount': 'إنشاء حساب',
|
||||
'register.startPlanning': 'ابدأ تخطيط رحلتك',
|
||||
'register.minChars': '6 أحرف على الأقل',
|
||||
'register.confirmPassword': 'تأكيد كلمة المرور',
|
||||
'register.repeatPassword': 'إعادة كلمة المرور',
|
||||
'register.registering': 'جارٍ التسجيل...',
|
||||
'register.register': 'تسجيل',
|
||||
'register.hasAccount': 'لديك حساب بالفعل؟',
|
||||
'register.signIn': 'تسجيل الدخول',
|
||||
};
|
||||
export default register;
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const reservations: TranslationStrings = {
|
||||
'reservations.title': 'الحجوزات',
|
||||
'reservations.empty': 'لا توجد حجوزات بعد',
|
||||
'reservations.emptyHint': 'أضف حجوزات للرحلات الجوية والفنادق وغير ذلك',
|
||||
'reservations.add': 'إضافة حجز',
|
||||
'reservations.addManual': 'حجز يدوي',
|
||||
'reservations.placeHint':
|
||||
'نصيحة: يُفضل إنشاء الحجوزات مباشرة من مكان لربطها بخطة اليوم.',
|
||||
'reservations.confirmed': 'مؤكد',
|
||||
'reservations.pending': 'قيد الانتظار',
|
||||
'reservations.summary': '{confirmed} مؤكدة، {pending} قيد الانتظار',
|
||||
'reservations.fromPlan': 'من الخطة',
|
||||
'reservations.showFiles': 'عرض الملفات',
|
||||
'reservations.editTitle': 'تعديل الحجز',
|
||||
'reservations.status': 'الحالة',
|
||||
'reservations.datetime': 'التاريخ والوقت',
|
||||
'reservations.startTime': 'وقت البداية',
|
||||
'reservations.endTime': 'وقت النهاية',
|
||||
'reservations.date': 'التاريخ',
|
||||
'reservations.time': 'الوقت',
|
||||
'reservations.timeAlt': 'الوقت (بديل، مثل 19:30)',
|
||||
'reservations.notes': 'ملاحظات',
|
||||
'reservations.notesPlaceholder': 'ملاحظات إضافية...',
|
||||
'reservations.meta.airline': 'شركة الطيران',
|
||||
'reservations.meta.flightNumber': 'رقم الرحلة',
|
||||
'reservations.meta.from': 'من',
|
||||
'reservations.meta.to': 'إلى',
|
||||
'reservations.needsReview': 'مراجعة',
|
||||
'reservations.needsReviewHint':
|
||||
'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.',
|
||||
'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...',
|
||||
'reservations.meta.trainNumber': 'رقم القطار',
|
||||
'reservations.meta.platform': 'المنصة',
|
||||
'reservations.meta.seat': 'المقعد',
|
||||
'reservations.meta.checkIn': 'تسجيل الوصول',
|
||||
'reservations.meta.checkInUntil': 'تسجيل الدخول حتى',
|
||||
'reservations.meta.checkOut': 'تسجيل المغادرة',
|
||||
'reservations.meta.linkAccommodation': 'الإقامة',
|
||||
'reservations.meta.pickAccommodation': 'ربط بالإقامة',
|
||||
'reservations.meta.noAccommodation': 'لا يوجد',
|
||||
'reservations.meta.hotelPlace': 'الإقامة',
|
||||
'reservations.meta.pickHotel': 'اختر الإقامة',
|
||||
'reservations.meta.fromDay': 'من',
|
||||
'reservations.meta.toDay': 'إلى',
|
||||
'reservations.meta.selectDay': 'اختر يومًا',
|
||||
'reservations.type.flight': 'رحلة جوية',
|
||||
'reservations.type.hotel': 'إقامة',
|
||||
'reservations.type.restaurant': 'مطعم',
|
||||
'reservations.type.train': 'قطار',
|
||||
'reservations.type.car': 'سيارة',
|
||||
'reservations.type.cruise': 'رحلة بحرية',
|
||||
'reservations.type.event': 'فعالية',
|
||||
'reservations.type.tour': 'جولة',
|
||||
'reservations.type.other': 'أخرى',
|
||||
'reservations.confirm.delete': 'هل تريد حذف الحجز "{name}"؟',
|
||||
'reservations.confirm.deleteTitle': 'حذف الحجز؟',
|
||||
'reservations.confirm.deleteBody': 'سيتم حذف "{name}" نهائيًا.',
|
||||
'reservations.toast.updated': 'تم تحديث الحجز',
|
||||
'reservations.toast.removed': 'تم حذف الحجز',
|
||||
'reservations.toast.fileUploaded': 'تم رفع الملف',
|
||||
'reservations.toast.uploadError': 'فشل الرفع',
|
||||
'reservations.newTitle': 'حجز جديد',
|
||||
'reservations.bookingType': 'نوع الحجز',
|
||||
'reservations.titleLabel': 'العنوان',
|
||||
'reservations.titlePlaceholder': 'مثال: Lufthansa LH123، فندق أدلون، ...',
|
||||
'reservations.locationAddress': 'الموقع / العنوان',
|
||||
'reservations.locationPlaceholder': 'العنوان، المطار، الفندق...',
|
||||
'reservations.confirmationCode': 'رمز الحجز',
|
||||
'reservations.confirmationPlaceholder': 'مثال: ABC12345',
|
||||
'reservations.day': 'اليوم',
|
||||
'reservations.noDay': 'بلا يوم',
|
||||
'reservations.place': 'المكان',
|
||||
'reservations.noPlace': 'بلا مكان',
|
||||
'reservations.pendingSave': 'سيتم الحفظ…',
|
||||
'reservations.uploading': 'جارٍ الرفع...',
|
||||
'reservations.attachFile': 'إرفاق ملف',
|
||||
'reservations.linkExisting': 'ربط ملف موجود',
|
||||
'reservations.toast.saveError': 'فشل الحفظ',
|
||||
'reservations.toast.updateError': 'فشل التحديث',
|
||||
'reservations.toast.deleteError': 'فشل الحذف',
|
||||
'reservations.confirm.remove': 'إزالة الحجز "{name}"؟',
|
||||
'reservations.linkAssignment': 'ربط بخطة اليوم',
|
||||
'reservations.pickAssignment': 'اختر عنصرًا من خطتك...',
|
||||
'reservations.noAssignment': 'بلا ربط',
|
||||
'reservations.price': 'السعر',
|
||||
'reservations.budgetCategory': 'فئة الميزانية',
|
||||
'reservations.budgetCategoryPlaceholder': 'مثال: المواصلات، الإقامة',
|
||||
'reservations.budgetCategoryAuto': 'تلقائي (حسب نوع الحجز)',
|
||||
'reservations.budgetHint':
|
||||
'سيتم إنشاء إدخال في الميزانية تلقائيًا عند الحفظ.',
|
||||
'reservations.departureDate': 'المغادرة',
|
||||
'reservations.arrivalDate': 'الوصول',
|
||||
'reservations.departureTime': 'وقت المغادرة',
|
||||
'reservations.arrivalTime': 'وقت الوصول',
|
||||
'reservations.pickupDate': 'الاستلام',
|
||||
'reservations.returnDate': 'الإرجاع',
|
||||
'reservations.pickupTime': 'وقت الاستلام',
|
||||
'reservations.returnTime': 'وقت الإرجاع',
|
||||
'reservations.endDate': 'تاريخ الانتهاء',
|
||||
'reservations.meta.departureTimezone': 'TZ المغادرة',
|
||||
'reservations.meta.arrivalTimezone': 'TZ الوصول',
|
||||
'reservations.span.departure': 'المغادرة',
|
||||
'reservations.span.arrival': 'الوصول',
|
||||
'reservations.span.inTransit': 'في الطريق',
|
||||
'reservations.span.pickup': 'الاستلام',
|
||||
'reservations.span.return': 'الإرجاع',
|
||||
'reservations.span.active': 'نشط',
|
||||
'reservations.span.start': 'البداية',
|
||||
'reservations.span.end': 'النهاية',
|
||||
'reservations.span.ongoing': 'جارٍ',
|
||||
'reservations.validation.endBeforeStart':
|
||||
'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
|
||||
'reservations.addBooking': 'إضافة حجز',
|
||||
};
|
||||
export default reservations;
|
||||
@@ -0,0 +1,274 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const settings: TranslationStrings = {
|
||||
'settings.title': 'الإعدادات',
|
||||
'settings.subtitle': 'ضبط إعداداتك الشخصية',
|
||||
'settings.tabs.display': 'العرض',
|
||||
'settings.tabs.map': 'الخريطة',
|
||||
'settings.tabs.notifications': 'الإشعارات',
|
||||
'settings.tabs.integrations': 'التكاملات',
|
||||
'settings.tabs.account': 'الحساب',
|
||||
'settings.tabs.about': 'حول',
|
||||
'settings.map': 'الخريطة',
|
||||
'settings.mapTemplate': 'قالب الخريطة',
|
||||
'settings.mapTemplatePlaceholder.select': 'اختر قالبًا...',
|
||||
'settings.mapDefaultHint': 'اتركه فارغًا لاستخدام OpenStreetMap افتراضيًا',
|
||||
'settings.mapHint': 'قالب URL لبلاطات الخريطة',
|
||||
'settings.mapProvider': 'مزود الخريطة',
|
||||
'settings.mapProviderHint':
|
||||
'يؤثر على خرائط Trip Planner و Journey. يستخدم Atlas دائمًا Leaflet.',
|
||||
'settings.mapLeafletSubtitle': '2D كلاسيكي، أي بلاطات نقطية',
|
||||
'settings.mapMapboxSubtitle': 'بلاطات متجهية ومبانٍ ثلاثية الأبعاد وتضاريس',
|
||||
'settings.mapExperimental': 'تجريبي',
|
||||
'settings.mapMapboxToken': 'رمز وصول Mapbox',
|
||||
'settings.mapMapboxTokenHint': 'الرمز العام (pk.*) من',
|
||||
'settings.mapMapboxTokenLink': 'mapbox.com ← رموز الوصول',
|
||||
'settings.mapStyle': 'نمط الخريطة',
|
||||
'settings.mapStylePlaceholder': 'اختر نمط Mapbox',
|
||||
'settings.mapStyleHint':
|
||||
'إعداد مسبق أو عنوان URL mapbox://styles/USER/ID خاص بك',
|
||||
'settings.map3dBuildings': 'مبانٍ ثلاثية الأبعاد وتضاريس',
|
||||
'settings.map3dHint':
|
||||
'إمالة + مبانٍ ثلاثية الأبعاد حقيقية — يعمل مع كل نمط بما في ذلك الأقمار الصناعية.',
|
||||
'settings.mapHighQuality': 'وضع الجودة العالية',
|
||||
'settings.mapHighQualityHint':
|
||||
'تحسين الحواف + إسقاط كروي لحواف أكثر حدة وعرض واقعي للعالم.',
|
||||
'settings.mapHighQualityWarning': 'قد يؤثر على الأداء في الأجهزة الأقل قدرة.',
|
||||
'settings.mapTipLabel': 'نصيحة:',
|
||||
'settings.mapTip':
|
||||
'انقر بزر الماوس الأيمن واسحب لتدوير/إمالة الخريطة. النقر الأوسط لإضافة مكان (النقر الأيمن مخصص للتدوير).',
|
||||
'settings.latitude': 'خط العرض',
|
||||
'settings.longitude': 'خط الطول',
|
||||
'settings.saveMap': 'حفظ الخريطة',
|
||||
'settings.apiKeys': 'مفاتيح API',
|
||||
'settings.mapsKey': 'مفتاح Google Maps API',
|
||||
'settings.mapsKeyHint': 'للبحث عن الأماكن. يتطلب Places API (New).',
|
||||
'settings.weatherKey': 'مفتاح OpenWeatherMap API',
|
||||
'settings.weatherKeyHint': 'لبيانات الطقس.',
|
||||
'settings.keyPlaceholder': 'أدخل المفتاح...',
|
||||
'settings.configured': 'مُعدّ',
|
||||
'settings.saveKeys': 'حفظ المفاتيح',
|
||||
'settings.display': 'العرض',
|
||||
'settings.colorMode': 'نمط الألوان',
|
||||
'settings.light': 'فاتح',
|
||||
'settings.dark': 'داكن',
|
||||
'settings.auto': 'تلقائي',
|
||||
'settings.language': 'اللغة',
|
||||
'settings.temperature': 'وحدة الحرارة',
|
||||
'settings.timeFormat': 'تنسيق الوقت',
|
||||
'settings.bookingLabels': 'تسميات مسارات الحجوزات',
|
||||
'settings.bookingLabelsHint':
|
||||
'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.',
|
||||
'settings.blurBookingCodes': 'إخفاء رموز الحجز',
|
||||
'settings.notifications': 'الإشعارات',
|
||||
'settings.notifyTripInvite': 'دعوات الرحلات',
|
||||
'settings.notifyBookingChange': 'تغييرات الحجز',
|
||||
'settings.notifyTripReminder': 'تذكيرات الرحلات',
|
||||
'settings.notifyTodoDue': 'مهمة مستحقة',
|
||||
'settings.notifyVacayInvite': 'دعوات دمج الإجازات',
|
||||
'settings.notifyPhotosShared': 'صور مشتركة (Immich)',
|
||||
'settings.notifyCollabMessage': 'رسائل الدردشة (Collab)',
|
||||
'settings.notifyPackingTagged': 'قائمة الأمتعة: التعيينات',
|
||||
'settings.notifyWebhook': 'إشعارات Webhook',
|
||||
'settings.notifyVersionAvailable': 'إصدار جديد متاح',
|
||||
'settings.notificationPreferences.noChannels':
|
||||
'لم يتم تكوين قنوات إشعارات. اطلب من المسؤول إعداد إشعارات البريد الإلكتروني أو webhook.',
|
||||
'settings.webhookUrl.label': 'رابط Webhook',
|
||||
'settings.webhookUrl.hint':
|
||||
'أدخل رابط Webhook الخاص بـ Discord أو Slack أو المخصص لتلقي الإشعارات.',
|
||||
'settings.webhookUrl.saved': 'تم حفظ رابط Webhook',
|
||||
'settings.webhookUrl.test': 'اختبار',
|
||||
'settings.webhookUrl.testSuccess': 'تم إرسال Webhook الاختباري بنجاح',
|
||||
'settings.webhookUrl.testFailed': 'فشل إرسال Webhook الاختباري',
|
||||
'settings.ntfyUrl.topicLabel': 'موضوع Ntfy',
|
||||
'settings.ntfyUrl.serverLabel': 'عنوان URL خادم Ntfy (اختياري)',
|
||||
'settings.ntfyUrl.hint':
|
||||
'أدخل موضوع Ntfy الخاص بك لتلقي الإشعارات الفورية. اترك حقل الخادم فارغاً لاستخدام الإعداد الافتراضي الذي حدده المسؤول.',
|
||||
'settings.ntfyUrl.tokenLabel': 'رمز الوصول (اختياري)',
|
||||
'settings.ntfyUrl.tokenHint': 'مطلوب للمواضيع المحمية بكلمة مرور.',
|
||||
'settings.ntfyUrl.saved': 'تم حفظ إعدادات Ntfy',
|
||||
'settings.ntfyUrl.test': 'اختبار',
|
||||
'settings.ntfyUrl.testSuccess': 'تم إرسال إشعار Ntfy التجريبي بنجاح',
|
||||
'settings.ntfyUrl.testFailed': 'فشل إشعار Ntfy التجريبي',
|
||||
'settings.ntfyUrl.tokenCleared': 'تم مسح رمز الوصول',
|
||||
'settings.notificationsDisabled':
|
||||
'الإشعارات غير مكوّنة. اطلب من المسؤول تفعيل إشعارات البريد الإلكتروني أو Webhook.',
|
||||
'settings.notificationsActive': 'القناة النشطة',
|
||||
'settings.notificationsManagedByAdmin':
|
||||
'يتم تكوين أحداث الإشعارات بواسطة المسؤول.',
|
||||
'settings.on': 'تشغيل',
|
||||
'settings.off': 'إيقاف',
|
||||
'settings.mcp.title': 'إعداد MCP',
|
||||
'settings.mcp.endpoint': 'نقطة نهاية MCP',
|
||||
'settings.mcp.clientConfig': 'إعداد العميل',
|
||||
'settings.mcp.clientConfigHint':
|
||||
'استبدل <your_token> برمز API من القائمة أدناه. قد يحتاج مسار npx إلى ضبط وفق نظامك (مثلاً C:\\PROGRA~1\\nodejs\\npx.cmd على Windows).',
|
||||
'settings.mcp.clientConfigHintOAuth':
|
||||
'استبدل <your_client_id> و<your_client_secret> ببيانات الاعتماد المعروضة في عميل OAuth 2.1 الذي أنشأته أعلاه. سيفتح mcp-remote متصفحك لإتمام التفويض في أول اتصال. قد يحتاج مسار npx إلى تعديل حسب نظامك (مثال: C:PROGRA~1\nodejs\npx.cmd على Windows).',
|
||||
'settings.mcp.copy': 'نسخ',
|
||||
'settings.mcp.copied': 'تم النسخ!',
|
||||
'settings.mcp.apiTokens': 'رموز API',
|
||||
'settings.mcp.createToken': 'إنشاء رمز جديد',
|
||||
'settings.mcp.noTokens': 'لا توجد رموز بعد. أنشئ رمزاً للاتصال بعملاء MCP.',
|
||||
'settings.mcp.tokenCreatedAt': 'أُنشئ',
|
||||
'settings.mcp.tokenUsedAt': 'استُخدم',
|
||||
'settings.mcp.deleteTokenTitle': 'حذف الرمز',
|
||||
'settings.mcp.deleteTokenMessage':
|
||||
'سيتوقف هذا الرمز عن العمل فوراً. أي عميل MCP يستخدمه سيفقد الوصول.',
|
||||
'settings.mcp.modal.createTitle': 'إنشاء رمز API',
|
||||
'settings.mcp.modal.tokenName': 'اسم الرمز',
|
||||
'settings.mcp.modal.tokenNamePlaceholder':
|
||||
'مثال: Claude Desktop، حاسوب العمل',
|
||||
'settings.mcp.modal.creating': 'جارٍ الإنشاء…',
|
||||
'settings.mcp.modal.create': 'إنشاء الرمز',
|
||||
'settings.mcp.modal.createdTitle': 'تم إنشاء الرمز',
|
||||
'settings.mcp.modal.createdWarning':
|
||||
'سيُعرض هذا الرمز مرة واحدة فقط. انسخه واحفظه الآن — لا يمكن استرداده.',
|
||||
'settings.mcp.modal.done': 'تم',
|
||||
'settings.mcp.toast.created': 'تم إنشاء الرمز',
|
||||
'settings.mcp.toast.createError': 'فشل إنشاء الرمز',
|
||||
'settings.mcp.toast.deleted': 'تم حذف الرمز',
|
||||
'settings.mcp.toast.deleteError': 'فشل حذف الرمز',
|
||||
'settings.mcp.apiTokensDeprecated':
|
||||
'رموز API قديمة وستُزال في إصدار مستقبلي. يُرجى استخدام عملاء OAuth 2.1 بدلاً منها.',
|
||||
'settings.oauth.clients': 'عملاء OAuth 2.1',
|
||||
'settings.oauth.clientsHint':
|
||||
'سجّل عملاء OAuth 2.1 للسماح لتطبيقات MCP الخارجية (Claude Web وCursor وغيرها) بالاتصال دون رموز ثابتة.',
|
||||
'settings.oauth.createClient': 'عميل جديد',
|
||||
'settings.oauth.noClients': 'لا يوجد عملاء OAuth مسجلون.',
|
||||
'settings.oauth.clientId': 'معرّف العميل',
|
||||
'settings.oauth.clientSecret': 'سر العميل',
|
||||
'settings.oauth.deleteClient': 'حذف العميل',
|
||||
'settings.oauth.deleteClientMessage':
|
||||
'سيتم حذف هذا العميل وجميع الجلسات النشطة بشكل دائم. ستفقد أي تطبيق يستخدمه وصوله فوراً.',
|
||||
'settings.oauth.rotateSecret': 'تجديد السر',
|
||||
'settings.oauth.rotateSecretMessage':
|
||||
'سيتم إنشاء سر عميل جديد وإبطال جميع الجلسات الحالية فوراً. حدّث تطبيقك قبل إغلاق هذا الحوار.',
|
||||
'settings.oauth.rotateSecretConfirm': 'تجديد',
|
||||
'settings.oauth.rotateSecretConfirming': 'جارٍ التجديد…',
|
||||
'settings.oauth.rotateSecretDoneTitle': 'تم إنشاء سر جديد',
|
||||
'settings.oauth.rotateSecretDoneWarning':
|
||||
'يُعرض هذا السر مرة واحدة فقط. انسخه الآن وحدّث تطبيقك — تم إبطال جميع الجلسات السابقة.',
|
||||
'settings.oauth.activeSessions': 'جلسات OAuth النشطة',
|
||||
'settings.oauth.sessionScopes': 'النطاقات',
|
||||
'settings.oauth.sessionExpires': 'تنتهي',
|
||||
'settings.oauth.revoke': 'إلغاء',
|
||||
'settings.oauth.revokeSession': 'إلغاء الجلسة',
|
||||
'settings.oauth.revokeSessionMessage':
|
||||
'سيؤدي هذا إلى إلغاء الوصول لهذه الجلسة OAuth فوراً.',
|
||||
'settings.oauth.modal.createTitle': 'تسجيل عميل OAuth',
|
||||
'settings.oauth.modal.presets': 'إعدادات سريعة',
|
||||
'settings.oauth.modal.clientName': 'اسم التطبيق',
|
||||
'settings.oauth.modal.clientNamePlaceholder':
|
||||
'مثال: Claude Web، تطبيق MCP الخاص بي',
|
||||
'settings.oauth.modal.redirectUris': 'عناوين URI لإعادة التوجيه',
|
||||
'settings.oauth.modal.redirectUrisHint':
|
||||
'عنوان URI واحد لكل سطر. يُطلب HTTPS (localhost مستثنى). يُطبق تطابق دقيق.',
|
||||
'settings.oauth.modal.scopes': 'النطاقات المسموح بها',
|
||||
'settings.oauth.modal.scopesHint':
|
||||
'list_trips وget_trip_summary متاحان دائماً — لا يُطلب نطاق. يساعدان الذكاء الاصطناعي في اكتشاف معرّفات الرحلات.',
|
||||
'settings.oauth.modal.selectAll': 'تحديد الكل',
|
||||
'settings.oauth.modal.deselectAll': 'إلغاء تحديد الكل',
|
||||
'settings.oauth.modal.creating': 'جارٍ التسجيل…',
|
||||
'settings.oauth.modal.create': 'تسجيل العميل',
|
||||
'settings.oauth.modal.createdTitle': 'تم تسجيل العميل',
|
||||
'settings.oauth.modal.createdWarning':
|
||||
'يُعرض سر العميل مرة واحدة فقط. انسخه الآن — لا يمكن استرداده.',
|
||||
'settings.oauth.toast.createError': 'فشل تسجيل عميل OAuth',
|
||||
'settings.oauth.toast.deleted': 'تم حذف عميل OAuth',
|
||||
'settings.oauth.toast.deleteError': 'فشل حذف عميل OAuth',
|
||||
'settings.oauth.toast.revoked': 'تم إلغاء الجلسة',
|
||||
'settings.oauth.toast.revokeError': 'فشل إلغاء الجلسة',
|
||||
'settings.oauth.toast.rotateError': 'فشل تجديد سر العميل',
|
||||
'settings.oauth.modal.machineClient':
|
||||
'عميل آلي (بدون تسجيل دخول عبر المتصفح)',
|
||||
'settings.oauth.modal.machineClientHint':
|
||||
'استخدام منحة client_credentials — لا تحتاج إلى عناوين إعادة التوجيه. يُصدر الرمز المميز مباشرةً عبر client_id + client_secret ويعمل بصلاحياتك ضمن النطاقات المحددة.',
|
||||
'settings.oauth.modal.machineClientUsage':
|
||||
'للحصول على رمز مميز: POST /oauth/token مع grant_type=client_credentials وclient_id وclient_secret. بدون متصفح، بدون رمز تحديث.',
|
||||
'settings.oauth.badge.machine': 'آلي',
|
||||
'settings.account': 'الحساب',
|
||||
'settings.about': 'حول',
|
||||
'settings.about.reportBug': 'الإبلاغ عن خطأ',
|
||||
'settings.about.reportBugHint': 'وجدت مشكلة؟ أخبرنا',
|
||||
'settings.about.featureRequest': 'اقتراح ميزة',
|
||||
'settings.about.featureRequestHint': 'اقترح ميزة جديدة',
|
||||
'settings.about.wikiHint': 'التوثيق والأدلة',
|
||||
'settings.about.supporters.badge': 'الداعمون الشهريون',
|
||||
'settings.about.supporters.title': 'رفاق رحلة TREK',
|
||||
'settings.about.supporters.subtitle':
|
||||
'بينما تخطّط لمسارك التالي، يساعد هؤلاء الأشخاص في التخطيط لمستقبل TREK. تذهب مساهمتهم الشهرية مباشرةً إلى التطوير والساعات الفعلية المبذولة — حتى يظلّ TREK مفتوح المصدر.',
|
||||
'settings.about.supporters.since': 'داعم منذ {date}',
|
||||
'settings.about.supporters.tierEmpty': 'كن الأول',
|
||||
'settings.about.description':
|
||||
'TREK هو مخطط سفر مستضاف ذاتيًا يساعدك على تنظيم رحلاتك من أول فكرة حتى آخر ذكرى. تخطيط يومي، ميزانية، قوائم تعبئة، صور والمزيد — كل شيء في مكان واحد، على خادمك الخاص.',
|
||||
'settings.about.madeWith': 'صُنع بـ',
|
||||
'settings.about.madeBy': 'بواسطة موريس ومجتمع مفتوح المصدر متنامٍ.',
|
||||
'settings.username': 'اسم المستخدم',
|
||||
'settings.email': 'البريد الإلكتروني',
|
||||
'settings.role': 'الدور',
|
||||
'settings.roleAdmin': 'مسؤول',
|
||||
'settings.oidcLinked': 'مرتبط مع',
|
||||
'settings.changePassword': 'تغيير كلمة المرور',
|
||||
'settings.currentPassword': 'كلمة المرور الحالية',
|
||||
'settings.currentPasswordRequired': 'كلمة المرور الحالية مطلوبة',
|
||||
'settings.newPassword': 'كلمة المرور الجديدة',
|
||||
'settings.confirmPassword': 'تأكيد كلمة المرور الجديدة',
|
||||
'settings.updatePassword': 'تحديث كلمة المرور',
|
||||
'settings.passwordRequired': 'أدخل كلمة المرور الحالية والجديدة',
|
||||
'settings.passwordTooShort': 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل',
|
||||
'settings.passwordMismatch': 'كلمتا المرور غير متطابقتين',
|
||||
'settings.passwordWeak':
|
||||
'يجب أن تحتوي كلمة المرور على حرف كبير وحرف صغير ورقم ورمز خاص',
|
||||
'settings.passwordChanged': 'تم تغيير كلمة المرور بنجاح',
|
||||
'settings.mustChangePassword':
|
||||
'يجب عليك تغيير كلمة المرور قبل المتابعة. يرجى تعيين كلمة مرور جديدة أدناه.',
|
||||
'settings.deleteAccount': 'حذف الحساب',
|
||||
'settings.deleteAccountTitle': 'هل تريد حذف حسابك؟',
|
||||
'settings.deleteAccountWarning':
|
||||
'سيتم حذف حسابك وجميع رحلاتك وأماكنك وملفاتك نهائيًا. لا يمكن التراجع عن ذلك.',
|
||||
'settings.deleteAccountConfirm': 'حذف نهائي',
|
||||
'settings.deleteBlockedTitle': 'الحذف غير ممكن',
|
||||
'settings.deleteBlockedMessage':
|
||||
'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.',
|
||||
'settings.roleUser': 'مستخدم',
|
||||
'settings.saveProfile': 'حفظ الملف الشخصي',
|
||||
'settings.toast.mapSaved': 'تم حفظ إعدادات الخريطة',
|
||||
'settings.toast.keysSaved': 'تم حفظ مفاتيح API',
|
||||
'settings.toast.displaySaved': 'تم حفظ إعدادات العرض',
|
||||
'settings.toast.profileSaved': 'تم حفظ الملف الشخصي',
|
||||
'settings.uploadAvatar': 'رفع صورة الملف الشخصي',
|
||||
'settings.removeAvatar': 'إزالة صورة الملف الشخصي',
|
||||
'settings.avatarUploaded': 'تم تحديث صورة الملف الشخصي',
|
||||
'settings.avatarRemoved': 'تمت إزالة صورة الملف الشخصي',
|
||||
'settings.avatarError': 'فشل الرفع',
|
||||
'settings.mfa.title': 'المصادقة الثنائية (2FA)',
|
||||
'settings.mfa.description':
|
||||
'تضيف خطوة ثانية عند تسجيل الدخول. استخدم تطبيق مصادقة (Google Authenticator، Authy، إلخ).',
|
||||
'settings.mfa.requiredByPolicy':
|
||||
'المسؤول يتطلب المصادقة الثنائية. اضبط تطبيق المصادقة أدناه قبل المتابعة.',
|
||||
'settings.mfa.backupTitle': 'رموز النسخ الاحتياطي',
|
||||
'settings.mfa.backupDescription':
|
||||
'استخدم هذه الرموز لمرة واحدة إذا فقدت الوصول إلى تطبيق المصادقة.',
|
||||
'settings.mfa.backupWarning':
|
||||
'احفظ هذه الرموز الآن. كل رمز يمكن استخدامه مرة واحدة فقط.',
|
||||
'settings.mfa.backupCopy': 'نسخ الرموز',
|
||||
'settings.mfa.backupDownload': 'تنزيل TXT',
|
||||
'settings.mfa.backupPrint': 'طباعة / PDF',
|
||||
'settings.mfa.backupCopied': 'تم نسخ رموز النسخ الاحتياطي',
|
||||
'settings.mfa.enabled': 'المصادقة الثنائية مفعّلة على حسابك.',
|
||||
'settings.mfa.disabled': 'المصادقة الثنائية غير مفعّلة.',
|
||||
'settings.mfa.setup': 'إعداد المصادقة',
|
||||
'settings.mfa.scanQr': 'امسح رمز QR بتطبيقك أو أدخل المفتاح يدويًا.',
|
||||
'settings.mfa.secretLabel': 'المفتاح السري (إدخال يدوي)',
|
||||
'settings.mfa.codePlaceholder': 'رمز من 6 أرقام',
|
||||
'settings.mfa.enable': 'تفعيل 2FA',
|
||||
'settings.mfa.cancelSetup': 'إلغاء',
|
||||
'settings.mfa.disableTitle': 'تعطيل 2FA',
|
||||
'settings.mfa.disableHint': 'أدخل كلمة مرور حسابك ورمزًا حاليًا من المصادقة.',
|
||||
'settings.mfa.disable': 'تعطيل 2FA',
|
||||
'settings.mfa.toastEnabled': 'تم تفعيل المصادقة الثنائية',
|
||||
'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية',
|
||||
'settings.mfa.demoBlocked': 'غير متاح في الوضع التجريبي',
|
||||
};
|
||||
export default settings;
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const share: TranslationStrings = {
|
||||
'share.linkTitle': 'رابط عام',
|
||||
'share.linkHint':
|
||||
'أنشئ رابطًا يمكن لأي شخص استخدامه لعرض هذه الرحلة بدون تسجيل الدخول. للقراءة فقط — لا يمكن التعديل.',
|
||||
'share.createLink': 'إنشاء رابط',
|
||||
'share.deleteLink': 'حذف الرابط',
|
||||
'share.createError': 'تعذر إنشاء الرابط',
|
||||
'share.permMap': 'الخريطة والخطة',
|
||||
'share.permBookings': 'الحجوزات',
|
||||
'share.permPacking': 'الأمتعة',
|
||||
'share.permBudget': 'الميزانية',
|
||||
'share.permCollab': 'الدردشة',
|
||||
};
|
||||
export default share;
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const shared: TranslationStrings = {
|
||||
'shared.expired': 'الرابط منتهي أو غير صالح',
|
||||
'shared.expiredHint': 'رابط الرحلة المشترك لم يعد نشطًا.',
|
||||
'shared.readOnly': 'عرض للقراءة فقط',
|
||||
'shared.tabPlan': 'الخطة',
|
||||
'shared.tabBookings': 'الحجوزات',
|
||||
'shared.tabPacking': 'قائمة التعبئة',
|
||||
'shared.tabBudget': 'الميزانية',
|
||||
'shared.tabChat': 'الدردشة',
|
||||
'shared.days': 'أيام',
|
||||
'shared.places': 'أماكن',
|
||||
'shared.other': 'أخرى',
|
||||
'shared.totalBudget': 'إجمالي الميزانية',
|
||||
'shared.messages': 'رسائل',
|
||||
'shared.sharedVia': 'تمت المشاركة عبر',
|
||||
'shared.confirmed': 'مؤكد',
|
||||
'shared.pending': 'قيد الانتظار',
|
||||
};
|
||||
export default shared;
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const stats: TranslationStrings = {
|
||||
'stats.countries': 'الدول',
|
||||
'stats.cities': 'المدن',
|
||||
'stats.trips': 'الرحلات',
|
||||
'stats.places': 'الأماكن',
|
||||
'stats.worldProgress': 'التقدم حول العالم',
|
||||
'stats.visited': 'تمت زيارتها',
|
||||
'stats.remaining': 'المتبقية',
|
||||
'stats.visitedCountries': 'الدول التي تمت زيارتها',
|
||||
};
|
||||
export default stats;
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const system_notice: TranslationStrings = {
|
||||
'system_notice.v3_photos.title': 'تم نقل الصور في الإصدار 3.0',
|
||||
'system_notice.v3_photos.body':
|
||||
'تمت إزالة تبويب **الصور** من مخطط الرحلة. صورك آمنة — لم يعدّل TREK مكتبتك على Immich أو Synology قطّ.\n\nتعيش الصور الآن في إضافة **Journey**. Journey اختيارية — إن لم تكن متاحة بعد، اطلب من المسؤول تفعيلها عبر Admin ← الإضافات.',
|
||||
'system_notice.v3_journey.title': 'تعرّف على Journey — مذكرة سفر',
|
||||
'system_notice.v3_journey.body':
|
||||
'وثّق رحلاتك كقصص غنية بخطوط زمنية ومعارض صور وخرائط تفاعلية.',
|
||||
'system_notice.v3_journey.cta_label': 'فتح Journey',
|
||||
'system_notice.v3_journey.highlight_timeline': 'جدول زمني يومي ومعرض',
|
||||
'system_notice.v3_journey.highlight_photos': 'استيراد من Immich أو Synology',
|
||||
'system_notice.v3_journey.highlight_share': 'مشاركة علنية — دون تسجيل دخول',
|
||||
'system_notice.v3_journey.highlight_export': 'تصدير كألبوم صور PDF',
|
||||
'system_notice.v3_features.title': 'مزيد من مميزات 3.0',
|
||||
'system_notice.v3_features.body':
|
||||
'بعض الجديد الآخر الجدير بالمعرفة في هذا الإصدار.',
|
||||
'system_notice.v3_features.highlight_dashboard':
|
||||
'إعادة تصميم لوحة التحكم mobile-first',
|
||||
'system_notice.v3_features.highlight_offline': 'وضع لا اتصال كامل كتطبيق PWA',
|
||||
'system_notice.v3_features.highlight_search': 'إكمال تلقائي في الوقت الفعلي',
|
||||
'system_notice.v3_features.highlight_import':
|
||||
'استيراد أماكن من ملفات KMZ/KML',
|
||||
'system_notice.v3_mcp.title': 'MCP: ترقية OAuth 2.1',
|
||||
'system_notice.v3_mcp.body':
|
||||
'تمت إعادة تصميم تكامل MCP بالكامل. OAuth 2.1 هو الآن طريقة المصادقة الموصى بها. الرموز الثابتة (trek_…) مهملة وستُزال في إصدار مستقبلي.',
|
||||
'system_notice.v3_mcp.highlight_oauth': 'OAuth 2.1 موصى به (mcp-remote)',
|
||||
'system_notice.v3_mcp.highlight_scopes': '24 نطاق أذونات دقيق',
|
||||
'system_notice.v3_mcp.highlight_deprecated': 'الرموز الثابتة trek_ مهملة',
|
||||
'system_notice.v3_mcp.highlight_tools': 'مجموعة أدوات وإرشادات موسعة',
|
||||
'system_notice.v3_thankyou.title': 'كلمة شخصية مني',
|
||||
'system_notice.v3_thankyou.body':
|
||||
'قبل أن تمضي — أريد أن أتوقف لحظة.\n\nبدأ TREK كمشروع جانبي بنيته لرحلاتي الخاصة. لم أتخيل يومًا أنه سيكبر ليصبح شيئًا يعتمد عليه 4,000 منكم لتخطيط مغامراتهم. كل نجمة، كل مشكلة، كل طلب ميزة — أقرأها جميعًا، وهي ما يبقيني مستمرًا في الليالي المتأخرة بين عمل بدوام كامل والجامعة.\n\nأريدكم أن تعرفوا: TREK سيبقى دائمًا مفتوح المصدر، دائمًا مستضافًا ذاتيًا، دائمًا ملككم. لا تتبع، لا اشتراكات، لا شروط خفية. مجرد أداة بناها شخص يحب السفر بقدر ما تحبونه.\n\nشكر خاص لـ [jubnl](https://github.com/jubnl) — لقد أصبحت متعاونًا رائعًا. الكثير مما يجعل الإصدار 3.0 عظيمًا يحمل بصماتك. شكرًا لإيمانك بهذا المشروع عندما كان لا يزال في بداياته.\n\nولكل واحد منكم ممن أبلغ عن خطأ، أو ترجم نصًا، أو شارك TREK مع صديق، أو ببساطة استخدمه لتخطيط رحلة — **شكرًا لكم**. أنتم السبب في وجود هذا.\n\nإلى المزيد من المغامرات معًا.\n\n— Maurice\n\n---\n\n[انضم إلى المجتمع على Discord](https://discord.gg/7Q6M6jDwzf)\n\nإذا جعل TREK رحلاتك أفضل، [فنجان قهوة صغير](https://ko-fi.com/mauriceboe) يبقي الأضواء مشتعلة.',
|
||||
'system_notice.v3014_whitespace_collision.title':
|
||||
'إجراء مطلوب: تعارض في حسابات المستخدمين',
|
||||
'system_notice.v3014_whitespace_collision.body':
|
||||
'اكتشف ترقية 3.0.14 تعارضًا في أسماء مستخدمين أو بريد إلكتروني ناتجًا عن مسافات بيضاء في بداية أو نهاية القيم المخزنة. تمت إعادة تسمية الحسابات المتأثرة تلقائيًا. تحقق من سجلات الخادم بحثًا عن أسطر تبدأ بـ **[migration] WHITESPACE COLLISION** لتحديد الحسابات التي تحتاج إلى مراجعة.',
|
||||
'system_notice.welcome_v1.title': 'مرحبًا بك في TREK',
|
||||
'system_notice.welcome_v1.body':
|
||||
'مخطط رحلاتك الشامل. أنشئ جداول السفر، وشارك رحلاتك مع الأصدقاء، وابقَ منظمًا — سواء كنت متصلاً بالإنترنت أم لا.',
|
||||
'system_notice.welcome_v1.cta_label': 'خطط لرحلة',
|
||||
'system_notice.welcome_v1.hero_alt': 'وجهة سفر خلابة مع واجهة تطبيق TREK',
|
||||
'system_notice.welcome_v1.highlight_plan': 'جداول رحلات يومية لكل سفرة',
|
||||
'system_notice.welcome_v1.highlight_share': 'تعاون مع شركاء السفر',
|
||||
'system_notice.welcome_v1.highlight_offline': 'يعمل بلا إنترنت على الهاتف',
|
||||
'system_notice.pager.prev': 'الإشعار السابق',
|
||||
'system_notice.pager.next': 'الإشعار التالي',
|
||||
'system_notice.pager.goto': 'الانتقال إلى الإشعار {n}',
|
||||
'system_notice.pager.position': 'الإشعار {current} من {total}',
|
||||
};
|
||||
export default system_notice;
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const todo: TranslationStrings = {
|
||||
'todo.subtab.packing': 'قائمة الأمتعة',
|
||||
'todo.subtab.todo': 'المهام',
|
||||
'todo.completed': 'مكتمل',
|
||||
'todo.filter.all': 'الكل',
|
||||
'todo.filter.open': 'مفتوح',
|
||||
'todo.filter.done': 'منجز',
|
||||
'todo.uncategorized': 'بدون تصنيف',
|
||||
'todo.namePlaceholder': 'اسم المهمة',
|
||||
'todo.descriptionPlaceholder': 'وصف (اختياري)',
|
||||
'todo.unassigned': 'غير مُسنَد',
|
||||
'todo.noCategory': 'بدون فئة',
|
||||
'todo.hasDescription': 'له وصف',
|
||||
'todo.addItem': 'إضافة مهمة جديدة',
|
||||
'todo.sidebar.sortBy': 'ترتيب حسب',
|
||||
'todo.priority': 'الأولوية',
|
||||
'todo.newCategoryLabel': 'جديد',
|
||||
'todo.newCategory': 'اسم الفئة',
|
||||
'todo.addCategory': 'إضافة فئة',
|
||||
'todo.newItem': 'مهمة جديدة',
|
||||
'todo.empty': 'لا توجد مهام بعد. أضف مهمة للبدء!',
|
||||
'todo.filter.my': 'مهامي',
|
||||
'todo.filter.overdue': 'متأخرة',
|
||||
'todo.sidebar.tasks': 'المهام',
|
||||
'todo.sidebar.categories': 'الفئات',
|
||||
'todo.detail.title': 'مهمة',
|
||||
'todo.detail.description': 'وصف',
|
||||
'todo.detail.category': 'فئة',
|
||||
'todo.detail.dueDate': 'تاريخ الاستحقاق',
|
||||
'todo.detail.assignedTo': 'مسند إلى',
|
||||
'todo.detail.delete': 'حذف',
|
||||
'todo.detail.save': 'حفظ التغييرات',
|
||||
'todo.sortByPrio': 'الأولوية',
|
||||
'todo.detail.priority': 'الأولوية',
|
||||
'todo.detail.noPriority': 'لا شيء',
|
||||
'todo.detail.create': 'إنشاء مهمة',
|
||||
};
|
||||
export default todo;
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const transport: TranslationStrings = {
|
||||
'transport.addTransport': 'إضافة وسيلة نقل',
|
||||
'transport.modalTitle.create': 'إضافة وسيلة نقل',
|
||||
'transport.modalTitle.edit': 'تعديل وسيلة النقل',
|
||||
'transport.title': 'المواصلات',
|
||||
'transport.addManual': 'نقل يدوي',
|
||||
};
|
||||
export default transport;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const trip: TranslationStrings = {
|
||||
'trip.tabs.plan': 'الخطة',
|
||||
'trip.tabs.transports': 'المواصلات',
|
||||
'trip.tabs.reservations': 'الحجوزات',
|
||||
'trip.tabs.reservationsShort': 'حجز',
|
||||
'trip.tabs.packing': 'قائمة التجهيز',
|
||||
'trip.tabs.packingShort': 'تجهيز',
|
||||
'trip.tabs.lists': 'القوائم',
|
||||
'trip.tabs.listsShort': 'القوائم',
|
||||
'trip.tabs.budget': 'الميزانية',
|
||||
'trip.tabs.files': 'الملفات',
|
||||
'trip.loading': 'جارٍ تحميل الرحلة...',
|
||||
'trip.loadingPhotos': 'جارٍ تحميل صور الأماكن...',
|
||||
'trip.mobilePlan': 'الخطة',
|
||||
'trip.mobilePlaces': 'الأماكن',
|
||||
'trip.toast.placeUpdated': 'تم تحديث المكان',
|
||||
'trip.toast.placeAdded': 'تمت إضافة المكان',
|
||||
'trip.toast.placeDeleted': 'تم حذف المكان',
|
||||
'trip.toast.selectDay': 'يرجى اختيار يوم أولًا',
|
||||
'trip.toast.assignedToDay': 'تم إسناد المكان إلى اليوم',
|
||||
'trip.toast.reorderError': 'فشل إعادة الترتيب',
|
||||
'trip.toast.reservationUpdated': 'تم تحديث الحجز',
|
||||
'trip.toast.reservationAdded': 'تمت إضافة الحجز',
|
||||
'trip.toast.deleted': 'تم الحذف',
|
||||
'trip.confirm.deletePlace': 'هل تريد حذف هذا المكان؟',
|
||||
'trip.confirm.deletePlaces': 'حذف {count} أماكن؟',
|
||||
'trip.toast.placesDeleted': 'تم حذف {count} أماكن',
|
||||
};
|
||||
export default trip;
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const trips: TranslationStrings = {
|
||||
'trips.memberRemoved': '{username} تمت إزالته',
|
||||
'trips.memberRemoveError': 'فشل في الإزالة',
|
||||
'trips.memberAdded': '{username} تمت إضافته',
|
||||
'trips.memberAddError': 'فشل في الإضافة',
|
||||
'trips.reminder': 'تذكير',
|
||||
'trips.reminderNone': 'بدون',
|
||||
'trips.reminderDay': 'يوم',
|
||||
'trips.reminderDays': 'أيام',
|
||||
'trips.reminderCustom': 'مخصص',
|
||||
'trips.reminderDaysBefore': 'أيام قبل المغادرة',
|
||||
'trips.reminderDisabledHint':
|
||||
'تذكيرات الرحلة معطلة. قم بتفعيلها من الإدارة > الإعدادات > الإشعارات.',
|
||||
};
|
||||
export default trips;
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const undo: TranslationStrings = {
|
||||
'undo.button': 'تراجع',
|
||||
'undo.tooltip': 'تراجع: {action}',
|
||||
'undo.assignPlace': 'تم تعيين المكان لليوم',
|
||||
'undo.removeAssignment': 'تم إزالة المكان من اليوم',
|
||||
'undo.reorder': 'تمت إعادة ترتيب الأماكن',
|
||||
'undo.optimize': 'تم تحسين المسار',
|
||||
'undo.deletePlace': 'تم حذف المكان',
|
||||
'undo.deletePlaces': 'تم حذف الأماكن',
|
||||
'undo.moveDay': 'تم نقل المكان إلى يوم آخر',
|
||||
'undo.lock': 'تم تبديل قفل المكان',
|
||||
'undo.importGpx': 'استيراد GPX',
|
||||
'undo.importKeyholeMarkup': 'استيراد KMZ/KML',
|
||||
'undo.importGoogleList': 'استيراد خرائط Google',
|
||||
'undo.importNaverList': 'استيراد خرائط Naver',
|
||||
'undo.addPlace': 'تمت إضافة المكان',
|
||||
'undo.done': 'تم التراجع: {action}',
|
||||
};
|
||||
export default undo;
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const vacay: TranslationStrings = {
|
||||
'vacay.subtitle': 'خطط وأدر أيام الإجازة',
|
||||
'vacay.settings': 'الإعدادات',
|
||||
'vacay.year': 'السنة',
|
||||
'vacay.addYear': 'إضافة السنة التالية',
|
||||
'vacay.addPrevYear': 'إضافة السنة السابقة',
|
||||
'vacay.removeYear': 'إزالة السنة',
|
||||
'vacay.removeYearConfirm': 'إزالة {year}؟',
|
||||
'vacay.removeYearHint':
|
||||
'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
|
||||
'vacay.remove': 'إزالة',
|
||||
'vacay.persons': 'الأشخاص',
|
||||
'vacay.noPersons': 'لم تتم إضافة أشخاص بعد',
|
||||
'vacay.addPerson': 'إضافة شخص',
|
||||
'vacay.editPerson': 'تعديل الشخص',
|
||||
'vacay.removePerson': 'إزالة الشخص',
|
||||
'vacay.removePersonConfirm': 'إزالة {name}؟',
|
||||
'vacay.removePersonHint': 'سيتم حذف جميع إدخالات الإجازة لهذا الشخص نهائيًا.',
|
||||
'vacay.personName': 'الاسم',
|
||||
'vacay.personNamePlaceholder': 'أدخل الاسم',
|
||||
'vacay.color': 'اللون',
|
||||
'vacay.add': 'إضافة',
|
||||
'vacay.legend': 'المفتاح',
|
||||
'vacay.publicHoliday': 'عطلة رسمية',
|
||||
'vacay.companyHoliday': 'عطلة شركة',
|
||||
'vacay.weekend': 'نهاية الأسبوع',
|
||||
'vacay.modeVacation': 'إجازة',
|
||||
'vacay.modeCompany': 'عطلة شركة',
|
||||
'vacay.entitlement': 'الاستحقاق',
|
||||
'vacay.entitlementDays': 'الأيام',
|
||||
'vacay.used': 'المستخدم',
|
||||
'vacay.remaining': 'المتبقي',
|
||||
'vacay.carriedOver': 'من {year}',
|
||||
'vacay.blockWeekends': 'حظر عطلة نهاية الأسبوع',
|
||||
'vacay.blockWeekendsHint': 'منع إدخالات الإجازة يومي السبت والأحد',
|
||||
'vacay.weekendDays': 'أيام عطلة نهاية الأسبوع',
|
||||
'vacay.mon': 'الاثنين',
|
||||
'vacay.tue': 'الثلاثاء',
|
||||
'vacay.wed': 'الأربعاء',
|
||||
'vacay.thu': 'الخميس',
|
||||
'vacay.fri': 'الجمعة',
|
||||
'vacay.sat': 'السبت',
|
||||
'vacay.sun': 'الأحد',
|
||||
'vacay.publicHolidays': 'العطل الرسمية',
|
||||
'vacay.publicHolidaysHint': 'وضع علامة على العطل الرسمية في التقويم',
|
||||
'vacay.selectCountry': 'اختر الدولة',
|
||||
'vacay.selectRegion': 'اختر المنطقة (اختياري)',
|
||||
'vacay.addCalendar': 'إضافة تقويم',
|
||||
'vacay.calendarLabel': 'التسمية',
|
||||
'vacay.calendarColor': 'اللون',
|
||||
'vacay.noCalendars': 'لا توجد تقويمات',
|
||||
'vacay.companyHolidays': 'عطل الشركة',
|
||||
'vacay.companyHolidaysHint': 'السماح بوضع علامة على أيام عطلات الشركة',
|
||||
'vacay.companyHolidaysNoDeduct': 'لا تُخصم عطل الشركة من أيام الإجازة.',
|
||||
'vacay.weekStart': 'يبدأ الأسبوع في',
|
||||
'vacay.weekStartHint': 'اختر ما إذا كان الأسبوع يبدأ يوم الاثنين أو الأحد',
|
||||
'vacay.carryOver': 'الترحيل',
|
||||
'vacay.carryOverHint':
|
||||
'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية',
|
||||
'vacay.sharing': 'المشاركة',
|
||||
'vacay.sharingHint': 'شارك خطة إجازاتك مع مستخدمي TREK الآخرين',
|
||||
'vacay.owner': 'المالك',
|
||||
'vacay.shareEmailPlaceholder': 'البريد الإلكتروني لمستخدم TREK',
|
||||
'vacay.shareSuccess': 'تمت مشاركة الخطة بنجاح',
|
||||
'vacay.shareError': 'تعذرت مشاركة الخطة',
|
||||
'vacay.dissolve': 'فك الدمج',
|
||||
'vacay.dissolveHint': 'افصل التقويمات مرة أخرى. سيتم الاحتفاظ بإدخالاتك.',
|
||||
'vacay.dissolveAction': 'فك',
|
||||
'vacay.dissolved': 'تم فصل التقويم',
|
||||
'vacay.fusedWith': 'مُدمج مع',
|
||||
'vacay.you': 'أنت',
|
||||
'vacay.noData': 'لا توجد بيانات',
|
||||
'vacay.changeColor': 'تغيير اللون',
|
||||
'vacay.inviteUser': 'دعوة مستخدم',
|
||||
'vacay.inviteHint': 'ادعُ مستخدم TREK آخرًا لمشاركة تقويم إجازة مشترك.',
|
||||
'vacay.selectUser': 'اختر مستخدمًا',
|
||||
'vacay.sendInvite': 'إرسال الدعوة',
|
||||
'vacay.inviteSent': 'تم إرسال الدعوة',
|
||||
'vacay.inviteError': 'تعذر إرسال الدعوة',
|
||||
'vacay.pending': 'قيد الانتظار',
|
||||
'vacay.noUsersAvailable': 'لا يوجد مستخدمون متاحون',
|
||||
'vacay.accept': 'قبول',
|
||||
'vacay.decline': 'رفض',
|
||||
'vacay.acceptFusion': 'قبول ودمج',
|
||||
'vacay.inviteTitle': 'طلب دمج',
|
||||
'vacay.inviteWantsToFuse': 'يريد مشاركة تقويم إجازة معك.',
|
||||
'vacay.fuseInfo1': 'سيرى كلاكما جميع إدخالات الإجازة في تقويم مشترك واحد.',
|
||||
'vacay.fuseInfo2': 'يمكن لكلا الطرفين إنشاء وتعديل الإدخالات لبعضهما البعض.',
|
||||
'vacay.fuseInfo3': 'يمكن لكلا الطرفين حذف الإدخالات وتغيير مستحقات الإجازة.',
|
||||
'vacay.fuseInfo4': 'تتم مشاركة الإعدادات مثل العطل الرسمية وعطل الشركة.',
|
||||
'vacay.fuseInfo5':
|
||||
'يمكن فك الدمج في أي وقت من قبل أي طرف. ستبقى إدخالاتك محفوظة.',
|
||||
};
|
||||
export default vacay;
|
||||
@@ -0,0 +1,367 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const admin: TranslationStrings = {
|
||||
'admin.notifications.title': 'Notificações',
|
||||
'admin.notifications.hint':
|
||||
'Escolha um canal de notificação. Apenas um pode estar ativo por vez.',
|
||||
'admin.notifications.none': 'Desativado',
|
||||
'admin.notifications.email': 'E-mail (SMTP)',
|
||||
'admin.notifications.webhook': 'Webhook',
|
||||
'admin.notifications.save': 'Salvar configurações de notificação',
|
||||
'admin.notifications.saved': 'Configurações de notificação salvas',
|
||||
'admin.notifications.testWebhook': 'Enviar webhook de teste',
|
||||
'admin.notifications.testWebhookSuccess':
|
||||
'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.testWebhookFailed': 'Falha ao enviar webhook de teste',
|
||||
'admin.smtp.title': 'E-mail e notificações',
|
||||
'admin.smtp.hint': 'Configuração SMTP para envio de notificações por e-mail.',
|
||||
'admin.smtp.testButton': 'Enviar e-mail de teste',
|
||||
'admin.webhook.hint':
|
||||
'Enviar notificações para um webhook externo (Discord, Slack, etc.).',
|
||||
'admin.smtp.testSuccess': 'E-mail de teste enviado com sucesso',
|
||||
'admin.smtp.testFailed': 'Falha ao enviar e-mail de teste',
|
||||
'admin.title': 'Administração',
|
||||
'admin.subtitle': 'Gestão de usuários e configurações do sistema',
|
||||
'admin.tabs.users': 'Usuários',
|
||||
'admin.tabs.categories': 'Categorias',
|
||||
'admin.tabs.backup': 'Backup',
|
||||
'admin.stats.users': 'Usuários',
|
||||
'admin.stats.trips': 'Viagens',
|
||||
'admin.stats.places': 'Lugares',
|
||||
'admin.stats.photos': 'Fotos',
|
||||
'admin.stats.files': 'Arquivos',
|
||||
'admin.table.user': 'Usuário',
|
||||
'admin.table.email': 'E-mail',
|
||||
'admin.table.role': 'Função',
|
||||
'admin.table.created': 'Criado',
|
||||
'admin.table.lastLogin': 'Último acesso',
|
||||
'admin.table.actions': 'Ações',
|
||||
'admin.you': '(Você)',
|
||||
'admin.editUser': 'Editar usuário',
|
||||
'admin.newPassword': 'Nova senha',
|
||||
'admin.newPasswordHint': 'Deixe em branco para manter a senha atual',
|
||||
'admin.deleteUser':
|
||||
'Excluir o usuário "{name}"? Todas as viagens serão excluídas permanentemente.',
|
||||
'admin.deleteUserTitle': 'Excluir usuário',
|
||||
'admin.newPasswordPlaceholder': 'Digite a nova senha…',
|
||||
'admin.toast.loadError': 'Falha ao carregar dados do admin',
|
||||
'admin.toast.userUpdated': 'Usuário atualizado',
|
||||
'admin.toast.updateError': 'Falha ao atualizar',
|
||||
'admin.toast.userDeleted': 'Usuário excluído',
|
||||
'admin.toast.deleteError': 'Falha ao excluir',
|
||||
'admin.toast.cannotDeleteSelf': 'Não é possível excluir a própria conta',
|
||||
'admin.toast.userCreated': 'Usuário criado',
|
||||
'admin.toast.createError': 'Falha ao criar usuário',
|
||||
'admin.toast.fieldsRequired':
|
||||
'Nome de usuário, e-mail e senha são obrigatórios',
|
||||
'admin.createUser': 'Criar usuário',
|
||||
'admin.invite.title': 'Links de convite',
|
||||
'admin.invite.subtitle': 'Crie links de cadastro de uso único',
|
||||
'admin.invite.create': 'Criar link',
|
||||
'admin.invite.createAndCopy': 'Criar e copiar',
|
||||
'admin.invite.empty': 'Nenhum link de convite criado ainda',
|
||||
'admin.invite.maxUses': 'Máx. usos',
|
||||
'admin.invite.expiry': 'Expira após',
|
||||
'admin.invite.uses': 'usado(s)',
|
||||
'admin.invite.expiresAt': 'expira',
|
||||
'admin.invite.createdBy': 'por',
|
||||
'admin.invite.active': 'Ativo',
|
||||
'admin.invite.expired': 'Expirado',
|
||||
'admin.invite.usedUp': 'Esgotado',
|
||||
'admin.invite.copied': 'Link de convite copiado para a área de transferência',
|
||||
'admin.invite.copyLink': 'Copiar link',
|
||||
'admin.invite.deleted': 'Link de convite excluído',
|
||||
'admin.invite.createError': 'Falha ao criar link de convite',
|
||||
'admin.invite.deleteError': 'Falha ao excluir link de convite',
|
||||
'admin.tabs.settings': 'Configurações',
|
||||
'admin.allowRegistration': 'Permitir cadastro',
|
||||
'admin.allowRegistrationHint': 'Novos usuários podem se cadastrar sozinhos',
|
||||
'admin.authMethods': 'Authentication Methods',
|
||||
'admin.passwordLogin': 'Password Login',
|
||||
'admin.passwordLoginHint': 'Allow users to sign in with email and password',
|
||||
'admin.passwordRegistration': 'Password Registration',
|
||||
'admin.passwordRegistrationHint':
|
||||
'Allow new users to register with email and password',
|
||||
'admin.oidcLogin': 'SSO Login',
|
||||
'admin.oidcLoginHint': 'Allow users to sign in with SSO',
|
||||
'admin.oidcRegistration': 'SSO Auto-Provisioning',
|
||||
'admin.oidcRegistrationHint':
|
||||
'Automatically create accounts for new SSO users',
|
||||
'admin.envOverrideHint':
|
||||
'Password login settings are controlled by the OIDC_ONLY environment variable and cannot be changed here.',
|
||||
'admin.lockoutWarning': 'At least one login method must remain enabled',
|
||||
'admin.requireMfa': 'Exigir autenticação em dois fatores (2FA)',
|
||||
'admin.requireMfaHint':
|
||||
'Usuários sem 2FA precisam concluir a configuração em Configurações antes de usar o app.',
|
||||
'admin.apiKeys': 'Chaves de API',
|
||||
'admin.apiKeysHint':
|
||||
'Opcional. Habilita dados estendidos de lugares, como fotos e clima.',
|
||||
'admin.mapsKey': 'Chave da API Google Maps',
|
||||
'admin.mapsKeyHint':
|
||||
'Necessária para busca de lugares. Obtenha em console.cloud.google.com',
|
||||
'admin.mapsKeyHintLong':
|
||||
'Sem chave de API, o OpenStreetMap é usado na busca. Com uma chave Google, também podem ser carregadas fotos, avaliações e horários. Obtenha em console.cloud.google.com.',
|
||||
'admin.recommended': 'Recomendado',
|
||||
'admin.weatherKey': 'Chave OpenWeatherMap',
|
||||
'admin.weatherKeyHint':
|
||||
'Para dados meteorológicos. Grátis em openweathermap.org',
|
||||
'admin.validateKey': 'Testar',
|
||||
'admin.keyValid': 'Conectado',
|
||||
'admin.keyInvalid': 'Inválida',
|
||||
'admin.keySaved': 'Chaves de API salvas',
|
||||
'admin.oidcTitle': 'Login Único (OIDC)',
|
||||
'admin.oidcSubtitle':
|
||||
'Permitir login via provedores externos como Google, Apple, Authentik ou Keycloak.',
|
||||
'admin.oidcDisplayName': 'Nome exibido',
|
||||
'admin.oidcIssuer': 'URL do emissor',
|
||||
'admin.oidcIssuerHint':
|
||||
'URL do emissor OpenID Connect do provedor, ex.: https://accounts.google.com',
|
||||
'admin.oidcSaved': 'Configuração OIDC salva',
|
||||
'admin.oidcOnlyMode': 'Desativar login por senha',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'Quando ativado, só é permitido login SSO. Login e cadastro por senha ficam bloqueados.',
|
||||
'admin.fileTypes': 'Tipos de arquivo permitidos',
|
||||
'admin.fileTypesHint':
|
||||
'Configure quais tipos de arquivo os usuários podem enviar.',
|
||||
'admin.fileTypesFormat':
|
||||
'Extensões separadas por vírgula (ex.: jpg,png,pdf,doc). Use * para permitir todos.',
|
||||
'admin.fileTypesSaved': 'Configurações de tipos de arquivo salvas',
|
||||
'admin.placesPhotos.title': 'Fotos de Locais',
|
||||
'admin.placesPhotos.subtitle':
|
||||
'Busca fotos da Google Places API. Desative para economizar cota da API. Fotos do Wikimedia não são afetadas.',
|
||||
'admin.placesAutocomplete.title': 'Autocompletar de Locais',
|
||||
'admin.placesAutocomplete.subtitle':
|
||||
'Usa a Google Places API para sugestões de pesquisa. Desative para economizar cota da API.',
|
||||
'admin.placesDetails.title': 'Detalhes do Local',
|
||||
'admin.placesDetails.subtitle':
|
||||
'Busca informações detalhadas do local (horários, avaliação, site) da Google Places API. Desative para economizar cota da API.',
|
||||
'admin.bagTracking.title': 'Rastreamento de malas',
|
||||
'admin.bagTracking.subtitle':
|
||||
'Ativar peso e atribuição de mala para itens da lista',
|
||||
'admin.collab.chat.title': 'Chat',
|
||||
'admin.collab.chat.subtitle': 'Mensagens em tempo real para colaboração',
|
||||
'admin.collab.notes.title': 'Notas',
|
||||
'admin.collab.notes.subtitle': 'Notas e documentos compartilhados',
|
||||
'admin.collab.polls.title': 'Enquetes',
|
||||
'admin.collab.polls.subtitle': 'Enquetes e votações em grupo',
|
||||
'admin.collab.whatsnext.title': 'Próximos passos',
|
||||
'admin.collab.whatsnext.subtitle':
|
||||
'Sugestões de atividades e próximos passos',
|
||||
'admin.tabs.config': 'Personalização',
|
||||
'admin.tabs.defaults': 'Padrões do usuário',
|
||||
'admin.defaultSettings.title': 'Configurações padrão do usuário',
|
||||
'admin.defaultSettings.description':
|
||||
'Defina padrões para toda a instância. Usuários que não alteraram uma configuração verão esses valores. As próprias alterações deles sempre têm prioridade.',
|
||||
'admin.defaultSettings.saved': 'Padrão salvo',
|
||||
'admin.defaultSettings.reset': 'Redefinir para o padrão integrado',
|
||||
'admin.defaultSettings.resetToBuiltIn': 'redefinir',
|
||||
'admin.tabs.templates': 'Modelos de mala',
|
||||
'admin.packingTemplates.title': 'Modelos de mala',
|
||||
'admin.packingTemplates.subtitle':
|
||||
'Crie listas de mala reutilizáveis para suas viagens',
|
||||
'admin.packingTemplates.create': 'Novo modelo',
|
||||
'admin.packingTemplates.namePlaceholder': 'Nome do modelo (ex.: Praia)',
|
||||
'admin.packingTemplates.empty': 'Nenhum modelo criado ainda',
|
||||
'admin.packingTemplates.items': 'itens',
|
||||
'admin.packingTemplates.categories': 'categorias',
|
||||
'admin.packingTemplates.itemName': 'Nome do item',
|
||||
'admin.packingTemplates.itemCategory': 'Categoria',
|
||||
'admin.packingTemplates.categoryName': 'Nome da categoria (ex.: Roupas)',
|
||||
'admin.packingTemplates.addCategory': 'Adicionar categoria',
|
||||
'admin.packingTemplates.created': 'Modelo criado',
|
||||
'admin.packingTemplates.deleted': 'Modelo excluído',
|
||||
'admin.packingTemplates.loadError': 'Falha ao carregar modelos',
|
||||
'admin.packingTemplates.createError': 'Falha ao criar modelo',
|
||||
'admin.packingTemplates.deleteError': 'Falha ao excluir modelo',
|
||||
'admin.packingTemplates.saveError': 'Falha ao salvar',
|
||||
'admin.tabs.addons': 'Complementos',
|
||||
'admin.addons.title': 'Complementos',
|
||||
'admin.addons.subtitle':
|
||||
'Ative ou desative recursos para personalizar sua experiência no TREK.',
|
||||
'admin.addons.catalog.memories.name': 'Memórias',
|
||||
'admin.addons.catalog.memories.description':
|
||||
'Álbuns de fotos compartilhados em cada viagem',
|
||||
'admin.addons.catalog.packing.name': 'Listas',
|
||||
'admin.addons.catalog.packing.description':
|
||||
'Listas de bagagem e tarefas a fazer para suas viagens',
|
||||
'admin.addons.catalog.budget.name': 'Orçamento',
|
||||
'admin.addons.catalog.budget.description':
|
||||
'Acompanhe despesas e planeje o orçamento da viagem',
|
||||
'admin.addons.catalog.documents.name': 'Documentos',
|
||||
'admin.addons.catalog.documents.description':
|
||||
'Armazene e gerencie documentos de viagem',
|
||||
'admin.addons.catalog.vacay.name': 'Férias',
|
||||
'admin.addons.catalog.vacay.description':
|
||||
'Planejador de férias pessoal com visão em calendário',
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'Mapa mundial com países visitados e estatísticas',
|
||||
'admin.addons.catalog.collab.name': 'Colab',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'Notas, enquetes e chat em tempo real para planejar a viagem',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description':
|
||||
'Model Context Protocol para integração com assistentes de IA',
|
||||
'admin.addons.subtitleBefore':
|
||||
'Ative ou desative recursos para personalizar sua ',
|
||||
'admin.addons.subtitleAfter': ' experiência.',
|
||||
'admin.addons.enabled': 'Ativado',
|
||||
'admin.addons.disabled': 'Desativado',
|
||||
'admin.addons.type.trip': 'Viagem',
|
||||
'admin.addons.type.global': 'Global',
|
||||
'admin.addons.type.integration': 'Integração',
|
||||
'admin.addons.tripHint': 'Disponível como aba em cada viagem',
|
||||
'admin.addons.globalHint':
|
||||
'Disponível como seção própria na navegação principal',
|
||||
'admin.addons.toast.updated': 'Complemento atualizado',
|
||||
'admin.addons.toast.error': 'Falha ao atualizar complemento',
|
||||
'admin.addons.noAddons': 'Nenhum complemento disponível',
|
||||
'admin.addons.integrationHint':
|
||||
'Serviços de backend e integrações de API sem página dedicada',
|
||||
'admin.weather.title': 'Dados meteorológicos',
|
||||
'admin.weather.badge': 'Desde 24 de março de 2026',
|
||||
'admin.weather.description':
|
||||
'O TREK usa Open-Meteo como fonte de clima. Open-Meteo é um serviço gratuito e de código aberto — sem chave de API.',
|
||||
'admin.weather.forecast': 'Previsão de 16 dias',
|
||||
'admin.weather.forecastDesc': 'Antes eram 5 dias (OpenWeatherMap)',
|
||||
'admin.weather.climate': 'Dados climáticos históricos',
|
||||
'admin.weather.climateDesc':
|
||||
'Médias dos últimos 85 anos para dias além da previsão de 16 dias',
|
||||
'admin.weather.requests': '10.000 requisições / dia',
|
||||
'admin.weather.requestsDesc': 'Grátis, sem chave de API',
|
||||
'admin.weather.locationHint':
|
||||
'O clima usa o primeiro lugar com coordenadas de cada dia. Se nenhum lugar estiver atribuído ao dia, qualquer lugar da lista serve como referência.',
|
||||
'admin.tabs.audit': 'Auditoria',
|
||||
'admin.audit.subtitle':
|
||||
'Eventos sensíveis de segurança e administração (backups, usuários, 2FA, configurações).',
|
||||
'admin.audit.empty': 'Nenhum registro de auditoria.',
|
||||
'admin.audit.refresh': 'Atualizar',
|
||||
'admin.audit.loadMore': 'Carregar mais',
|
||||
'admin.audit.showing': '{count} carregados · {total} no total',
|
||||
'admin.audit.col.time': 'Hora',
|
||||
'admin.audit.col.user': 'Usuário',
|
||||
'admin.audit.col.action': 'Ação',
|
||||
'admin.audit.col.resource': 'Recurso',
|
||||
'admin.audit.col.ip': 'IP',
|
||||
'admin.audit.col.details': 'Detalhes',
|
||||
'admin.tabs.github': 'GitHub',
|
||||
'admin.github.title': 'Histórico de versões',
|
||||
'admin.github.subtitle': 'Últimas atualizações de {repo}',
|
||||
'admin.github.latest': 'Mais recente',
|
||||
'admin.github.prerelease': 'Pré-lançamento',
|
||||
'admin.github.showDetails': 'Mostrar detalhes',
|
||||
'admin.github.hideDetails': 'Ocultar detalhes',
|
||||
'admin.github.loadMore': 'Carregar mais',
|
||||
'admin.github.loading': 'Carregando...',
|
||||
'admin.github.error': 'Falha ao carregar versões',
|
||||
'admin.github.by': 'por',
|
||||
'admin.github.support': 'Ajuda a continuar desenvolvendo o TREK',
|
||||
'admin.update.available': 'Atualização disponível',
|
||||
'admin.update.text':
|
||||
'O TREK {version} está disponível. Você está na {current}.',
|
||||
'admin.update.button': 'Ver no GitHub',
|
||||
'admin.update.install': 'Instalar atualização',
|
||||
'admin.update.confirmTitle': 'Instalar atualização?',
|
||||
'admin.update.confirmText':
|
||||
'O TREK será atualizado de {current} para {version}. O servidor reiniciará automaticamente em seguida.',
|
||||
'admin.update.dataInfo':
|
||||
'Todos os seus dados (viagens, usuários, chaves de API, envios, Vacay, Atlas, orçamentos) serão preservados.',
|
||||
'admin.update.warning':
|
||||
'O app ficará brevemente indisponível durante o reinício.',
|
||||
'admin.update.confirm': 'Atualizar agora',
|
||||
'admin.update.installing': 'Atualizando…',
|
||||
'admin.update.success': 'Atualização instalada! O servidor está reiniciando…',
|
||||
'admin.update.failed': 'Falha na atualização',
|
||||
'admin.update.backupHint': 'Recomendamos criar um backup antes de atualizar.',
|
||||
'admin.update.backupLink': 'Ir para Backup',
|
||||
'admin.update.howTo': 'Como atualizar',
|
||||
'admin.update.dockerText':
|
||||
'Sua instância TREK roda no Docker. Para atualizar para {version}, execute no servidor:',
|
||||
'admin.update.reloadHint': 'Recarregue a página em alguns segundos.',
|
||||
'admin.tabs.permissions': 'Permissões',
|
||||
'admin.tabs.mcpTokens': 'Acesso MCP',
|
||||
'admin.mcpTokens.title': 'Acesso MCP',
|
||||
'admin.mcpTokens.subtitle':
|
||||
'Gerenciar sessões OAuth e tokens de API de todos os usuários',
|
||||
'admin.mcpTokens.sectionTitle': 'Tokens de API',
|
||||
'admin.mcpTokens.owner': 'Proprietário',
|
||||
'admin.mcpTokens.tokenName': 'Nome do Token',
|
||||
'admin.mcpTokens.created': 'Criado',
|
||||
'admin.mcpTokens.lastUsed': 'Último uso',
|
||||
'admin.mcpTokens.never': 'Nunca',
|
||||
'admin.mcpTokens.empty': 'Nenhum token MCP foi criado ainda',
|
||||
'admin.mcpTokens.deleteTitle': 'Excluir Token',
|
||||
'admin.mcpTokens.deleteMessage':
|
||||
'Isso revogará o token imediatamente. O usuário perderá o acesso MCP por este token.',
|
||||
'admin.mcpTokens.deleteSuccess': 'Token excluído',
|
||||
'admin.mcpTokens.deleteError': 'Falha ao excluir token',
|
||||
'admin.mcpTokens.loadError': 'Falha ao carregar tokens',
|
||||
'admin.oauthSessions.sectionTitle': 'Sessões OAuth',
|
||||
'admin.oauthSessions.clientName': 'Cliente',
|
||||
'admin.oauthSessions.owner': 'Proprietário',
|
||||
'admin.oauthSessions.scopes': 'Permissões',
|
||||
'admin.oauthSessions.created': 'Criado',
|
||||
'admin.oauthSessions.empty': 'Nenhuma sessão OAuth ativa',
|
||||
'admin.oauthSessions.revokeTitle': 'Revogar sessão',
|
||||
'admin.oauthSessions.revokeMessage':
|
||||
'Esta sessão OAuth será revogada imediatamente. O cliente perderá o acesso MCP.',
|
||||
'admin.oauthSessions.revokeSuccess': 'Sessão revogada',
|
||||
'admin.oauthSessions.revokeError': 'Falha ao revogar sessão',
|
||||
'admin.oauthSessions.loadError': 'Falha ao carregar sessões OAuth',
|
||||
'admin.notifications.emailPanel.title': 'Email (SMTP)',
|
||||
'admin.notifications.webhookPanel.title': 'Webhook',
|
||||
'admin.notifications.inappPanel.title': 'In-App',
|
||||
'admin.notifications.inappPanel.hint':
|
||||
'As notificações no aplicativo estão sempre ativas e não podem ser desativadas globalmente.',
|
||||
'admin.notifications.adminWebhookPanel.title': 'Webhook de admin',
|
||||
'admin.notifications.adminWebhookPanel.hint':
|
||||
'Este webhook é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos webhooks de usuários e dispara automaticamente quando uma URL está configurada.',
|
||||
'admin.notifications.adminWebhookPanel.saved':
|
||||
'URL do webhook de admin salva',
|
||||
'admin.notifications.adminWebhookPanel.testSuccess':
|
||||
'Webhook de teste enviado com sucesso',
|
||||
'admin.notifications.adminWebhookPanel.testFailed':
|
||||
'Falha no webhook de teste',
|
||||
'admin.notifications.adminWebhookPanel.alwaysOnHint':
|
||||
'O webhook de admin dispara automaticamente quando uma URL está configurada',
|
||||
'admin.notifications.ntfy': 'Ntfy',
|
||||
'admin.ntfy.hint':
|
||||
'Permite que os usuários configurem seus próprios tópicos ntfy para notificações push. Configure o servidor padrão abaixo para preencher as configurações do usuário.',
|
||||
'admin.notifications.testNtfy': 'Enviar Ntfy de teste',
|
||||
'admin.notifications.testNtfySuccess': 'Ntfy de teste enviado com sucesso',
|
||||
'admin.notifications.testNtfyFailed': 'Falha ao enviar Ntfy de teste',
|
||||
'admin.notifications.adminNtfyPanel.title': 'Ntfy de admin',
|
||||
'admin.notifications.adminNtfyPanel.hint':
|
||||
'Este tópico Ntfy é usado exclusivamente para notificações de admin (ex. alertas de versão). É independente dos tópicos por usuário e sempre dispara quando configurado.',
|
||||
'admin.notifications.adminNtfyPanel.serverLabel': 'URL do servidor Ntfy',
|
||||
'admin.notifications.adminNtfyPanel.serverHint':
|
||||
'Também usado como servidor padrão para notificações ntfy dos usuários. Deixe em branco para usar ntfy.sh. Os usuários podem substituir isso em suas próprias configurações.',
|
||||
'admin.notifications.adminNtfyPanel.serverPlaceholder': 'https://ntfy.sh',
|
||||
'admin.notifications.adminNtfyPanel.topicLabel': 'Tópico de admin',
|
||||
'admin.notifications.adminNtfyPanel.topicPlaceholder': 'trek-admin-alerts',
|
||||
'admin.notifications.adminNtfyPanel.tokenLabel': 'Token de acesso (opcional)',
|
||||
'admin.notifications.adminNtfyPanel.tokenCleared':
|
||||
'Token de acesso admin removido',
|
||||
'admin.notifications.adminNtfyPanel.saved':
|
||||
'Configurações de Ntfy de admin salvas',
|
||||
'admin.notifications.adminNtfyPanel.test': 'Enviar Ntfy de teste',
|
||||
'admin.notifications.adminNtfyPanel.testSuccess':
|
||||
'Ntfy de teste enviado com sucesso',
|
||||
'admin.notifications.adminNtfyPanel.testFailed':
|
||||
'Falha ao enviar Ntfy de teste',
|
||||
'admin.notifications.adminNtfyPanel.alwaysOnHint':
|
||||
'O Ntfy de admin sempre dispara quando um tópico está configurado',
|
||||
'admin.notifications.adminNotificationsHint':
|
||||
'Configure quais canais entregam notificações de admin (ex. alertas de versão). O webhook dispara automaticamente se uma URL de webhook de admin estiver definida.',
|
||||
'admin.notifications.tripReminders.title': 'Lembretes de viagem',
|
||||
'admin.notifications.tripReminders.hint':
|
||||
'Envia uma notificação de lembrete antes do início de uma viagem (requer dias de lembrete definidos na viagem).',
|
||||
'admin.notifications.tripReminders.enabled': 'Lembretes de viagem ativados',
|
||||
'admin.notifications.tripReminders.disabled':
|
||||
'Lembretes de viagem desativados',
|
||||
'admin.tabs.notifications': 'Notificações',
|
||||
'admin.addons.catalog.journey.name': 'Jornada',
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Rastreamento de viagens e diário de viajante com check-ins, fotos e histórias diárias',
|
||||
};
|
||||
export default admin;
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const airport: TranslationStrings = {
|
||||
'airport.searchPlaceholder': 'Código ou cidade do aeroporto (ex. FRA)',
|
||||
};
|
||||
export default airport;
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const atlas: TranslationStrings = {
|
||||
'atlas.subtitle': 'Sua pegada de viagens pelo mundo',
|
||||
'atlas.countries': 'Países',
|
||||
'atlas.trips': 'Viagens',
|
||||
'atlas.places': 'Lugares',
|
||||
'atlas.unmark': 'Remover',
|
||||
'atlas.confirmMark': 'Marcar este país como visitado?',
|
||||
'atlas.confirmUnmark': 'Remover este país da lista de visitados?',
|
||||
'atlas.confirmUnmarkRegion': 'Remover esta região da lista de visitados?',
|
||||
'atlas.markVisited': 'Marcar como visitado',
|
||||
'atlas.markVisitedHint': 'Adicionar este país à lista de visitados',
|
||||
'atlas.markRegionVisitedHint': 'Adicionar esta região à lista de visitados',
|
||||
'atlas.addToBucket': 'Adicionar à lista de desejos',
|
||||
'atlas.addPoi': 'Adicionar lugar',
|
||||
'atlas.searchCountry': 'Buscar um país...',
|
||||
'atlas.bucketNamePlaceholder': 'Nome (país, cidade, lugar…)',
|
||||
'atlas.month': 'Mês',
|
||||
'atlas.year': 'Ano',
|
||||
'atlas.addToBucketHint': 'Salvar como lugar que você quer visitar',
|
||||
'atlas.bucketWhen': 'Quando pretende visitar?',
|
||||
'atlas.statsTab': 'Estatísticas',
|
||||
'atlas.bucketTab': 'Lista de desejos',
|
||||
'atlas.addBucket': 'Adicionar à lista de desejos',
|
||||
'atlas.bucketNotesPlaceholder': 'Notas (opcional)',
|
||||
'atlas.bucketEmpty': 'Sua lista de desejos está vazia',
|
||||
'atlas.bucketEmptyHint': 'Adicione lugares que sonha em visitar',
|
||||
'atlas.days': 'Dias',
|
||||
'atlas.visitedCountries': 'Países visitados',
|
||||
'atlas.cities': 'Cidades',
|
||||
'atlas.noData': 'Ainda sem dados de viagem',
|
||||
'atlas.noDataHint':
|
||||
'Crie uma viagem e adicione lugares para ver o mapa mundial',
|
||||
'atlas.lastTrip': 'Última viagem',
|
||||
'atlas.nextTrip': 'Próxima viagem',
|
||||
'atlas.daysLeft': 'dias restantes',
|
||||
'atlas.streak': 'Sequência',
|
||||
'atlas.years': 'anos',
|
||||
'atlas.yearInRow': 'ano seguido',
|
||||
'atlas.yearsInRow': 'anos seguidos',
|
||||
'atlas.tripIn': 'viagem em',
|
||||
'atlas.tripsIn': 'viagens em',
|
||||
'atlas.since': 'desde',
|
||||
'atlas.europe': 'Europa',
|
||||
'atlas.asia': 'Ásia',
|
||||
'atlas.northAmerica': 'América do Norte',
|
||||
'atlas.southAmerica': 'América do Sul',
|
||||
'atlas.africa': 'África',
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Outro',
|
||||
'atlas.firstVisit': 'Primeira viagem',
|
||||
'atlas.lastVisitLabel': 'Última viagem',
|
||||
'atlas.tripSingular': 'Viagem',
|
||||
'atlas.tripPlural': 'Viagens',
|
||||
'atlas.placeVisited': 'Lugar visitado',
|
||||
'atlas.placesVisited': 'Lugares visitados',
|
||||
};
|
||||
export default atlas;
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const backup: TranslationStrings = {
|
||||
'backup.title': 'Backup de dados',
|
||||
'backup.subtitle': 'Banco de dados e todos os arquivos enviados',
|
||||
'backup.refresh': 'Atualizar',
|
||||
'backup.upload': 'Enviar backup',
|
||||
'backup.uploading': 'Enviando…',
|
||||
'backup.create': 'Criar backup',
|
||||
'backup.creating': 'Criando…',
|
||||
'backup.empty': 'Nenhum backup ainda',
|
||||
'backup.createFirst': 'Criar primeiro backup',
|
||||
'backup.download': 'Baixar',
|
||||
'backup.restore': 'Restaurar',
|
||||
'backup.confirm.restore':
|
||||
'Restaurar o backup "{name}"?\n\nTodos os dados atuais serão substituídos pelo backup.',
|
||||
'backup.confirm.uploadRestore':
|
||||
'Enviar e restaurar o arquivo "{name}"?\n\nTodos os dados atuais serão sobrescritos.',
|
||||
'backup.confirm.delete': 'Excluir o backup "{name}"?',
|
||||
'backup.toast.loadError': 'Falha ao carregar backups',
|
||||
'backup.toast.created': 'Backup criado com sucesso',
|
||||
'backup.toast.createError': 'Falha ao criar backup',
|
||||
'backup.toast.restored': 'Backup restaurado. A página será recarregada…',
|
||||
'backup.toast.restoreError': 'Falha ao restaurar',
|
||||
'backup.toast.uploadError': 'Falha no envio',
|
||||
'backup.toast.deleted': 'Backup excluído',
|
||||
'backup.toast.deleteError': 'Falha ao excluir',
|
||||
'backup.toast.downloadError': 'Falha no download',
|
||||
'backup.toast.settingsSaved': 'Configurações de backup automático salvas',
|
||||
'backup.toast.settingsError': 'Falha ao salvar configurações',
|
||||
'backup.auto.title': 'Backup automático',
|
||||
'backup.auto.subtitle': 'Backup automático em agenda',
|
||||
'backup.auto.enable': 'Ativar backup automático',
|
||||
'backup.auto.enableHint':
|
||||
'Backups serão criados automaticamente conforme a agenda escolhida',
|
||||
'backup.auto.interval': 'Intervalo',
|
||||
'backup.auto.hour': 'Executar no horário',
|
||||
'backup.auto.hourHint': 'Horário local do servidor (formato {format})',
|
||||
'backup.auto.dayOfWeek': 'Dia da semana',
|
||||
'backup.auto.dayOfMonth': 'Dia do mês',
|
||||
'backup.auto.dayOfMonthHint':
|
||||
'Limitado a 1–28 para compatibilidade com todos os meses',
|
||||
'backup.auto.scheduleSummary': 'Agenda',
|
||||
'backup.auto.summaryDaily': 'Todos os dias às {hour}:00',
|
||||
'backup.auto.summaryWeekly': 'Toda {day} às {hour}:00',
|
||||
'backup.auto.summaryMonthly': 'Dia {day} de cada mês às {hour}:00',
|
||||
'backup.auto.envLocked': 'Docker',
|
||||
'backup.auto.envLockedHint':
|
||||
'O backup automático é configurado via variáveis de ambiente Docker. Para alterar essas configurações, atualize o docker-compose.yml e reinicie o contêiner.',
|
||||
'backup.auto.copyEnv': 'Copiar variáveis de ambiente Docker',
|
||||
'backup.auto.envCopied':
|
||||
'Variáveis de ambiente Docker copiadas para a área de transferência',
|
||||
'backup.auto.keepLabel': 'Excluir backups antigos após',
|
||||
'backup.dow.sunday': 'Dom',
|
||||
'backup.dow.monday': 'Seg',
|
||||
'backup.dow.tuesday': 'Ter',
|
||||
'backup.dow.wednesday': 'Qua',
|
||||
'backup.dow.thursday': 'Qui',
|
||||
'backup.dow.friday': 'Sex',
|
||||
'backup.dow.saturday': 'Sáb',
|
||||
'backup.interval.hourly': 'A cada hora',
|
||||
'backup.interval.daily': 'Diário',
|
||||
'backup.interval.weekly': 'Semanal',
|
||||
'backup.interval.monthly': 'Mensal',
|
||||
'backup.keep.1day': '1 dia',
|
||||
'backup.keep.3days': '3 dias',
|
||||
'backup.keep.7days': '7 dias',
|
||||
'backup.keep.14days': '14 dias',
|
||||
'backup.keep.30days': '30 dias',
|
||||
'backup.keep.forever': 'Manter para sempre',
|
||||
'backup.restoreConfirmTitle': 'Restaurar backup?',
|
||||
'backup.restoreWarning':
|
||||
'Todos os dados atuais (viagens, lugares, usuários, envios) serão permanentemente substituídos pelo backup. Esta ação não pode ser desfeita.',
|
||||
'backup.restoreTip':
|
||||
'Dica: crie um backup do estado atual antes de restaurar.',
|
||||
'backup.restoreConfirm': 'Sim, restaurar',
|
||||
};
|
||||
export default backup;
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const budget: TranslationStrings = {
|
||||
'budget.title': 'Orçamento',
|
||||
'budget.exportCsv': 'Exportar CSV',
|
||||
'budget.emptyTitle': 'Nenhum orçamento criado ainda',
|
||||
'budget.emptyText':
|
||||
'Crie categorias e lançamentos para planejar o orçamento da viagem',
|
||||
'budget.emptyPlaceholder': 'Nome da categoria...',
|
||||
'budget.createCategory': 'Criar categoria',
|
||||
'budget.category': 'Categoria',
|
||||
'budget.categoryName': 'Nome da categoria',
|
||||
'budget.table.name': 'Nome',
|
||||
'budget.table.total': 'Total',
|
||||
'budget.table.persons': 'Pessoas',
|
||||
'budget.table.days': 'Dias',
|
||||
'budget.table.perPerson': 'Por pessoa',
|
||||
'budget.table.perDay': 'Por dia',
|
||||
'budget.table.perPersonDay': 'P. p. / dia',
|
||||
'budget.table.note': 'Obs.',
|
||||
'budget.table.date': 'Data',
|
||||
'budget.newEntry': 'Novo lançamento',
|
||||
'budget.defaultEntry': 'Novo lançamento',
|
||||
'budget.defaultCategory': 'Nova categoria',
|
||||
'budget.total': 'Total',
|
||||
'budget.totalBudget': 'Orçamento total',
|
||||
'budget.byCategory': 'Por categoria',
|
||||
'budget.editTooltip': 'Clique para editar',
|
||||
'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome por lá',
|
||||
'budget.confirm.deleteCategory':
|
||||
'Excluir a categoria "{name}" com {count} lançamento(s)?',
|
||||
'budget.deleteCategory': 'Excluir categoria',
|
||||
'budget.perPerson': 'Por pessoa',
|
||||
'budget.paid': 'Pago',
|
||||
'budget.open': 'Em aberto',
|
||||
'budget.noMembers': 'Nenhum membro atribuído',
|
||||
'budget.settlement': 'Acerto',
|
||||
'budget.settlementInfo':
|
||||
'Clique no avatar de um membro em um item do orçamento para marcá-lo em verde — significa que ele pagou. O acerto mostra quem deve quanto a quem.',
|
||||
'budget.netBalances': 'Saldos líquidos',
|
||||
'budget.categoriesLabel': 'categorias',
|
||||
};
|
||||
export default budget;
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const categories: TranslationStrings = {
|
||||
'categories.title': 'Categorias',
|
||||
'categories.subtitle': 'Gerenciar categorias de lugares',
|
||||
'categories.new': 'Nova categoria',
|
||||
'categories.empty': 'Nenhuma categoria ainda',
|
||||
'categories.namePlaceholder': 'Nome da categoria',
|
||||
'categories.icon': 'Ícone',
|
||||
'categories.color': 'Cor',
|
||||
'categories.customColor': 'Escolher cor personalizada',
|
||||
'categories.preview': 'Pré-visualização',
|
||||
'categories.defaultName': 'Categoria',
|
||||
'categories.update': 'Atualizar',
|
||||
'categories.create': 'Criar',
|
||||
'categories.confirm.delete':
|
||||
'Excluir categoria? Os lugares desta categoria não serão excluídos.',
|
||||
'categories.toast.loadError': 'Falha ao carregar categorias',
|
||||
'categories.toast.nameRequired': 'Digite um nome',
|
||||
'categories.toast.updated': 'Categoria atualizada',
|
||||
'categories.toast.created': 'Categoria criada',
|
||||
'categories.toast.saveError': 'Falha ao salvar',
|
||||
'categories.toast.deleted': 'Categoria excluída',
|
||||
'categories.toast.deleteError': 'Falha ao excluir',
|
||||
};
|
||||
export default categories;
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const collab: TranslationStrings = {
|
||||
'collab.tabs.chat': 'Chat',
|
||||
'collab.tabs.notes': 'Notas',
|
||||
'collab.tabs.polls': 'Enquetes',
|
||||
'collab.whatsNext.title': 'Próximos passos',
|
||||
'collab.whatsNext.today': 'Hoje',
|
||||
'collab.whatsNext.tomorrow': 'Amanhã',
|
||||
'collab.whatsNext.empty': 'Nenhuma atividade próxima',
|
||||
'collab.whatsNext.until': 'até',
|
||||
'collab.whatsNext.emptyHint': 'Atividades com horário aparecerão aqui',
|
||||
'collab.chat.send': 'Enviar',
|
||||
'collab.chat.placeholder': 'Digite uma mensagem...',
|
||||
'collab.chat.empty': 'Inicie a conversa',
|
||||
'collab.chat.emptyHint':
|
||||
'As mensagens são compartilhadas com todos os membros da viagem',
|
||||
'collab.chat.emptyDesc':
|
||||
'Compartilhe ideias, planos e atualizações com o grupo',
|
||||
'collab.chat.today': 'Hoje',
|
||||
'collab.chat.yesterday': 'Ontem',
|
||||
'collab.chat.deletedMessage': 'apagou uma mensagem',
|
||||
'collab.chat.reply': 'Responder',
|
||||
'collab.chat.loadMore': 'Carregar mensagens antigas',
|
||||
'collab.chat.justNow': 'agora mesmo',
|
||||
'collab.chat.minutesAgo': 'há {n} min',
|
||||
'collab.chat.hoursAgo': 'há {n} h',
|
||||
'collab.notes.title': 'Notas',
|
||||
'collab.notes.new': 'Nova nota',
|
||||
'collab.notes.empty': 'Nenhuma nota ainda',
|
||||
'collab.notes.emptyHint': 'Comece a registrar ideias e planos',
|
||||
'collab.notes.all': 'Todas',
|
||||
'collab.notes.titlePlaceholder': 'Título da nota',
|
||||
'collab.notes.contentPlaceholder': 'Escreva algo...',
|
||||
'collab.notes.categoryPlaceholder': 'Categoria',
|
||||
'collab.notes.newCategory': 'Nova categoria...',
|
||||
'collab.notes.category': 'Categoria',
|
||||
'collab.notes.noCategory': 'Sem categoria',
|
||||
'collab.notes.color': 'Cor',
|
||||
'collab.notes.save': 'Salvar',
|
||||
'collab.notes.cancel': 'Cancelar',
|
||||
'collab.notes.edit': 'Editar',
|
||||
'collab.notes.delete': 'Excluir',
|
||||
'collab.notes.pin': 'Fixar',
|
||||
'collab.notes.unpin': 'Desafixar',
|
||||
'collab.notes.daysAgo': 'há {n} d',
|
||||
'collab.notes.categorySettings': 'Gerenciar categorias',
|
||||
'collab.notes.create': 'Criar',
|
||||
'collab.notes.website': 'Site',
|
||||
'collab.notes.websitePlaceholder': 'https://...',
|
||||
'collab.notes.attachFiles': 'Anexar arquivos',
|
||||
'collab.notes.noCategoriesYet': 'Nenhuma categoria ainda',
|
||||
'collab.notes.emptyDesc': 'Crie uma nota para começar',
|
||||
'collab.polls.title': 'Enquetes',
|
||||
'collab.polls.new': 'Nova enquete',
|
||||
'collab.polls.empty': 'Nenhuma enquete ainda',
|
||||
'collab.polls.emptyHint': 'Pergunte ao grupo e votem juntos',
|
||||
'collab.polls.question': 'Pergunta',
|
||||
'collab.polls.questionPlaceholder': 'O que vamos fazer?',
|
||||
'collab.polls.addOption': '+ Adicionar opção',
|
||||
'collab.polls.optionPlaceholder': 'Opção {n}',
|
||||
'collab.polls.create': 'Criar enquete',
|
||||
'collab.polls.close': 'Encerrar',
|
||||
'collab.polls.closed': 'Encerrada',
|
||||
'collab.polls.votes': '{n} votos',
|
||||
'collab.polls.vote': '{n} voto',
|
||||
'collab.polls.multipleChoice': 'Múltipla escolha',
|
||||
'collab.polls.multiChoice': 'Múltipla escolha',
|
||||
'collab.polls.deadline': 'Prazo',
|
||||
'collab.polls.option': 'Opção',
|
||||
'collab.polls.options': 'Opções',
|
||||
'collab.polls.delete': 'Excluir',
|
||||
'collab.polls.closedSection': 'Encerradas',
|
||||
};
|
||||
export default collab;
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const common: TranslationStrings = {
|
||||
'common.save': 'Salvar',
|
||||
'common.showMore': 'Mostrar mais',
|
||||
'common.showLess': 'Mostrar menos',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.clear': 'Limpar',
|
||||
'common.delete': 'Excluir',
|
||||
'common.edit': 'Editar',
|
||||
'common.add': 'Adicionar',
|
||||
'common.loading': 'Carregando...',
|
||||
'common.import': 'Importar',
|
||||
'common.select': 'Selecionar',
|
||||
'common.selectAll': 'Selecionar tudo',
|
||||
'common.deselectAll': 'Desmarcar tudo',
|
||||
'common.error': 'Erro',
|
||||
'common.unknownError': 'Erro desconhecido',
|
||||
'common.tooManyAttempts': 'Muitas tentativas. Tente novamente mais tarde.',
|
||||
'common.back': 'Voltar',
|
||||
'common.all': 'Todos',
|
||||
'common.close': 'Fechar',
|
||||
'common.open': 'Abrir',
|
||||
'common.upload': 'Enviar',
|
||||
'common.search': 'Buscar',
|
||||
'common.confirm': 'Confirmar',
|
||||
'common.ok': 'OK',
|
||||
'common.yes': 'Sim',
|
||||
'common.no': 'Não',
|
||||
'common.or': 'ou',
|
||||
'common.none': 'Nenhum',
|
||||
'common.date': 'Data',
|
||||
'common.rename': 'Renomear',
|
||||
'common.discardChanges': 'Descartar alterações',
|
||||
'common.discard': 'Descartar',
|
||||
'common.name': 'Nome',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Senha',
|
||||
'common.saving': 'Salvando...',
|
||||
'common.saved': 'Salvo',
|
||||
'common.expand': 'Expandir',
|
||||
'common.collapse': 'Recolher',
|
||||
'common.update': 'Atualizar',
|
||||
'common.change': 'Alterar',
|
||||
'common.uploading': 'Enviando…',
|
||||
'common.backToPlanning': 'Voltar ao planejamento',
|
||||
'common.reset': 'Redefinir',
|
||||
'common.copy': 'Copiar',
|
||||
'common.copied': 'Copiado',
|
||||
'common.justNow': 'agora mesmo',
|
||||
'common.hoursAgo': 'há {count}h',
|
||||
'common.daysAgo': 'há {count}d',
|
||||
};
|
||||
export default common;
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const dashboard: TranslationStrings = {
|
||||
'dashboard.title': 'Minhas viagens',
|
||||
'dashboard.subtitle.loading': 'Carregando viagens...',
|
||||
'dashboard.subtitle.trips': '{count} viagens ({archived} arquivadas)',
|
||||
'dashboard.subtitle.empty': 'Comece sua primeira viagem',
|
||||
'dashboard.subtitle.activeOne': '{count} viagem ativa',
|
||||
'dashboard.subtitle.activeMany': '{count} viagens ativas',
|
||||
'dashboard.subtitle.archivedSuffix': ' · {count} arquivadas',
|
||||
'dashboard.newTrip': 'Nova viagem',
|
||||
'dashboard.gridView': 'Grade',
|
||||
'dashboard.listView': 'Lista',
|
||||
'dashboard.currency': 'Moeda',
|
||||
'dashboard.timezone': 'Fusos horários',
|
||||
'dashboard.localTime': 'Local',
|
||||
'dashboard.timezoneCustomTitle': 'Fuso personalizado',
|
||||
'dashboard.timezoneCustomLabelPlaceholder': 'Rótulo (opcional)',
|
||||
'dashboard.timezoneCustomTzPlaceholder': 'ex.: America/Sao_Paulo',
|
||||
'dashboard.timezoneCustomAdd': 'Adicionar',
|
||||
'dashboard.timezoneCustomErrorEmpty': 'Informe um identificador de fuso',
|
||||
'dashboard.timezoneCustomErrorInvalid':
|
||||
'Fuso inválido. Use o formato Europe/Berlin',
|
||||
'dashboard.timezoneCustomErrorDuplicate': 'Já adicionado',
|
||||
'dashboard.emptyTitle': 'Nenhuma viagem ainda',
|
||||
'dashboard.emptyText': 'Crie sua primeira viagem e comece a planejar!',
|
||||
'dashboard.emptyButton': 'Criar primeira viagem',
|
||||
'dashboard.nextTrip': 'Próxima viagem',
|
||||
'dashboard.shared': 'Compartilhada',
|
||||
'dashboard.sharedBy': 'Compartilhada por {name}',
|
||||
'dashboard.days': 'Dias',
|
||||
'dashboard.places': 'Lugares',
|
||||
'dashboard.members': 'Parceiros de viagem',
|
||||
'dashboard.archive': 'Arquivar',
|
||||
'dashboard.copyTrip': 'Copiar',
|
||||
'dashboard.copySuffix': 'cópia',
|
||||
'dashboard.restore': 'Restaurar',
|
||||
'dashboard.archived': 'Arquivada',
|
||||
'dashboard.status.ongoing': 'Em andamento',
|
||||
'dashboard.status.today': 'Hoje',
|
||||
'dashboard.status.tomorrow': 'Amanhã',
|
||||
'dashboard.status.past': 'Passada',
|
||||
'dashboard.status.daysLeft': 'Faltam {count} dias',
|
||||
'dashboard.toast.loadError': 'Não foi possível carregar as viagens',
|
||||
'dashboard.toast.created': 'Viagem criada com sucesso!',
|
||||
'dashboard.toast.createError': 'Não foi possível criar a viagem',
|
||||
'dashboard.toast.updated': 'Viagem atualizada!',
|
||||
'dashboard.toast.updateError': 'Não foi possível atualizar a viagem',
|
||||
'dashboard.toast.deleted': 'Viagem excluída',
|
||||
'dashboard.toast.deleteError': 'Não foi possível excluir a viagem',
|
||||
'dashboard.toast.archived': 'Viagem arquivada',
|
||||
'dashboard.toast.archiveError': 'Não foi possível arquivar',
|
||||
'dashboard.toast.restored': 'Viagem restaurada',
|
||||
'dashboard.toast.restoreError': 'Não foi possível restaurar',
|
||||
'dashboard.toast.copied': 'Viagem copiada!',
|
||||
'dashboard.toast.copyError': 'Não foi possível copiar a viagem',
|
||||
'dashboard.confirm.delete':
|
||||
'Excluir a viagem "{title}"? Todos os lugares e planos serão excluídos permanentemente.',
|
||||
'dashboard.editTrip': 'Editar viagem',
|
||||
'dashboard.createTrip': 'Criar nova viagem',
|
||||
'dashboard.tripTitle': 'Título',
|
||||
'dashboard.tripTitlePlaceholder': 'ex.: Verão no Japão',
|
||||
'dashboard.tripDescription': 'Descrição',
|
||||
'dashboard.tripDescriptionPlaceholder': 'Sobre o que é esta viagem?',
|
||||
'dashboard.startDate': 'Data de início',
|
||||
'dashboard.endDate': 'Data de término',
|
||||
'dashboard.dayCount': 'Número de dias',
|
||||
'dashboard.dayCountHint':
|
||||
'Quantos dias planejar quando nenhuma data de viagem for definida.',
|
||||
'dashboard.noDateHint':
|
||||
'Sem datas — serão criados 7 dias padrão. Você pode alterar depois.',
|
||||
'dashboard.coverImage': 'Imagem de capa',
|
||||
'dashboard.addCoverImage': 'Adicionar capa (ou arrastar e soltar)',
|
||||
'dashboard.addMembers': 'Companheiros de viagem',
|
||||
'dashboard.addMember': 'Adicionar membro',
|
||||
'dashboard.coverSaved': 'Capa salva',
|
||||
'dashboard.coverUploadError': 'Falha no envio',
|
||||
'dashboard.coverRemoveError': 'Falha ao remover',
|
||||
'dashboard.titleRequired': 'O título é obrigatório',
|
||||
'dashboard.endDateError': 'A data final deve ser depois da inicial',
|
||||
'dashboard.greeting.morning': 'Bom dia,',
|
||||
'dashboard.greeting.afternoon': 'Boa tarde,',
|
||||
'dashboard.greeting.evening': 'Boa noite,',
|
||||
'dashboard.mobile.liveNow': 'Ao vivo agora',
|
||||
'dashboard.mobile.tripProgress': 'Progresso da viagem',
|
||||
'dashboard.mobile.daysLeft': '{count} dias restantes',
|
||||
'dashboard.mobile.places': 'Lugares',
|
||||
'dashboard.mobile.buddies': 'Companheiros',
|
||||
'dashboard.mobile.newTrip': 'Nova viagem',
|
||||
'dashboard.mobile.currency': 'Moeda',
|
||||
'dashboard.mobile.timezone': 'Fuso horário',
|
||||
'dashboard.mobile.upcomingTrips': 'Próximas viagens',
|
||||
'dashboard.mobile.yourTrips': 'Suas viagens',
|
||||
'dashboard.mobile.trips': 'viagens',
|
||||
'dashboard.mobile.starts': 'Começa',
|
||||
'dashboard.mobile.duration': 'Duração',
|
||||
'dashboard.mobile.day': 'dia',
|
||||
'dashboard.mobile.days': 'dias',
|
||||
'dashboard.mobile.ongoing': 'Em andamento',
|
||||
'dashboard.mobile.startsToday': 'Começa hoje',
|
||||
'dashboard.mobile.tomorrow': 'Amanhã',
|
||||
'dashboard.mobile.inDays': 'Em {count} dias',
|
||||
'dashboard.mobile.inMonths': 'Em {count} meses',
|
||||
'dashboard.mobile.completed': 'Concluído',
|
||||
'dashboard.mobile.currencyConverter': 'Conversor de moedas',
|
||||
};
|
||||
export default dashboard;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const day: TranslationStrings = {
|
||||
'day.precipProb': 'Probabilidade de chuva',
|
||||
'day.precipitation': 'Precipitação',
|
||||
'day.wind': 'Vento',
|
||||
'day.sunrise': 'Nascer do sol',
|
||||
'day.sunset': 'Pôr do sol',
|
||||
'day.hourlyForecast': 'Previsão por hora',
|
||||
'day.climateHint':
|
||||
'Médias históricas — previsão real disponível até 16 dias desta data.',
|
||||
'day.noWeather':
|
||||
'Sem dados meteorológicos. Adicione um lugar com coordenadas.',
|
||||
'day.overview': 'Resumo do dia',
|
||||
'day.accommodation': 'Hospedagem',
|
||||
'day.addAccommodation': 'Adicionar hospedagem',
|
||||
'day.hotelDayRange': 'Aplicar aos dias',
|
||||
'day.noPlacesForHotel': 'Adicione lugares à viagem primeiro',
|
||||
'day.allDays': 'Todos',
|
||||
'day.checkIn': 'Check-in',
|
||||
'day.checkInUntil': 'Até',
|
||||
'day.checkOut': 'Check-out',
|
||||
'day.confirmation': 'Confirmação',
|
||||
'day.editAccommodation': 'Editar hospedagem',
|
||||
'day.reservations': 'Reservas',
|
||||
};
|
||||
export default day;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const dayplan: TranslationStrings = {
|
||||
'dayplan.icsTooltip': 'Exportar calendário (ICS)',
|
||||
'dayplan.emptyDay': 'Nenhum lugar planejado para este dia',
|
||||
'dayplan.addNote': 'Adicionar nota',
|
||||
'dayplan.editNote': 'Editar nota',
|
||||
'dayplan.noteAdd': 'Adicionar nota',
|
||||
'dayplan.noteEdit': 'Editar nota',
|
||||
'dayplan.noteTitle': 'Nota',
|
||||
'dayplan.noteSubtitle': 'Nota do dia',
|
||||
'dayplan.totalCost': 'Custo total',
|
||||
'dayplan.days': 'Dias',
|
||||
'dayplan.dayN': 'Dia {n}',
|
||||
'dayplan.calculating': 'Calculando...',
|
||||
'dayplan.route': 'Rota',
|
||||
'dayplan.optimize': 'Otimizar',
|
||||
'dayplan.optimized': 'Rota otimizada',
|
||||
'dayplan.routeError': 'Falha ao calcular a rota',
|
||||
'dayplan.toast.needTwoPlaces':
|
||||
'São necessários pelo menos dois lugares para otimizar a rota',
|
||||
'dayplan.toast.routeOptimized': 'Rota otimizada',
|
||||
'dayplan.toast.noGeoPlaces':
|
||||
'Nenhum lugar com coordenadas para calcular a rota',
|
||||
'dayplan.confirmed': 'Confirmada',
|
||||
'dayplan.pendingRes': 'Pendente',
|
||||
'dayplan.pdf': 'PDF',
|
||||
'dayplan.pdfTooltip': 'Exportar plano do dia em PDF',
|
||||
'dayplan.pdfError': 'Falha ao exportar PDF',
|
||||
'dayplan.cannotReorderTransport':
|
||||
'Reservas com horário fixo não podem ser reordenadas',
|
||||
'dayplan.confirmRemoveTimeTitle': 'Remover horário?',
|
||||
'dayplan.confirmRemoveTimeBody':
|
||||
'Este lugar tem um horário fixo ({time}). Movê-lo removerá o horário e permitirá ordenação livre.',
|
||||
'dayplan.confirmRemoveTimeAction': 'Remover horário e mover',
|
||||
'dayplan.cannotDropOnTimed':
|
||||
'Itens não podem ser colocados entre entradas com horário fixo',
|
||||
'dayplan.cannotBreakChronology':
|
||||
'Isso quebraria a ordem cronológica dos itens e reservas agendados',
|
||||
'dayplan.mobile.addPlace': 'Adicionar lugar',
|
||||
'dayplan.mobile.searchPlaces': 'Buscar lugares...',
|
||||
'dayplan.mobile.allAssigned': 'Todos os lugares atribuídos',
|
||||
'dayplan.mobile.noMatch': 'Sem correspondência',
|
||||
'dayplan.mobile.createNew': 'Criar novo lugar',
|
||||
};
|
||||
export default dayplan;
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { NotificationLocale } from '../externalNotifications/types';
|
||||
|
||||
const br: NotificationLocale = {
|
||||
email: {
|
||||
footer: 'Você recebeu isso porque tem as notificações ativadas no TREK.',
|
||||
manage: 'Gerenciar preferências nas configurações',
|
||||
madeWith: 'Made with',
|
||||
openTrek: 'Abrir TREK',
|
||||
},
|
||||
events: {
|
||||
trip_invite: (p) => ({
|
||||
title: `Convite para "${p.trip}"`,
|
||||
body: `${p.actor} convidou ${p.invitee || 'um membro'} para a viagem "${p.trip}".`,
|
||||
}),
|
||||
booking_change: (p) => ({
|
||||
title: `Nova reserva: ${p.booking}`,
|
||||
body: `${p.actor} adicionou uma reserva "${p.booking}" (${p.type}) em "${p.trip}".`,
|
||||
}),
|
||||
trip_reminder: (p) => ({
|
||||
title: `Lembrete: ${p.trip}`,
|
||||
body: `Sua viagem "${p.trip}" está chegando!`,
|
||||
}),
|
||||
todo_due: (p) => ({
|
||||
title: `Tarefa com vencimento: ${p.todo}`,
|
||||
body: `"${p.todo}" em "${p.trip}" vence em ${p.due}.`,
|
||||
}),
|
||||
vacay_invite: (p) => ({
|
||||
title: 'Convite Vacay Fusion',
|
||||
body: `${p.actor} convidou você para fundir planos de férias. Abra o TREK para aceitar ou recusar.`,
|
||||
}),
|
||||
photos_shared: (p) => ({
|
||||
title: `${p.count} fotos compartilhadas`,
|
||||
body: `${p.actor} compartilhou ${p.count} foto(s) em "${p.trip}".`,
|
||||
}),
|
||||
collab_message: (p) => ({
|
||||
title: `Nova mensagem em "${p.trip}"`,
|
||||
body: `${p.actor}: ${p.preview}`,
|
||||
}),
|
||||
packing_tagged: (p) => ({
|
||||
title: `Bagagem: ${p.category}`,
|
||||
body: `${p.actor} atribuiu você à categoria "${p.category}" em "${p.trip}".`,
|
||||
}),
|
||||
version_available: (p) => ({
|
||||
title: 'Nova versão do TREK disponível',
|
||||
body: `O TREK ${p.version} está disponível. Acesse o painel de administração para atualizar.`,
|
||||
}),
|
||||
synology_session_cleared: () => ({
|
||||
title: 'Sessão Synology encerrada',
|
||||
body: 'Sua conta ou URL do Synology foi alterada. Você foi desconectado do Synology Photos.',
|
||||
}),
|
||||
},
|
||||
passwordReset: {
|
||||
subject: 'Redefinir sua senha',
|
||||
greeting: 'Olá',
|
||||
body: 'Recebemos um pedido para redefinir a senha da sua conta TREK. Clique no botão abaixo para definir uma nova senha.',
|
||||
ctaIntro: 'Redefinir senha',
|
||||
expiry: 'Este link expira em 60 minutos.',
|
||||
ignore:
|
||||
'Se você não solicitou isto, pode ignorar este e-mail — sua senha não será alterada.',
|
||||
},
|
||||
};
|
||||
|
||||
export default br;
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const files: TranslationStrings = {
|
||||
'files.title': 'Arquivos',
|
||||
'files.pageTitle': 'Arquivos e documentos',
|
||||
'files.subtitle': '{count} arquivos para {trip}',
|
||||
'files.download': 'Baixar',
|
||||
'files.openError': 'Não foi possível abrir o arquivo',
|
||||
'files.downloadPdf': 'Baixar PDF',
|
||||
'files.count': '{count} arquivos',
|
||||
'files.countSingular': '1 arquivo',
|
||||
'files.uploaded': '{count} enviado(s)',
|
||||
'files.uploadError': 'Falha no envio',
|
||||
'files.dropzone': 'Solte os arquivos aqui',
|
||||
'files.dropzoneHint': 'ou clique para escolher',
|
||||
'files.allowedTypes':
|
||||
'Imagens, PDF, DOC, DOCX, XLS, XLSX, TXT, CSV · Máx. 50 MB',
|
||||
'files.uploading': 'Enviando...',
|
||||
'files.filterAll': 'Todos',
|
||||
'files.filterPdf': 'PDFs',
|
||||
'files.filterImages': 'Imagens',
|
||||
'files.filterDocs': 'Documentos',
|
||||
'files.filterCollab': 'Notas Colab',
|
||||
'files.sourceCollab': 'Das notas Colab',
|
||||
'files.empty': 'Nenhum arquivo ainda',
|
||||
'files.emptyHint': 'Envie arquivos para anexá-los à viagem',
|
||||
'files.openTab': 'Abrir em nova aba',
|
||||
'files.confirm.delete': 'Excluir este arquivo?',
|
||||
'files.toast.deleted': 'Arquivo excluído',
|
||||
'files.toast.deleteError': 'Falha ao excluir arquivo',
|
||||
'files.sourcePlan': 'Plano do dia',
|
||||
'files.sourceBooking': 'Reserva',
|
||||
'files.sourceTransport': 'Transporte',
|
||||
'files.attach': 'Anexar',
|
||||
'files.pasteHint':
|
||||
'Você também pode colar imagens da área de transferência (Ctrl+V)',
|
||||
'files.trash': 'Lixeira',
|
||||
'files.trashEmpty': 'A lixeira está vazia',
|
||||
'files.emptyTrash': 'Esvaziar lixeira',
|
||||
'files.restore': 'Restaurar',
|
||||
'files.star': 'Favoritar',
|
||||
'files.unstar': 'Remover favorito',
|
||||
'files.assign': 'Atribuir',
|
||||
'files.assignTitle': 'Atribuir arquivo',
|
||||
'files.assignPlace': 'Lugar',
|
||||
'files.assignBooking': 'Reserva',
|
||||
'files.assignTransport': 'Transporte',
|
||||
'files.unassigned': 'Não atribuído',
|
||||
'files.unlink': 'Remover vínculo',
|
||||
'files.toast.trashed': 'Movido para a lixeira',
|
||||
'files.toast.restored': 'Arquivo restaurado',
|
||||
'files.toast.trashEmptied': 'Lixeira esvaziada',
|
||||
'files.toast.assigned': 'Arquivo atribuído',
|
||||
'files.toast.assignError': 'Falha na atribuição',
|
||||
'files.toast.restoreError': 'Falha ao restaurar',
|
||||
'files.confirm.permanentDelete':
|
||||
'Excluir permanentemente este arquivo? Não é possível desfazer.',
|
||||
'files.confirm.emptyTrash':
|
||||
'Excluir permanentemente todos os arquivos na lixeira? Não é possível desfazer.',
|
||||
'files.noteLabel': 'Nota',
|
||||
'files.notePlaceholder': 'Adicione uma nota...',
|
||||
};
|
||||
export default files;
|
||||
@@ -0,0 +1,86 @@
|
||||
import admin from './admin';
|
||||
import airport from './airport';
|
||||
import atlas from './atlas';
|
||||
import backup from './backup';
|
||||
import budget from './budget';
|
||||
import categories from './categories';
|
||||
import collab from './collab';
|
||||
import common from './common';
|
||||
import dashboard from './dashboard';
|
||||
import day from './day';
|
||||
import dayplan from './dayplan';
|
||||
import files from './files';
|
||||
import inspector from './inspector';
|
||||
import journey from './journey';
|
||||
import login from './login';
|
||||
import map from './map';
|
||||
import members from './members';
|
||||
import memories from './memories';
|
||||
import nav from './nav';
|
||||
import notif from './notif';
|
||||
import notifications from './notifications';
|
||||
import oauth from './oauth';
|
||||
import packing from './packing';
|
||||
import pdf from './pdf';
|
||||
import perm from './perm';
|
||||
import photos from './photos';
|
||||
import places from './places';
|
||||
import planner from './planner';
|
||||
import register from './register';
|
||||
import reservations from './reservations';
|
||||
import settings from './settings';
|
||||
import share from './share';
|
||||
import shared from './shared';
|
||||
import stats from './stats';
|
||||
import system_notice from './system_notice';
|
||||
import todo from './todo';
|
||||
import transport from './transport';
|
||||
import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
...trips,
|
||||
...nav,
|
||||
...dashboard,
|
||||
...settings,
|
||||
...admin,
|
||||
...dayplan,
|
||||
...share,
|
||||
...shared,
|
||||
...login,
|
||||
...register,
|
||||
...vacay,
|
||||
...atlas,
|
||||
...trip,
|
||||
...places,
|
||||
...inspector,
|
||||
...reservations,
|
||||
...airport,
|
||||
...map,
|
||||
...budget,
|
||||
...files,
|
||||
...packing,
|
||||
...members,
|
||||
...categories,
|
||||
...backup,
|
||||
...photos,
|
||||
...pdf,
|
||||
...planner,
|
||||
...stats,
|
||||
...day,
|
||||
...collab,
|
||||
...memories,
|
||||
...perm,
|
||||
...undo,
|
||||
...notifications,
|
||||
...todo,
|
||||
...notif,
|
||||
...journey,
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
};
|
||||
export default locale;
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const inspector: TranslationStrings = {
|
||||
'inspector.opened': 'Aberto',
|
||||
'inspector.closed': 'Fechado',
|
||||
'inspector.openingHours': 'Horário de funcionamento',
|
||||
'inspector.showHours': 'Mostrar horário de funcionamento',
|
||||
'inspector.files': 'Arquivos',
|
||||
'inspector.filesCount': '{count} arquivos',
|
||||
'inspector.removeFromDay': 'Remover do dia',
|
||||
'inspector.remove': 'Remover',
|
||||
'inspector.addToDay': 'Adicionar ao dia',
|
||||
'inspector.confirmedRes': 'Reserva confirmada',
|
||||
'inspector.pendingRes': 'Reserva pendente',
|
||||
'inspector.google': 'Abrir no Google Maps',
|
||||
'inspector.website': 'Abrir site',
|
||||
'inspector.addRes': 'Reserva',
|
||||
'inspector.editRes': 'Editar reserva',
|
||||
'inspector.participants': 'Participantes',
|
||||
'inspector.trackStats': 'Dados da trilha',
|
||||
};
|
||||
export default inspector;
|
||||
@@ -0,0 +1,240 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const journey: TranslationStrings = {
|
||||
'journey.search.placeholder': 'Buscar jornadas…',
|
||||
'journey.search.noResults': 'Nenhuma jornada corresponde a "{query}"',
|
||||
'journey.title': 'Jornada',
|
||||
'journey.subtitle': 'Registre suas viagens em tempo real',
|
||||
'journey.new': 'Nova jornada',
|
||||
'journey.create': 'Criar',
|
||||
'journey.titlePlaceholder': 'Para onde você vai?',
|
||||
'journey.empty': 'Nenhuma jornada ainda',
|
||||
'journey.emptyHint': 'Comece a documentar sua próxima viagem',
|
||||
'journey.deleted': 'Jornada excluída',
|
||||
'journey.createError': 'Não foi possível criar a jornada',
|
||||
'journey.deleteError': 'Não foi possível excluir a jornada',
|
||||
'journey.deleteConfirmTitle': 'Excluir',
|
||||
'journey.deleteConfirmMessage':
|
||||
'Excluir "{title}"? Isso não pode ser desfeito.',
|
||||
'journey.deleteConfirmGeneric': 'Tem certeza de que deseja excluir isso?',
|
||||
'journey.notFound': 'Jornada não encontrada',
|
||||
'journey.photos': 'Fotos',
|
||||
'journey.timelineEmpty': 'Nenhuma parada ainda',
|
||||
'journey.timelineEmptyHint':
|
||||
'Adicione um check-in ou escreva uma entrada no diário para começar',
|
||||
'journey.status.draft': 'Rascunho',
|
||||
'journey.status.active': 'Ativa',
|
||||
'journey.status.completed': 'Concluída',
|
||||
'journey.status.upcoming': 'Próxima',
|
||||
'journey.status.archived': 'Arquivado',
|
||||
'journey.checkin.add': 'Fazer check-in',
|
||||
'journey.checkin.namePlaceholder': 'Nome do local',
|
||||
'journey.checkin.notesPlaceholder': 'Notas (opcional)',
|
||||
'journey.checkin.save': 'Salvar',
|
||||
'journey.checkin.error': 'Não foi possível salvar o check-in',
|
||||
'journey.entry.add': 'Diário',
|
||||
'journey.entry.edit': 'Editar entrada',
|
||||
'journey.entry.titlePlaceholder': 'Título (opcional)',
|
||||
'journey.entry.bodyPlaceholder': 'O que aconteceu hoje?',
|
||||
'journey.entry.save': 'Salvar',
|
||||
'journey.entry.error': 'Não foi possível salvar a entrada',
|
||||
'journey.photo.add': 'Foto',
|
||||
'journey.photo.uploadError': 'Falha no envio',
|
||||
'journey.share.share': 'Compartilhar',
|
||||
'journey.share.public': 'Público',
|
||||
'journey.share.linkCopied': 'Link público copiado',
|
||||
'journey.share.disabled': 'Compartilhamento público desativado',
|
||||
'journey.editor.titlePlaceholder': 'Dê um nome a este momento...',
|
||||
'journey.editor.bodyPlaceholder': 'Conte a história deste dia...',
|
||||
'journey.editor.placePlaceholder': 'Localização (opcional)',
|
||||
'journey.editor.tagsPlaceholder':
|
||||
'Tags: joia escondida, melhor refeição, preciso voltar...',
|
||||
'journey.visibility.private': 'Privado',
|
||||
'journey.visibility.shared': 'Compartilhado',
|
||||
'journey.visibility.public': 'Público',
|
||||
'journey.emptyState.title': 'Sua história começa aqui',
|
||||
'journey.emptyState.subtitle':
|
||||
'Faça check-in em um lugar ou escreva sua primeira entrada no diário',
|
||||
'journey.frontpage.subtitle':
|
||||
'Transforme suas viagens em histórias que você nunca vai esquecer',
|
||||
'journey.frontpage.createJourney': 'Criar jornada',
|
||||
'journey.frontpage.activeJourney': 'Jornada ativa',
|
||||
'journey.frontpage.allJourneys': 'Todas as jornadas',
|
||||
'journey.frontpage.journeys': 'jornadas',
|
||||
'journey.frontpage.createNew': 'Criar uma nova jornada',
|
||||
'journey.frontpage.createNewSub':
|
||||
'Escolha viagens, escreva histórias, compartilhe suas aventuras',
|
||||
'journey.frontpage.live': 'Ao vivo',
|
||||
'journey.frontpage.synced': 'Sincronizado',
|
||||
'journey.frontpage.continueWriting': 'Continuar escrevendo',
|
||||
'journey.frontpage.updated': 'Atualizado {time}',
|
||||
'journey.frontpage.suggestionLabel': 'A viagem acabou de terminar',
|
||||
'journey.frontpage.suggestionText':
|
||||
'Transforme <strong>{title}</strong> em uma jornada',
|
||||
'journey.frontpage.dismiss': 'Dispensar',
|
||||
'journey.frontpage.journeyName': 'Nome da jornada',
|
||||
'journey.frontpage.namePlaceholder': 'ex. Sudeste Asiático 2026',
|
||||
'journey.frontpage.selectTrips': 'Selecionar viagens',
|
||||
'journey.frontpage.tripsSelected': 'viagens selecionadas',
|
||||
'journey.frontpage.trips': 'viagens',
|
||||
'journey.frontpage.placesImported': 'lugares serão importados',
|
||||
'journey.frontpage.places': 'lugares',
|
||||
'journey.detail.backToJourney': 'Voltar à jornada',
|
||||
'journey.detail.syncedWithTrips': 'Sincronizado com viagens',
|
||||
'journey.detail.addEntry': 'Adicionar entrada',
|
||||
'journey.detail.newEntry': 'Nova entrada',
|
||||
'journey.detail.editEntry': 'Editar entrada',
|
||||
'journey.detail.noEntries': 'Nenhuma entrada ainda',
|
||||
'journey.detail.noEntriesHint':
|
||||
'Adicione uma viagem para começar com entradas preliminares',
|
||||
'journey.detail.noPhotos': 'Nenhuma foto ainda',
|
||||
'journey.detail.noPhotosHint':
|
||||
'Envie fotos para as entradas ou explore sua biblioteca do Immich/Synology',
|
||||
'journey.detail.journeyStats': 'Estatísticas da jornada',
|
||||
'journey.detail.syncedTrips': 'Viagens sincronizadas',
|
||||
'journey.detail.noTripsLinked': 'Nenhuma viagem vinculada ainda',
|
||||
'journey.detail.contributors': 'Colaboradores',
|
||||
'journey.detail.readMore': 'Ler mais',
|
||||
'journey.detail.prosCons': 'Prós e contras',
|
||||
'journey.detail.photos': 'fotos',
|
||||
'journey.detail.day': 'Dia {number}',
|
||||
'journey.detail.places': 'lugares',
|
||||
'journey.stats.days': 'Dias',
|
||||
'journey.stats.cities': 'Cidades',
|
||||
'journey.stats.entries': 'Entradas',
|
||||
'journey.stats.photos': 'Fotos',
|
||||
'journey.stats.places': 'Lugares',
|
||||
'journey.skeletons.show': 'Mostrar sugestões',
|
||||
'journey.skeletons.hide': 'Ocultar sugestões',
|
||||
'journey.verdict.lovedIt': 'Adorei',
|
||||
'journey.verdict.couldBeBetter': 'Poderia ser melhor',
|
||||
'journey.synced.places': 'lugares',
|
||||
'journey.synced.synced': 'sincronizado',
|
||||
'journey.editor.discardChangesConfirm':
|
||||
'Você tem alterações não salvas. Descartá-las?',
|
||||
'journey.editor.uploadFailed': 'Falha ao enviar fotos',
|
||||
'journey.editor.uploadPhotos': 'Enviar fotos',
|
||||
'journey.editor.uploading': 'Enviando...',
|
||||
'journey.editor.uploadingProgress': 'Enviando {done}/{total}…',
|
||||
'journey.editor.uploadPartialFailed':
|
||||
'{failed} de {total} fotos falharam — salve novamente para tentar',
|
||||
'journey.editor.fromGallery': 'Da galeria',
|
||||
'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas',
|
||||
'journey.editor.writeStory': 'Escreva sua história...',
|
||||
'journey.editor.prosCons': 'Prós e contras',
|
||||
'journey.editor.pros': 'Prós',
|
||||
'journey.editor.cons': 'Contras',
|
||||
'journey.editor.proPlaceholder': 'Algo ótimo...',
|
||||
'journey.editor.conPlaceholder': 'Não tão bom...',
|
||||
'journey.editor.addAnother': 'Adicionar outro',
|
||||
'journey.editor.date': 'Data',
|
||||
'journey.editor.location': 'Localização',
|
||||
'journey.editor.searchLocation': 'Buscar localização...',
|
||||
'journey.editor.mood': 'Humor',
|
||||
'journey.editor.weather': 'Clima',
|
||||
'journey.editor.photoFirst': '1º',
|
||||
'journey.editor.makeFirst': 'Tornar 1º',
|
||||
'journey.editor.searching': 'Pesquisando...',
|
||||
'journey.mood.amazing': 'Incrível',
|
||||
'journey.mood.good': 'Bom',
|
||||
'journey.mood.neutral': 'Neutro',
|
||||
'journey.mood.rough': 'Difícil',
|
||||
'journey.weather.sunny': 'Ensolarado',
|
||||
'journey.weather.partly': 'Parcialmente nublado',
|
||||
'journey.weather.cloudy': 'Nublado',
|
||||
'journey.weather.rainy': 'Chuvoso',
|
||||
'journey.weather.stormy': 'Tempestuoso',
|
||||
'journey.weather.cold': 'Nevando',
|
||||
'journey.trips.linkTrip': 'Vincular viagem',
|
||||
'journey.trips.searchTrip': 'Buscar viagem',
|
||||
'journey.trips.searchPlaceholder': 'Nome da viagem ou destino...',
|
||||
'journey.trips.noTripsAvailable': 'Nenhuma viagem disponível',
|
||||
'journey.trips.link': 'Vincular',
|
||||
'journey.trips.tripLinked': 'Viagem vinculada',
|
||||
'journey.trips.linkFailed': 'Não foi possível vincular a viagem',
|
||||
'journey.trips.addTrip': 'Adicionar viagem',
|
||||
'journey.trips.unlinkTrip': 'Desvincular viagem',
|
||||
'journey.trips.unlinkMessage':
|
||||
'Desvincular "{title}"? Todas as entradas e fotos sincronizadas desta viagem serão excluídas permanentemente. Isso não pode ser desfeito.',
|
||||
'journey.trips.unlink': 'Desvincular',
|
||||
'journey.trips.tripUnlinked': 'Viagem desvinculada',
|
||||
'journey.trips.unlinkFailed': 'Não foi possível desvincular a viagem',
|
||||
'journey.trips.noTripsLinkedSettings': 'Nenhuma viagem vinculada',
|
||||
'journey.contributors.invite': 'Convidar colaborador',
|
||||
'journey.contributors.searchUser': 'Buscar usuário',
|
||||
'journey.contributors.searchPlaceholder': 'Nome de usuário ou e-mail...',
|
||||
'journey.contributors.noUsers': 'Nenhum usuário encontrado',
|
||||
'journey.contributors.role': 'Função',
|
||||
'journey.contributors.added': 'Colaborador adicionado',
|
||||
'journey.contributors.addFailed': 'Não foi possível adicionar o colaborador',
|
||||
'journey.share.publicShare': 'Compartilhamento público',
|
||||
'journey.share.createLink': 'Criar link de compartilhamento',
|
||||
'journey.share.linkCreated': 'Link de compartilhamento criado',
|
||||
'journey.share.createFailed': 'Não foi possível criar o link',
|
||||
'journey.share.copy': 'Copiar',
|
||||
'journey.share.copied': 'Copiado!',
|
||||
'journey.share.timeline': 'Linha do tempo',
|
||||
'journey.share.gallery': 'Galeria',
|
||||
'journey.share.map': 'Mapa',
|
||||
'journey.share.removeLink': 'Remover link de compartilhamento',
|
||||
'journey.share.linkDeleted': 'Link de compartilhamento removido',
|
||||
'journey.share.deleteFailed': 'Não foi possível excluir',
|
||||
'journey.share.updateFailed': 'Não foi possível atualizar',
|
||||
'journey.invite.role': 'Função',
|
||||
'journey.invite.viewer': 'Visualizador',
|
||||
'journey.invite.editor': 'Editor',
|
||||
'journey.invite.invite': 'Convidar',
|
||||
'journey.invite.inviting': 'Convidando...',
|
||||
'journey.settings.title': 'Configurações da jornada',
|
||||
'journey.settings.coverImage': 'Imagem de capa',
|
||||
'journey.settings.changeCover': 'Alterar capa',
|
||||
'journey.settings.addCover': 'Adicionar imagem de capa',
|
||||
'journey.settings.name': 'Nome',
|
||||
'journey.settings.subtitle': 'Subtítulo',
|
||||
'journey.settings.subtitlePlaceholder': 'ex. Tailândia, Vietnã e Camboja',
|
||||
'journey.settings.endJourney': 'Arquivar Jornada',
|
||||
'journey.settings.reopenJourney': 'Restaurar Jornada',
|
||||
'journey.settings.archived': 'Jornada arquivada',
|
||||
'journey.settings.reopened': 'Jornada reaberta',
|
||||
'journey.settings.endDescription':
|
||||
'Oculta o selo Ao Vivo. Você pode reabrir a qualquer momento.',
|
||||
'journey.settings.delete': 'Excluir',
|
||||
'journey.settings.deleteJourney': 'Excluir jornada',
|
||||
'journey.settings.deleteMessage':
|
||||
'Excluir "{title}"? Todas as entradas e fotos serão perdidas.',
|
||||
'journey.settings.saved': 'Configurações salvas',
|
||||
'journey.settings.saveFailed': 'Não foi possível salvar',
|
||||
'journey.settings.coverUpdated': 'Capa atualizada',
|
||||
'journey.settings.coverFailed': 'Falha no envio',
|
||||
'journey.settings.failedToDelete': 'Falha ao excluir',
|
||||
'journey.entries.deleteTitle': 'Excluir entrada',
|
||||
'journey.photosUploaded': '{count} fotos enviadas',
|
||||
'journey.photosUploadFailed': 'Algumas fotos não foram enviadas',
|
||||
'journey.photosAdded': '{count} fotos adicionadas',
|
||||
'journey.public.notFound': 'Não encontrado',
|
||||
'journey.public.notFoundMessage':
|
||||
'Esta jornada não existe ou o link expirou.',
|
||||
'journey.public.readOnly': 'Somente leitura · Jornada pública',
|
||||
'journey.public.tagline': 'Kit de recursos e exploração de viagens',
|
||||
'journey.public.sharedVia': 'Compartilhado via',
|
||||
'journey.public.madeWith': 'Feito com',
|
||||
'journey.pdf.journeyBook': 'Livro da jornada',
|
||||
'journey.pdf.madeWith': 'Feito com TREK',
|
||||
'journey.pdf.day': 'Dia',
|
||||
'journey.pdf.theEnd': 'Fim',
|
||||
'journey.pdf.saveAsPdf': 'Salvar como PDF',
|
||||
'journey.pdf.pages': 'páginas',
|
||||
'journey.picker.tripPeriod': 'Período da viagem',
|
||||
'journey.picker.dateRange': 'Período',
|
||||
'journey.picker.allPhotos': 'Todas as fotos',
|
||||
'journey.picker.albums': 'Álbuns',
|
||||
'journey.picker.selected': 'selecionados',
|
||||
'journey.picker.addTo': 'Adicionar a',
|
||||
'journey.picker.newGallery': 'Nova galeria',
|
||||
'journey.picker.selectAll': 'Selecionar tudo',
|
||||
'journey.picker.deselectAll': 'Desmarcar tudo',
|
||||
'journey.picker.noAlbums': 'Nenhum álbum encontrado',
|
||||
'journey.picker.selectDate': 'Selecionar data',
|
||||
'journey.picker.search': 'Pesquisar',
|
||||
};
|
||||
export default journey;
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const login: TranslationStrings = {
|
||||
'login.error': 'Falha no login. Verifique suas credenciais.',
|
||||
'login.tagline': 'Suas viagens.\nSeu plano.',
|
||||
'login.description':
|
||||
'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.',
|
||||
'login.features.maps': 'Mapas interativos',
|
||||
'login.features.mapsDesc': 'Google Places, rotas e agrupamento',
|
||||
'login.features.realtime': 'Sincronização em tempo real',
|
||||
'login.features.realtimeDesc': 'Planejem juntos via WebSocket',
|
||||
'login.features.budget': 'Controle de orçamento',
|
||||
'login.features.budgetDesc': 'Categorias, gráficos e custo por pessoa',
|
||||
'login.features.collab': 'Colaboração',
|
||||
'login.features.collabDesc': 'Vários usuários com viagens compartilhadas',
|
||||
'login.features.packing': 'Listas de malas',
|
||||
'login.features.packingDesc': 'Categorias, progresso e sugestões',
|
||||
'login.features.bookings': 'Reservas',
|
||||
'login.features.bookingsDesc': 'Voos, hotéis, restaurantes e mais',
|
||||
'login.features.files': 'Documentos',
|
||||
'login.features.filesDesc': 'Envie e gerencie documentos',
|
||||
'login.features.routes': 'Rotas inteligentes',
|
||||
'login.features.routesDesc': 'Otimize e exporte para o Google Maps',
|
||||
'login.selfHosted': 'Auto-hospedado · Código aberto · Seus dados são seus',
|
||||
'login.title': 'Entrar',
|
||||
'login.subtitle': 'Bem-vindo de volta',
|
||||
'login.signingIn': 'Entrando…',
|
||||
'login.signIn': 'Entrar',
|
||||
'login.createAdmin': 'Criar conta de administrador',
|
||||
'login.createAdminHint':
|
||||
'Configure a primeira conta de administrador do TREK.',
|
||||
'login.setNewPassword': 'Definir nova senha',
|
||||
'login.setNewPasswordHint': 'Você deve alterar sua senha antes de continuar.',
|
||||
'login.createAccount': 'Criar conta',
|
||||
'login.createAccountHint': 'Cadastre uma nova conta.',
|
||||
'login.creating': 'Criando…',
|
||||
'login.noAccount': 'Não tem conta?',
|
||||
'login.hasAccount': 'Já tem conta?',
|
||||
'login.register': 'Cadastrar',
|
||||
'login.emailPlaceholder': 'seu@email.com',
|
||||
'login.username': 'Nome de usuário',
|
||||
'login.oidc.registrationDisabled':
|
||||
'Cadastro desativado. Fale com o administrador.',
|
||||
'login.oidc.noEmail': 'Nenhum e-mail recebido do provedor.',
|
||||
'login.oidc.tokenFailed': 'Falha na autenticação.',
|
||||
'login.oidc.invalidState': 'Sessão inválida. Tente novamente.',
|
||||
'login.demoFailed': 'Falha no login de demonstração',
|
||||
'login.oidcSignIn': 'Entrar com {name}',
|
||||
'login.oidcOnly': 'Login por senha desativado. Use o provedor SSO.',
|
||||
'login.oidcLoggedOut':
|
||||
'Você foi desconectado. Entre novamente usando o provedor SSO.',
|
||||
'login.demoHint': 'Experimente a demonstração — sem cadastro',
|
||||
'login.mfaTitle': 'Autenticação em duas etapas',
|
||||
'login.mfaSubtitle': 'Digite o código de 6 dígitos do seu app autenticador.',
|
||||
'login.mfaCodeLabel': 'Código de verificação',
|
||||
'login.mfaCodeRequired': 'Digite o código do app autenticador.',
|
||||
'login.mfaHint': 'Abra o Google Authenticator, Authy ou outro app TOTP.',
|
||||
'login.mfaBack': '← Voltar ao login',
|
||||
'login.mfaVerify': 'Verificar',
|
||||
'login.invalidInviteLink': 'Link de convite inválido ou expirado',
|
||||
'login.oidcFailed': 'Falha no login OIDC',
|
||||
'login.usernameRequired': 'Nome de usuário é obrigatório',
|
||||
'login.passwordMinLength': 'A senha deve ter pelo menos 8 caracteres',
|
||||
'login.forgotPassword': 'Esqueceu a senha?',
|
||||
'login.forgotPasswordTitle': 'Redefinir sua senha',
|
||||
'login.forgotPasswordBody':
|
||||
'Digite o e-mail cadastrado. Se houver uma conta, enviaremos um link de redefinição.',
|
||||
'login.forgotPasswordSubmit': 'Enviar link',
|
||||
'login.forgotPasswordSentTitle': 'Verifique seu e-mail',
|
||||
'login.forgotPasswordSentBody':
|
||||
'Se houver uma conta para esse e-mail, o link está a caminho. Ele expira em 60 minutos.',
|
||||
'login.forgotPasswordSmtpHintOff':
|
||||
'Observação: seu administrador não configurou SMTP, então o link de redefinição será gravado no console do servidor em vez de ser enviado por e-mail.',
|
||||
'login.backToLogin': 'Voltar ao login',
|
||||
'login.newPassword': 'Nova senha',
|
||||
'login.confirmPassword': 'Confirmar nova senha',
|
||||
'login.passwordsDontMatch': 'As senhas não coincidem',
|
||||
'login.mfaCode': 'Código 2FA',
|
||||
'login.resetPasswordTitle': 'Definir uma nova senha',
|
||||
'login.resetPasswordBody':
|
||||
'Escolha uma senha forte que você ainda não tenha usado aqui. Mínimo de 8 caracteres.',
|
||||
'login.resetPasswordMfaBody':
|
||||
'Digite seu código 2FA ou um código de backup para concluir a redefinição.',
|
||||
'login.resetPasswordSubmit': 'Redefinir senha',
|
||||
'login.resetPasswordVerify': 'Verificar e redefinir',
|
||||
'login.resetPasswordSuccessTitle': 'Senha atualizada',
|
||||
'login.resetPasswordSuccessBody':
|
||||
'Agora você pode entrar com sua nova senha.',
|
||||
'login.resetPasswordInvalidLink': 'Link de redefinição inválido',
|
||||
'login.resetPasswordInvalidLinkBody':
|
||||
'Este link está ausente ou corrompido. Solicite um novo para continuar.',
|
||||
'login.resetPasswordFailed':
|
||||
'Falha na redefinição. O link pode ter expirado.',
|
||||
};
|
||||
export default login;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const map: TranslationStrings = {
|
||||
'map.connections': 'Conexões',
|
||||
'map.showConnections': 'Mostrar rotas de reservas',
|
||||
'map.hideConnections': 'Ocultar rotas de reservas',
|
||||
};
|
||||
export default map;
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const members: TranslationStrings = {
|
||||
'members.shareTrip': 'Compartilhar viagem',
|
||||
'members.inviteUser': 'Convidar usuário',
|
||||
'members.selectUser': 'Selecionar usuário…',
|
||||
'members.invite': 'Convidar',
|
||||
'members.allHaveAccess': 'Todos os usuários já têm acesso.',
|
||||
'members.access': 'Acesso',
|
||||
'members.person': 'pessoa',
|
||||
'members.persons': 'pessoas',
|
||||
'members.you': 'você',
|
||||
'members.owner': 'Proprietário',
|
||||
'members.leaveTrip': 'Sair da viagem',
|
||||
'members.removeAccess': 'Remover acesso',
|
||||
'members.confirmLeave': 'Sair da viagem? Você perderá o acesso.',
|
||||
'members.confirmRemove': 'Remover o acesso deste usuário?',
|
||||
'members.loadError': 'Falha ao carregar membros',
|
||||
'members.added': 'adicionado',
|
||||
'members.addError': 'Falha ao adicionar',
|
||||
'members.removed': 'Membro removido',
|
||||
'members.removeError': 'Falha ao remover',
|
||||
};
|
||||
export default members;
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const memories: TranslationStrings = {
|
||||
'memories.title': 'Fotos',
|
||||
'memories.notConnected': 'Immich não conectado',
|
||||
'memories.notConnectedHint':
|
||||
'Conecte sua instância Immich nas Configurações para ver suas fotos de viagem aqui.',
|
||||
'memories.notConnectedMultipleHint':
|
||||
'Conecte um destes provedores de fotos: {provider_names} nas Configurações para poder adicionar fotos a esta viagem.',
|
||||
'memories.noDates': 'Adicione datas à sua viagem para carregar fotos.',
|
||||
'memories.noPhotos': 'Nenhuma foto encontrada',
|
||||
'memories.noPhotosHint':
|
||||
'Nenhuma foto encontrada no Immich para o período desta viagem.',
|
||||
'memories.photosFound': 'fotos',
|
||||
'memories.fromOthers': 'de outros',
|
||||
'memories.sharePhotos': 'Compartilhar fotos',
|
||||
'memories.sharing': 'Compartilhando',
|
||||
'memories.reviewTitle': 'Revise suas fotos',
|
||||
'memories.reviewHint':
|
||||
'Clique nas fotos para excluí-las do compartilhamento.',
|
||||
'memories.shareCount': 'Compartilhar {count} fotos',
|
||||
'memories.providerUrl': 'URL do servidor',
|
||||
'memories.providerApiKey': 'Chave de API',
|
||||
'memories.providerUsername': 'Nome de usuário',
|
||||
'memories.providerPassword': 'Senha',
|
||||
'memories.providerOTP': 'Código MFA (se habilitado)',
|
||||
'memories.skipSSLVerification': 'Pular verificação de certificado SSL',
|
||||
'memories.immichAutoUpload': 'Espelhar fotos da jornada no Immich ao enviar',
|
||||
'memories.providerUrlHintSynology':
|
||||
'Inclua o caminho do aplicativo Photos na URL, ex. https://nas:5001/photo',
|
||||
'memories.testConnection': 'Testar conexão',
|
||||
'memories.testShort': 'Testar',
|
||||
'memories.testFirst': 'Teste a conexão primeiro',
|
||||
'memories.connected': 'Conectado',
|
||||
'memories.disconnected': 'Não conectado',
|
||||
'memories.connectionSuccess': 'Conectado ao Immich',
|
||||
'memories.connectionError': 'Não foi possível conectar ao Immich',
|
||||
'memories.saved': 'Configurações do {provider_name} salvas',
|
||||
'memories.providerDisconnectedBanner':
|
||||
'Sua conexão com {provider_name} foi perdida. Reconecte nas Configurações para ver as fotos.',
|
||||
'memories.saveError':
|
||||
'Não foi possível salvar as configurações de {provider_name}',
|
||||
'memories.addPhotos': 'Adicionar fotos',
|
||||
'memories.linkAlbum': 'Vincular álbum',
|
||||
'memories.selectAlbum': 'Selecionar álbum do Immich',
|
||||
'memories.selectAlbumMultiple': 'Selecionar álbum',
|
||||
'memories.noAlbums': 'Nenhum álbum encontrado',
|
||||
'memories.syncAlbum': 'Sincronizar álbum',
|
||||
'memories.unlinkAlbum': 'Desvincular',
|
||||
'memories.photos': 'fotos',
|
||||
'memories.selectPhotos': 'Selecionar fotos do Immich',
|
||||
'memories.selectPhotosMultiple': 'Selecionar fotos',
|
||||
'memories.selectHint': 'Toque nas fotos para selecioná-las.',
|
||||
'memories.selected': 'selecionadas',
|
||||
'memories.addSelected': 'Adicionar {count} fotos',
|
||||
'memories.alreadyAdded': 'Já adicionada',
|
||||
'memories.private': 'Privado',
|
||||
'memories.stopSharing': 'Parar de compartilhar',
|
||||
'memories.oldest': 'Mais antigas',
|
||||
'memories.newest': 'Mais recentes',
|
||||
'memories.allLocations': 'Todos os locais',
|
||||
'memories.tripDates': 'Datas da viagem',
|
||||
'memories.allPhotos': 'Todas as fotos',
|
||||
'memories.confirmShareTitle': 'Compartilhar com membros da viagem?',
|
||||
'memories.confirmShareHint':
|
||||
'{count} fotos serão visíveis para todos os membros desta viagem. Você pode tornar fotos individuais privadas depois.',
|
||||
'memories.confirmShareButton': 'Compartilhar fotos',
|
||||
'memories.error.loadAlbums': 'Falha ao carregar álbuns',
|
||||
'memories.error.linkAlbum': 'Falha ao vincular álbum',
|
||||
'memories.error.unlinkAlbum': 'Falha ao desvincular álbum',
|
||||
'memories.error.syncAlbum': 'Falha ao sincronizar álbum',
|
||||
'memories.error.loadPhotos': 'Falha ao carregar fotos',
|
||||
'memories.error.addPhotos': 'Falha ao adicionar fotos',
|
||||
'memories.error.removePhoto': 'Falha ao remover foto',
|
||||
'memories.error.toggleSharing': 'Falha ao atualizar compartilhamento',
|
||||
'memories.saveRouteNotConfigured':
|
||||
'A rota de salvamento não está configurada para este provedor',
|
||||
'memories.testRouteNotConfigured':
|
||||
'A rota de teste não está configurada para este provedor',
|
||||
'memories.fillRequiredFields':
|
||||
'Por favor preencha todos os campos obrigatórios',
|
||||
};
|
||||
export default memories;
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const nav: TranslationStrings = {
|
||||
'nav.trip': 'Viagem',
|
||||
'nav.share': 'Compartilhar',
|
||||
'nav.settings': 'Configurações',
|
||||
'nav.admin': 'Admin',
|
||||
'nav.logout': 'Sair',
|
||||
'nav.lightMode': 'Modo claro',
|
||||
'nav.darkMode': 'Modo escuro',
|
||||
'nav.autoMode': 'Automático',
|
||||
'nav.administrator': 'Administrador',
|
||||
'nav.myTrips': 'Minhas viagens',
|
||||
'nav.profile': 'Perfil',
|
||||
'nav.bottomSettings': 'Configurações',
|
||||
'nav.bottomAdmin': 'Administração',
|
||||
'nav.bottomLogout': 'Sair',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
};
|
||||
export default nav;
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const notif: TranslationStrings = {
|
||||
'notif.test.title': '[Teste] Notificação',
|
||||
'notif.test.simple.text': 'Esta é uma notificação de teste simples.',
|
||||
'notif.test.boolean.text': 'Você aceita esta notificação de teste?',
|
||||
'notif.test.navigate.text': 'Clique abaixo para ir ao painel.',
|
||||
'notif.trip_invite.title': 'Convite para viagem',
|
||||
'notif.trip_invite.text': '{actor} convidou você para {trip}',
|
||||
'notif.booking_change.title': 'Reserva atualizada',
|
||||
'notif.booking_change.text': '{actor} atualizou uma reserva em {trip}',
|
||||
'notif.trip_reminder.title': 'Lembrete de viagem',
|
||||
'notif.trip_reminder.text': 'Sua viagem {trip} está chegando!',
|
||||
'notif.todo_due.title': 'Tarefa com vencimento',
|
||||
'notif.todo_due.text': '{todo} em {trip} vence em {due}',
|
||||
'notif.vacay_invite.title': 'Convite Vacay Fusion',
|
||||
'notif.vacay_invite.text':
|
||||
'{actor} convidou você para fundir planos de férias',
|
||||
'notif.photos_shared.title': 'Fotos compartilhadas',
|
||||
'notif.photos_shared.text': '{actor} compartilhou {count} foto(s) em {trip}',
|
||||
'notif.collab_message.title': 'Nova mensagem',
|
||||
'notif.collab_message.text': '{actor} enviou uma mensagem em {trip}',
|
||||
'notif.packing_tagged.title': 'Atribuição de bagagem',
|
||||
'notif.packing_tagged.text': '{actor} atribuiu você a {category} em {trip}',
|
||||
'notif.version_available.title': 'Nova versão disponível',
|
||||
'notif.version_available.text': 'TREK {version} está disponível',
|
||||
'notif.action.view_trip': 'Ver viagem',
|
||||
'notif.action.view_collab': 'Ver mensagens',
|
||||
'notif.action.view_packing': 'Ver bagagem',
|
||||
'notif.action.view_photos': 'Ver fotos',
|
||||
'notif.action.view_vacay': 'Ver Vacay',
|
||||
'notif.action.view_admin': 'Ir para admin',
|
||||
'notif.action.view': 'Ver',
|
||||
'notif.action.accept': 'Aceitar',
|
||||
'notif.action.decline': 'Recusar',
|
||||
'notif.generic.title': 'Notificação',
|
||||
'notif.generic.text': 'Você tem uma nova notificação',
|
||||
'notif.dev.unknown_event.title': '[DEV] Evento desconhecido',
|
||||
'notif.dev.unknown_event.text':
|
||||
'O tipo de evento "{event}" não está registrado em EVENT_NOTIFICATION_CONFIG',
|
||||
};
|
||||
export default notif;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user