v3.1.1 bug fixes (#1228)

* fix(shared-view): render each leg of multi-leg flights correctly

The read-only shared view showed the overall trip start/end airports and
the first leg's flight number on every leg of a multi-leg flight. The Day
Plan already expands legs (each carries __leg), but the renderer ignored it
and read flat top-level metadata; the Bookings tab had the same bug.

- Day Plan: use __leg for per-leg airline/flight number/route, plus dep-arr time
- Bookings tab: list each leg via getFlightLegs()
- unique React keys for multi-leg rows

Closes #1219

* feat(pdf): add legs to pdf export

* fix(demo): skip first-run admin seed in demo mode

When DEMO_MODE is on, the demo seeder creates its own admin (admin@trek.app,
username "admin") right after the generic seeds run. The first-run admin
bootstrap was grabbing username "admin" first, so the demo seeder hit the
UNIQUE(username) constraint and aborted before the demo user was ever created
- which surfaced as a 500 "Demo user not found" on demo-login. Skip the
generic admin bootstrap when demo mode owns the admin account.

* fix(docker): ship the encryption-key migration script in the image

The production image only copied server/dist, so the documented rotation
command `node --import tsx scripts/migrate-encryption.ts` failed inside the
container with a module-not-found error - the raw .ts was never present. The
script runs via tsx straight from source and only pulls node builtins plus
better-sqlite3 (both prod deps), so copying the single file into
/app/server/scripts is enough to make the rotation work again.

* fix(vacay): keep the mode toolbar above the mobile bottom nav

The floating Vacation/Company toolbar was pinned at bottom-3 with z-30, so on
mobile it landed in the same band as the fixed bottom nav (z-60) and got hidden
behind it - and could scroll out of reach entirely. Pin it above the nav with
the shared --bottom-nav-h variable (0px on desktop, so nothing changes there)
and reserve matching space below the calendar grid so it never gets swallowed.

* fix(dashboard): show the correct reservation date regardless of timezone

The upcoming-reservations widget built the date with new Date(reservation_time)
.toISOString(), which reinterprets the stored naive local time as UTC and can
roll the displayed day forward in non-UTC timezones (e.g. a 23:30 reservation
showing the next day). Read the date and time straight from the stored string
parts via splitReservationDateTime, and format the time with the shared
formatTime helper so it also honours the user's 12h/24h preference.

* fix(atlas): cursor-following tooltips and removing countries from search

Two related Atlas fixes:

- Country tooltips were bound with sticky:false, which anchors them at the
  feature's bounds centre. For countries with overseas territories (e.g.
  France) that centre sits far out in the ocean, so the tooltip popped up
  nowhere near the area being hovered. Make them sticky so they track the
  cursor.

- Selecting an already-visited country from the search bar always opened the
  "Mark / Bucket" dialog, with no way to remove it. Tiny countries like
  Vatican City or Singapore are hard to hit on the map, so search was the only
  way in. Mirror the map-click behaviour: a manually-marked country opens the
  Remove confirmation, a trip/place-backed one opens its detail.

* fix(oidc): keep dots in generated usernames

The OIDC username sanitizer stripped dots because they were missing from the
allowed character class, so a name claim like "first.last" became "firstlast".
Dots are valid usernames (the profile validator already allows
^[a-zA-Z0-9_.-]+$), so add the dot to the sanitizer.

* fix(collab): show poll option labels in the UI

The poll API formatted each option as { label, voters }, but the React poll
component renders opt.text - so every option button came out blank. Emit text
alongside label (kept for any other consumer) so options render again.

* feat(backup): make the upload size limit configurable

The restore upload was capped at a hard-coded 500 MB, so instances whose
backup archive (uploads/ included) grew past that got a 413 "File too large"
with no way to raise it. Add a BACKUP_UPLOAD_LIMIT_MB env var (default 500,
invalid values warn and fall back), documented in .env.example.

* feat(costs): create an expense from a booking, fix editing total-only items

Replace the inline price + budget-category fields in the Transport and
Reservation booking modals with a "Create expense" flow: the modal saves the
booking, then opens the full Costs editor prefilled (name + category mapped from
the booking type) and linked to the reservation. A booking with a linked expense
shows it inline with edit / remove.

Also fix the Costs editor so an expense with a recorded total but no payers
(transport-derived or pre-rework items) opens with its amount, lets you set the
currency, and saves - it previously showed 0 everywhere and could not be saved.
Legacy / localized categories now map to the fixed keys, and changing a booking's
type keeps its linked expense category in sync (unless it was manually set).

- shared: reservation_id on budget create, typeToCostCategory helper, i18n keys
- server: createBudgetItem stores reservation_id; keep total_price for payerless
  items; a booking update no longer wipes its linked expense and syncs the
  category on type change
- client: shared BookingCostsSection, exported ExpenseModal with prefill and an
  editable total, page-level save-then-open wiring

* test(reservations): align syncBudgetOnUpdate unit tests with no-wipe + type-sync

The service now leaves a linked expense alone when no budget entry is on the
payload (only an explicit total_price 0 deletes it) and syncs the category on a
booking type change. Update the unit tests accordingly - the old "price cleared"
case passed entry: undefined, which is now a no-op and left a mocked return
queued that leaked into the next test.

* fix(planner): keep a reservation on its day when edited (#1237)

Editing a booking forced its day_id to the globally selected day, which is null
when editing from the Book tab - so the booking lost its day and vanished from
the Plan. Preserve the reservation own day_id on edit instead.

* fix(planner): derive a booking day from its date when none is set (#1237)

The client always sends day_id on a reservation update, so the server only
derived it from reservation_time when the field was absent. A non-transport
booking saved without a selected day (Book tab) therefore got day_id null and
vanished from the Plan, even though its date matched a day. Derive the day from
reservation_time whenever day_id is null, mirroring create.

* fix(planner): let a booking's day follow its date when edited (#1237)

Preserving the old day_id on edit left a re-dated booking on its previous start
day while end_day_id followed the new date, so it spanned both. Stop sending
day_id from the edit modal entirely - the server derives both ends from the
booking's date (and keeps the current day when there is no date), so a re-dated
booking moves cleanly to the matching day.

* fix(atlas): keep the continent breakdown in sync on mark/unmark (#1225)

The optimistic mark/unmark updates bumped the country total but never the
per-continent counts, so the continent column froze until a full reload. Move
the country to continent map into @trek/shared (single source for server and
client) and adjust the matching continent count at every optimistic site: the
country confirm flow plus the choose / region mark and region unmark handlers.

* feat(admin): let admins set a default currency for new users

Adds a currency picker to Admin > User Defaults. Stored as the default_currency
user-default, so users who have not picked their own currency inherit it in
Costs.

* fix(atlas): give every sub-national region a distinct code (#1217)

geoBoundaries fills shapeISO with the bare country code for some countries (every
Spanish region got "ESP", every Chinese "CHN", also Chile/Oman), so marking one
region lit up the whole country. build-atlas-geo.mjs now keeps shapeISO only when
it is a real "XX-..." subdivision code and otherwise synthesizes a unique
per-country id from the region name. Regenerated admin1.geojson.gz: Spain/China/
Chile/Oman now carry distinct region codes (countries with real codes, e.g.
Germany, are unchanged).

* fix(dashboard): never crash on a malformed reservation date

A reservation with an invalid date blanked the whole My Trips page: the old
Upcoming widget did new Date(value).toISOString(), which throws "Invalid time
value" (fixed in #1222 by reading the string parts). Also guard splitDate so a
bad date renders a dash instead of "Invalid Date" or throwing.

* fix(airtrail): gate airtrail update behind a user setting, on airtrail update: rebuild payload from fresh data to prevent any data loss

* fix(airtrail): add back missing tests

* fix(costs): rework the cost panel UX wise and apply prettier on the shared package

* chore(prettier) prettier this file

* fix(airtrail): don't use cabin class as seat on import

When an AirTrail flight has a cabin class but no seat number, the mapper
fell back to the class for metadata.seat, so reservations showed e.g.
"economy" as the seat. Use only the seat number; leave the seat blank
otherwise. The class is still surfaced separately in the import picker.

Closes #1246

* fix(airtrail): import scheduled flight times instead of actual

AirTrail exposes both scheduled (departureScheduled/arrivalScheduled) and
actual (departure/arrival) times. TREK read the actual times, so a delayed or
early flight imported the wrong time for planning.

Read the scheduled times on import and on poll-sync (both go through
mapFlightToReservation); when a flight has no scheduled time, leave the clock
blank (date preserved) rather than fabricating 00:00 or falling back to actual.
The change-detection hash now tracks the scheduled values, so existing linked
reservations re-sync once on the next poll. The opt-in writeback mirrors the
read, pushing TREK edits to the scheduled fields so they round-trip.

* fix(planner): hydrate per-assignment times when editing a place from the pool

Times live per day-assignment, not on the pool place, so reopening a
place from the Places panel / inspector showed empty Start/End fields
(#1247). The editor now resolves a place's lone assignment when no day
is in context and hydrates the fields from it; ambiguous (0 or 2+ days)
edits hide the fields instead of showing non-persisting inputs.

* fix(mcp): make write tools return client-valid, hydrated entities

Audit of all write tools under server/src/mcp/tools (issue #1244 anchor).

S1 (broken):
- create_budget_item / create_budget_item_with_members now default the
  split to all trip members when member_ids omitted, so the entry passes
  the client save-gate instead of being member-less (#1244).
- create_transport / update_transport backfill lat/lng/timezone for
  code-only flight endpoints (NOT NULL columns) and return a clean error
  for unresolvable endpoints instead of crashing.

S2 (under-hydration): set_budget_item_members, create_journey,
create_journey_entry, create_packing_bag, bulk_import_packing and
update_vacay_plan now return the hydrated shape the matching read/REST
route returns; bulk_import widened to accept bag/weight_grams/checked.

S3 (parity): check_in_end added to accommodation tools; atlas
mark_region_visited echoes the client shape; update_journey_entry/
update_journey_preferences, set_bag_members, set_packing_category_assignees,
apply_packing_template return hydrated payloads; set_vacay_color echoes
the color.

Auth: save_packing_template now requires admin, matching the REST gate.

Also refactors server/src/config.ts (JWT-secret handling).

Adds getBudgetItem hydrated getter, exports EndpointInput, and MCP
regression tests (incl. new tools-transports and tools-journey suites).

* fix(mcp): fix ICS/maps/accommodation bugs, add settlement & template tools

Bugs:
- export_trip_ics: include flights that store times per-endpoint
  (local_date/local_time) instead of a top-level reservation_time
- resolve_maps_url: follow redirects for cid=/share links and fall back
  to parsing the page body, all SSRF-guarded
- link_hotel_accommodation: normalize accommodation_id (TEXT column) to an
  integer in the reservation read paths so it no longer returns "14.0"

Gaps:
- packing: save_packing_template returns the new template id; add
  list_packing_templates (read) and delete_packing_template (admin)
- budget: update_budget_item accepts payers/member_ids; clarify create/
  update/members descriptions to ask which members share the expense and
  who paid
- budget: add settlement tools — get_settlement_summary, list_settlements,
  create/update/delete_settlement (budget_edit, mirrors REST + WS events)

* chore: bump nodemailer

* chore: bump multer

---------

Co-authored-by: Maurice <mauriceboe@icloud.com>
This commit is contained in:
jubnl
2026-06-18 20:13:30 +02:00
committed by GitHub
parent f6af1d67a2
commit d152f9d02b
659 changed files with 10954 additions and 12275 deletions
+27 -51
View File
@@ -14,19 +14,16 @@ const admin: TranslationStrings = {
'admin.notifications.emailPanel.title': '電子郵件 (SMTP)',
'admin.notifications.webhookPanel.title': 'Webhook',
'admin.notifications.inappPanel.title': '應用程式內通知',
'admin.notifications.inappPanel.hint':
'應用程式內通知始終啟用,無法全域性停用。',
'admin.notifications.inappPanel.hint': '應用程式內通知始終啟用,無法全域性停用。',
'admin.notifications.adminWebhookPanel.title': '管理員 Webhook',
'admin.notifications.adminWebhookPanel.hint':
'此 Webhook 專用於管理員通知(例如版本提醒)。它與每位使用者的 Webhook 分開,設定後始終會觸發。',
'admin.notifications.adminWebhookPanel.saved': '管理員 Webhook URL 已儲存',
'admin.notifications.adminWebhookPanel.testSuccess': '測試 Webhook 傳送成功',
'admin.notifications.adminWebhookPanel.testFailed': '測試 Webhook 傳送失敗',
'admin.notifications.adminWebhookPanel.alwaysOnHint':
'配置 URL 後,管理員 Webhook 始終觸發',
'admin.notifications.adminWebhookPanel.alwaysOnHint': '配置 URL 後,管理員 Webhook 始終觸發',
'admin.notifications.ntfy': 'Ntfy',
'admin.ntfy.hint':
'允許使用者設定自己的 ntfy 主題以接收推播通知。在下方設定預設伺服器以預先填入使用者設定。',
'admin.ntfy.hint': '允許使用者設定自己的 ntfy 主題以接收推播通知。在下方設定預設伺服器以預先填入使用者設定。',
'admin.notifications.testNtfy': '傳送測試 Ntfy',
'admin.notifications.testNtfySuccess': '測試 Ntfy 傳送成功',
'admin.notifications.testNtfyFailed': '測試 Ntfy 失敗',
@@ -45,20 +42,16 @@ const admin: TranslationStrings = {
'admin.notifications.adminNtfyPanel.test': '傳送測試 Ntfy',
'admin.notifications.adminNtfyPanel.testSuccess': '測試 Ntfy 傳送成功',
'admin.notifications.adminNtfyPanel.testFailed': '測試 Ntfy 失敗',
'admin.notifications.adminNtfyPanel.alwaysOnHint':
'設定主題後管理員 Ntfy 始終觸發',
'admin.notifications.adminNotificationsHint':
'配置哪些渠道傳遞僅管理員通知(例如版本提醒)。',
'admin.notifications.adminNtfyPanel.alwaysOnHint': '設定主題後管理員 Ntfy 始終觸發',
'admin.notifications.adminNotificationsHint': '配置哪些渠道傳遞僅管理員通知(例如版本提醒)。',
'admin.notifications.tripReminders.title': '行程提醒',
'admin.notifications.tripReminders.hint':
'在行程開始前發送提醒通知(需要在行程中設定提醒天數)。',
'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 URL 以接收通知(Discord、Slack 等)。',
'admin.webhook.hint': '允許使用者配置自己的 Webhook URL 以接收通知(Discord、Slack 等)。',
'admin.smtp.testSuccess': '測試郵件傳送成功',
'admin.smtp.testFailed': '測試郵件傳送失敗',
'admin.title': '管理後臺',
@@ -121,19 +114,16 @@ const admin: TranslationStrings = {
'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.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.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': '要求雙因素身份驗證(2FA',
'admin.requireMfaHint':
'未啟用 2FA 的使用者必須先完成設定中的配置才能使用應用。',
'admin.requireMfaHint': '未啟用 2FA 的使用者必須先完成設定中的配置才能使用應用。',
'admin.apiKeys': 'API 金鑰',
'admin.apiKeysHint': '可選。啟用地點的擴充套件資料,如照片和天氣。',
'admin.mapsKey': 'Google Maps API 金鑰',
@@ -148,26 +138,21 @@ const admin: TranslationStrings = {
'admin.keyInvalid': '無效',
'admin.keySaved': 'API 金鑰已儲存',
'admin.oidcTitle': '單點登入 (OIDC)',
'admin.oidcSubtitle':
'允許透過 Google、Apple、Authentik 或 Keycloak 等外部提供商登入。',
'admin.oidcSubtitle': '允許透過 Google、Apple、Authentik 或 Keycloak 等外部提供商登入。',
'admin.oidcDisplayName': '顯示名稱',
'admin.oidcIssuer': '頒發者 URL',
'admin.oidcIssuerHint':
'提供商的 OpenID Connect 頒發者 URL。如 https://accounts.google.com',
'admin.oidcIssuerHint': '提供商的 OpenID Connect 頒發者 URL。如 https://accounts.google.com',
'admin.oidcSaved': 'OIDC 配置已儲存',
'admin.oidcOnlyMode': '停用密碼登入',
'admin.oidcOnlyModeHint': '啟用後,僅允許 SSO 登入。密碼登入和註冊將被停用。',
'admin.fileTypes': '允許的檔案型別',
'admin.fileTypesHint': '配置使用者可以上傳的檔案型別。',
'admin.fileTypesFormat':
'以逗號分隔的副檔名(如 jpg,png,pdf,doc)。使用 * 允許所有型別。',
'admin.fileTypesFormat': '以逗號分隔的副檔名(如 jpg,png,pdf,doc)。使用 * 允許所有型別。',
'admin.fileTypesSaved': '檔案型別設定已儲存',
'admin.placesPhotos.title': '地點照片',
'admin.placesPhotos.subtitle':
'從 Google Places API 獲取照片。停用可節省 API 配額。Wikimedia 照片不受影響。',
'admin.placesPhotos.subtitle': '從 Google Places API 獲取照片。停用可節省 API 配額。Wikimedia 照片不受影響。',
'admin.placesAutocomplete.title': '地點自動補全',
'admin.placesAutocomplete.subtitle':
'使用 Google Places API 提供搜尋建議。停用可節省 API 配額。',
'admin.placesAutocomplete.subtitle': '使用 Google Places API 提供搜尋建議。停用可節省 API 配額。',
'admin.placesDetails.title': '地點詳情',
'admin.placesDetails.subtitle':
'從 Google Places API 獲取地點詳細資訊(營業時間、評分、網站)。停用可節省 API 配額。',
@@ -223,8 +208,7 @@ const admin: TranslationStrings = {
'admin.addons.catalog.vacay.name': 'Vacay',
'admin.addons.catalog.vacay.description': '帶日曆檢視的個人假期規劃器',
'admin.addons.catalog.atlas.name': 'Atlas',
'admin.addons.catalog.atlas.description':
'標記已訪問國家和旅行統計的世界地圖',
'admin.addons.catalog.atlas.description': '標記已訪問國家和旅行統計的世界地圖',
'admin.addons.catalog.collab.name': 'Collab',
'admin.addons.catalog.collab.description': '旅行規劃的即時筆記、投票和聊天',
'admin.addons.subtitleBefore': '啟用或停用功能以自定義你的 ',
@@ -263,8 +247,7 @@ const admin: TranslationStrings = {
'admin.mcpTokens.never': '從未',
'admin.mcpTokens.empty': '尚未建立任何 MCP 令牌',
'admin.mcpTokens.deleteTitle': '刪除令牌',
'admin.mcpTokens.deleteMessage':
'此令牌將立即被撤銷。使用者將失去透過此令牌的 MCP 訪問許可權。',
'admin.mcpTokens.deleteMessage': '此令牌將立即被撤銷。使用者將失去透過此令牌的 MCP 訪問許可權。',
'admin.mcpTokens.deleteSuccess': '令牌已刪除',
'admin.mcpTokens.deleteError': '刪除令牌失敗',
'admin.mcpTokens.loadError': '載入令牌失敗',
@@ -275,8 +258,7 @@ const admin: TranslationStrings = {
'admin.oauthSessions.created': '建立時間',
'admin.oauthSessions.empty': '目前沒有活躍的 OAuth 工作階段',
'admin.oauthSessions.revokeTitle': '撤銷工作階段',
'admin.oauthSessions.revokeMessage':
'此 OAuth 工作階段將立即被撤銷。客戶端將失去 MCP 存取權限。',
'admin.oauthSessions.revokeMessage': '此 OAuth 工作階段將立即被撤銷。客戶端將失去 MCP 存取權限。',
'admin.oauthSessions.revokeSuccess': '工作階段已撤銷',
'admin.oauthSessions.revokeError': '撤銷工作階段失敗',
'admin.oauthSessions.loadError': '載入 OAuth 工作階段失敗',
@@ -308,10 +290,8 @@ const admin: TranslationStrings = {
'admin.update.button': '在 GitHub 檢視',
'admin.update.install': '安裝更新',
'admin.update.confirmTitle': '確定安裝更新?',
'admin.update.confirmText':
'TREK 將從 {current} 更新到 {version}。伺服器將自動重啟。',
'admin.update.dataInfo':
'你的所有資料(旅行、使用者、API 金鑰、上傳檔案、Vacay、Atlas、預算)將被保留。',
'admin.update.confirmText': 'TREK 將從 {current} 更新到 {version}。伺服器將自動重啟。',
'admin.update.dataInfo': '你的所有資料(旅行、使用者、API 金鑰、上傳檔案、Vacay、Atlas、預算)將被保留。',
'admin.update.warning': '重啟期間應用將短暫不可用。',
'admin.update.confirm': '立即更新',
'admin.update.installing': '更新中…',
@@ -320,29 +300,24 @@ const admin: TranslationStrings = {
'admin.update.backupHint': '建議在更新前建立備份。',
'admin.update.backupLink': '前往備份',
'admin.update.howTo': '如何更新',
'admin.update.dockerText':
'你的 TREK 例項執行在 Docker 中。要更新到 {version},請在伺服器上執行以下命令:',
'admin.update.dockerText': '你的 TREK 例項執行在 Docker 中。要更新到 {version},請在伺服器上執行以下命令:',
'admin.update.reloadHint': '請在幾秒後重新整理頁面。',
'admin.tabs.permissions': '許可權',
'admin.addons.catalog.journey.name': '旅程',
'admin.addons.catalog.journey.description':
'旅行追蹤與旅行日誌,包含打卡、照片和每日故事',
'admin.addons.catalog.journey.description': '旅行追蹤與旅行日誌,包含打卡、照片和每日故事',
'admin.passkey.title': 'Passkey 登入',
'admin.passkey.cardHint': '讓使用者使用 PasskeyWebAuthn)登入。預設為關閉。',
'admin.passkey.login': '啟用 Passkey 登入',
'admin.passkey.loginHint':
'顯示「使用 Passkey 登入」選項,並讓使用者在設定中註冊 Passkey。',
'admin.passkey.loginHint': '顯示「使用 Passkey 登入」選項,並讓使用者在設定中註冊 Passkey。',
'admin.passkey.notConfigured':
'此部署尚未解析出任何 WebAuthn 網域。請設定下方的 APP_URL 或 Relying Party ID——在此之前 Passkey 將保持隱藏。',
'admin.passkey.rpId': 'Relying Party ID(網域)',
'admin.passkey.rpIdHint':
'Passkey 綁定的純網域,例如 trek.example.org。留空則從 APP_URL 推導。日後變更將使現有 Passkey 失效。',
'admin.passkey.origins': '允許的來源',
'admin.passkey.originsHint':
'以逗號分隔的完整來源,例如 https://trek.example.org。留空則使用 APP_URL。',
'admin.passkey.originsHint': '以逗號分隔的完整來源,例如 https://trek.example.org。留空則使用 APP_URL。',
'admin.passkey.reset': '重設 Passkey',
'admin.passkey.resetHint':
'移除此使用者的所有 Passkey(例如裝置遺失時)。他們仍可使用密碼登入。',
'admin.passkey.resetHint': '移除此使用者的所有 Passkey(例如裝置遺失時)。他們仍可使用密碼登入。',
'admin.passkey.resetConfirm': '要移除 {name} 的所有 Passkey 嗎?',
'admin.passkey.resetDone': '已移除 {count} 個 Passkey',
'admin.defaultSettings.mapProvider': '地圖引擎',
@@ -350,7 +325,8 @@ const admin: TranslationStrings = {
'admin.defaultSettings.providerLeaflet': '標準(免費)',
'admin.defaultSettings.providerMapbox': 'Mapbox3D',
'admin.defaultSettings.mapboxToken': '共用的 Mapbox 權杖',
'admin.defaultSettings.mapboxTokenHint': '用於每一位尚未輸入自己權杖的使用者 — 如此整個執行個體都能使用 Mapbox,而無需個別共享金鑰。以加密方式儲存。',
'admin.defaultSettings.mapboxTokenHint':
'用於每一位尚未輸入自己權杖的使用者 — 如此整個執行個體都能使用 Mapbox,而無需個別共享金鑰。以加密方式儲存。',
'admin.defaultSettings.mapboxStyle': '地圖樣式',
'admin.defaultSettings.mapboxStylePlaceholder': '選擇樣式…',
'admin.defaultSettings.mapbox3d': '3D 建築物與地形',
+4 -8
View File
@@ -12,10 +12,8 @@ const backup: TranslationStrings = {
'backup.createFirst': '建立第一個備份',
'backup.download': '下載',
'backup.restore': '恢復',
'backup.confirm.restore':
'恢復備份「{name}」?\n\n所有當前資料將被備份資料替換。',
'backup.confirm.uploadRestore':
'上傳並恢復備份檔案「{name}」?\n\n所有當前資料將被覆蓋。',
'backup.confirm.restore': '恢復備份「{name}」?\n\n所有當前資料將被備份資料替換。',
'backup.confirm.uploadRestore': '上傳並恢復備份檔案「{name}」?\n\n所有當前資料將被覆蓋。',
'backup.confirm.delete': '刪除備份「{name}」?',
'backup.toast.loadError': '載入備份失敗',
'backup.toast.created': '備份建立成功',
@@ -43,8 +41,7 @@ const backup: TranslationStrings = {
'backup.auto.summaryWeekly': '每{day} {hour}:00',
'backup.auto.summaryMonthly': '每月 {day} 號 {hour}:00',
'backup.auto.envLocked': 'Docker',
'backup.auto.envLockedHint':
'自動備份透過 Docker 環境變數配置。要更改設定,請更新 docker-compose.yml 並重啟容器。',
'backup.auto.envLockedHint': '自動備份透過 Docker 環境變數配置。要更改設定,請更新 docker-compose.yml 並重啟容器。',
'backup.auto.copyEnv': '複製 Docker 環境變數',
'backup.auto.envCopied': 'Docker 環境變數已複製到剪貼簿',
'backup.auto.keepLabel': '自動刪除舊備份',
@@ -66,8 +63,7 @@ const backup: TranslationStrings = {
'backup.keep.30days': '30 天',
'backup.keep.forever': '永久保留',
'backup.restoreConfirmTitle': '恢復備份?',
'backup.restoreWarning':
'所有當前資料(旅行、地點、使用者、上傳檔案)將被備份資料永久替換。此操作無法撤銷。',
'backup.restoreWarning': '所有當前資料(旅行、地點、使用者、上傳檔案)將被備份資料永久替換。此操作無法撤銷。',
'backup.restoreTip': '提示:恢復前建議先備份當前狀態。',
'backup.restoreConfirm': '確認恢復',
};
+81 -76
View File
@@ -26,90 +26,95 @@ const budget: TranslationStrings = {
'budget.byCategory': '按分類',
'budget.editTooltip': '點選編輯',
'budget.linkedToReservation': '已連結至預訂——請在那裡編輯名稱',
'budget.confirm.deleteCategory':
'確定刪除分類「{name}」及其 {count} 個條目?',
'budget.confirm.deleteCategory': '確定刪除分類「{name}」及其 {count} 個條目?',
'budget.deleteCategory': '刪除分類',
'budget.perPerson': '人均',
'budget.paid': '已支付',
'budget.open': '未支付',
'budget.noMembers': '未分配成員',
'budget.settlement': '結算',
'budget.settlementInfo':
'點選預算專案上的成員頭像將其標記為綠色——表示該成員已付款。結算會顯示誰欠誰多少。',
'budget.settlementInfo': '點選預算專案上的成員頭像將其標記為綠色——表示該成員已付款。結算會顯示誰欠誰多少。',
'budget.netBalances': '淨餘額',
'budget.categoriesLabel': '類別',
"costs.you": "你",
"costs.youShort": "Y",
"costs.youLower": "你",
"costs.youOwe": "你欠款",
"costs.youOweSub": "你需付款給他人",
"costs.youreOwed": "他人欠你",
"costs.youreOwedSub": "他人需付款給你",
"costs.totalSpend": "旅程總支出",
"costs.totalSpendSub": "所有旅伴合計",
"costs.to": "給",
"costs.from": "來自",
"costs.allSettled": "你已全部結清",
"costs.nothingOwed": "沒有人欠你",
"costs.yourShare": "你的分攤",
"costs.youPaid": "你支付了",
"costs.expenses": "支出",
"costs.entries": "{count} 筆",
"costs.searchPlaceholder": "搜尋支出…",
"costs.filter.all": "全部",
"costs.filter.mine": "我支付的",
"costs.filter.owed": "他人欠我",
"costs.addExpense": "新增支出",
"costs.editExpense": "編輯支出",
"costs.noMatch": "沒有符合搜尋的支出。",
"costs.emptyText": "尚無支出,新增第一筆吧。",
"costs.spent": "支出 {amount}",
"costs.noDate": "無日期",
"costs.noOnePaid": "尚無人付款",
"costs.youLent": "你借出 {amount}",
"costs.youBorrowed": "你借入 {amount}",
"costs.settleUp": "結清",
"costs.history": "歷史紀錄",
"costs.everyoneSquare": "大家都已結清",
"costs.nothingOutstanding": "目前沒有待付款項。",
"costs.pay": "支付",
"costs.pays": "支付",
"costs.settle": "結算",
"costs.balances": "餘額",
"costs.byCategory": "按分類",
"costs.noCategories": "尚無支出。",
"costs.settleHistory": "結算紀錄",
"costs.noSettlements": "尚無已結清的款項。",
"costs.paymentsSettled": "已結清 {count} 筆款項",
"costs.paid": "已付",
"costs.undo": "復原",
"costs.whatFor": "這筆是什麼支出?",
"costs.namePlaceholder": "例如:晚餐、紀念品、油費…",
"costs.totalAmount": "總金額",
"costs.currency": "貨幣",
"costs.day": "日期",
"costs.rateLabel": "1 {from} 兌 {to}",
"costs.category": "分類",
"costs.whoPaid": "誰付的款?",
"costs.splitBetween": "平均分攤給",
"costs.pickSomeone": "至少選擇一人來分攤。",
"costs.splitSummary": "分 {count} 份 · 每份 {amount}",
"costs.cat.accommodation": "住宿",
"costs.cat.food": "餐飲",
"costs.cat.groceries": "雜貨",
"costs.cat.transport": "交通",
"costs.cat.flights": "機票",
"costs.cat.activities": "活動",
"costs.cat.sightseeing": "觀光",
"costs.cat.shopping": "購物",
"costs.cat.fees": "費用與票券",
"costs.cat.health": "健康",
"costs.cat.tips": "小費",
"costs.cat.other": "其他",
"costs.daysCount": "{count} 天",
"costs.travelers": "{count} 位旅伴",
"costs.liveRate": "即時匯率",
"costs.settleAll": "全部結清",
'costs.you': '你',
'costs.youShort': 'Y',
'costs.youLower': '你',
'costs.youOwe': '你欠款',
'costs.youOweSub': '你需付款給他人',
'costs.youreOwed': '他人欠你',
'costs.youreOwedSub': '他人需付款給你',
'costs.totalSpend': '旅程總支出',
'costs.totalSpendSub': '所有旅伴合計',
'costs.to': '給',
'costs.from': '來自',
'costs.allSettled': '你已全部結清',
'costs.nothingOwed': '沒有人欠你',
'costs.yourShare': '你的分攤',
'costs.youPaid': '你支付了',
'costs.expenses': '支出',
'costs.entries': '{count} 筆',
'costs.searchPlaceholder': '搜尋支出…',
'costs.filter.all': '全部',
'costs.filter.mine': '我支付的',
'costs.filter.owed': '他人欠我',
'costs.addExpense': '新增支出',
'costs.editExpense': '編輯支出',
'costs.noMatch': '沒有符合搜尋的支出。',
'costs.emptyText': '尚無支出,新增第一筆吧。',
'costs.spent': '支出 {amount}',
'costs.noDate': '無日期',
'costs.noOnePaid': '尚無人付款',
'costs.youLent': '你借出 {amount}',
'costs.youBorrowed': '你借入 {amount}',
'costs.settleUp': '結清',
'costs.history': '歷史紀錄',
'costs.everyoneSquare': '大家都已結清',
'costs.nothingOutstanding': '目前沒有待付款項。',
'costs.pay': '支付',
'costs.pays': '支付',
'costs.settle': '結算',
'costs.balances': '餘額',
'costs.byCategory': '按分類',
'costs.noCategories': '尚無支出。',
'costs.settleHistory': '結算紀錄',
'costs.noSettlements': '尚無已結清的款項。',
'costs.paymentsSettled': '已結清 {count} 筆款項',
'costs.paid': '已付',
'costs.undo': '復原',
'costs.whatFor': '這筆是什麼支出?',
'costs.namePlaceholder': '例如:晚餐、紀念品、油費…',
'costs.totalAmount': '總金額',
'costs.currency': '貨幣',
'costs.day': '日期',
'costs.rateLabel': '1 {from} 兌 {to}',
'costs.category': '分類',
'costs.whoPaid': '誰付的款?',
'costs.splitBetween': '平均分攤給',
'costs.pickSomeone': '至少選擇一人來分攤。',
'costs.splitSummary': '分 {count} 份 · 每份 {amount}',
'costs.cat.accommodation': '住宿',
'costs.cat.food': '餐飲',
'costs.cat.groceries': '雜貨',
'costs.cat.transport': '交通',
'costs.cat.flights': '機票',
'costs.cat.activities': '活動',
'costs.cat.sightseeing': '觀光',
'costs.cat.shopping': '購物',
'costs.cat.fees': '費用與票券',
'costs.cat.health': '健康',
'costs.cat.tips': '小費',
'costs.cat.other': '其他',
'costs.daysCount': '{count} 天',
'costs.travelers': '{count} 位旅伴',
'costs.liveRate': '即時匯率',
'costs.settleAll': '全部結清',
'costs.payment': '付款',
'costs.editPayment': '編輯付款',
'costs.addPayment': '新增付款',
'costs.unfinished': '未完成',
'costs.unfinishedHint': '僅計入總額 — 尚未結算',
'costs.tapToInclude': '點按以加入',
'costs.amount': '金額',
};
export default budget;
+2 -4
View File
@@ -20,8 +20,7 @@ const dashboard: TranslationStrings = {
'dashboard.timezoneCustomTzPlaceholder': '如 America/New_York',
'dashboard.timezoneCustomAdd': '新增',
'dashboard.timezoneCustomErrorEmpty': '請輸入時區識別符號',
'dashboard.timezoneCustomErrorInvalid':
'無效的時區。請使用 Europe/Berlin 這樣的格式',
'dashboard.timezoneCustomErrorInvalid': '無效的時區。請使用 Europe/Berlin 這樣的格式',
'dashboard.timezoneCustomErrorDuplicate': '已新增',
'dashboard.emptyTitle': '暫無旅行',
'dashboard.emptyText': '建立你的第一次旅行,開始規劃吧!',
@@ -55,8 +54,7 @@ const dashboard: TranslationStrings = {
'dashboard.toast.restoreError': '恢復旅行失敗',
'dashboard.toast.copied': '旅行已複製!',
'dashboard.toast.copyError': '複製旅行失敗',
'dashboard.confirm.delete':
'刪除旅行「{title}」?所有地點和計劃將被永久刪除。',
'dashboard.confirm.delete': '刪除旅行「{title}」?所有地點和計劃將被永久刪除。',
'dashboard.editTrip': '編輯旅行',
'dashboard.createTrip': '建立新旅行',
'dashboard.tripTitle': '標題',
+1 -2
View File
@@ -28,8 +28,7 @@ const dayplan: TranslationStrings = {
'dayplan.pdfError': 'PDF 匯出失敗',
'dayplan.cannotReorderTransport': '有固定時間的預訂無法重新排序',
'dayplan.confirmRemoveTimeTitle': '移除時間?',
'dayplan.confirmRemoveTimeBody':
'此地點有固定時間({time})。移動後將移除時間並允許自由排序。',
'dayplan.confirmRemoveTimeBody': '此地點有固定時間({time})。移動後將移除時間並允許自由排序。',
'dayplan.confirmRemoveTimeAction': '移除時間並移動',
'dayplan.confirmDeleteNoteTitle': '刪除筆記?',
'dayplan.confirmDeleteNoteBody': '此筆記將被永久刪除。',
+1 -2
View File
@@ -13,8 +13,7 @@ const files: TranslationStrings = {
'files.uploadError': '上傳失敗',
'files.dropzone': '將檔案拖放到此處',
'files.dropzoneHint': '或點選瀏覽',
'files.allowedTypes':
'圖片、PDF、DOC、DOCX、XLS、XLSX、TXT、CSV · 最大 50 MB',
'files.allowedTypes': '圖片、PDF、DOC、DOCX、XLS、XLSX、TXT、CSV · 最大 50 MB',
'files.uploading': '上傳中...',
'files.filterAll': '全部',
'files.filterPdf': 'PDF',
+4 -8
View File
@@ -63,8 +63,7 @@ const journey: TranslationStrings = {
'journey.frontpage.continueWriting': '繼續撰寫',
'journey.frontpage.updated': '更新於 {time}',
'journey.frontpage.suggestionLabel': '旅行剛結束',
'journey.frontpage.suggestionText':
'將 <strong>{title}</strong> 變成一段旅程',
'journey.frontpage.suggestionText': '將 <strong>{title}</strong> 變成一段旅程',
'journey.frontpage.dismiss': '忽略',
'journey.frontpage.journeyName': '旅程名稱',
'journey.frontpage.namePlaceholder': '例如 東南亞 2026',
@@ -81,8 +80,7 @@ const journey: TranslationStrings = {
'journey.detail.noEntries': '還沒有條目',
'journey.detail.noEntriesHint': '新增一個旅行以產生骨架條目',
'journey.detail.noPhotos': '還沒有照片',
'journey.detail.noPhotosHint':
'上傳照片到條目或瀏覽你的 Immich/Synology 相簿',
'journey.detail.noPhotosHint': '上傳照片到條目或瀏覽你的 Immich/Synology 相簿',
'journey.detail.journeyStats': '旅程統計',
'journey.detail.syncedTrips': '已同步的旅行',
'journey.detail.noTripsLinked': '尚未關聯旅行',
@@ -108,8 +106,7 @@ const journey: TranslationStrings = {
'journey.editor.uploadPhotos': '上傳照片',
'journey.editor.uploading': '上傳中...',
'journey.editor.uploadingProgress': '上傳中 {done}/{total}…',
'journey.editor.uploadPartialFailed':
'{total} 張中有 {failed} 張上傳失敗 — 再次儲存以重試',
'journey.editor.uploadPartialFailed': '{total} 張中有 {failed} 張上傳失敗 — 再次儲存以重試',
'journey.editor.fromGallery': '從相簿',
'journey.editor.allPhotosAdded': '所有照片已新增',
'journey.editor.writeStory': '寫下你的故事...',
@@ -146,8 +143,7 @@ const journey: TranslationStrings = {
'journey.trips.linkFailed': '關聯旅行失敗',
'journey.trips.addTrip': '新增旅行',
'journey.trips.unlinkTrip': '取消關聯旅行',
'journey.trips.unlinkMessage':
'取消關聯「{title}」?此旅行中所有已同步的條目和照片將被永久刪除。此操作無法復原。',
'journey.trips.unlinkMessage': '取消關聯「{title}」?此旅行中所有已同步的條目和照片將被永久刪除。此操作無法復原。',
'journey.trips.unlink': '取消關聯',
'journey.trips.tripUnlinked': '旅行已取消關聯',
'journey.trips.unlinkFailed': '取消關聯失敗',
+5 -10
View File
@@ -53,30 +53,25 @@ const login: TranslationStrings = {
'login.forgotPassword': '忘記密碼?',
'login.rememberMe': '記住我',
'login.forgotPasswordTitle': '重設密碼',
'login.forgotPasswordBody':
'請輸入您註冊時使用的電子郵件。若帳號存在,我們將傳送重設連結。',
'login.forgotPasswordBody': '請輸入您註冊時使用的電子郵件。若帳號存在,我們將傳送重設連結。',
'login.forgotPasswordSubmit': '傳送重設連結',
'login.forgotPasswordSentTitle': '請查看您的電子郵件',
'login.forgotPasswordSentBody':
'若此電子郵件存在帳號,重設連結正在傳送中。連結將於 60 分鐘後失效。',
'login.forgotPasswordSmtpHintOff':
'提醒:管理員尚未設定 SMTP,重設連結將寫入伺服器控制台,而非透過電子郵件寄送。',
'login.forgotPasswordSentBody': '若此電子郵件存在帳號,重設連結正在傳送中。連結將於 60 分鐘後失效。',
'login.forgotPasswordSmtpHintOff': '提醒:管理員尚未設定 SMTP,重設連結將寫入伺服器控制台,而非透過電子郵件寄送。',
'login.backToLogin': '返回登入',
'login.newPassword': '新密碼',
'login.confirmPassword': '確認新密碼',
'login.passwordsDontMatch': '兩次輸入的密碼不一致',
'login.mfaCode': '2FA 驗證碼',
'login.resetPasswordTitle': '設定新密碼',
'login.resetPasswordBody':
'請選擇您在此處尚未使用過的強密碼。至少 8 個字元。',
'login.resetPasswordBody': '請選擇您在此處尚未使用過的強密碼。至少 8 個字元。',
'login.resetPasswordMfaBody': '請輸入您的 2FA 驗證碼或備用代碼以完成重設。',
'login.resetPasswordSubmit': '重設密碼',
'login.resetPasswordVerify': '驗證並重設',
'login.resetPasswordSuccessTitle': '密碼已更新',
'login.resetPasswordSuccessBody': '您現在可以使用新密碼登入。',
'login.resetPasswordInvalidLink': '無效的重設連結',
'login.resetPasswordInvalidLinkBody':
'此連結遺失或已損壞。請重新申請以繼續。',
'login.resetPasswordInvalidLinkBody': '此連結遺失或已損壞。請重新申請以繼續。',
'login.resetPasswordFailed': '重設失敗。連結可能已過期。',
'login.oidc.tokenFailed': '認證失敗。',
'login.oidc.invalidState': '會話無效,請重試。',
+5 -10
View File
@@ -3,10 +3,8 @@ import type { TranslationStrings } from '../types';
const memories: TranslationStrings = {
'memories.title': '照片',
'memories.notConnected': '{provider_name} 未連線',
'memories.notConnectedHint':
'在設定中連線您的 {provider_name} 例項以在此旅行中新增照片。',
'memories.notConnectedMultipleHint':
'在設定中連線以下任一照片提供商:{provider_names} 以在此旅行中新增照片。',
'memories.notConnectedHint': '在設定中連線您的 {provider_name} 例項以在此旅行中新增照片。',
'memories.notConnectedMultipleHint': '在設定中連線以下任一照片提供商:{provider_names} 以在此旅行中新增照片。',
'memories.noDates': '為旅行新增日期以載入照片。',
'memories.noPhotos': '未找到照片',
'memories.noPhotosHint': '{provider_name} 中未找到此旅行日期範圍內的照片。',
@@ -24,8 +22,7 @@ const memories: TranslationStrings = {
'memories.providerOTP': 'MFA 驗證碼(如已啟用)',
'memories.skipSSLVerification': '跳過 SSL 憑證驗證',
'memories.immichAutoUpload': '上傳 Journey 照片時同步到 Immich',
'memories.providerUrlHintSynology':
'在網址中包含照片應用程式路徑,例如 https://nas:5001/photo',
'memories.providerUrlHintSynology': '在網址中包含照片應用程式路徑,例如 https://nas:5001/photo',
'memories.testConnection': '測試連線',
'memories.testShort': '測試',
'memories.testFirst': '請先測試連線',
@@ -34,8 +31,7 @@ const memories: TranslationStrings = {
'memories.connectionSuccess': '已連線到 {provider_name}',
'memories.connectionError': '無法連線到 {provider_name}',
'memories.saved': '{provider_name} 設定已儲存',
'memories.providerDisconnectedBanner':
'您與 {provider_name} 的連線已中斷。請在設定中重新連線以查看照片。',
'memories.providerDisconnectedBanner': '您與 {provider_name} 的連線已中斷。請在設定中重新連線以查看照片。',
'memories.saveError': '無法儲存 {provider_name} 設定',
'memories.oldest': '最早優先',
'memories.newest': '最新優先',
@@ -59,8 +55,7 @@ const memories: TranslationStrings = {
'memories.tripDates': '旅行日期',
'memories.allPhotos': '所有照片',
'memories.confirmShareTitle': '與旅行成員分享?',
'memories.confirmShareHint':
'{count} 張照片將對本次旅行的所有成員可見。你可以稍後將單張照片設為私密。',
'memories.confirmShareHint': '{count} 張照片將對本次旅行的所有成員可見。你可以稍後將單張照片設為私密。',
'memories.confirmShareButton': '分享照片',
'memories.error.loadAlbums': '載入相簿失敗',
'memories.error.linkAlbum': '關聯相簿失敗',
+1 -2
View File
@@ -35,7 +35,6 @@ const notif: TranslationStrings = {
'notif.generic.title': '通知',
'notif.generic.text': '你有一則新通知',
'notif.dev.unknown_event.title': '[DEV] 未知事件',
'notif.dev.unknown_event.text':
'事件類型「{event}」未在 EVENT_NOTIFICATION_CONFIG 中註冊',
'notif.dev.unknown_event.text': '事件類型「{event}」未在 EVENT_NOTIFICATION_CONFIG 中註冊',
};
export default notif;
+1 -2
View File
@@ -14,8 +14,7 @@ const notifications: TranslationStrings = {
'notifications.delete': '刪除',
'notifications.system': '系統',
'notifications.synologySessionCleared.title': 'Synology Photos 已斷線',
'notifications.synologySessionCleared.text':
'您的伺服器或帳號已更改 — 請前往設定重新測試連線。',
'notifications.synologySessionCleared.text': '您的伺服器或帳號已更改 — 請前往設定重新測試連線。',
'notifications.test.title': '來自 {actor} 的測試通知',
'notifications.test.text': '這是一條簡單的測試通知。',
'notifications.test.booleanTitle': '{actor} 請求您的審批',
+10 -20
View File
@@ -33,13 +33,11 @@ const oauth: TranslationStrings = {
'oauth.scope.packing:read.label': '檢視行李清單',
'oauth.scope.packing:read.description': '讀取行李物品、行李袋及類別負責人',
'oauth.scope.packing:write.label': '管理行李清單',
'oauth.scope.packing:write.description':
'新增、更新、刪除、勾選及重新排序行李物品和行李袋',
'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.todos:write.description': '建立、更新、勾選、刪除及重新排序待辦事項',
'oauth.scope.budget:read.label': '檢視預算',
'oauth.scope.budget:read.description': '讀取預算項目及費用明細',
'oauth.scope.budget:write.label': '管理預算',
@@ -47,13 +45,11 @@ const oauth: TranslationStrings = {
'oauth.scope.reservations:read.label': '檢視預訂',
'oauth.scope.reservations:read.description': '讀取預訂及住宿詳情',
'oauth.scope.reservations:write.label': '管理預訂',
'oauth.scope.reservations:write.description':
'建立、更新、刪除及重新排序預訂',
'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.collab:write.description': '建立、更新及刪除協作筆記、投票及訊息',
'oauth.scope.notifications:read.label': '檢視通知',
'oauth.scope.notifications:read.description': '讀取應用程式通知及未讀數量',
'oauth.scope.notifications:write.label': '管理通知',
@@ -63,8 +59,7 @@ const oauth: TranslationStrings = {
'oauth.scope.vacay:write.label': '管理假期計畫',
'oauth.scope.vacay:write.description': '建立及管理假期項目、節假日及團隊計畫',
'oauth.scope.geo:read.label': '地圖與地理編碼',
'oauth.scope.geo:read.description':
'搜尋地點、解析地圖 URL 及反向地理編碼坐標',
'oauth.scope.geo:read.description': '搜尋地點、解析地圖 URL 及反向地理編碼坐標',
'oauth.scope.weather:read.label': '天氣預報',
'oauth.scope.weather:read.description': '取得行程地點及日期的天氣預報',
'oauth.scope.journey:read.label': '檢視旅程',
@@ -77,14 +72,11 @@ const oauth: TranslationStrings = {
'oauth.authorize.loading': 'Loading…', // en-fallback
'oauth.authorize.errorTitle': 'Authorization Error', // en-fallback
'oauth.authorize.loginTitle': 'Sign in to continue', // en-fallback
'oauth.authorize.loginDescription':
'{client} wants access to your TREK account. Please sign in first.', // en-fallback
'oauth.authorize.loginDescription': '{client} wants access to your TREK account. Please sign in first.', // en-fallback
'oauth.authorize.loginButton': 'Sign in to TREK', // en-fallback
'oauth.authorize.requestLabel': 'Authorization Request', // en-fallback
'oauth.authorize.requestDescription':
'This application is requesting access to your TREK account.', // en-fallback
'oauth.authorize.trustNote':
'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.requestDescription': 'This application is requesting access to your TREK account.', // en-fallback
'oauth.authorize.trustNote': 'Only grant access to applications you trust. Your data stays on your server.', // en-fallback
'oauth.authorize.selectScope': 'Select at least one scope', // en-fallback
'oauth.authorize.approveOneScope': 'Approve ({count} scope)', // en-fallback
'oauth.authorize.approveManyScopes': 'Approve ({count} scopes)', // en-fallback
@@ -93,9 +85,7 @@ const oauth: TranslationStrings = {
'oauth.authorize.choosePermissions': 'Choose which permissions to grant', // en-fallback
'oauth.authorize.permissionsRequested': 'Permissions requested', // en-fallback
'oauth.authorize.alwaysIncluded': 'Always included', // en-fallback
'oauth.authorize.alwaysTool.listTrips':
'List your trips so the AI can discover trip IDs', // en-fallback
'oauth.authorize.alwaysTool.getTripSummary':
'Read a trip overview needed to use any other tool', // en-fallback
'oauth.authorize.alwaysTool.listTrips': 'List your trips so the AI can discover trip IDs', // en-fallback
'oauth.authorize.alwaysTool.getTripSummary': 'Read a trip overview needed to use any other tool', // en-fallback
};
export default oauth;
+1 -2
View File
@@ -5,8 +5,7 @@ const packing: TranslationStrings = {
'packing.empty': '行李清單為空',
'packing.import': '匯入',
'packing.importTitle': '匯入裝箱清單',
'packing.importHint':
'每行一個物品。可選用逗號、分號或製表符分隔類別和數量:名稱, 類別, 數量',
'packing.importHint': '每行一個物品。可選用逗號、分號或製表符分隔類別和數量:名稱, 類別, 數量',
'packing.importPlaceholder': '牙刷\n防曬霜, 衛生\nT恤, 衣物, 5\n護照, 證件',
'packing.importCsv': '載入 CSV/TXT',
'packing.importAction': '匯入 {count}',
+6 -12
View File
@@ -4,12 +4,10 @@ const places: TranslationStrings = {
'places.addPlace': '新增地點/活動',
'places.importFile': '匯入檔案',
'places.sidebarDrop': '拖放以匯入',
'places.importFileHint':
'從 Google My Maps、Google Earth 或 GPS 追蹤器等工具匯入 .gpx、.kml 或 .kmz 檔案。',
'places.importFileHint': '從 Google My Maps、Google Earth 或 GPS 追蹤器等工具匯入 .gpx、.kml 或 .kmz 檔案。',
'places.importFileDropHere': '點選以選取檔案或拖放至此處',
'places.importFileDropActive': '放開檔案以選取',
'places.importFileUnsupported':
'不支援的檔案類型,請使用 .gpx、.kml 或 .kmz。',
'places.importFileUnsupported': '不支援的檔案類型,請使用 .gpx、.kml 或 .kmz。',
'places.importFileTooLarge': '檔案過大。最大上傳大小為 {maxMb} MB。',
'places.importFileError': '匯入失敗',
'places.importAllSkipped': '所有地點已在行程中。',
@@ -28,8 +26,7 @@ const places: TranslationStrings = {
'places.kmlKmzImported': '已從 KMZ/KML 匯入 {count} 個地點',
'places.urlResolved': '已從 URL 匯入地點',
'places.importList': '列表匯入',
'places.kmlKmzSummaryValues':
'Placemarks{total} • 已匯入:{created} • 已略過:{skipped}',
'places.kmlKmzSummaryValues': 'Placemarks{total} • 已匯入:{created} • 已略過:{skipped}',
'places.importGoogleList': 'Google 列表',
'places.importNaverList': 'Naver 列表',
'places.googleListHint': '貼上共享的 Google Maps 列表連結以匯入所有地點。',
@@ -76,17 +73,14 @@ const places: TranslationStrings = {
'places.mapsSearchPlaceholder': '搜尋地點...',
'places.mapsSearchError': '地點搜尋失敗。',
'places.loadingDetails': '正在載入地點詳情…',
'places.osmHint':
'使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
'places.osmActive':
'透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。',
'places.osmHint': '使用 OpenStreetMap 搜尋(無照片、營業時間或評分)。在設定中新增 Google API 金鑰以獲取完整資訊。',
'places.osmActive': '透過 OpenStreetMap 搜尋(無照片、評分或營業時間)。在設定中新增 Google API 金鑰以獲取增強資料。',
'places.categoryCreateError': '建立分類失敗',
'places.nameRequired': '請輸入名稱',
'places.saveError': '儲存失敗',
'places.duplicateExists': "'{name}' 已在此行程中。",
'places.addAnyway': '仍要新增',
'places.enrichOnImport': '透過 Google 豐富地點資訊',
'places.enrichOnImportHint':
'查詢每個匯入的地點以補上照片、地址與聯絡資訊。需要 Google Maps 金鑰。',
'places.enrichOnImportHint': '查詢每個匯入的地點以補上照片、地址與聯絡資訊。需要 Google Maps 金鑰。',
};
export default places;
+1 -2
View File
@@ -33,8 +33,7 @@ const planner: TranslationStrings = {
'planner.resConfirmed': '預訂已確認 · ',
'planner.notePlaceholder': '備註…',
'planner.noteTimePlaceholder': '時間(可選)',
'planner.noteExamplePlaceholder':
'如:14:30 從中央車站乘 S3,7 號碼頭渡輪,午餐休息…',
'planner.noteExamplePlaceholder': '如:14:30 從中央車站乘 S3,7 號碼頭渡輪,午餐休息…',
'planner.totalCost': '總費用',
'planner.searchPlaces': '搜尋地點…',
'planner.allCategories': '所有分類',
+7 -4
View File
@@ -6,8 +6,7 @@ const reservations: TranslationStrings = {
'reservations.emptyHint': '新增航班、酒店等預訂資訊',
'reservations.add': '新增預訂',
'reservations.addManual': '手動新增',
'reservations.placeHint':
'提示:建議從地點直接建立預訂,以便與日程計劃關聯。',
'reservations.placeHint': '提示:建議從地點直接建立預訂,以便與日程計劃關聯。',
'reservations.confirmed': '已確認',
'reservations.pending': '待確認',
'reservations.summary': '{confirmed} 已確認,{pending} 待確認',
@@ -118,8 +117,7 @@ const reservations: TranslationStrings = {
'reservations.span.start': '開始',
'reservations.span.end': '結束',
'reservations.span.ongoing': '進行中',
'reservations.validation.endBeforeStart':
'結束日期/時間必須晚於開始日期/時間',
'reservations.validation.endBeforeStart': '結束日期/時間必須晚於開始日期/時間',
'reservations.addBooking': '新增預訂',
'reservations.import.title': '匯入訂位確認',
'reservations.import.cta': '從檔案匯入',
@@ -155,5 +153,10 @@ const reservations: TranslationStrings = {
'reservations.airtrail.otherFlights': '其他航班',
'reservations.airtrail.empty': '在你的 AirTrail 帳戶中找不到任何航班。',
'reservations.airtrail.importCta': '匯入 {count}',
'reservations.costsLabel': 'Costs',
'reservations.createExpense': 'Create expense',
'reservations.createExpenseHint': 'Saves the booking, then opens the Costs editor.',
'reservations.linkedExpense': 'Linked expense',
'reservations.removeExpense': 'Remove expense',
};
export default reservations;
+31 -54
View File
@@ -14,12 +14,10 @@ const settings: TranslationStrings = {
'settings.mapTemplate': '地圖模板',
'settings.mapTemplatePlaceholder.select': '選擇模板...',
'settings.mapDefaultHint': '留空則使用 OpenStreetMap(預設)',
'settings.mapTemplatePlaceholder':
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'settings.mapTemplatePlaceholder': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'settings.mapHint': '地圖瓦片 URL 模板',
'settings.mapProvider': '地圖提供商',
'settings.mapProviderHint':
'影響行程規劃和旅程地圖。Atlas 始終使用 Leaflet。',
'settings.mapProviderHint': '影響行程規劃和旅程地圖。Atlas 始終使用 Leaflet。',
'settings.mapLeafletSubtitle': '經典 2D,任何柵格瓦片',
'settings.mapMapboxSubtitle': '向量瓦片、3D 建築和地形',
'settings.mapExperimental': '實驗性',
@@ -32,19 +30,16 @@ const settings: TranslationStrings = {
'settings.map3dBuildings': '3D 建築和地形',
'settings.map3dHint': '傾斜 + 真實 3D 建築拉伸 — 適用於所有樣式,包括衛星。',
'settings.mapHighQuality': '高畫質模式',
'settings.mapHighQualityHint':
'抗鋸齒 + 地球投影,帶來更清晰的邊緣和更真實的世界視圖。',
'settings.mapHighQualityHint': '抗鋸齒 + 地球投影,帶來更清晰的邊緣和更真實的世界視圖。',
'settings.mapHighQualityWarning': '可能影響低階裝置的效能。',
'settings.mapTipLabel': '提示:',
'settings.mapTip':
'右鍵點擊並拖曳以旋轉/傾斜地圖。中鍵點擊新增地點(右鍵用於旋轉)。',
'settings.mapTip': '右鍵點擊並拖曳以旋轉/傾斜地圖。中鍵點擊新增地點(右鍵用於旋轉)。',
'settings.latitude': '緯度',
'settings.longitude': '經度',
'settings.saveMap': '儲存地圖',
'settings.apiKeys': 'API 金鑰',
'settings.mapsKey': 'Google Maps API 金鑰',
'settings.mapsKeyHint':
'用於地點搜尋。需要 Places API (New)。在 console.cloud.google.com 獲取',
'settings.mapsKeyHint': '用於地點搜尋。需要 Places API (New)。在 console.cloud.google.com 獲取',
'settings.weatherKey': 'OpenWeatherMap API 金鑰',
'settings.weatherKeyHint': '用於天氣資料。在 openweathermap.org/api 免費獲取',
'settings.keyPlaceholder': '輸入金鑰...',
@@ -77,12 +72,10 @@ const settings: TranslationStrings = {
'settings.notificationPreferences.webhook': 'Webhook',
'settings.notificationPreferences.inapp': '應用程式內',
'settings.notificationPreferences.ntfy': 'Ntfy',
'settings.notificationPreferences.noChannels':
'未配置通知渠道。請聯絡管理員設定電子郵件或 Webhook 通知。',
'settings.notificationPreferences.noChannels': '未配置通知渠道。請聯絡管理員設定電子郵件或 Webhook 通知。',
'settings.webhookUrl.label': 'Webhook URL',
'settings.webhookUrl.placeholder': 'https://discord.com/api/webhooks/...',
'settings.webhookUrl.hint':
'輸入您的 Discord、Slack 或自訂 Webhook URL 以接收通知。',
'settings.webhookUrl.hint': '輸入您的 Discord、Slack 或自訂 Webhook URL 以接收通知。',
'settings.webhookUrl.saved': 'Webhook URL 已儲存',
'settings.webhookUrl.test': '測試',
'settings.webhookUrl.testSuccess': '測試 Webhook 傳送成功',
@@ -91,8 +84,7 @@ const settings: TranslationStrings = {
'settings.ntfyUrl.topicPlaceholder': 'my-trek-alerts',
'settings.ntfyUrl.serverLabel': 'Ntfy 伺服器 URL(選填)',
'settings.ntfyUrl.serverPlaceholder': 'https://ntfy.sh',
'settings.ntfyUrl.hint':
'輸入您的 Ntfy 主題以接收推播通知。將伺服器留空以使用管理員設定的預設值。',
'settings.ntfyUrl.hint': '輸入您的 Ntfy 主題以接收推播通知。將伺服器留空以使用管理員設定的預設值。',
'settings.ntfyUrl.tokenLabel': '存取權杖(選填)',
'settings.ntfyUrl.tokenHint': '受密碼保護的主題需要此項目。',
'settings.ntfyUrl.saved': 'Ntfy 設定已儲存',
@@ -100,8 +92,7 @@ const settings: TranslationStrings = {
'settings.ntfyUrl.testSuccess': '測試 Ntfy 通知傳送成功',
'settings.ntfyUrl.testFailed': '測試 Ntfy 通知失敗',
'settings.ntfyUrl.tokenCleared': '存取權杖已清除',
'settings.notificationsDisabled':
'通知尚未配置。請聯絡管理員啟用電子郵件或 Webhook 通知。',
'settings.notificationsDisabled': '通知尚未配置。請聯絡管理員啟用電子郵件或 Webhook 通知。',
'settings.notificationsActive': '活躍頻道',
'settings.notificationsManagedByAdmin': '通知事件由管理員配置。',
'settings.on': '開',
@@ -121,23 +112,20 @@ const settings: TranslationStrings = {
'settings.mcp.tokenCreatedAt': '創建於',
'settings.mcp.tokenUsedAt': '使用於',
'settings.mcp.deleteTokenTitle': '刪除令牌',
'settings.mcp.deleteTokenMessage':
'此令牌將立即失效,使用它的所有 MCP 客戶端將失去訪問許可權。',
'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.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.mcp.apiTokensDeprecated': 'API 金鑰已棄用,將於未來版本中移除。請改用 OAuth 2.1 客戶端。',
'settings.oauth.clients': 'OAuth 2.1 客戶端',
'settings.oauth.clientsHint':
'註冊 OAuth 2.1 客戶端,讓第三方 MCP 應用程式(Claude Web、Cursor 等)無需靜態金鑰即可連線。',
@@ -161,18 +149,14 @@ const settings: TranslationStrings = {
'settings.oauth.sessionExpires': '到期時間',
'settings.oauth.revoke': '撤銷',
'settings.oauth.revokeSession': '撤銷工作階段',
'settings.oauth.revokeSessionMessage':
'這將立即撤銷此 OAuth 工作階段的存取權限。',
'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.clientNamePlaceholder': '例如 Claude Web、我的 MCP 應用程式',
'settings.oauth.modal.redirectUris': '重新導向 URI',
'settings.oauth.modal.redirectUrisPlaceholder':
'https://your-app.com/callback\nhttps://your-app.com/auth',
'settings.oauth.modal.redirectUrisHint':
'每行一個 URI。需要 HTTPSlocalhost 除外)。需要完全符合。',
'settings.oauth.modal.redirectUrisPlaceholder': 'https://your-app.com/callback\nhttps://your-app.com/auth',
'settings.oauth.modal.redirectUrisHint': '每行一個 URI。需要 HTTPSlocalhost 除外)。需要完全符合。',
'settings.oauth.modal.scopes': '允許的授權範圍',
'settings.oauth.modal.scopesHint':
'list_trips 和 get_trip_summary 始終可用——不需要授權範圍。它們可幫助 AI 找到所需的行程 ID。',
@@ -181,8 +165,7 @@ const settings: TranslationStrings = {
'settings.oauth.modal.creating': '註冊中…',
'settings.oauth.modal.create': '註冊客戶端',
'settings.oauth.modal.createdTitle': '客戶端已註冊',
'settings.oauth.modal.createdWarning':
'客戶端密鑰僅顯示一次。請立即複製——無法恢復。',
'settings.oauth.modal.createdWarning': '客戶端密鑰僅顯示一次。請立即複製——無法恢復。',
'settings.oauth.toast.createError': '註冊 OAuth 客戶端失敗',
'settings.oauth.toast.deleted': 'OAuth 客戶端已刪除',
'settings.oauth.toast.deleteError': '刪除 OAuth 客戶端失敗',
@@ -210,8 +193,7 @@ const settings: TranslationStrings = {
'settings.about.supporters.tierEmpty': '成為第一個',
'settings.about.supporter.tier.noReturnTicket': 'No Return Ticket',
'settings.about.supporter.tier.lostLuggageVip': 'Lost Luggage VIP',
'settings.about.supporter.tier.businessClassDreamer':
'Business Class Dreamer',
'settings.about.supporter.tier.businessClassDreamer': 'Business Class Dreamer',
'settings.about.supporter.tier.budgetTraveller': 'Budget Traveller',
'settings.about.supporter.tier.hostelBunkmate': 'Hostel Bunkmate',
'settings.about.description':
@@ -237,24 +219,18 @@ const settings: TranslationStrings = {
'settings.passwordChanged': '密碼修改成功',
'settings.deleteAccount': '刪除賬戶',
'settings.deleteAccountTitle': '確定刪除賬戶?',
'settings.deleteAccountWarning':
'你的賬戶以及所有旅行、地點和檔案將被永久刪除。此操作無法撤銷。',
'settings.deleteAccountWarning': '你的賬戶以及所有旅行、地點和檔案將被永久刪除。此操作無法撤銷。',
'settings.deleteAccountConfirm': '永久刪除',
'settings.deleteBlockedTitle': '無法刪除',
'settings.deleteBlockedMessage':
'你是唯一的管理員。請先將其他使用者提升為管理員,然後再刪除賬戶。',
'settings.deleteBlockedMessage': '你是唯一的管理員。請先將其他使用者提升為管理員,然後再刪除賬戶。',
'settings.roleUser': '使用者',
'settings.saveProfile': '儲存資料',
'settings.mfa.title': '雙因素認證 (2FA)',
'settings.mfa.description':
'登入時新增第二步驗證。使用身份驗證器應用(Google Authenticator、Authy 等)。',
'settings.mfa.requiredByPolicy':
'管理員要求雙因素身份驗證。請先完成下方的身份驗證器設定後再繼續。',
'settings.mfa.description': '登入時新增第二步驗證。使用身份驗證器應用(Google Authenticator、Authy 等)。',
'settings.mfa.requiredByPolicy': '管理員要求雙因素身份驗證。請先完成下方的身份驗證器設定後再繼續。',
'settings.mfa.backupTitle': '備用程式碼',
'settings.mfa.backupDescription':
'如果你無法使用身份驗證器應用,可使用這些一次性備用程式碼登入。',
'settings.mfa.backupWarning':
'請立即儲存這些程式碼。每個程式碼只能使用一次。',
'settings.mfa.backupDescription': '如果你無法使用身份驗證器應用,可使用這些一次性備用程式碼登入。',
'settings.mfa.backupWarning': '請立即儲存這些程式碼。每個程式碼只能使用一次。',
'settings.mfa.backupCopy': '複製程式碼',
'settings.mfa.backupDownload': '下載 TXT',
'settings.mfa.backupPrint': '列印 / PDF',
@@ -283,15 +259,13 @@ const settings: TranslationStrings = {
'settings.avatarRemoved': '頭像已移除',
'settings.avatarError': '上傳失敗',
'settings.bookingLabels': '預訂路線標籤',
'settings.bookingLabelsHint':
'在地圖上顯示車站 / 機場名稱。關閉時僅顯示圖示。',
"settings.currency": "Currency",
"settings.currencyHint": "All amounts in Costs are converted to and shown in this currency.",
'settings.bookingLabelsHint': '在地圖上顯示車站 / 機場名稱。關閉時僅顯示圖示。',
'settings.currency': 'Currency',
'settings.currencyHint': 'All amounts in Costs are converted to and shown in this currency.',
'settings.passkey.title': 'Passkey',
'settings.passkey.description':
'使用 Passkey 更快登入,並可抵禦網路釣魚——透過你的指紋、臉部、PIN 碼或硬體金鑰。你的密碼仍會保留作為備援。',
'settings.passkey.notConfigured':
'Passkey 已啟用,但此伺服器尚未完成設定。請聯絡管理員設定 WebAuthn 網域。',
'settings.passkey.notConfigured': 'Passkey 已啟用,但此伺服器尚未完成設定。請聯絡管理員設定 WebAuthn 網域。',
'settings.passkey.add': '新增 Passkey',
'settings.passkey.addTitle': '新增 Passkey',
'settings.passkey.passwordPrompt': '請確認你目前的密碼,然後依照裝置提示操作。',
@@ -319,6 +293,9 @@ const settings: TranslationStrings = {
'settings.airtrail.apiKeyHint': '在 AirTrail 的「設定 → 安全性」中產生。以加密方式儲存。',
'settings.airtrail.allowInsecureTls': '允許自簽憑證',
'settings.airtrail.allowInsecureTlsHint': '僅在你自己網路上受信任的執行個體啟用。',
'settings.airtrail.writeBack': '將變更寫回 AirTrail',
'settings.airtrail.writeBackHint':
'預設關閉:AirTrail 是資料來源,TREK 僅從中讀取。開啟後會將在 TREK 中所做的修改寫回 AirTrail。',
'settings.airtrail.connected': '已連接',
'settings.airtrail.notConnected': '未連接',
'settings.airtrail.toast.saved': '已儲存 AirTrail 連接',
+1 -2
View File
@@ -2,8 +2,7 @@ import type { TranslationStrings } from '../types';
const share: TranslationStrings = {
'share.linkTitle': '公開連結',
'share.linkHint':
'建立一個連結,任何人無需登入即可檢視此旅行。僅可檢視,無法編輯。',
'share.linkHint': '建立一個連結,任何人無需登入即可檢視此旅行。僅可檢視,無法編輯。',
'share.createLink': '建立連結',
'share.deleteLink': '刪除連結',
'share.createError': '無法建立連結',
+1 -2
View File
@@ -20,8 +20,7 @@ const system_notice: TranslationStrings = {
'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.body': '將您的旅程記錄為具有時間軸、相片畫庫與互動地圖的豐富旅行故事。',
'system_notice.v3_journey.cta_label': '開啟 Journey',
'system_notice.v3_journey.highlight_timeline': '每日時間軸與畫庫',
'system_notice.v3_journey.highlight_photos': '從 Immich 或 Synology 匯入',
+1 -1
View File
@@ -9,7 +9,7 @@ const trip: TranslationStrings = {
'trip.tabs.packingShort': '行李',
'trip.tabs.lists': '清單',
'trip.tabs.listsShort': '清單',
'trip.tabs.budget': "Costs",
'trip.tabs.budget': 'Costs',
'trip.tabs.files': '檔案',
'trip.loading': '載入旅行中...',
'trip.loadingPhotos': '正在載入地點照片...',
+1 -2
View File
@@ -11,7 +11,6 @@ const trips: TranslationStrings = {
'trips.reminderDays': '天',
'trips.reminderCustom': '自定義',
'trips.reminderDaysBefore': '天前提醒',
'trips.reminderDisabledHint':
'旅行提醒已停用。請在管理 > 設定 > 通知中啟用。',
'trips.reminderDisabledHint': '旅行提醒已停用。請在管理 > 設定 > 通知中啟用。',
};
export default trips;