From 57503a6a102eb746a8114ffb0789a2a5edd2e925 Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sat, 11 Apr 2026 17:54:50 -0300 Subject: [PATCH 1/9] feat(login): add language dropdown, browser auto-detection and configurable default Replace the language cycling button on the login page with a dropdown showing all 14 supported languages. Add automatic browser/OS language detection via navigator.languages, falling back to a configurable DEFAULT_LANGUAGE env var, then 'en' as last resort. Co-Authored-By: Claude Sonnet 4.6 --- client/src/api/client.ts | 5 ++ client/src/i18n/TranslationContext.tsx | 21 +++++ client/src/i18n/index.ts | 1 + client/src/pages/LoginPage.tsx | 112 +++++++++++++++++++------ client/src/store/settingsStore.ts | 8 ++ server/.env.example | 3 + server/src/app.ts | 3 +- server/src/config.ts | 5 ++ 8 files changed, 130 insertions(+), 28 deletions(-) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 156da726..41f026b5 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -378,6 +378,11 @@ export const weatherApi = { getDetailed: (lat: number, lng: number, date: string, lang?: string) => apiClient.get('/weather/detailed', { params: { lat, lng, date, lang } }).then(r => r.data), } +export const configApi = { + getPublicConfig: (): Promise<{ defaultLanguage: string }> => + fetch('/api/config').then(r => r.json()), +} + export const settingsApi = { get: () => apiClient.get('/settings').then(r => r.data), set: (key: string, value: unknown) => apiClient.put('/settings', { key, value }).then(r => r.data), diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index a8a595a9..e73e2caa 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -51,6 +51,27 @@ 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. +export function detectBrowserLanguage(): string | null { + const browserLangs = navigator.languages?.length ? navigator.languages : [navigator.language] + const supported = SUPPORTED_LANGUAGES.map(l => l.value) + + for (const lang of browserLangs) { + // Exact match (e.g. 'de', 'zh-TW') + if (supported.includes(lang)) return lang + + // Portuguese variants → our code is 'br' (pt-BR) + if (lang.startsWith('pt')) return 'br' + + // Prefix match (e.g. 'de-AT' → 'de', 'zh-CN' → 'zh') + const prefix = lang.split('-')[0] + if (supported.includes(prefix)) return prefix + } + + return null +} + interface TranslationContextValue { t: (key: string, params?: Record) => string language: string diff --git a/client/src/i18n/index.ts b/client/src/i18n/index.ts index 4d221cd0..6895bdf2 100644 --- a/client/src/i18n/index.ts +++ b/client/src/i18n/index.ts @@ -4,5 +4,6 @@ export { getLocaleForLanguage, getIntlLanguage, isRtlLanguage, + detectBrowserLanguage, SUPPORTED_LANGUAGES, } from './TranslationContext' diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index ac5fb4f3..64b20666 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -2,10 +2,10 @@ import React, { useState, useEffect, useMemo, useRef } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' -import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n' -import { authApi } from '../api/client' +import { SUPPORTED_LANGUAGES, useTranslation, detectBrowserLanguage } from '../i18n' +import { authApi, configApi } from '../api/client' import { getApiErrorMessage } from '../types' -import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe, Zap, Users, Wallet, Map, CheckSquare, BookMarked, FolderOpen, Route, Shield, KeyRound } from 'lucide-react' +import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe, Zap, Users, Wallet, Map, CheckSquare, BookMarked, FolderOpen, Route, Shield, KeyRound, ChevronDown } from 'lucide-react' interface AppConfig { has_users: boolean @@ -36,8 +36,10 @@ export default function LoginPage(): React.ReactElement { const [inviteValid, setInviteValid] = useState(false) const exchangeInitiated = useRef(false) + const [langDropdownOpen, setLangDropdownOpen] = useState(false) + const { login, register, demoLogin, completeMfaLogin, loadUser } = useAuthStore() - const { setLanguageLocal } = useSettingsStore() + const { setLanguageLocal, setLanguageTransient } = useSettingsStore() const navigate = useNavigate() const location = useLocation() const noRedirect = !!(location.state as { noRedirect?: boolean } | null)?.noRedirect @@ -116,6 +118,32 @@ export default function LoginPage(): React.ReactElement { }) }, [navigate, t, noRedirect]) + // Language detection chain (runs once on mount, only if user has no saved preference): + // 1. localStorage → already in store initial state, skip + // 2. Browser/OS language (navigator.languages) + // 3. Server default (DEFAULT_LANGUAGE env var) + // 4. 'en' → hardcoded fallback already in store + useEffect(() => { + if (localStorage.getItem('app_language')) return + + const detected = detectBrowserLanguage() + if (detected) { + setLanguageTransient(detected) + return + } + + configApi.getPublicConfig() + .then(({ defaultLanguage }) => { if (defaultLanguage) setLanguageTransient(defaultLanguage) }) + .catch(() => {}) + }, [setLanguageTransient]) + + useEffect(() => { + if (!langDropdownOpen) return + const close = () => setLangDropdownOpen(false) + document.addEventListener('click', close) + return () => document.removeEventListener('click', close) + }, [langDropdownOpen]) + const handleDemoLogin = async (): Promise => { setError('') setIsLoading(true) @@ -364,29 +392,59 @@ export default function LoginPage(): React.ReactElement { return (
- {/* Language toggle */} - + {/* Language dropdown */} +
+ + + {langDropdownOpen && ( +
e.stopPropagation()} + style={{ + position: 'absolute', top: '100%', right: 0, marginTop: 4, + background: 'white', borderRadius: 12, + boxShadow: '0 4px 24px rgba(0,0,0,0.12)', + border: '1px solid rgba(0,0,0,0.08)', + minWidth: 190, maxHeight: 320, overflowY: 'auto', + }} + > + {SUPPORTED_LANGUAGES.map(({ value, label }) => ( + + ))} +
+ )} +
{/* Left — branding */}
Promise updateSetting: (key: keyof Settings, value: Settings[keyof Settings]) => Promise setLanguageLocal: (lang: string) => void + setLanguageTransient: (lang: string) => void updateSettings: (settingsObj: Partial) => Promise } @@ -59,6 +60,13 @@ export const useSettingsStore = create((set, get) => ({ set((state) => ({ settings: { ...state.settings, language: lang } })) }, + // Applies a language for the current session without persisting to localStorage. + // Used for automatic detection (browser/server default) — only explicit user + // choices via the UI should be persisted. + setLanguageTransient: (lang: string) => { + set((state) => ({ settings: { ...state.settings, language: lang } })) + }, + updateSettings: async (settingsObj: Partial) => { set((state) => ({ settings: { ...state.settings, ...settingsObj }, diff --git a/server/.env.example b/server/.env.example index 932a274f..ba9da901 100644 --- a/server/.env.example +++ b/server/.env.example @@ -6,6 +6,9 @@ NODE_ENV=development # development = development mode; production = production m # existing encrypted data remains readable, then re-save credentials via the admin panel. # Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" TZ=UTC # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin) +# DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference (default: en) +# Supported values: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar +# Note: browser/OS language is detected automatically first; this is the fallback when no match is found. LOG_LEVEL=info # info = concise user actions; debug = verbose admin-level details ALLOWED_ORIGINS=https://trek.example.com # Comma-separated origins for CORS and email links diff --git a/server/src/app.ts b/server/src/app.ts index 3bf2336a..f88ac81d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -6,7 +6,7 @@ import path from 'node:path'; import fs from 'node:fs'; import jwt from 'jsonwebtoken'; -import { JWT_SECRET } from './config'; +import { JWT_SECRET, DEFAULT_LANGUAGE } from './config'; import { logDebug, logWarn, logError } from './services/auditLog'; import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy'; import { authenticate } from './middleware/auth'; @@ -193,6 +193,7 @@ export function createApp(): express.Application { app.use('/api/trips/:tripId/reservations', reservationsRoutes); app.use('/api/trips/:tripId/days/:dayId/notes', dayNotesRoutes); app.get('/api/health', (_req: Request, res: Response) => res.json({ status: 'ok' })); + app.get('/api/config', (_req: Request, res: Response) => res.json({ defaultLanguage: DEFAULT_LANGUAGE })); app.use('/api', assignmentsRoutes); app.use('/api/tags', tagsRoutes); app.use('/api/categories', categoriesRoutes); diff --git a/server/src/config.ts b/server/src/config.ts index be5d2bcc..1f3e787d 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -98,3 +98,8 @@ if (_encryptionKey) { } 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 +export const DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE || 'en'; From abed22661a7aa05e88484ca53216a295cbb033e9 Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 18:46:03 -0300 Subject: [PATCH 2/9] fix(login): address PR review feedback - Use apiClient instead of raw fetch() in configApi.getPublicConfig - Validate DEFAULT_LANGUAGE against supported codes on server startup - Log warning instead of silently swallowing fetch errors in LoginPage - Case-insensitive browser language matching in detectBrowserLanguage - Guard against undefined navigator in detectBrowserLanguage - Validate language code in setLanguageTransient before applying - Import directly from TranslationContext instead of barrel index --- client/src/api/client.ts | 2 +- client/src/i18n/TranslationContext.tsx | 19 ++++++++++++------- client/src/pages/LoginPage.tsx | 4 ++-- client/src/store/settingsStore.ts | 2 ++ server/src/config.ts | 7 ++++++- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 41f026b5..c8334e8a 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -380,7 +380,7 @@ export const weatherApi = { export const configApi = { getPublicConfig: (): Promise<{ defaultLanguage: string }> => - fetch('/api/config').then(r => r.json()), + apiClient.get('/config').then(r => r.data), } export const settingsApi = { diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index e73e2caa..aee8ff71 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -54,19 +54,24 @@ export function isRtlLanguage(language: string): boolean { // 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. export function detectBrowserLanguage(): string | null { - const browserLangs = navigator.languages?.length ? navigator.languages : [navigator.language] + if (typeof navigator === 'undefined') return null + const browserLangs = navigator.languages?.length + ? navigator.languages + : navigator.language ? [navigator.language] : [] const supported = SUPPORTED_LANGUAGES.map(l => l.value) for (const lang of browserLangs) { - // Exact match (e.g. 'de', 'zh-TW') - if (supported.includes(lang)) return lang + // Exact match (e.g. 'de', 'zh-TW') — case-insensitive + const exactMatch = supported.find(s => s.toLowerCase() === lang.toLowerCase()) + if (exactMatch) return exactMatch // Portuguese variants → our code is 'br' (pt-BR) - if (lang.startsWith('pt')) return 'br' + if (lang.toLowerCase().startsWith('pt')) return 'br' - // Prefix match (e.g. 'de-AT' → 'de', 'zh-CN' → 'zh') - const prefix = lang.split('-')[0] - if (supported.includes(prefix)) return prefix + // Prefix match (e.g. 'de-AT' → 'de', 'zh-CN' → 'zh') — case-insensitive + const prefix = lang.split('-')[0].toLowerCase() + const prefixMatch = supported.find(s => s.toLowerCase() === prefix) + if (prefixMatch) return prefixMatch } return null diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 64b20666..8a309b62 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' -import { SUPPORTED_LANGUAGES, useTranslation, detectBrowserLanguage } from '../i18n' +import { SUPPORTED_LANGUAGES, useTranslation, detectBrowserLanguage } from '../i18n/TranslationContext' import { authApi, configApi } from '../api/client' import { getApiErrorMessage } from '../types' import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe, Zap, Users, Wallet, Map, CheckSquare, BookMarked, FolderOpen, Route, Shield, KeyRound, ChevronDown } from 'lucide-react' @@ -134,7 +134,7 @@ export default function LoginPage(): React.ReactElement { configApi.getPublicConfig() .then(({ defaultLanguage }) => { if (defaultLanguage) setLanguageTransient(defaultLanguage) }) - .catch(() => {}) + .catch((err) => console.warn('Failed to fetch default language config:', err)) }, [setLanguageTransient]) useEffect(() => { diff --git a/client/src/store/settingsStore.ts b/client/src/store/settingsStore.ts index 179527df..9e56e505 100644 --- a/client/src/store/settingsStore.ts +++ b/client/src/store/settingsStore.ts @@ -64,6 +64,8 @@ export const useSettingsStore = create((set, get) => ({ // Used for automatic detection (browser/server default) — only explicit user // choices via the UI should be persisted. setLanguageTransient: (lang: string) => { + const supported = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar'] + if (!supported.includes(lang)) return set((state) => ({ settings: { ...state.settings, language: lang } })) }, diff --git a/server/src/config.ts b/server/src/config.ts index 1f3e787d..50a6dc97 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -102,4 +102,9 @@ 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 -export const DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE || 'en'; +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 || 'en'; +if (!SUPPORTED_LANG_CODES.includes(rawDefaultLang)) { + console.warn(`DEFAULT_LANGUAGE="${rawDefaultLang}" is not supported. Falling back to "en". Supported: ${SUPPORTED_LANG_CODES.join(', ')}`); +} +export const DEFAULT_LANGUAGE = SUPPORTED_LANG_CODES.includes(rawDefaultLang) ? rawDefaultLang : 'en'; From 91f7c3778fcdebf50e553963f7e80c3d549c126d Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 18:50:44 -0300 Subject: [PATCH 3/9] refactor(i18n): extract SUPPORTED_LANGUAGES to avoid duplication Move language list to supportedLanguages.ts so TranslationContext and settingsStore can import from a single source of truth, eliminating the hardcoded array in setLanguageTransient. --- client/src/i18n/TranslationContext.tsx | 20 +++----------------- client/src/i18n/supportedLanguages.ts | 20 ++++++++++++++++++++ client/src/store/settingsStore.ts | 4 ++-- 3 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 client/src/i18n/supportedLanguages.ts diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index aee8ff71..83a2a127 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -14,26 +14,12 @@ import ar from './translations/ar' import br from './translations/br' import cs from './translations/cs' import pl from './translations/pl' +import { SUPPORTED_LANGUAGES } from './supportedLanguages' + +export { SUPPORTED_LANGUAGES } type TranslationStrings = Record -export const SUPPORTED_LANGUAGES = [ - { value: 'de', label: 'Deutsch' }, - { value: 'en', label: 'English' }, - { value: 'es', label: 'Español' }, - { value: 'fr', label: 'Français' }, - { value: 'hu', label: 'Magyar' }, - { value: 'nl', label: 'Nederlands' }, - { value: 'br', label: 'Português (Brasil)' }, - { value: 'cs', label: 'Česky' }, - { value: 'pl', label: 'Polski' }, - { value: 'ru', label: 'Русский' }, - { value: 'zh', label: '简体中文' }, - { value: 'zh-TW', label: '繁體中文' }, - { value: 'it', label: 'Italiano' }, - { value: 'ar', label: 'العربية' }, -] as const - const translations: Record = { de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, ar, br, cs, pl } const LOCALES: Record = { de: 'de-DE', en: 'en-US', es: 'es-ES', fr: 'fr-FR', hu: 'hu-HU', it: 'it-IT', ru: 'ru-RU', zh: 'zh-CN', 'zh-TW': 'zh-TW', nl: 'nl-NL', ar: 'ar-SA', br: 'pt-BR', cs: 'cs-CZ', pl: 'pl-PL' } const RTL_LANGUAGES = new Set(['ar']) diff --git a/client/src/i18n/supportedLanguages.ts b/client/src/i18n/supportedLanguages.ts new file mode 100644 index 00000000..496e34b2 --- /dev/null +++ b/client/src/i18n/supportedLanguages.ts @@ -0,0 +1,20 @@ +export const SUPPORTED_LANGUAGES = [ + { value: 'de', label: 'Deutsch' }, + { value: 'en', label: 'English' }, + { value: 'es', label: 'Español' }, + { value: 'fr', label: 'Français' }, + { value: 'hu', label: 'Magyar' }, + { value: 'nl', label: 'Nederlands' }, + { value: 'br', label: 'Português (Brasil)' }, + { value: 'cs', label: 'Česky' }, + { value: 'pl', label: 'Polski' }, + { value: 'ru', label: 'Русский' }, + { value: 'zh', label: '简体中文' }, + { value: 'zh-TW', label: '繁體中文' }, + { value: 'it', label: 'Italiano' }, + { value: 'ar', label: 'العربية' }, +] as const + +export type SupportedLanguageCode = typeof SUPPORTED_LANGUAGES[number]['value'] + +export const SUPPORTED_LANGUAGE_CODES = SUPPORTED_LANGUAGES.map(l => l.value) diff --git a/client/src/store/settingsStore.ts b/client/src/store/settingsStore.ts index 9e56e505..f10ff7d8 100644 --- a/client/src/store/settingsStore.ts +++ b/client/src/store/settingsStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { settingsApi } from '../api/client' import type { Settings } from '../types' import { getApiErrorMessage } from '../types' +import { SUPPORTED_LANGUAGE_CODES } from '../i18n/supportedLanguages' interface SettingsState { settings: Settings @@ -64,8 +65,7 @@ export const useSettingsStore = create((set, get) => ({ // Used for automatic detection (browser/server default) — only explicit user // choices via the UI should be persisted. setLanguageTransient: (lang: string) => { - const supported = ['de', 'en', 'es', 'fr', 'hu', 'nl', 'br', 'cs', 'pl', 'ru', 'zh', 'zh-TW', 'it', 'ar'] - if (!supported.includes(lang)) return + if (!SUPPORTED_LANGUAGE_CODES.includes(lang)) return set((state) => ({ settings: { ...state.settings, language: lang } })) }, From bf3649942c820e0906a61facbb8ef49201480e18 Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 18:54:26 -0300 Subject: [PATCH 4/9] refactor(i18n): add locale to SUPPORTED_LANGUAGES and derive LOCALES from it LOCALES is now built via Object.fromEntries from SUPPORTED_LANGUAGES, so adding a new language only requires one change in supportedLanguages.ts. Also types translations as Record so TypeScript enforces that every supported language has a translation entry. --- client/src/i18n/TranslationContext.tsx | 14 +++++++++--- client/src/i18n/supportedLanguages.ts | 30 +++++++++++++------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/client/src/i18n/TranslationContext.tsx b/client/src/i18n/TranslationContext.tsx index 83a2a127..f18dcf20 100644 --- a/client/src/i18n/TranslationContext.tsx +++ b/client/src/i18n/TranslationContext.tsx @@ -14,14 +14,22 @@ import ar from './translations/ar' import br from './translations/br' import cs from './translations/cs' import pl from './translations/pl' -import { SUPPORTED_LANGUAGES } from './supportedLanguages' +import { SUPPORTED_LANGUAGES, SupportedLanguageCode } from './supportedLanguages' export { SUPPORTED_LANGUAGES } type TranslationStrings = Record -const translations: Record = { de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, ar, br, cs, pl } -const LOCALES: Record = { de: 'de-DE', en: 'en-US', es: 'es-ES', fr: 'fr-FR', hu: 'hu-HU', it: 'it-IT', ru: 'ru-RU', zh: 'zh-CN', 'zh-TW': 'zh-TW', nl: 'nl-NL', ar: 'ar-SA', br: 'pt-BR', cs: 'cs-CZ', pl: 'pl-PL' } +// Keyed by SupportedLanguageCode so TypeScript enforces all languages have a translation. +const translations: Record = { + de, en, es, fr, hu, it, ru, zh, 'zh-TW': zhTw, nl, ar, br, cs, pl, +} + +// Derived from SUPPORTED_LANGUAGES — add new languages there, not here. +const LOCALES: Record = Object.fromEntries( + SUPPORTED_LANGUAGES.map(l => [l.value, l.locale]) +) + const RTL_LANGUAGES = new Set(['ar']) export function getLocaleForLanguage(language: string): string { diff --git a/client/src/i18n/supportedLanguages.ts b/client/src/i18n/supportedLanguages.ts index 496e34b2..28d957b8 100644 --- a/client/src/i18n/supportedLanguages.ts +++ b/client/src/i18n/supportedLanguages.ts @@ -1,20 +1,20 @@ export const SUPPORTED_LANGUAGES = [ - { value: 'de', label: 'Deutsch' }, - { value: 'en', label: 'English' }, - { value: 'es', label: 'Español' }, - { value: 'fr', label: 'Français' }, - { value: 'hu', label: 'Magyar' }, - { value: 'nl', label: 'Nederlands' }, - { value: 'br', label: 'Português (Brasil)' }, - { value: 'cs', label: 'Česky' }, - { value: 'pl', label: 'Polski' }, - { value: 'ru', label: 'Русский' }, - { value: 'zh', label: '简体中文' }, - { value: 'zh-TW', label: '繁體中文' }, - { value: 'it', label: 'Italiano' }, - { value: 'ar', label: 'العربية' }, + { 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: 'ar', label: 'العربية', locale: 'ar-SA' }, ] as const export type SupportedLanguageCode = typeof SUPPORTED_LANGUAGES[number]['value'] -export const SUPPORTED_LANGUAGE_CODES = SUPPORTED_LANGUAGES.map(l => l.value) +export const SUPPORTED_LANGUAGE_CODES: string[] = SUPPORTED_LANGUAGES.map(l => l.value) From f46f484d5f8a28f34a87ad5f6f109e41bcdb3b6c Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 19:01:39 -0300 Subject: [PATCH 5/9] test(i18n): update SUPPORTED_LANGUAGES assertions to use objectContaining Entries now include a locale field, so exact equality checks were failing. objectContaining matches on value/label only. --- client/tests/unit/i18n/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/tests/unit/i18n/index.test.ts b/client/tests/unit/i18n/index.test.ts index 3e529b3f..ae1f7f4b 100644 --- a/client/tests/unit/i18n/index.test.ts +++ b/client/tests/unit/i18n/index.test.ts @@ -91,8 +91,8 @@ describe('SUPPORTED_LANGUAGES', () => { it('FE-COMP-I18N-009: contains expected entries with value/label shape', () => { expect(Array.isArray(SUPPORTED_LANGUAGES)).toBe(true) expect(SUPPORTED_LANGUAGES).toHaveLength(14) - expect(SUPPORTED_LANGUAGES).toContainEqual({ value: 'en', label: 'English' }) - expect(SUPPORTED_LANGUAGES).toContainEqual({ value: 'ar', label: 'العربية' }) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'en', label: 'English' })) + expect(SUPPORTED_LANGUAGES).toContainEqual(expect.objectContaining({ value: 'ar', label: 'العربية' })) }) }) From c19e65b46b4ba310fb349160b4629673c3ef788b Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 19:52:01 -0300 Subject: [PATCH 7/9] fix(i18n): remove duplicate translation keys in 8 language files --- client/src/i18n/translations/br.ts | 54 +----------------------------- client/src/i18n/translations/cs.ts | 51 +--------------------------- client/src/i18n/translations/es.ts | 12 ------- client/src/i18n/translations/fr.ts | 12 ------- client/src/i18n/translations/hu.ts | 51 +--------------------------- client/src/i18n/translations/it.ts | 12 ------- client/src/i18n/translations/nl.ts | 12 ------- client/src/i18n/translations/pl.ts | 54 +----------------------------- 8 files changed, 4 insertions(+), 254 deletions(-) diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 437096d2..3bb65c9b 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -8,8 +8,6 @@ const br: Record = { 'common.loading': 'Carregando...', 'common.import': 'Importar', 'common.error': 'Erro', - 'common.unknownError': 'Erro desconhecido', - 'common.tooManyAttempts': 'Muitas tentativas. Tente novamente mais tarde.', 'common.back': 'Voltar', 'common.all': 'Todos', 'common.close': 'Fechar', @@ -29,17 +27,11 @@ const br: Record = { 'common.password': 'Senha', 'common.saving': 'Salvando...', 'common.saved': 'Salvo', - 'common.expand': 'Expandir', - 'common.collapse': 'Recolher', 'trips.reminder': 'Lembrete', 'trips.reminderNone': 'Nenhum', 'trips.reminderDay': 'dia', 'trips.reminderDays': 'dias', 'trips.reminderCustom': 'Personalizado', - 'trips.memberRemoved': '{username} removido', - 'trips.memberRemoveError': 'Falha ao remover', - 'trips.memberAdded': '{username} adicionado', - 'trips.memberAddError': 'Falha ao adicionar', 'trips.reminderDaysBefore': 'dias antes da partida', 'trips.reminderDisabledHint': 'Os lembretes de viagem estão desativados. Ative-os em Admin > Configurações > Notificações.', 'common.update': 'Atualizar', @@ -419,10 +411,6 @@ const br: Record = { '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', // Register 'register.passwordMismatch': 'As senhas não coincidem', @@ -915,7 +903,6 @@ const br: Record = { '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', @@ -1066,13 +1053,9 @@ const br: Record = { '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.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', // Files 'files.title': 'Arquivos', - 'files.pageTitle': 'Arquivos e documentos', - 'files.subtitle': '{count} arquivos para {trip}', - 'files.downloadPdf': 'Baixar PDF', 'files.count': '{count} arquivos', 'files.countSingular': '1 arquivo', 'files.uploaded': '{count} enviado(s)', @@ -1141,9 +1124,6 @@ const br: Record = { 'packing.allPacked': 'Tudo na mala!', 'packing.addPlaceholder': 'Adicionar item...', 'packing.categoryPlaceholder': 'Categoria...', - 'packing.saveAsTemplate': 'Salvar como modelo', - 'packing.templateName': 'Nome do modelo', - 'packing.templateSaved': 'Lista de bagagem salva como modelo', 'packing.filterAll': 'Todos', 'packing.filterOpen': 'Abertos', 'packing.filterDone': 'Prontos', @@ -1154,6 +1134,7 @@ const br: Record = { 'packing.menuCheckAll': 'Marcar todos', 'packing.menuUncheckAll': 'Desmarcar todos', 'packing.menuDeleteCat': 'Excluir categoria', + 'packing.assignUser': 'Atribuir usuário', 'packing.noMembers': 'Nenhum membro na viagem', 'packing.addItem': 'Adicionar item', 'packing.addItemPlaceholder': 'Nome do item...', @@ -1321,13 +1302,6 @@ const br: Record = { 'backup.keep.forever': 'Manter para sempre', // Photos - 'photos.title': 'Fotos', - 'photos.subtitle': '{count} fotos para {trip}', - 'photos.dropHere': 'Arraste fotos aqui...', - 'photos.dropHereActive': 'Arraste fotos aqui', - 'photos.captionForAll': 'Legenda (para todos)', - 'photos.captionPlaceholder': 'Legenda opcional...', - 'photos.addCaption': 'Adicionar legenda...', 'photos.allDays': 'Todos os dias', 'photos.noPhotos': 'Nenhuma foto ainda', 'photos.uploadHint': 'Envie suas fotos de viagem', @@ -1335,12 +1309,6 @@ const br: Record = { 'photos.linkPlace': 'Vincular lugar', 'photos.noPlace': 'Sem lugar', 'photos.uploadN': 'Enviar {n} foto(s)', - 'photos.linkDay': 'Vincular dia', - 'photos.noDay': 'Nenhum dia', - 'photos.dayLabel': 'Dia {number}', - 'photos.photoSelected': 'Foto selecionada', - 'photos.photosSelected': 'Fotos selecionadas', - 'photos.fileTypeHint': 'JPG, PNG, WebP · máx. 10 MB · até 30 fotos', // Backup restore modal 'backup.restoreConfirmTitle': 'Restaurar backup?', @@ -1367,7 +1335,6 @@ const br: Record = { 'planner.routeCalculated': 'Rota calculada', 'planner.routeCalcFailed': 'Não foi possível calcular a rota', 'planner.routeError': 'Erro ao calcular a rota', - 'planner.icsExportFailed': 'Falha ao exportar ICS', 'planner.routeOptimized': 'Rota otimizada', 'planner.reservationUpdated': 'Reserva atualizada', 'planner.reservationAdded': 'Reserva adicionada', @@ -1804,21 +1771,6 @@ const br: Record = { 'common.justNow': 'agora mesmo', 'common.hoursAgo': 'há {count}h', 'common.daysAgo': 'há {count}d', - 'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', - 'packing.saveAsTemplate': 'Salvar como modelo', - 'packing.templateName': 'Nome do modelo', - 'packing.templateSaved': 'Lista de bagagem salva como modelo', - 'memories.notConnectedMultipleHint': 'Conecte qualquer um destes provedores de fotos: {provider_names} em Configurações para poder adicionar fotos a esta viagem.', - 'memories.providerUrl': 'URL do servidor', - 'memories.providerApiKey': 'Chave da API', - 'memories.providerUsername': 'Nome de usuário', - 'memories.providerPassword': 'Senha', - 'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}', - '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', - 'memories.selectAlbumMultiple': 'Selecionar álbum', - 'memories.selectPhotosMultiple': 'Selecionar fotos', 'journey.title': 'Jornada', 'journey.subtitle': 'Registre suas viagens em tempo real', 'journey.new': 'Nova jornada', @@ -1986,10 +1938,6 @@ const br: Record = { '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.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', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 87b35787..cf7fe0c8 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -8,8 +8,6 @@ const cs: Record = { 'common.loading': 'Načítání...', 'common.import': 'Importovat', 'common.error': 'Chyba', - 'common.unknownError': 'Neznámá chyba', - 'common.tooManyAttempts': 'Příliš mnoho pokusů. Zkuste to prosím znovu.', 'common.back': 'Zpět', 'common.all': 'Vše', 'common.close': 'Zavřít', @@ -28,12 +26,6 @@ const cs: Record = { 'common.email': 'E-mail', 'common.password': 'Heslo', 'common.saving': 'Ukládání...', - 'trips.memberRemoved': '{username} odebrán', - 'trips.memberRemoveError': 'Odebrání se nezdařilo', - 'trips.memberAdded': '{username} přidán', - 'trips.memberAddError': 'Přidání se nezdařilo', - 'common.expand': 'Rozbalit', - 'common.collapse': 'Sbalit', 'common.saved': 'Uloženo', 'trips.reminder': 'Připomínka', 'trips.reminderNone': 'Žádná', @@ -419,10 +411,6 @@ const cs: Record = { 'login.mfaHint': 'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.', 'login.mfaBack': '← Zpět k přihlášení', 'login.mfaVerify': 'Ověřit', - 'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou', - 'login.oidcFailed': 'Přihlášení přes OIDC se nezdařilo', - 'login.usernameRequired': 'Uživatelské jméno je povinné', - 'login.passwordMinLength': 'Heslo musí mít alespoň 8 znaků', // Registrace (Register) 'register.passwordMismatch': 'Hesla se neshodují', @@ -944,7 +932,6 @@ const cs: Record = { 'inspector.files': 'Soubory', 'inspector.filesCount': '{count} souborů', 'inspector.removeFromDay': 'Odebrat ze dne', - 'inspector.remove': 'Odstranit', 'inspector.addToDay': 'Přidat ke dni', 'inspector.confirmedRes': 'Potvrzená rezervace', 'inspector.pendingRes': 'Čekající rezervace', @@ -1095,13 +1082,9 @@ const cs: Record = { 'budget.settlement': 'Vyúčtování', 'budget.settlementInfo': 'Klikněte na avatar člena u rozpočtové položky pro zelené označení – to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.', 'budget.netBalances': 'Čisté zůstatky', - 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', // Soubory (Files) 'files.title': 'Soubory', - 'files.pageTitle': 'Soubory a dokumenty', - 'files.subtitle': '{count} souborů pro {trip}', - 'files.downloadPdf': 'Stáhnout PDF', 'files.count': '{count} souborů', 'files.countSingular': '1 soubor', 'files.uploaded': '{count} nahráno', @@ -1180,6 +1163,7 @@ const cs: Record = { 'packing.menuCheckAll': 'Označit vše', 'packing.menuUncheckAll': 'Odznačit vše', 'packing.menuDeleteCat': 'Smazat kategorii', + 'packing.assignUser': 'Přiřadit uživatele', 'packing.noMembers': 'Žádní členové cesty', 'packing.addItem': 'Přidat položku', 'packing.addItemPlaceholder': 'Název položky...', @@ -1347,13 +1331,6 @@ const cs: Record = { 'backup.keep.forever': 'Uchovávat navždy', // Fotky - 'photos.title': 'Fotografie', - 'photos.subtitle': '{count} fotek pro {trip}', - 'photos.dropHere': 'Přetáhněte fotografie sem...', - 'photos.dropHereActive': 'Přetáhněte fotografie sem', - 'photos.captionForAll': 'Popisek (pro všechny)', - 'photos.captionPlaceholder': 'Volitelný popisek...', - 'photos.addCaption': 'Přidat popisek...', 'photos.allDays': 'Všechny dny', 'photos.noPhotos': 'Zatím žádné fotky', 'photos.uploadHint': 'Nahrajte své cestovní fotky', @@ -1361,12 +1338,6 @@ const cs: Record = { 'photos.linkPlace': 'Propojit s místem', 'photos.noPlace': 'Žádné místo', 'photos.uploadN': 'Nahrát {n} fotek', - 'photos.linkDay': 'Propojit den', - 'photos.noDay': 'Žádný den', - 'photos.dayLabel': 'Den {number}', - 'photos.photoSelected': 'Fotografie vybrána', - 'photos.photosSelected': 'Fotografie vybrány', - 'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · až 30 fotografií', // Obnovení zálohy 'backup.restoreConfirmTitle': 'Obnovit zálohu?', @@ -1393,7 +1364,6 @@ const cs: Record = { 'planner.routeCalculated': 'Trasa vypočtena', 'planner.routeCalcFailed': 'Trasu se nepodařilo vypočítat', 'planner.routeError': 'Chyba při výpočtu trasy', - 'planner.icsExportFailed': 'Export ICS se nezdařil', 'planner.routeOptimized': 'Trasa optimalizována', 'planner.reservationUpdated': 'Rezervace aktualizována', 'planner.reservationAdded': 'Rezervace přidána', @@ -1806,21 +1776,6 @@ const cs: Record = { 'common.justNow': 'právě teď', 'common.hoursAgo': 'před {count} h', 'common.daysAgo': 'před {count} d', - 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', - 'packing.saveAsTemplate': 'Uložit jako šablonu', - 'packing.templateName': 'Název šablony', - 'packing.templateSaved': 'Balicí seznam uložen jako šablona', - 'memories.notConnectedMultipleHint': 'Připojte některého z těchto poskytovatelů fotek: {provider_names} v Nastavení, abyste mohli přidávat fotky k tomuto výletu.', - 'memories.providerUrl': 'URL serveru', - 'memories.providerApiKey': 'API klíč', - 'memories.providerUsername': 'Uživatelské jméno', - 'memories.providerPassword': 'Heslo', - 'memories.saveError': 'Nepodařilo se uložit nastavení {provider_name}', - 'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele', - 'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele', - 'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole', - 'memories.selectAlbumMultiple': 'Vybrat album', - 'memories.selectPhotosMultiple': 'Vybrat fotky', 'journey.title': 'Cestovní deník', 'journey.subtitle': 'Zaznamenávejte své cesty průběžně', 'journey.new': 'Nový cestovní deník', @@ -1988,10 +1943,6 @@ const cs: Record = { 'journey.settings.saveFailed': 'Uložení selhalo', 'journey.settings.coverUpdated': 'Obal aktualizován', 'journey.settings.coverFailed': 'Nahrávání selhalo', - 'journey.settings.failedToDelete': 'Smazání se nezdařilo', - 'journey.entries.deleteTitle': 'Smazat záznam', - 'journey.photosUploaded': '{count} fotografií nahráno', - 'journey.photosAdded': '{count} fotografií přidáno', 'journey.public.notFound': 'Nenalezeno', 'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.', 'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 1b68816f..ac258712 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1811,18 +1811,6 @@ const es: Record = { 'common.justNow': 'justo ahora', 'common.hoursAgo': 'hace {count}h', 'common.daysAgo': 'hace {count}d', - 'budget.linkedToReservation': 'Vinculado a una reserva — edita el nombre allí', - 'packing.saveAsTemplate': 'Guardar como plantilla', - 'packing.templateName': 'Nombre de la plantilla', - 'packing.templateSaved': 'Lista de equipaje guardada como plantilla', - 'memories.notConnectedMultipleHint': 'Conecta cualquiera de estos proveedores de fotos: {provider_names} en Ajustes para poder añadir fotos a este viaje.', - 'memories.providerUrl': 'URL del servidor', - 'memories.providerApiKey': 'Clave API', - 'memories.providerUsername': 'Nombre de usuario', - 'memories.providerPassword': 'Contraseña', - 'memories.saveError': 'No se pudo guardar la configuración de {provider_name}', - 'memories.selectAlbumMultiple': 'Seleccionar álbum', - 'memories.selectPhotosMultiple': 'Seleccionar fotos', 'journey.title': 'Travesía', 'journey.subtitle': 'Registra tus viajes en tiempo real', 'journey.new': 'Nueva travesía', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index d424831d..42dd3a60 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1805,18 +1805,6 @@ const fr: Record = { 'common.justNow': 'à l\'instant', 'common.hoursAgo': 'il y a {count}h', 'common.daysAgo': 'il y a {count}j', - 'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom là-bas', - 'packing.saveAsTemplate': 'Enregistrer comme modèle', - 'packing.templateName': 'Nom du modèle', - 'packing.templateSaved': 'Liste de bagages enregistrée comme modèle', - 'memories.notConnectedMultipleHint': 'Connectez l\'un de ces fournisseurs de photos : {provider_names} dans les Paramètres pour pouvoir ajouter des photos à ce voyage.', - 'memories.providerUrl': 'URL du serveur', - 'memories.providerApiKey': 'Clé API', - 'memories.providerUsername': 'Nom d\'utilisateur', - 'memories.providerPassword': 'Mot de passe', - 'memories.saveError': 'Impossible d\'enregistrer les paramètres de {provider_name}', - 'memories.selectAlbumMultiple': 'Sélectionner un album', - 'memories.selectPhotosMultiple': 'Sélectionner des photos', 'journey.title': 'Journal de voyage', 'journey.subtitle': 'Suivez vos voyages en temps réel', 'journey.new': 'Nouveau journal', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 759ef703..009b4897 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -8,8 +8,6 @@ const hu: Record = { 'common.loading': 'Betöltés...', 'common.import': 'Importálás', 'common.error': 'Hiba', - 'common.unknownError': 'Ismeretlen hiba', - 'common.tooManyAttempts': 'Túl sok próbálkozás. Kérjük, próbálja újra később.', 'common.back': 'Vissza', 'common.all': 'Összes', 'common.close': 'Bezárás', @@ -28,12 +26,6 @@ const hu: Record = { 'common.email': 'E-mail', 'common.password': 'Jelszó', 'common.saving': 'Mentés...', - 'trips.memberRemoved': '{username} eltávolítva', - 'trips.memberRemoveError': 'Eltávolítás sikertelen', - 'trips.memberAdded': '{username} hozzáadva', - 'trips.memberAddError': 'Hozzáadás sikertelen', - 'common.expand': 'Kibontás', - 'common.collapse': 'Összecsukás', 'common.saved': 'Mentve', 'trips.reminder': 'Emlékeztető', 'trips.reminderNone': 'Nincs', @@ -419,10 +411,6 @@ const hu: Record = { 'login.mfaHint': 'Nyisd meg a Google Authenticator, Authy vagy más TOTP alkalmazást.', 'login.mfaBack': '← Vissza a bejelentkezéshez', 'login.mfaVerify': 'Ellenőrzés', - 'login.invalidInviteLink': 'Érvénytelen vagy lejárt meghívólink', - 'login.oidcFailed': 'OIDC bejelentkezés sikertelen', - 'login.usernameRequired': 'A felhasználónév kötelező', - 'login.passwordMinLength': 'A jelszónak legalább 8 karakter hosszúnak kell lennie', // Regisztráció 'register.passwordMismatch': 'A jelszavak nem egyeznek', @@ -943,7 +931,6 @@ const hu: Record = { 'inspector.files': 'Fájlok', 'inspector.filesCount': '{count} fájl', 'inspector.removeFromDay': 'Eltávolítás a napról', - 'inspector.remove': 'Eltávolítás', 'inspector.addToDay': 'Hozzáadás a naphoz', 'inspector.confirmedRes': 'Megerősített foglalás', 'inspector.pendingRes': 'Függőben lévő foglalás', @@ -1094,13 +1081,9 @@ const hu: Record = { 'budget.settlement': 'Elszámolás', 'budget.settlementInfo': 'Kattints egy tag avatárjára egy költségvetési tételen a zöld jelöléshez — ez azt jelenti, hogy fizetett. Az elszámolás ezután mutatja, ki kinek mennyivel tartozik.', 'budget.netBalances': 'Nettó egyenlegek', - 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — ott módosítsa a nevet', // Fájlok 'files.title': 'Fájlok', - 'files.pageTitle': 'Fájlok és dokumentumok', - 'files.subtitle': '{count} fájl a következőhöz: {trip}', - 'files.downloadPdf': 'PDF letöltése', 'files.count': '{count} fájl', 'files.countSingular': '1 fájl', 'files.uploaded': '{count} feltöltve', @@ -1179,6 +1162,7 @@ const hu: Record = { 'packing.menuCheckAll': 'Összes kipipálása', 'packing.menuUncheckAll': 'Összes jelölés törlése', 'packing.menuDeleteCat': 'Kategória törlése', + 'packing.assignUser': 'Felhasználó hozzárendelése', 'packing.noMembers': 'Nincsenek utazási tagok', 'packing.addItem': 'Tétel hozzáadása', 'packing.addItemPlaceholder': 'Tétel neve...', @@ -1346,13 +1330,6 @@ const hu: Record = { 'backup.keep.forever': 'Örökre megőrzés', // Fotók - 'photos.title': 'Fotók', - 'photos.subtitle': '{count} fotó a következőhöz: {trip}', - 'photos.dropHere': 'Húzza ide a fényképeket...', - 'photos.dropHereActive': 'Húzza ide a fényképeket', - 'photos.captionForAll': 'Felirat (mindenkinek)', - 'photos.captionPlaceholder': 'Opcionális felirat...', - 'photos.addCaption': 'Felirat hozzáadása...', 'photos.allDays': 'Minden nap', 'photos.noPhotos': 'Még nincsenek fotók', 'photos.uploadHint': 'Töltsd fel az úti fotóidat', @@ -1360,12 +1337,6 @@ const hu: Record = { 'photos.linkPlace': 'Hely társítása', 'photos.noPlace': 'Nincs hely', 'photos.uploadN': '{n} fotó feltöltése', - 'photos.linkDay': 'Nap csatolása', - 'photos.noDay': 'Nincs nap', - 'photos.dayLabel': '{number}. nap', - 'photos.photoSelected': 'Fotó kiválasztva', - 'photos.photosSelected': 'Fotók kiválasztva', - 'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · legfeljebb 30 fotó', // Mentés visszaállítása modal 'backup.restoreConfirmTitle': 'Mentés visszaállítása?', @@ -1392,7 +1363,6 @@ const hu: Record = { 'planner.routeCalculated': 'Útvonal kiszámítva', 'planner.routeCalcFailed': 'Nem sikerült kiszámítani az útvonalat', 'planner.routeError': 'Hiba az útvonalszámítás során', - 'planner.icsExportFailed': 'Az ICS-exportálás sikertelen', 'planner.routeOptimized': 'Útvonal optimalizálva', 'planner.reservationUpdated': 'Foglalás frissítve', 'planner.reservationAdded': 'Foglalás hozzáadva', @@ -1803,21 +1773,6 @@ const hu: Record = { 'common.justNow': 'az imént', 'common.hoursAgo': '{count} órája', 'common.daysAgo': '{count} napja', - 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — a nevet ott módosítsd', - 'packing.saveAsTemplate': 'Mentés sablonként', - 'packing.templateName': 'Sablon neve', - 'packing.templateSaved': 'Csomaglista sablonként mentve', - 'memories.notConnectedMultipleHint': 'Csatlakoztasd valamelyik fotószolgáltatót: {provider_names} a Beállításokban, hogy fotókat adhass hozzá ehhez az úthoz.', - 'memories.providerUrl': 'Szerver URL', - 'memories.providerApiKey': 'API-kulcs', - 'memories.providerUsername': 'Felhasználónév', - 'memories.providerPassword': 'Jelszó', - 'memories.saveError': 'Nem sikerült menteni a(z) {provider_name} beállításait', - 'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz', - 'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz', - 'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt', - 'memories.selectAlbumMultiple': 'Album kiválasztása', - 'memories.selectPhotosMultiple': 'Fotók kiválasztása', 'journey.title': 'Útinaplók', 'journey.subtitle': 'Kövesse nyomon utazásait valós időben', 'journey.new': 'Új útinapló', @@ -1985,10 +1940,6 @@ const hu: Record = { 'journey.settings.saveFailed': 'Nem sikerült menteni', 'journey.settings.coverUpdated': 'Borítókép frissítve', 'journey.settings.coverFailed': 'A feltöltés sikertelen', - 'journey.settings.failedToDelete': 'Törlés sikertelen', - 'journey.entries.deleteTitle': 'Bejegyzés törlése', - 'journey.photosUploaded': '{count} fotó feltöltve', - 'journey.photosAdded': '{count} fotó hozzáadva', 'journey.public.notFound': 'Nem található', 'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.', 'journey.public.readOnly': 'Csak olvasható · Nyilvános útinapló', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index dabaf43a..0b960049 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1806,18 +1806,6 @@ const it: Record = { 'common.justNow': 'proprio ora', 'common.hoursAgo': '{count}h fa', 'common.daysAgo': '{count}g fa', - 'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì', - 'packing.saveAsTemplate': 'Salva come modello', - 'packing.templateName': 'Nome del modello', - 'packing.templateSaved': 'Lista bagagli salvata come modello', - 'memories.notConnectedMultipleHint': 'Collega uno di questi fornitori di foto: {provider_names} nelle Impostazioni per poter aggiungere foto a questo viaggio.', - 'memories.providerUrl': 'URL del server', - 'memories.providerApiKey': 'Chiave API', - 'memories.providerUsername': 'Nome utente', - 'memories.providerPassword': 'Password', - 'memories.saveError': 'Impossibile salvare le impostazioni di {provider_name}', - 'memories.selectAlbumMultiple': 'Seleziona album', - 'memories.selectPhotosMultiple': 'Seleziona foto', 'journey.title': 'Diario di viaggio', 'journey.subtitle': 'Segui i tuoi viaggi in tempo reale', 'journey.new': 'Nuovo diario', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 3f077d72..ec25b662 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1805,18 +1805,6 @@ const nl: Record = { 'common.justNow': 'zojuist', 'common.hoursAgo': '{count}u geleden', 'common.daysAgo': '{count}d geleden', - 'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar', - 'packing.saveAsTemplate': 'Opslaan als sjabloon', - 'packing.templateName': 'Sjabloonnaam', - 'packing.templateSaved': 'Paklijst opgeslagen als sjabloon', - 'memories.notConnectedMultipleHint': 'Verbind een van deze foto-aanbieders: {provider_names} in Instellingen om foto\'s aan deze reis toe te voegen.', - 'memories.providerUrl': 'Server-URL', - 'memories.providerApiKey': 'API-sleutel', - 'memories.providerUsername': 'Gebruikersnaam', - 'memories.providerPassword': 'Wachtwoord', - 'memories.saveError': 'Kon {provider_name}-instellingen niet opslaan', - 'memories.selectAlbumMultiple': 'Selecteer album', - 'memories.selectPhotosMultiple': 'Selecteer foto\'s', 'journey.title': 'Reisverslag', 'journey.subtitle': 'Leg je reizen vast terwijl je onderweg bent', 'journey.new': 'Nieuw reisverslag', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 8d96eb66..38282631 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -7,8 +7,6 @@ const pl: Record = { 'common.add': 'Dodaj', 'common.loading': 'Ładowanie...', 'common.error': 'Błąd', - 'common.unknownError': 'Nieznany błąd', - 'common.tooManyAttempts': 'Zbyt wiele prób. Spróbuj ponownie później.', 'common.back': 'Wstecz', 'common.all': 'Wszystko', 'common.close': 'Zamknij', @@ -27,12 +25,6 @@ const pl: Record = { 'common.email': 'E-mail', 'common.password': 'Hasło', 'common.saving': 'Zapisywanie...', - 'trips.memberRemoved': '{username} usunięty', - 'trips.memberRemoveError': 'Nie udało się usunąć', - 'trips.memberAdded': '{username} dodany', - 'trips.memberAddError': 'Nie udało się dodać', - 'common.expand': 'Rozwiń', - 'common.collapse': 'Zwiń', 'common.update': 'Aktualizuj', 'common.change': 'Zmień', 'common.uploading': 'Przesyłanie...', @@ -389,10 +381,6 @@ const pl: Record = { 'login.mfaHint': 'Otwórz Google Authenticator, Authy lub inną aplikację TOTP.', 'login.mfaBack': '← Powrót do logowania', 'login.mfaVerify': 'Weryfikuj', - 'login.invalidInviteLink': 'Nieprawidłowy lub wygasły link zaproszenia', - 'login.oidcFailed': 'Logowanie OIDC nie powiodło się', - 'login.usernameRequired': 'Nazwa użytkownika jest wymagana', - 'login.passwordMinLength': 'Hasło musi mieć co najmniej 8 znaków', // Register 'register.passwordMismatch': 'Hasła nie są identyczne', @@ -904,7 +892,6 @@ const pl: Record = { 'inspector.files': 'Pliki', 'inspector.filesCount': '{count} plików', 'inspector.removeFromDay': 'Usuń z dnia', - 'inspector.remove': 'Usuń', 'inspector.addToDay': 'Dodaj do dnia', 'inspector.confirmedRes': 'Potwierdzona rezerwacja', 'inspector.pendingRes': 'Oczekująca rezerwacja', @@ -1055,9 +1042,6 @@ const pl: Record = { // Files 'files.title': 'Pliki', - 'files.pageTitle': 'Pliki i dokumenty', - 'files.subtitle': '{count} plików dla {trip}', - 'files.downloadPdf': 'Pobierz PDF', 'files.count': '{count} plików', 'files.countSingular': '1 plik', 'files.uploaded': '{count} przesłanych', @@ -1136,9 +1120,7 @@ const pl: Record = { 'packing.menuCheckAll': 'Zaznacz wszystko', 'packing.menuUncheckAll': 'Odznacz wszystko', 'packing.menuDeleteCat': 'Usuń kategorię', - 'packing.saveAsTemplate': 'Zapisz jako szablon', - 'packing.templateName': 'Nazwa szablonu', - 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', + 'packing.assignUser': 'Przypisz użytkownika', 'packing.noMembers': 'Brak członków podróży', 'packing.addItem': 'Dodaj przedmiot', 'packing.addItemPlaceholder': 'Nazwa przedmiotu...', @@ -1306,13 +1288,6 @@ const pl: Record = { 'backup.keep.forever': 'Przechowuj na zawsze', // Photos - 'photos.title': 'Zdjęcia', - 'photos.subtitle': '{count} zdjęć dla {trip}', - 'photos.dropHere': 'Przeciągnij zdjęcia tutaj...', - 'photos.dropHereActive': 'Przeciągnij zdjęcia tutaj', - 'photos.captionForAll': 'Podpis (dla wszystkich)', - 'photos.captionPlaceholder': 'Opcjonalny podpis...', - 'photos.addCaption': 'Dodaj podpis...', 'photos.allDays': 'Wszystkie dni', 'photos.noPhotos': 'Brak zdjęć', 'photos.uploadHint': 'Prześlij zdjęcia z podróży', @@ -1320,12 +1295,6 @@ const pl: Record = { 'photos.linkPlace': 'Połącz z miejscem', 'photos.noPlace': 'Brak miejsca', 'photos.uploadN': 'Prześlij {n} zdjęć', - 'photos.linkDay': 'Połącz dzień', - 'photos.noDay': 'Brak dnia', - 'photos.dayLabel': 'Dzień {number}', - 'photos.photoSelected': 'Zdjęcie wybrane', - 'photos.photosSelected': 'Zdjęcia wybrane', - 'photos.fileTypeHint': 'JPG, PNG, WebP · maks. 10 MB · do 30 zdjęć', // Backup restore modal 'backup.restoreConfirmTitle': 'Przywrócić kopię zapasową?', @@ -1352,7 +1321,6 @@ const pl: Record = { 'planner.routeCalculated': 'Trasa została obliczona', 'planner.routeCalcFailed': 'Nie udało się obliczyć trasy', 'planner.routeError': 'Błąd obliczania trasy', - 'planner.icsExportFailed': 'Eksport ICS nie powiódł się', 'planner.routeOptimized': 'Trasa została zoptymalizowana', 'planner.reservationUpdated': 'Rezerwacja została zaktualizowana', 'planner.reservationAdded': 'Rezerwacja została dodana', @@ -1614,7 +1582,6 @@ const pl: Record = { 'inspector.trackStats': 'Statystyki trasy', 'budget.exportCsv': 'Eksportuj CSV', 'budget.table.date': 'Data', - 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', 'memories.testFirst': 'Najpierw przetestuj połączenie', 'memories.linkAlbum': 'Połącz album', 'memories.selectAlbum': 'Wybierz album Immich', @@ -1798,21 +1765,6 @@ const pl: Record = { 'common.justNow': 'przed chwilą', 'common.hoursAgo': '{count} godz. temu', 'common.daysAgo': '{count} dn. temu', - 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', - 'packing.saveAsTemplate': 'Zapisz jako szablon', - 'packing.templateName': 'Nazwa szablonu', - 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', - 'memories.notConnectedMultipleHint': 'Połącz jednego z tych dostawców zdjęć: {provider_names} w Ustawieniach, aby dodawać zdjęcia do tej podróży.', - 'memories.providerUrl': 'Adres URL serwera', - 'memories.providerApiKey': 'Klucz API', - 'memories.providerUsername': 'Nazwa użytkownika', - 'memories.providerPassword': 'Hasło', - 'memories.saveError': 'Nie udało się zapisać ustawień {provider_name}', - 'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy', - 'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy', - 'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola', - 'memories.selectAlbumMultiple': 'Wybierz album', - 'memories.selectPhotosMultiple': 'Wybierz zdjęcia', 'journey.title': 'Dziennik podróży', 'journey.subtitle': 'Dokumentuj swoje podróże na bieżąco', 'journey.new': 'Nowy dziennik podróży', @@ -1980,10 +1932,6 @@ const pl: Record = { 'journey.settings.saveFailed': 'Zapisywanie nie powiodło się', 'journey.settings.coverUpdated': 'Okładka zaktualizowana', 'journey.settings.coverFailed': 'Przesyłanie nie powiodło się', - 'journey.settings.failedToDelete': 'Nie udało się usunąć', - 'journey.entries.deleteTitle': 'Usuń wpis', - 'journey.photosUploaded': '{count} zdjęć przesłanych', - 'journey.photosAdded': '{count} zdjęć dodanych', 'journey.public.notFound': 'Nie znaleziono', 'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.', 'journey.public.readOnly': 'Tylko do odczytu · Publiczny dziennik podróży', From b84381a8dece98dcf085689c2ab2032e9d232cce Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 20:09:35 -0300 Subject: [PATCH 8/9] Revert "fix(i18n): remove duplicate translation keys in 8 language files" This reverts commit c19e65b46b4ba310fb349160b4629673c3ef788b. --- client/src/i18n/translations/br.ts | 54 +++++++++++++++++++++++++++++- client/src/i18n/translations/cs.ts | 51 +++++++++++++++++++++++++++- client/src/i18n/translations/es.ts | 12 +++++++ client/src/i18n/translations/fr.ts | 12 +++++++ client/src/i18n/translations/hu.ts | 51 +++++++++++++++++++++++++++- client/src/i18n/translations/it.ts | 12 +++++++ client/src/i18n/translations/nl.ts | 12 +++++++ client/src/i18n/translations/pl.ts | 54 +++++++++++++++++++++++++++++- 8 files changed, 254 insertions(+), 4 deletions(-) diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 3bb65c9b..437096d2 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -8,6 +8,8 @@ const br: Record = { 'common.loading': 'Carregando...', 'common.import': 'Importar', 'common.error': 'Erro', + 'common.unknownError': 'Erro desconhecido', + 'common.tooManyAttempts': 'Muitas tentativas. Tente novamente mais tarde.', 'common.back': 'Voltar', 'common.all': 'Todos', 'common.close': 'Fechar', @@ -27,11 +29,17 @@ const br: Record = { 'common.password': 'Senha', 'common.saving': 'Salvando...', 'common.saved': 'Salvo', + 'common.expand': 'Expandir', + 'common.collapse': 'Recolher', 'trips.reminder': 'Lembrete', 'trips.reminderNone': 'Nenhum', 'trips.reminderDay': 'dia', 'trips.reminderDays': 'dias', 'trips.reminderCustom': 'Personalizado', + 'trips.memberRemoved': '{username} removido', + 'trips.memberRemoveError': 'Falha ao remover', + 'trips.memberAdded': '{username} adicionado', + 'trips.memberAddError': 'Falha ao adicionar', 'trips.reminderDaysBefore': 'dias antes da partida', 'trips.reminderDisabledHint': 'Os lembretes de viagem estão desativados. Ative-os em Admin > Configurações > Notificações.', 'common.update': 'Atualizar', @@ -411,6 +419,10 @@ const br: Record = { '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', // Register 'register.passwordMismatch': 'As senhas não coincidem', @@ -903,6 +915,7 @@ const br: Record = { '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', @@ -1053,9 +1066,13 @@ const br: Record = { '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.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', // Files 'files.title': 'Arquivos', + 'files.pageTitle': 'Arquivos e documentos', + 'files.subtitle': '{count} arquivos para {trip}', + 'files.downloadPdf': 'Baixar PDF', 'files.count': '{count} arquivos', 'files.countSingular': '1 arquivo', 'files.uploaded': '{count} enviado(s)', @@ -1124,6 +1141,9 @@ const br: Record = { 'packing.allPacked': 'Tudo na mala!', 'packing.addPlaceholder': 'Adicionar item...', 'packing.categoryPlaceholder': 'Categoria...', + 'packing.saveAsTemplate': 'Salvar como modelo', + 'packing.templateName': 'Nome do modelo', + 'packing.templateSaved': 'Lista de bagagem salva como modelo', 'packing.filterAll': 'Todos', 'packing.filterOpen': 'Abertos', 'packing.filterDone': 'Prontos', @@ -1134,7 +1154,6 @@ const br: Record = { 'packing.menuCheckAll': 'Marcar todos', 'packing.menuUncheckAll': 'Desmarcar todos', 'packing.menuDeleteCat': 'Excluir categoria', - 'packing.assignUser': 'Atribuir usuário', 'packing.noMembers': 'Nenhum membro na viagem', 'packing.addItem': 'Adicionar item', 'packing.addItemPlaceholder': 'Nome do item...', @@ -1302,6 +1321,13 @@ const br: Record = { 'backup.keep.forever': 'Manter para sempre', // Photos + 'photos.title': 'Fotos', + 'photos.subtitle': '{count} fotos para {trip}', + 'photos.dropHere': 'Arraste fotos aqui...', + 'photos.dropHereActive': 'Arraste fotos aqui', + 'photos.captionForAll': 'Legenda (para todos)', + 'photos.captionPlaceholder': 'Legenda opcional...', + 'photos.addCaption': 'Adicionar legenda...', 'photos.allDays': 'Todos os dias', 'photos.noPhotos': 'Nenhuma foto ainda', 'photos.uploadHint': 'Envie suas fotos de viagem', @@ -1309,6 +1335,12 @@ const br: Record = { 'photos.linkPlace': 'Vincular lugar', 'photos.noPlace': 'Sem lugar', 'photos.uploadN': 'Enviar {n} foto(s)', + 'photos.linkDay': 'Vincular dia', + 'photos.noDay': 'Nenhum dia', + 'photos.dayLabel': 'Dia {number}', + 'photos.photoSelected': 'Foto selecionada', + 'photos.photosSelected': 'Fotos selecionadas', + 'photos.fileTypeHint': 'JPG, PNG, WebP · máx. 10 MB · até 30 fotos', // Backup restore modal 'backup.restoreConfirmTitle': 'Restaurar backup?', @@ -1335,6 +1367,7 @@ const br: Record = { 'planner.routeCalculated': 'Rota calculada', 'planner.routeCalcFailed': 'Não foi possível calcular a rota', 'planner.routeError': 'Erro ao calcular a rota', + 'planner.icsExportFailed': 'Falha ao exportar ICS', 'planner.routeOptimized': 'Rota otimizada', 'planner.reservationUpdated': 'Reserva atualizada', 'planner.reservationAdded': 'Reserva adicionada', @@ -1771,6 +1804,21 @@ const br: Record = { 'common.justNow': 'agora mesmo', 'common.hoursAgo': 'há {count}h', 'common.daysAgo': 'há {count}d', + 'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', + 'packing.saveAsTemplate': 'Salvar como modelo', + 'packing.templateName': 'Nome do modelo', + 'packing.templateSaved': 'Lista de bagagem salva como modelo', + 'memories.notConnectedMultipleHint': 'Conecte qualquer um destes provedores de fotos: {provider_names} em Configurações para poder adicionar fotos a esta viagem.', + 'memories.providerUrl': 'URL do servidor', + 'memories.providerApiKey': 'Chave da API', + 'memories.providerUsername': 'Nome de usuário', + 'memories.providerPassword': 'Senha', + 'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}', + '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', + 'memories.selectAlbumMultiple': 'Selecionar álbum', + 'memories.selectPhotosMultiple': 'Selecionar fotos', 'journey.title': 'Jornada', 'journey.subtitle': 'Registre suas viagens em tempo real', 'journey.new': 'Nova jornada', @@ -1938,6 +1986,10 @@ const br: Record = { '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.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', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index cf7fe0c8..87b35787 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -8,6 +8,8 @@ const cs: Record = { 'common.loading': 'Načítání...', 'common.import': 'Importovat', 'common.error': 'Chyba', + 'common.unknownError': 'Neznámá chyba', + 'common.tooManyAttempts': 'Příliš mnoho pokusů. Zkuste to prosím znovu.', 'common.back': 'Zpět', 'common.all': 'Vše', 'common.close': 'Zavřít', @@ -26,6 +28,12 @@ const cs: Record = { 'common.email': 'E-mail', 'common.password': 'Heslo', 'common.saving': 'Ukládání...', + 'trips.memberRemoved': '{username} odebrán', + 'trips.memberRemoveError': 'Odebrání se nezdařilo', + 'trips.memberAdded': '{username} přidán', + 'trips.memberAddError': 'Přidání se nezdařilo', + 'common.expand': 'Rozbalit', + 'common.collapse': 'Sbalit', 'common.saved': 'Uloženo', 'trips.reminder': 'Připomínka', 'trips.reminderNone': 'Žádná', @@ -411,6 +419,10 @@ const cs: Record = { 'login.mfaHint': 'Otevřete Google Authenticator, Authy nebo jinou TOTP aplikaci.', 'login.mfaBack': '← Zpět k přihlášení', 'login.mfaVerify': 'Ověřit', + 'login.invalidInviteLink': 'Neplatný nebo vypršelý odkaz s pozvánkou', + 'login.oidcFailed': 'Přihlášení přes OIDC se nezdařilo', + 'login.usernameRequired': 'Uživatelské jméno je povinné', + 'login.passwordMinLength': 'Heslo musí mít alespoň 8 znaků', // Registrace (Register) 'register.passwordMismatch': 'Hesla se neshodují', @@ -932,6 +944,7 @@ const cs: Record = { 'inspector.files': 'Soubory', 'inspector.filesCount': '{count} souborů', 'inspector.removeFromDay': 'Odebrat ze dne', + 'inspector.remove': 'Odstranit', 'inspector.addToDay': 'Přidat ke dni', 'inspector.confirmedRes': 'Potvrzená rezervace', 'inspector.pendingRes': 'Čekající rezervace', @@ -1082,9 +1095,13 @@ const cs: Record = { 'budget.settlement': 'Vyúčtování', 'budget.settlementInfo': 'Klikněte na avatar člena u rozpočtové položky pro zelené označení – to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.', 'budget.netBalances': 'Čisté zůstatky', + 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', // Soubory (Files) 'files.title': 'Soubory', + 'files.pageTitle': 'Soubory a dokumenty', + 'files.subtitle': '{count} souborů pro {trip}', + 'files.downloadPdf': 'Stáhnout PDF', 'files.count': '{count} souborů', 'files.countSingular': '1 soubor', 'files.uploaded': '{count} nahráno', @@ -1163,7 +1180,6 @@ const cs: Record = { 'packing.menuCheckAll': 'Označit vše', 'packing.menuUncheckAll': 'Odznačit vše', 'packing.menuDeleteCat': 'Smazat kategorii', - 'packing.assignUser': 'Přiřadit uživatele', 'packing.noMembers': 'Žádní členové cesty', 'packing.addItem': 'Přidat položku', 'packing.addItemPlaceholder': 'Název položky...', @@ -1331,6 +1347,13 @@ const cs: Record = { 'backup.keep.forever': 'Uchovávat navždy', // Fotky + 'photos.title': 'Fotografie', + 'photos.subtitle': '{count} fotek pro {trip}', + 'photos.dropHere': 'Přetáhněte fotografie sem...', + 'photos.dropHereActive': 'Přetáhněte fotografie sem', + 'photos.captionForAll': 'Popisek (pro všechny)', + 'photos.captionPlaceholder': 'Volitelný popisek...', + 'photos.addCaption': 'Přidat popisek...', 'photos.allDays': 'Všechny dny', 'photos.noPhotos': 'Zatím žádné fotky', 'photos.uploadHint': 'Nahrajte své cestovní fotky', @@ -1338,6 +1361,12 @@ const cs: Record = { 'photos.linkPlace': 'Propojit s místem', 'photos.noPlace': 'Žádné místo', 'photos.uploadN': 'Nahrát {n} fotek', + 'photos.linkDay': 'Propojit den', + 'photos.noDay': 'Žádný den', + 'photos.dayLabel': 'Den {number}', + 'photos.photoSelected': 'Fotografie vybrána', + 'photos.photosSelected': 'Fotografie vybrány', + 'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · až 30 fotografií', // Obnovení zálohy 'backup.restoreConfirmTitle': 'Obnovit zálohu?', @@ -1364,6 +1393,7 @@ const cs: Record = { 'planner.routeCalculated': 'Trasa vypočtena', 'planner.routeCalcFailed': 'Trasu se nepodařilo vypočítat', 'planner.routeError': 'Chyba při výpočtu trasy', + 'planner.icsExportFailed': 'Export ICS se nezdařil', 'planner.routeOptimized': 'Trasa optimalizována', 'planner.reservationUpdated': 'Rezervace aktualizována', 'planner.reservationAdded': 'Rezervace přidána', @@ -1776,6 +1806,21 @@ const cs: Record = { 'common.justNow': 'právě teď', 'common.hoursAgo': 'před {count} h', 'common.daysAgo': 'před {count} d', + 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', + 'packing.saveAsTemplate': 'Uložit jako šablonu', + 'packing.templateName': 'Název šablony', + 'packing.templateSaved': 'Balicí seznam uložen jako šablona', + 'memories.notConnectedMultipleHint': 'Připojte některého z těchto poskytovatelů fotek: {provider_names} v Nastavení, abyste mohli přidávat fotky k tomuto výletu.', + 'memories.providerUrl': 'URL serveru', + 'memories.providerApiKey': 'API klíč', + 'memories.providerUsername': 'Uživatelské jméno', + 'memories.providerPassword': 'Heslo', + 'memories.saveError': 'Nepodařilo se uložit nastavení {provider_name}', + 'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele', + 'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele', + 'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole', + 'memories.selectAlbumMultiple': 'Vybrat album', + 'memories.selectPhotosMultiple': 'Vybrat fotky', 'journey.title': 'Cestovní deník', 'journey.subtitle': 'Zaznamenávejte své cesty průběžně', 'journey.new': 'Nový cestovní deník', @@ -1943,6 +1988,10 @@ const cs: Record = { 'journey.settings.saveFailed': 'Uložení selhalo', 'journey.settings.coverUpdated': 'Obal aktualizován', 'journey.settings.coverFailed': 'Nahrávání selhalo', + 'journey.settings.failedToDelete': 'Smazání se nezdařilo', + 'journey.entries.deleteTitle': 'Smazat záznam', + 'journey.photosUploaded': '{count} fotografií nahráno', + 'journey.photosAdded': '{count} fotografií přidáno', 'journey.public.notFound': 'Nenalezeno', 'journey.public.notFoundMessage': 'Tento cestovní deník neexistuje nebo odkaz vypršel.', 'journey.public.readOnly': 'Pouze ke čtení · Veřejný cestovní deník', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index ac258712..1b68816f 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1811,6 +1811,18 @@ const es: Record = { 'common.justNow': 'justo ahora', 'common.hoursAgo': 'hace {count}h', 'common.daysAgo': 'hace {count}d', + 'budget.linkedToReservation': 'Vinculado a una reserva — edita el nombre allí', + 'packing.saveAsTemplate': 'Guardar como plantilla', + 'packing.templateName': 'Nombre de la plantilla', + 'packing.templateSaved': 'Lista de equipaje guardada como plantilla', + 'memories.notConnectedMultipleHint': 'Conecta cualquiera de estos proveedores de fotos: {provider_names} en Ajustes para poder añadir fotos a este viaje.', + 'memories.providerUrl': 'URL del servidor', + 'memories.providerApiKey': 'Clave API', + 'memories.providerUsername': 'Nombre de usuario', + 'memories.providerPassword': 'Contraseña', + 'memories.saveError': 'No se pudo guardar la configuración de {provider_name}', + 'memories.selectAlbumMultiple': 'Seleccionar álbum', + 'memories.selectPhotosMultiple': 'Seleccionar fotos', 'journey.title': 'Travesía', 'journey.subtitle': 'Registra tus viajes en tiempo real', 'journey.new': 'Nueva travesía', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 42dd3a60..d424831d 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1805,6 +1805,18 @@ const fr: Record = { 'common.justNow': 'à l\'instant', 'common.hoursAgo': 'il y a {count}h', 'common.daysAgo': 'il y a {count}j', + 'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom là-bas', + 'packing.saveAsTemplate': 'Enregistrer comme modèle', + 'packing.templateName': 'Nom du modèle', + 'packing.templateSaved': 'Liste de bagages enregistrée comme modèle', + 'memories.notConnectedMultipleHint': 'Connectez l\'un de ces fournisseurs de photos : {provider_names} dans les Paramètres pour pouvoir ajouter des photos à ce voyage.', + 'memories.providerUrl': 'URL du serveur', + 'memories.providerApiKey': 'Clé API', + 'memories.providerUsername': 'Nom d\'utilisateur', + 'memories.providerPassword': 'Mot de passe', + 'memories.saveError': 'Impossible d\'enregistrer les paramètres de {provider_name}', + 'memories.selectAlbumMultiple': 'Sélectionner un album', + 'memories.selectPhotosMultiple': 'Sélectionner des photos', 'journey.title': 'Journal de voyage', 'journey.subtitle': 'Suivez vos voyages en temps réel', 'journey.new': 'Nouveau journal', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 009b4897..759ef703 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -8,6 +8,8 @@ const hu: Record = { 'common.loading': 'Betöltés...', 'common.import': 'Importálás', 'common.error': 'Hiba', + 'common.unknownError': 'Ismeretlen hiba', + 'common.tooManyAttempts': 'Túl sok próbálkozás. Kérjük, próbálja újra később.', 'common.back': 'Vissza', 'common.all': 'Összes', 'common.close': 'Bezárás', @@ -26,6 +28,12 @@ const hu: Record = { 'common.email': 'E-mail', 'common.password': 'Jelszó', 'common.saving': 'Mentés...', + 'trips.memberRemoved': '{username} eltávolítva', + 'trips.memberRemoveError': 'Eltávolítás sikertelen', + 'trips.memberAdded': '{username} hozzáadva', + 'trips.memberAddError': 'Hozzáadás sikertelen', + 'common.expand': 'Kibontás', + 'common.collapse': 'Összecsukás', 'common.saved': 'Mentve', 'trips.reminder': 'Emlékeztető', 'trips.reminderNone': 'Nincs', @@ -411,6 +419,10 @@ const hu: Record = { 'login.mfaHint': 'Nyisd meg a Google Authenticator, Authy vagy más TOTP alkalmazást.', 'login.mfaBack': '← Vissza a bejelentkezéshez', 'login.mfaVerify': 'Ellenőrzés', + 'login.invalidInviteLink': 'Érvénytelen vagy lejárt meghívólink', + 'login.oidcFailed': 'OIDC bejelentkezés sikertelen', + 'login.usernameRequired': 'A felhasználónév kötelező', + 'login.passwordMinLength': 'A jelszónak legalább 8 karakter hosszúnak kell lennie', // Regisztráció 'register.passwordMismatch': 'A jelszavak nem egyeznek', @@ -931,6 +943,7 @@ const hu: Record = { 'inspector.files': 'Fájlok', 'inspector.filesCount': '{count} fájl', 'inspector.removeFromDay': 'Eltávolítás a napról', + 'inspector.remove': 'Eltávolítás', 'inspector.addToDay': 'Hozzáadás a naphoz', 'inspector.confirmedRes': 'Megerősített foglalás', 'inspector.pendingRes': 'Függőben lévő foglalás', @@ -1081,9 +1094,13 @@ const hu: Record = { 'budget.settlement': 'Elszámolás', 'budget.settlementInfo': 'Kattints egy tag avatárjára egy költségvetési tételen a zöld jelöléshez — ez azt jelenti, hogy fizetett. Az elszámolás ezután mutatja, ki kinek mennyivel tartozik.', 'budget.netBalances': 'Nettó egyenlegek', + 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — ott módosítsa a nevet', // Fájlok 'files.title': 'Fájlok', + 'files.pageTitle': 'Fájlok és dokumentumok', + 'files.subtitle': '{count} fájl a következőhöz: {trip}', + 'files.downloadPdf': 'PDF letöltése', 'files.count': '{count} fájl', 'files.countSingular': '1 fájl', 'files.uploaded': '{count} feltöltve', @@ -1162,7 +1179,6 @@ const hu: Record = { 'packing.menuCheckAll': 'Összes kipipálása', 'packing.menuUncheckAll': 'Összes jelölés törlése', 'packing.menuDeleteCat': 'Kategória törlése', - 'packing.assignUser': 'Felhasználó hozzárendelése', 'packing.noMembers': 'Nincsenek utazási tagok', 'packing.addItem': 'Tétel hozzáadása', 'packing.addItemPlaceholder': 'Tétel neve...', @@ -1330,6 +1346,13 @@ const hu: Record = { 'backup.keep.forever': 'Örökre megőrzés', // Fotók + 'photos.title': 'Fotók', + 'photos.subtitle': '{count} fotó a következőhöz: {trip}', + 'photos.dropHere': 'Húzza ide a fényképeket...', + 'photos.dropHereActive': 'Húzza ide a fényképeket', + 'photos.captionForAll': 'Felirat (mindenkinek)', + 'photos.captionPlaceholder': 'Opcionális felirat...', + 'photos.addCaption': 'Felirat hozzáadása...', 'photos.allDays': 'Minden nap', 'photos.noPhotos': 'Még nincsenek fotók', 'photos.uploadHint': 'Töltsd fel az úti fotóidat', @@ -1337,6 +1360,12 @@ const hu: Record = { 'photos.linkPlace': 'Hely társítása', 'photos.noPlace': 'Nincs hely', 'photos.uploadN': '{n} fotó feltöltése', + 'photos.linkDay': 'Nap csatolása', + 'photos.noDay': 'Nincs nap', + 'photos.dayLabel': '{number}. nap', + 'photos.photoSelected': 'Fotó kiválasztva', + 'photos.photosSelected': 'Fotók kiválasztva', + 'photos.fileTypeHint': 'JPG, PNG, WebP · max. 10 MB · legfeljebb 30 fotó', // Mentés visszaállítása modal 'backup.restoreConfirmTitle': 'Mentés visszaállítása?', @@ -1363,6 +1392,7 @@ const hu: Record = { 'planner.routeCalculated': 'Útvonal kiszámítva', 'planner.routeCalcFailed': 'Nem sikerült kiszámítani az útvonalat', 'planner.routeError': 'Hiba az útvonalszámítás során', + 'planner.icsExportFailed': 'Az ICS-exportálás sikertelen', 'planner.routeOptimized': 'Útvonal optimalizálva', 'planner.reservationUpdated': 'Foglalás frissítve', 'planner.reservationAdded': 'Foglalás hozzáadva', @@ -1773,6 +1803,21 @@ const hu: Record = { 'common.justNow': 'az imént', 'common.hoursAgo': '{count} órája', 'common.daysAgo': '{count} napja', + 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — a nevet ott módosítsd', + 'packing.saveAsTemplate': 'Mentés sablonként', + 'packing.templateName': 'Sablon neve', + 'packing.templateSaved': 'Csomaglista sablonként mentve', + 'memories.notConnectedMultipleHint': 'Csatlakoztasd valamelyik fotószolgáltatót: {provider_names} a Beállításokban, hogy fotókat adhass hozzá ehhez az úthoz.', + 'memories.providerUrl': 'Szerver URL', + 'memories.providerApiKey': 'API-kulcs', + 'memories.providerUsername': 'Felhasználónév', + 'memories.providerPassword': 'Jelszó', + 'memories.saveError': 'Nem sikerült menteni a(z) {provider_name} beállításait', + 'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz', + 'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz', + 'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt', + 'memories.selectAlbumMultiple': 'Album kiválasztása', + 'memories.selectPhotosMultiple': 'Fotók kiválasztása', 'journey.title': 'Útinaplók', 'journey.subtitle': 'Kövesse nyomon utazásait valós időben', 'journey.new': 'Új útinapló', @@ -1940,6 +1985,10 @@ const hu: Record = { 'journey.settings.saveFailed': 'Nem sikerült menteni', 'journey.settings.coverUpdated': 'Borítókép frissítve', 'journey.settings.coverFailed': 'A feltöltés sikertelen', + 'journey.settings.failedToDelete': 'Törlés sikertelen', + 'journey.entries.deleteTitle': 'Bejegyzés törlése', + 'journey.photosUploaded': '{count} fotó feltöltve', + 'journey.photosAdded': '{count} fotó hozzáadva', 'journey.public.notFound': 'Nem található', 'journey.public.notFoundMessage': 'Ez az útinapló nem létezik vagy a link lejárt.', 'journey.public.readOnly': 'Csak olvasható · Nyilvános útinapló', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 0b960049..dabaf43a 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1806,6 +1806,18 @@ const it: Record = { 'common.justNow': 'proprio ora', 'common.hoursAgo': '{count}h fa', 'common.daysAgo': '{count}g fa', + 'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì', + 'packing.saveAsTemplate': 'Salva come modello', + 'packing.templateName': 'Nome del modello', + 'packing.templateSaved': 'Lista bagagli salvata come modello', + 'memories.notConnectedMultipleHint': 'Collega uno di questi fornitori di foto: {provider_names} nelle Impostazioni per poter aggiungere foto a questo viaggio.', + 'memories.providerUrl': 'URL del server', + 'memories.providerApiKey': 'Chiave API', + 'memories.providerUsername': 'Nome utente', + 'memories.providerPassword': 'Password', + 'memories.saveError': 'Impossibile salvare le impostazioni di {provider_name}', + 'memories.selectAlbumMultiple': 'Seleziona album', + 'memories.selectPhotosMultiple': 'Seleziona foto', 'journey.title': 'Diario di viaggio', 'journey.subtitle': 'Segui i tuoi viaggi in tempo reale', 'journey.new': 'Nuovo diario', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index ec25b662..3f077d72 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1805,6 +1805,18 @@ const nl: Record = { 'common.justNow': 'zojuist', 'common.hoursAgo': '{count}u geleden', 'common.daysAgo': '{count}d geleden', + 'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar', + 'packing.saveAsTemplate': 'Opslaan als sjabloon', + 'packing.templateName': 'Sjabloonnaam', + 'packing.templateSaved': 'Paklijst opgeslagen als sjabloon', + 'memories.notConnectedMultipleHint': 'Verbind een van deze foto-aanbieders: {provider_names} in Instellingen om foto\'s aan deze reis toe te voegen.', + 'memories.providerUrl': 'Server-URL', + 'memories.providerApiKey': 'API-sleutel', + 'memories.providerUsername': 'Gebruikersnaam', + 'memories.providerPassword': 'Wachtwoord', + 'memories.saveError': 'Kon {provider_name}-instellingen niet opslaan', + 'memories.selectAlbumMultiple': 'Selecteer album', + 'memories.selectPhotosMultiple': 'Selecteer foto\'s', 'journey.title': 'Reisverslag', 'journey.subtitle': 'Leg je reizen vast terwijl je onderweg bent', 'journey.new': 'Nieuw reisverslag', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 38282631..8d96eb66 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -7,6 +7,8 @@ const pl: Record = { 'common.add': 'Dodaj', 'common.loading': 'Ładowanie...', 'common.error': 'Błąd', + 'common.unknownError': 'Nieznany błąd', + 'common.tooManyAttempts': 'Zbyt wiele prób. Spróbuj ponownie później.', 'common.back': 'Wstecz', 'common.all': 'Wszystko', 'common.close': 'Zamknij', @@ -25,6 +27,12 @@ const pl: Record = { 'common.email': 'E-mail', 'common.password': 'Hasło', 'common.saving': 'Zapisywanie...', + 'trips.memberRemoved': '{username} usunięty', + 'trips.memberRemoveError': 'Nie udało się usunąć', + 'trips.memberAdded': '{username} dodany', + 'trips.memberAddError': 'Nie udało się dodać', + 'common.expand': 'Rozwiń', + 'common.collapse': 'Zwiń', 'common.update': 'Aktualizuj', 'common.change': 'Zmień', 'common.uploading': 'Przesyłanie...', @@ -381,6 +389,10 @@ const pl: Record = { 'login.mfaHint': 'Otwórz Google Authenticator, Authy lub inną aplikację TOTP.', 'login.mfaBack': '← Powrót do logowania', 'login.mfaVerify': 'Weryfikuj', + 'login.invalidInviteLink': 'Nieprawidłowy lub wygasły link zaproszenia', + 'login.oidcFailed': 'Logowanie OIDC nie powiodło się', + 'login.usernameRequired': 'Nazwa użytkownika jest wymagana', + 'login.passwordMinLength': 'Hasło musi mieć co najmniej 8 znaków', // Register 'register.passwordMismatch': 'Hasła nie są identyczne', @@ -892,6 +904,7 @@ const pl: Record = { 'inspector.files': 'Pliki', 'inspector.filesCount': '{count} plików', 'inspector.removeFromDay': 'Usuń z dnia', + 'inspector.remove': 'Usuń', 'inspector.addToDay': 'Dodaj do dnia', 'inspector.confirmedRes': 'Potwierdzona rezerwacja', 'inspector.pendingRes': 'Oczekująca rezerwacja', @@ -1042,6 +1055,9 @@ const pl: Record = { // Files 'files.title': 'Pliki', + 'files.pageTitle': 'Pliki i dokumenty', + 'files.subtitle': '{count} plików dla {trip}', + 'files.downloadPdf': 'Pobierz PDF', 'files.count': '{count} plików', 'files.countSingular': '1 plik', 'files.uploaded': '{count} przesłanych', @@ -1120,7 +1136,9 @@ const pl: Record = { 'packing.menuCheckAll': 'Zaznacz wszystko', 'packing.menuUncheckAll': 'Odznacz wszystko', 'packing.menuDeleteCat': 'Usuń kategorię', - 'packing.assignUser': 'Przypisz użytkownika', + 'packing.saveAsTemplate': 'Zapisz jako szablon', + 'packing.templateName': 'Nazwa szablonu', + 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', 'packing.noMembers': 'Brak członków podróży', 'packing.addItem': 'Dodaj przedmiot', 'packing.addItemPlaceholder': 'Nazwa przedmiotu...', @@ -1288,6 +1306,13 @@ const pl: Record = { 'backup.keep.forever': 'Przechowuj na zawsze', // Photos + 'photos.title': 'Zdjęcia', + 'photos.subtitle': '{count} zdjęć dla {trip}', + 'photos.dropHere': 'Przeciągnij zdjęcia tutaj...', + 'photos.dropHereActive': 'Przeciągnij zdjęcia tutaj', + 'photos.captionForAll': 'Podpis (dla wszystkich)', + 'photos.captionPlaceholder': 'Opcjonalny podpis...', + 'photos.addCaption': 'Dodaj podpis...', 'photos.allDays': 'Wszystkie dni', 'photos.noPhotos': 'Brak zdjęć', 'photos.uploadHint': 'Prześlij zdjęcia z podróży', @@ -1295,6 +1320,12 @@ const pl: Record = { 'photos.linkPlace': 'Połącz z miejscem', 'photos.noPlace': 'Brak miejsca', 'photos.uploadN': 'Prześlij {n} zdjęć', + 'photos.linkDay': 'Połącz dzień', + 'photos.noDay': 'Brak dnia', + 'photos.dayLabel': 'Dzień {number}', + 'photos.photoSelected': 'Zdjęcie wybrane', + 'photos.photosSelected': 'Zdjęcia wybrane', + 'photos.fileTypeHint': 'JPG, PNG, WebP · maks. 10 MB · do 30 zdjęć', // Backup restore modal 'backup.restoreConfirmTitle': 'Przywrócić kopię zapasową?', @@ -1321,6 +1352,7 @@ const pl: Record = { 'planner.routeCalculated': 'Trasa została obliczona', 'planner.routeCalcFailed': 'Nie udało się obliczyć trasy', 'planner.routeError': 'Błąd obliczania trasy', + 'planner.icsExportFailed': 'Eksport ICS nie powiódł się', 'planner.routeOptimized': 'Trasa została zoptymalizowana', 'planner.reservationUpdated': 'Rezerwacja została zaktualizowana', 'planner.reservationAdded': 'Rezerwacja została dodana', @@ -1582,6 +1614,7 @@ const pl: Record = { 'inspector.trackStats': 'Statystyki trasy', 'budget.exportCsv': 'Eksportuj CSV', 'budget.table.date': 'Data', + 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', 'memories.testFirst': 'Najpierw przetestuj połączenie', 'memories.linkAlbum': 'Połącz album', 'memories.selectAlbum': 'Wybierz album Immich', @@ -1765,6 +1798,21 @@ const pl: Record = { 'common.justNow': 'przed chwilą', 'common.hoursAgo': '{count} godz. temu', 'common.daysAgo': '{count} dn. temu', + 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', + 'packing.saveAsTemplate': 'Zapisz jako szablon', + 'packing.templateName': 'Nazwa szablonu', + 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', + 'memories.notConnectedMultipleHint': 'Połącz jednego z tych dostawców zdjęć: {provider_names} w Ustawieniach, aby dodawać zdjęcia do tej podróży.', + 'memories.providerUrl': 'Adres URL serwera', + 'memories.providerApiKey': 'Klucz API', + 'memories.providerUsername': 'Nazwa użytkownika', + 'memories.providerPassword': 'Hasło', + 'memories.saveError': 'Nie udało się zapisać ustawień {provider_name}', + 'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy', + 'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy', + 'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola', + 'memories.selectAlbumMultiple': 'Wybierz album', + 'memories.selectPhotosMultiple': 'Wybierz zdjęcia', 'journey.title': 'Dziennik podróży', 'journey.subtitle': 'Dokumentuj swoje podróże na bieżąco', 'journey.new': 'Nowy dziennik podróży', @@ -1932,6 +1980,10 @@ const pl: Record = { 'journey.settings.saveFailed': 'Zapisywanie nie powiodło się', 'journey.settings.coverUpdated': 'Okładka zaktualizowana', 'journey.settings.coverFailed': 'Przesyłanie nie powiodło się', + 'journey.settings.failedToDelete': 'Nie udało się usunąć', + 'journey.entries.deleteTitle': 'Usuń wpis', + 'journey.photosUploaded': '{count} zdjęć przesłanych', + 'journey.photosAdded': '{count} zdjęć dodanych', 'journey.public.notFound': 'Nie znaleziono', 'journey.public.notFoundMessage': 'Ten dziennik podróży nie istnieje lub link wygasł.', 'journey.public.readOnly': 'Tylko do odczytu · Publiczny dziennik podróży', From 60c5755647d9eab12348dd259498f79b624cc7aa Mon Sep 17 00:00:00 2001 From: Isaias Tavares Date: Sun, 12 Apr 2026 20:10:07 -0300 Subject: [PATCH 9/9] fix(i18n): remove only true duplicate translation keys in 8 language files --- client/src/i18n/translations/br.ts | 16 ---------------- client/src/i18n/translations/cs.ts | 13 ------------- client/src/i18n/translations/es.ts | 13 ------------- client/src/i18n/translations/fr.ts | 13 ------------- client/src/i18n/translations/hu.ts | 13 ------------- client/src/i18n/translations/it.ts | 13 ------------- client/src/i18n/translations/nl.ts | 13 ------------- client/src/i18n/translations/pl.ts | 16 ---------------- 8 files changed, 110 deletions(-) diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 437096d2..3c89034d 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -1066,7 +1066,6 @@ const br: Record = { '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.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', // Files 'files.title': 'Arquivos', @@ -1163,9 +1162,6 @@ const br: Record = { 'packing.template': 'Modelo', 'packing.templateApplied': '{count} itens adicionados do modelo', 'packing.templateError': 'Falha ao aplicar modelo', - 'packing.saveAsTemplate': 'Salvar como modelo', - 'packing.templateName': 'Nome do modelo', - 'packing.templateSaved': 'Lista de bagagem salva como modelo', 'packing.bags': 'Malas', 'packing.noBag': 'Sem mala', 'packing.totalWeight': 'Peso total', @@ -1804,21 +1800,9 @@ const br: Record = { 'common.justNow': 'agora mesmo', 'common.hoursAgo': 'há {count}h', 'common.daysAgo': 'há {count}d', - 'budget.linkedToReservation': 'Vinculado a uma reserva — edite o nome lá', - 'packing.saveAsTemplate': 'Salvar como modelo', - 'packing.templateName': 'Nome do modelo', - 'packing.templateSaved': 'Lista de bagagem salva como modelo', - 'memories.notConnectedMultipleHint': 'Conecte qualquer um destes provedores de fotos: {provider_names} em Configurações para poder adicionar fotos a esta viagem.', - 'memories.providerUrl': 'URL do servidor', - 'memories.providerApiKey': 'Chave da API', - 'memories.providerUsername': 'Nome de usuário', - 'memories.providerPassword': 'Senha', - 'memories.saveError': 'Não foi possível salvar as configurações de {provider_name}', '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', - 'memories.selectAlbumMultiple': 'Selecionar álbum', - 'memories.selectPhotosMultiple': 'Selecionar fotos', 'journey.title': 'Jornada', 'journey.subtitle': 'Registre suas viagens em tempo real', 'journey.new': 'Nova jornada', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 87b35787..34754f29 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -1095,7 +1095,6 @@ const cs: Record = { 'budget.settlement': 'Vyúčtování', 'budget.settlementInfo': 'Klikněte na avatar člena u rozpočtové položky pro zelené označení – to znamená, že zaplatil. Vyúčtování pak ukazuje, kdo komu a kolik dluží.', 'budget.netBalances': 'Čisté zůstatky', - 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', // Soubory (Files) 'files.title': 'Soubory', @@ -1806,21 +1805,9 @@ const cs: Record = { 'common.justNow': 'právě teď', 'common.hoursAgo': 'před {count} h', 'common.daysAgo': 'před {count} d', - 'budget.linkedToReservation': 'Propojeno s rezervací — upravte název tam', - 'packing.saveAsTemplate': 'Uložit jako šablonu', - 'packing.templateName': 'Název šablony', - 'packing.templateSaved': 'Balicí seznam uložen jako šablona', - 'memories.notConnectedMultipleHint': 'Připojte některého z těchto poskytovatelů fotek: {provider_names} v Nastavení, abyste mohli přidávat fotky k tomuto výletu.', - 'memories.providerUrl': 'URL serveru', - 'memories.providerApiKey': 'API klíč', - 'memories.providerUsername': 'Uživatelské jméno', - 'memories.providerPassword': 'Heslo', - 'memories.saveError': 'Nepodařilo se uložit nastavení {provider_name}', 'memories.saveRouteNotConfigured': 'Trasa uložení není nakonfigurována pro tohoto poskytovatele', 'memories.testRouteNotConfigured': 'Testovací trasa není nakonfigurována pro tohoto poskytovatele', 'memories.fillRequiredFields': 'Prosím vyplňte všechna povinná pole', - 'memories.selectAlbumMultiple': 'Vybrat album', - 'memories.selectPhotosMultiple': 'Vybrat fotky', 'journey.title': 'Cestovní deník', 'journey.subtitle': 'Zaznamenávejte své cesty průběžně', 'journey.new': 'Nový cestovní deník', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 1b68816f..27a18f6e 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -1053,7 +1053,6 @@ const es: Record = { 'budget.settlement': 'Liquidación', 'budget.settlementInfo': 'Haz clic en el avatar de un miembro en una partida del presupuesto para marcarlo en verde — esto significa que ha pagado. La liquidación muestra quién debe cuánto a quién.', 'budget.netBalances': 'Saldos netos', - 'budget.linkedToReservation': 'Vinculado a una reserva — edita el nombre allí', // Files 'files.title': 'Archivos', @@ -1811,18 +1810,6 @@ const es: Record = { 'common.justNow': 'justo ahora', 'common.hoursAgo': 'hace {count}h', 'common.daysAgo': 'hace {count}d', - 'budget.linkedToReservation': 'Vinculado a una reserva — edita el nombre allí', - 'packing.saveAsTemplate': 'Guardar como plantilla', - 'packing.templateName': 'Nombre de la plantilla', - 'packing.templateSaved': 'Lista de equipaje guardada como plantilla', - 'memories.notConnectedMultipleHint': 'Conecta cualquiera de estos proveedores de fotos: {provider_names} en Ajustes para poder añadir fotos a este viaje.', - 'memories.providerUrl': 'URL del servidor', - 'memories.providerApiKey': 'Clave API', - 'memories.providerUsername': 'Nombre de usuario', - 'memories.providerPassword': 'Contraseña', - 'memories.saveError': 'No se pudo guardar la configuración de {provider_name}', - 'memories.selectAlbumMultiple': 'Seleccionar álbum', - 'memories.selectPhotosMultiple': 'Seleccionar fotos', 'journey.title': 'Travesía', 'journey.subtitle': 'Registra tus viajes en tiempo real', 'journey.new': 'Nueva travesía', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index d424831d..e34aab0d 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1093,7 +1093,6 @@ const fr: Record = { 'budget.settlement': 'Règlement', 'budget.settlementInfo': 'Cliquez sur l\'avatar d\'un membre sur un poste budgétaire pour le marquer en vert — cela signifie qu\'il a payé. Le règlement indique ensuite qui doit combien à qui.', 'budget.netBalances': 'Soldes nets', - 'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom là-bas', // Files 'files.title': 'Fichiers', @@ -1805,18 +1804,6 @@ const fr: Record = { 'common.justNow': 'à l\'instant', 'common.hoursAgo': 'il y a {count}h', 'common.daysAgo': 'il y a {count}j', - 'budget.linkedToReservation': 'Lié à une réservation — modifiez le nom là-bas', - 'packing.saveAsTemplate': 'Enregistrer comme modèle', - 'packing.templateName': 'Nom du modèle', - 'packing.templateSaved': 'Liste de bagages enregistrée comme modèle', - 'memories.notConnectedMultipleHint': 'Connectez l\'un de ces fournisseurs de photos : {provider_names} dans les Paramètres pour pouvoir ajouter des photos à ce voyage.', - 'memories.providerUrl': 'URL du serveur', - 'memories.providerApiKey': 'Clé API', - 'memories.providerUsername': 'Nom d\'utilisateur', - 'memories.providerPassword': 'Mot de passe', - 'memories.saveError': 'Impossible d\'enregistrer les paramètres de {provider_name}', - 'memories.selectAlbumMultiple': 'Sélectionner un album', - 'memories.selectPhotosMultiple': 'Sélectionner des photos', 'journey.title': 'Journal de voyage', 'journey.subtitle': 'Suivez vos voyages en temps réel', 'journey.new': 'Nouveau journal', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 759ef703..961eeb25 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -1094,7 +1094,6 @@ const hu: Record = { 'budget.settlement': 'Elszámolás', 'budget.settlementInfo': 'Kattints egy tag avatárjára egy költségvetési tételen a zöld jelöléshez — ez azt jelenti, hogy fizetett. Az elszámolás ezután mutatja, ki kinek mennyivel tartozik.', 'budget.netBalances': 'Nettó egyenlegek', - 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — ott módosítsa a nevet', // Fájlok 'files.title': 'Fájlok', @@ -1803,21 +1802,9 @@ const hu: Record = { 'common.justNow': 'az imént', 'common.hoursAgo': '{count} órája', 'common.daysAgo': '{count} napja', - 'budget.linkedToReservation': 'Foglaláshoz kapcsolva — a nevet ott módosítsd', - 'packing.saveAsTemplate': 'Mentés sablonként', - 'packing.templateName': 'Sablon neve', - 'packing.templateSaved': 'Csomaglista sablonként mentve', - 'memories.notConnectedMultipleHint': 'Csatlakoztasd valamelyik fotószolgáltatót: {provider_names} a Beállításokban, hogy fotókat adhass hozzá ehhez az úthoz.', - 'memories.providerUrl': 'Szerver URL', - 'memories.providerApiKey': 'API-kulcs', - 'memories.providerUsername': 'Felhasználónév', - 'memories.providerPassword': 'Jelszó', - 'memories.saveError': 'Nem sikerült menteni a(z) {provider_name} beállításait', 'memories.saveRouteNotConfigured': 'A mentési útvonal nincs konfigurálva ehhez a szolgáltatóhoz', 'memories.testRouteNotConfigured': 'A tesztútvonal nincs konfigurálva ehhez a szolgáltatóhoz', 'memories.fillRequiredFields': 'Kérjük töltse ki az összes kötelező mezőt', - 'memories.selectAlbumMultiple': 'Album kiválasztása', - 'memories.selectPhotosMultiple': 'Fotók kiválasztása', 'journey.title': 'Útinaplók', 'journey.subtitle': 'Kövesse nyomon utazásait valós időben', 'journey.new': 'Új útinapló', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index dabaf43a..90c934cc 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1094,7 +1094,6 @@ const it: Record = { 'budget.settlement': 'Regolamento', 'budget.settlementInfo': 'Clicca sull\'avatar di un membro su una voce di budget per contrassegnarlo in verde — significa che ha pagato. Il regolamento mostra poi chi deve quanto a chi.', 'budget.netBalances': 'Saldi netti', - 'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì', // Files 'files.title': 'File', @@ -1806,18 +1805,6 @@ const it: Record = { 'common.justNow': 'proprio ora', 'common.hoursAgo': '{count}h fa', 'common.daysAgo': '{count}g fa', - 'budget.linkedToReservation': 'Collegato a una prenotazione — modifica il nome lì', - 'packing.saveAsTemplate': 'Salva come modello', - 'packing.templateName': 'Nome del modello', - 'packing.templateSaved': 'Lista bagagli salvata come modello', - 'memories.notConnectedMultipleHint': 'Collega uno di questi fornitori di foto: {provider_names} nelle Impostazioni per poter aggiungere foto a questo viaggio.', - 'memories.providerUrl': 'URL del server', - 'memories.providerApiKey': 'Chiave API', - 'memories.providerUsername': 'Nome utente', - 'memories.providerPassword': 'Password', - 'memories.saveError': 'Impossibile salvare le impostazioni di {provider_name}', - 'memories.selectAlbumMultiple': 'Seleziona album', - 'memories.selectPhotosMultiple': 'Seleziona foto', 'journey.title': 'Diario di viaggio', 'journey.subtitle': 'Segui i tuoi viaggi in tempo reale', 'journey.new': 'Nuovo diario', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 3f077d72..76aca58f 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1093,7 +1093,6 @@ const nl: Record = { 'budget.settlement': 'Afrekening', 'budget.settlementInfo': 'Klik op de avatar van een lid bij een budgetpost om deze groen te markeren — dit betekent dat diegene heeft betaald. De afrekening toont vervolgens wie wie hoeveel verschuldigd is.', 'budget.netBalances': 'Nettosaldi', - 'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar', // Files 'files.title': 'Bestanden', @@ -1805,18 +1804,6 @@ const nl: Record = { 'common.justNow': 'zojuist', 'common.hoursAgo': '{count}u geleden', 'common.daysAgo': '{count}d geleden', - 'budget.linkedToReservation': 'Gekoppeld aan een reservering — bewerk de naam daar', - 'packing.saveAsTemplate': 'Opslaan als sjabloon', - 'packing.templateName': 'Sjabloonnaam', - 'packing.templateSaved': 'Paklijst opgeslagen als sjabloon', - 'memories.notConnectedMultipleHint': 'Verbind een van deze foto-aanbieders: {provider_names} in Instellingen om foto\'s aan deze reis toe te voegen.', - 'memories.providerUrl': 'Server-URL', - 'memories.providerApiKey': 'API-sleutel', - 'memories.providerUsername': 'Gebruikersnaam', - 'memories.providerPassword': 'Wachtwoord', - 'memories.saveError': 'Kon {provider_name}-instellingen niet opslaan', - 'memories.selectAlbumMultiple': 'Selecteer album', - 'memories.selectPhotosMultiple': 'Selecteer foto\'s', 'journey.title': 'Reisverslag', 'journey.subtitle': 'Leg je reizen vast terwijl je onderweg bent', 'journey.new': 'Nieuw reisverslag', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 8d96eb66..be892ee7 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -1148,9 +1148,6 @@ const pl: Record = { 'packing.template': 'Szablon', 'packing.templateApplied': '{count} przedmiotów dodanych z szablonu', 'packing.templateError': 'Nie udało się zastosować szablonu', - 'packing.saveAsTemplate': 'Zapisz jako szablon', - 'packing.templateName': 'Nazwa szablonu', - 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', 'packing.bags': 'Torby', 'packing.noBag': 'Nieprzypisane', 'packing.totalWeight': 'Waga całkowita', @@ -1614,7 +1611,6 @@ const pl: Record = { 'inspector.trackStats': 'Statystyki trasy', 'budget.exportCsv': 'Eksportuj CSV', 'budget.table.date': 'Data', - 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', 'memories.testFirst': 'Najpierw przetestuj połączenie', 'memories.linkAlbum': 'Połącz album', 'memories.selectAlbum': 'Wybierz album Immich', @@ -1798,21 +1794,9 @@ const pl: Record = { 'common.justNow': 'przed chwilą', 'common.hoursAgo': '{count} godz. temu', 'common.daysAgo': '{count} dn. temu', - 'budget.linkedToReservation': 'Powiązane z rezerwacją — edytuj nazwę tam', - 'packing.saveAsTemplate': 'Zapisz jako szablon', - 'packing.templateName': 'Nazwa szablonu', - 'packing.templateSaved': 'Lista pakowania zapisana jako szablon', - 'memories.notConnectedMultipleHint': 'Połącz jednego z tych dostawców zdjęć: {provider_names} w Ustawieniach, aby dodawać zdjęcia do tej podróży.', - 'memories.providerUrl': 'Adres URL serwera', - 'memories.providerApiKey': 'Klucz API', - 'memories.providerUsername': 'Nazwa użytkownika', - 'memories.providerPassword': 'Hasło', - 'memories.saveError': 'Nie udało się zapisać ustawień {provider_name}', 'memories.saveRouteNotConfigured': 'Trasa zapisu nie jest skonfigurowana dla tego dostawcy', 'memories.testRouteNotConfigured': 'Trasa testowa nie jest skonfigurowana dla tego dostawcy', 'memories.fillRequiredFields': 'Proszę wypełnić wszystkie wymagane pola', - 'memories.selectAlbumMultiple': 'Wybierz album', - 'memories.selectPhotosMultiple': 'Wybierz zdjęcia', 'journey.title': 'Dziennik podróży', 'journey.subtitle': 'Dokumentuj swoje podróże na bieżąco', 'journey.new': 'Nowy dziennik podróży',