diff --git a/AGENTS.md b/AGENTS.md
index 2f92d4cea..eb5315d91 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
# AGENTS.md — FreeLingo
-**Current version: 1.9.20**
+**Current version: 1.9.25**
## Project
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a9756650..11ffa8195 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,31 @@
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.9.25] - 2026-09-25
+
+### Added
+
+- Turkish (`tr`), Swedish (`sv`), Danish (`da`), Finnish (`fi`), and Croatian (`hr`) interface and native-language options across registration, Settings, and administration, with translated UI, learning feedback, voice-session titles, and emails. These are not study languages.
+- Dashboard announcements can use these five languages as source or translations alongside the original ten.
+
+### Changed
+
+- Dashboard announcements with ten through fourteen translations remain readable; saving requires all fifteen and completing a translation creates a new revision. The editor loads the chosen source language's text before translating.
+- What's New presents the five interface languages (Croatian, Danish, Finnish, Swedish, and Turkish) in each of the fifteen interface locales alongside a general improvements note.
+- Interface text, resource counts, accessible controls, payment-recovery and error messages, administrator badges, review ratings, learning-language names, dates, numbers, and prices follow the selected interface language. Study help displays the user's native-language name, and voice-session dates follow native-language conventions.
+- The review prompt waits for the existing-review lookup before showing the rating form. The Dutch, Polish, Romanian, and Russian FAQs explain the default public-registration setting.
+
+### Fixed
+
+- Saving the interface language synchronizes locale cookies and applies the selection when the displayed language differs, including when that selection was already saved in the profile.
+- Initial locale detection preserves an existing supported interface-language cookie and ignores `Accept-Language` entries with zero or out-of-range quality weights.
+- Post-assessment voice-trial prompts and Premium continuation messages are translated in French, Portuguese, German, Italian, Polish, Dutch, Romanian, and Russian.
+- Voice transcription, tutor-response, speech-generation, and transport errors display localized messages instead of raw backend or WebSocket diagnostics.
+- Croatian feedback comment counts use the correct plural forms, including counts such as 21 and 31.
+- Password reset displays the current password requirements and validates the 10-25 character length consistently with the backend, with translated errors across all fifteen interface locales.
+- Transactional emails declare the language of their rendered content in HTML, including the English fallback for unsupported locales.
## [1.9.20] - 2026-09-24
diff --git a/README.md b/README.md
index 5617e636b..d38e86685 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@



-
+
([])
@@ -494,7 +495,7 @@ export default function AdminFeedbackPage() {
{t('by')} {entry.author.display_name}
- · {formatDate(entry.created_at)}
+ · {formatDate(entry.created_at, locale)}
@@ -595,7 +596,7 @@ export default function AdminFeedbackPage() {
{t('by')} {entry.author.display_name}
- · {formatDate(entry.created_at)}
+ · {formatDate(entry.created_at, locale)}
diff --git a/frontend/src/app/(app)/admin/reviews/page.tsx b/frontend/src/app/(app)/admin/reviews/page.tsx
index ac939c972..8b665c6cb 100644
--- a/frontend/src/app/(app)/admin/reviews/page.tsx
+++ b/frontend/src/app/(app)/admin/reviews/page.tsx
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import {
CheckCircle2,
FilterX,
@@ -32,18 +32,14 @@ const PAGE_SIZE = 10
type ApprovalFilter = 'all' | 'pending' | 'approved'
-function formatDate(iso: string): string {
- return new Date(iso).toLocaleDateString(undefined, {
+function formatDate(iso: string, locale: string): string {
+ return new Date(iso).toLocaleDateString(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
-function languageLabel(code: string): string {
- return getLanguageByCode(code)?.nameEn ?? code
-}
-
function stars(rating: number): string {
return '★'.repeat(rating) + '☆'.repeat(Math.max(0, 5 - rating))
}
@@ -51,6 +47,14 @@ function stars(rating: number): string {
export default function AdminReviewsPage() {
const t = useTranslations('adminReviews')
const tAdmin = useTranslations('admin')
+ const tTarget = useTranslations('targetLanguages')
+ const tStars = useTranslations('landingReviews')
+ const locale = useLocale()
+
+ function languageLabel(code: string): string {
+ const language = getLanguageByCode(code)
+ return language ? tTarget(language.code) : code
+ }
const [reviews, setReviews] = useState([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(0)
@@ -255,7 +259,9 @@ export default function AdminReviewsPage() {
{stars(review.rating)}
@@ -264,7 +270,7 @@ export default function AdminReviewsPage() {
{t('learningLanguage', {
language: languageLabel(review.target_language),
})}{' '}
- · {formatDate(review.created_at)}
+ · {formatDate(review.created_at, locale)}
{review.comment || t('ratingOnly')}
diff --git a/frontend/src/app/(app)/admin/system/page.tsx b/frontend/src/app/(app)/admin/system/page.tsx
index 1ce66da38..2f4cd3a4b 100644
--- a/frontend/src/app/(app)/admin/system/page.tsx
+++ b/frontend/src/app/(app)/admin/system/page.tsx
@@ -21,6 +21,11 @@ const BANNER_LOCALES = [
'nl',
'pl',
'ro',
+ 'tr',
+ 'sv',
+ 'da',
+ 'fi',
+ 'hr',
] as const
type BannerLocale = (typeof BANNER_LOCALES)[number]
@@ -30,7 +35,7 @@ interface AdminDashboardBanner {
source_locale: BannerLocale
is_active: boolean
revision: number
- translations: BannerTranslations
+ translations: Partial
created_at: string
updated_at: string
}
@@ -50,6 +55,12 @@ function emptyTranslations(): BannerTranslations {
export default function AdminSystemPage() {
const t = useTranslations('admin')
const currentLocale = useLocale()
+ const sortedBannerLocales = [...BANNER_LOCALES].sort((a, b) =>
+ t(`dashboardBanner.locales.${a}`).localeCompare(
+ t(`dashboardBanner.locales.${b}`),
+ currentLocale
+ )
+ )
const defaultLocale = BANNER_LOCALES.includes(currentLocale as BannerLocale)
? (currentLocale as BannerLocale)
: 'en'
@@ -80,8 +91,10 @@ export default function AdminSystemPage() {
if (banner) {
setSourceLocale(banner.source_locale)
setEditorLocale(banner.source_locale)
- setSource({ ...banner.translations[banner.source_locale] })
- setTranslations(banner.translations)
+ setSource({
+ ...(banner.translations[banner.source_locale] ?? EMPTY_TRANSLATION),
+ })
+ setTranslations({ ...emptyTranslations(), ...banner.translations })
setHasTranslations(true)
setIsActive(banner.is_active)
setRevision(banner.revision)
@@ -163,11 +176,15 @@ export default function AdminSystemPage() {
const saved: AdminDashboardBanner = await response.json()
setRevision(saved.revision)
setUpdatedAt(saved.updated_at)
- setTranslations(saved.translations)
+ const savedTranslations = {
+ ...emptyTranslations(),
+ ...saved.translations,
+ }
+ setTranslations(savedTranslations)
setIsActive(saved.is_active)
useConfigStore.setState({
dashboardBanner: saved.is_active
- ? { revision: saved.revision, translations: saved.translations }
+ ? { revision: saved.revision, translations: savedTranslations }
: null,
})
setBannerSuccess(t('dashboardBanner.saveSuccess'))
@@ -299,12 +316,14 @@ export default function AdminSystemPage() {
- setSourceLocale(event.target.value as BannerLocale)
- }
+ onChange={(event) => {
+ const locale = event.target.value as BannerLocale
+ setSourceLocale(locale)
+ setSource({ ...translations[locale] })
+ }}
className="border-fl-border bg-fl-bg text-fl-fg w-full border px-3 py-2"
>
- {BANNER_LOCALES.map((locale) => (
+ {sortedBannerLocales.map((locale) => (
{t(`dashboardBanner.locales.${locale}`)}
@@ -411,14 +430,14 @@ export default function AdminSystemPage() {
}
className="border-fl-border bg-fl-bg text-fl-fg min-w-52 border px-3 py-2"
>
- {BANNER_LOCALES.map((locale) => {
+ {sortedBannerLocales.map((locale) => {
const complete = Object.values(
translations[locale]
).every((value) => value.trim())
return (
- {complete ? '✓ ' : ''}
{t(`dashboardBanner.locales.${locale}`)}
+ {!complete && ` — ${t('dashboardBanner.pending')}`}
)
})}
diff --git a/frontend/src/app/(app)/admin/users/[id]/page.tsx b/frontend/src/app/(app)/admin/users/[id]/page.tsx
index 521ef0ff6..ee0b12284 100644
--- a/frontend/src/app/(app)/admin/users/[id]/page.tsx
+++ b/frontend/src/app/(app)/admin/users/[id]/page.tsx
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import {
Activity,
BadgeCheck,
@@ -157,6 +157,7 @@ function subscriptionStatusClass(status: string) {
}
export default function AdminUserStatsPage() {
+ const locale = useLocale()
const t = useTranslations('admin')
const tLang = useTranslations('languages')
const tBilling = useTranslations('billing')
@@ -433,7 +434,7 @@ export default function AdminUserStatsPage() {
- {t('statsXp')}: {pl.xp_total.toLocaleString()}
+ {t('statsXp')}: {pl.xp_total.toLocaleString(locale)}
{t('statsStreak')}: {pl.streak_current} {t('statsDays')}
@@ -615,7 +616,7 @@ export default function AdminUserStatsPage() {
>
)}
@@ -756,7 +757,9 @@ export default function AdminUserStatsPage() {
{user.subscription_ends_at && (
)}
{user.stripe_customer_id && (
diff --git a/frontend/src/app/(app)/admin/users/page.tsx b/frontend/src/app/(app)/admin/users/page.tsx
index 7cb6e40f4..0d7336eb2 100644
--- a/frontend/src/app/(app)/admin/users/page.tsx
+++ b/frontend/src/app/(app)/admin/users/page.tsx
@@ -54,6 +54,11 @@ const LANGUAGES = [
'nl',
'ro',
'ru',
+ 'tr',
+ 'sv',
+ 'da',
+ 'fi',
+ 'hr',
] as const
const PAGE_SIZE = 10
@@ -81,6 +86,7 @@ function statusBadgeClass(status: string) {
export default function AdminUsersPage() {
const t = useTranslations('admin')
const tCommon = useTranslations('common')
+ const tRegister = useTranslations('auth.register')
const tBilling = useTranslations('billing')
const tLang = useTranslations('languages')
const tTarget = useTranslations('targetLanguages')
@@ -326,7 +332,13 @@ export default function AdminUsersPage() {
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
- throw new Error(data.detail || t('createUserError'))
+ throw new Error(
+ data.detail === 'Username already taken'
+ ? tRegister('usernameTaken')
+ : data.detail === 'Email already taken'
+ ? tRegister('emailTaken')
+ : t('createUserError')
+ )
}
setShowCreate(false)
setForm({
@@ -345,8 +357,13 @@ export default function AdminUsersPage() {
roleFilter,
activeFilter
)
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('createUserError'))
+ } catch (err) {
+ const knownErrors = [tRegister('usernameTaken'), tRegister('emailTaken')]
+ setError(
+ err instanceof Error && knownErrors.includes(err.message)
+ ? err.message
+ : t('createUserError')
+ )
} finally {
setCreateSaving(false)
}
@@ -362,8 +379,7 @@ export default function AdminUsersPage() {
setActivePending(null)
setActionBusy(null)
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- setError(data.detail || t('updateUserError'))
+ setError(t('updateUserError'))
return
}
await loadUsers(
@@ -383,8 +399,7 @@ export default function AdminUsersPage() {
setDeletePending(null)
setActionBusy(null)
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- setError(data.detail || t('deleteUserError'))
+ setError(t('deleteUserError'))
} else {
const newTotal = total - 1
const maxPage = Math.max(0, Math.ceil(newTotal / PAGE_SIZE) - 1)
diff --git a/frontend/src/app/(app)/assessment/level-test/page.tsx b/frontend/src/app/(app)/assessment/level-test/page.tsx
index e3651ccec..20a5f8976 100644
--- a/frontend/src/app/(app)/assessment/level-test/page.tsx
+++ b/frontend/src/app/(app)/assessment/level-test/page.tsx
@@ -109,12 +109,7 @@ export default function LevelTestPage() {
const res = await apiFetch(
`/api/assessment/level-test/questions/${planIdNum}`
)
- if (!res.ok) {
- const d = await res.json().catch(() => ({}))
- throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
- )
- }
+ if (!res.ok) throw new Error(t('levelTest.loadFailed'))
const data = (await res.json()) as {
plan_id: number
cefr_level: string
@@ -127,7 +122,11 @@ export default function LevelTestPage() {
setCefrLevel(data.cefr_level)
setStep('quiz')
} catch (err) {
- setError(err instanceof Error ? err.message : t('levelTest.loadFailed'))
+ setError(
+ err instanceof Error && err.message === t('levelTest.noQuestions')
+ ? err.message
+ : t('levelTest.loadFailed')
+ )
setStep('error')
}
}, [planId, startConfirmed, t])
@@ -183,17 +182,12 @@ export default function LevelTestPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: planIdNum, answers: finalAnswers }),
})
- if (!res.ok) {
- const d = await res.json().catch(() => ({}))
- throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
- )
- }
+ if (!res.ok) throw new Error(t('levelTest.submitFailed'))
const data = (await res.json()) as LevelTestResult
setResult(data)
setStep('result')
- } catch (err) {
- setError(err instanceof Error ? err.message : t('levelTest.submitFailed'))
+ } catch {
+ setError(t('levelTest.submitFailed'))
setStep('error')
}
}
diff --git a/frontend/src/app/(app)/assessment/page.tsx b/frontend/src/app/(app)/assessment/page.tsx
index 822fee619..9db2d4469 100644
--- a/frontend/src/app/(app)/assessment/page.tsx
+++ b/frontend/src/app/(app)/assessment/page.tsx
@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { apiFetch } from '@/lib/api'
import { useLanguageStore } from '@/store/language'
import { isSubscribed, useAuthStore } from '@/store/auth'
@@ -87,6 +87,7 @@ function adjustLevel(current: CEFRLevel, direction: 'up' | 'down'): CEFRLevel {
export default function AssessmentPage() {
const t = useTranslations('assessment')
const tCommon = useTranslations('common')
+ const locale = useLocale()
const router = useRouter()
const activeLanguage = useLanguageStore((s) => s.activeLanguage)
const user = useAuthStore((s) => s.user)
@@ -249,23 +250,13 @@ export default function AssessmentPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers: answersToSend }),
})
- if (!res.ok) {
- const d = await res.json().catch(() => ({}))
- throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
- )
- }
+ if (!res.ok) throw new Error(tCommon('errorMessage'))
const data = (await res.json()) as AssessmentResult
setResult(data)
setSelectedLevel(data.cefr_level as CEFRLevel)
setStep('result')
- } catch (err) {
- const msg = err instanceof Error ? err.message : ''
- setError(
- msg === 'ai_service_error' || msg === 'ai_service_unavailable'
- ? tCommon('errorMessage')
- : msg || 'Evaluation failed'
- )
+ } catch {
+ setError(tCommon('errorMessage'))
} finally {
setEvaluating(false)
}
@@ -293,12 +284,7 @@ export default function AssessmentPage() {
target_language: activeLanguage?.code ?? undefined,
}),
})
- if (!res.ok) {
- const d = await res.json().catch(() => ({}))
- throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
- )
- }
+ if (!res.ok) throw new Error(tCommon('errorMessage'))
const data = (await res.json()) as AssessmentCompleteResponse
setCreatedPlanId(data.plan_id)
if (data.voice_trial?.available && data.voice_trial.token) {
@@ -308,13 +294,8 @@ export default function AssessmentPage() {
return
}
router.push('/plan')
- } catch (err) {
- const msg = err instanceof Error ? err.message : ''
- setError(
- msg === 'ai_service_error' || msg === 'ai_service_unavailable'
- ? tCommon('errorMessage')
- : msg || 'Failed to create plan'
- )
+ } catch {
+ setError(tCommon('errorMessage'))
setSubmitting(false)
}
}
@@ -344,12 +325,7 @@ export default function AssessmentPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_language: activeLanguage?.code }),
})
- if (!res.ok) {
- const d = await res.json().catch(() => ({}))
- throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
- )
- }
+ if (!res.ok) throw new Error(tCommon('errorMessage'))
const data = (await res.json()) as AssessmentCompleteResponse
if (data.voice_trial?.available && data.voice_trial.token) {
setCreatedPlanId(data.plan_id)
@@ -359,9 +335,8 @@ export default function AssessmentPage() {
return
}
setError(tCommon('errorMessage'))
- } catch (err) {
- const msg = err instanceof Error ? err.message : ''
- setError(msg || tCommon('errorMessage'))
+ } catch {
+ setError(tCommon('errorMessage'))
} finally {
setTrialLoading(false)
}
@@ -380,7 +355,7 @@ export default function AssessmentPage() {
// ── Existing plan ─────────────────────────────────────────────────────────
if (step === 'existing' && existingPlan) {
const assessedDate = new Date(existingPlan.created_at).toLocaleDateString(
- undefined,
+ locale,
{
year: 'numeric',
month: 'long',
diff --git a/frontend/src/app/(app)/chat/page.tsx b/frontend/src/app/(app)/chat/page.tsx
index b172f63b4..9934502b6 100644
--- a/frontend/src/app/(app)/chat/page.tsx
+++ b/frontend/src/app/(app)/chat/page.tsx
@@ -49,6 +49,7 @@ export default function ChatPage() {
const t = useTranslations('chat')
const tCommon = useTranslations('common')
const tLang = useTranslations('targetLanguages')
+ const tQuota = useTranslations('conversation')
const router = useRouter()
const user = useAuthStore((s) => s.user)
const activeLanguage = useLanguageStore((s) => s.activeLanguage)
@@ -244,8 +245,18 @@ export default function ChatPage() {
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail || `Error ${res.status}`)
+ let message = t('errorMessage')
+ if (res.status === 429) {
+ const data = await res.json().catch(() => ({}))
+ if (
+ typeof data.detail === 'string' &&
+ data.detail.startsWith('Monthly token limit reached')
+ ) {
+ message = tQuota('quotaExceededTokens')
+ }
+ }
+ setError(message)
+ return
}
let assistantContent = ''
@@ -280,7 +291,7 @@ export default function ChatPage() {
}
if (data.error) {
streamCompleted = true
- setError(data.error)
+ setError(t('errorMessage'))
}
if (data.done) {
streamCompleted = true
@@ -295,8 +306,8 @@ export default function ChatPage() {
if (data.memory_updated) showMemoryToast()
}
if (!streamCompleted) throw new Error(t('errorMessage'))
- } catch (e) {
- setError(e instanceof Error ? e.message : t('errorMessage'))
+ } catch {
+ setError(t('errorMessage'))
} finally {
setSending(false)
inputRef.current?.focus()
@@ -381,7 +392,7 @@ export default function ChatPage() {
{c.source === 'voice' && (
🎤
@@ -394,7 +405,8 @@ export default function ChatPage() {
setDeletePending(c.id)
}}
className="text-fl-label text-fl-error-fg hover:text-fl-error shrink-0 font-mono opacity-0 transition-all group-hover:opacity-100"
- title="Delete"
+ title={t('deleteConfirm')}
+ aria-label={t('deleteConfirm')}
>
✕
@@ -478,7 +490,7 @@ export default function ChatPage() {
{msg.role === 'assistant' ? (
)}
diff --git a/frontend/src/app/(app)/dashboard/page.tsx b/frontend/src/app/(app)/dashboard/page.tsx
index 4176f300a..8e2a80a9e 100644
--- a/frontend/src/app/(app)/dashboard/page.tsx
+++ b/frontend/src/app/(app)/dashboard/page.tsx
@@ -193,10 +193,8 @@ export default function DashboardPage() {
if (!res.ok) throw new Error(tBilling('portalError'))
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setPortalError(
- err instanceof Error ? err.message : tBilling('portalError')
- )
+ } catch {
+ setPortalError(tBilling('portalError'))
setPortalLoading(false)
}
}
diff --git a/frontend/src/app/(app)/feedback/page.tsx b/frontend/src/app/(app)/feedback/page.tsx
index 063ce5644..994fb2ed8 100644
--- a/frontend/src/app/(app)/feedback/page.tsx
+++ b/frontend/src/app/(app)/feedback/page.tsx
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { apiFetch } from '@/lib/api'
import { useAuthStore } from '@/store/auth'
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
@@ -59,8 +59,8 @@ const STATUS_STYLES: Record = {
// Helpers
// ---------------------------------------------------------------------------
-function formatDate(iso: string): string {
- return new Date(iso).toLocaleDateString(undefined, {
+function formatDate(iso: string, locale: string): string {
+ return new Date(iso).toLocaleDateString(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -258,6 +258,7 @@ function DetailView({
onEntryDeleted,
}: DetailViewProps) {
const t = useTranslations('feedback')
+ const locale = useLocale()
const [entry, setEntry] = useState(initialEntry)
const [comments, setComments] = useState([])
const [commentBody, setCommentBody] = useState('')
@@ -375,7 +376,7 @@ function DetailView({
{t('by')} {entry.author.display_name}
-
· {formatDate(entry.created_at)}
+
· {formatDate(entry.created_at, locale)}
{/* Vote button — only for features */}
{entry.type === 'feature' && (
@@ -416,7 +417,7 @@ function DetailView({
{c.author.display_name}
- · {formatDate(c.created_at)}
+ · {formatDate(c.created_at, locale)}
{currentUserId === c.author.id && (
s.user?.id)
const isAdmin = useAuthStore((s) => s.user?.role === 'admin')
@@ -732,6 +734,9 @@ export default function FeedbackPage() {
{entries.map((entry, i) => {
const canDelete = currentUserId === entry.author.id || isAdmin
+ const voteLabel = t(
+ entry.voted_by_me ? 'removeVoteAction' : 'voteAction'
+ )
return (
▲
@@ -806,7 +812,7 @@ export default function FeedbackPage() {
{t('by')} {entry.author.display_name}
-
· {formatDate(entry.created_at)}
+
· {formatDate(entry.created_at, locale)}
{entry.comment_count > 0 && (
diff --git a/frontend/src/app/(app)/flashcards/page.tsx b/frontend/src/app/(app)/flashcards/page.tsx
index 146589f7d..9cb9c5b53 100644
--- a/frontend/src/app/(app)/flashcards/page.tsx
+++ b/frontend/src/app/(app)/flashcards/page.tsx
@@ -276,7 +276,7 @@ export default function FlashcardsPage() {
<>
- {current + 1} / {cards.length} due
+ {current + 1} / {cards.length} {t('due')}
{/* Mode toggle */}
diff --git a/frontend/src/app/(app)/flashcards/vocabulary/page.tsx b/frontend/src/app/(app)/flashcards/vocabulary/page.tsx
index 85ead5653..548ef7a07 100644
--- a/frontend/src/app/(app)/flashcards/vocabulary/page.tsx
+++ b/frontend/src/app/(app)/flashcards/vocabulary/page.tsx
@@ -20,6 +20,7 @@ const LIMIT = 10
export default function VocabularyPage() {
const t = useTranslations('flashcards')
+ const tCommon = useTranslations('common')
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
@@ -146,7 +147,7 @@ export default function VocabularyPage() {
onClick={() => deleteItem(item.id)}
disabled={deletingId === item.id}
className="text-fl-muted-3 shrink-0 font-mono text-xs transition-colors hover:text-red-400 disabled:opacity-40"
- aria-label="Delete"
+ aria-label={tCommon('delete')}
>
✕
diff --git a/frontend/src/app/(app)/grammar/[slug]/page.tsx b/frontend/src/app/(app)/grammar/[slug]/page.tsx
index 65e50e332..c6ffbac2c 100644
--- a/frontend/src/app/(app)/grammar/[slug]/page.tsx
+++ b/frontend/src/app/(app)/grammar/[slug]/page.tsx
@@ -92,11 +92,11 @@ export default function GrammarDetailPage({
const t = useTranslations('grammar')
const tCommon = useTranslations('common')
const tNav = useTranslations('nav')
- const tTargetLang = useTranslations('targetLanguages')
+ const tLang = useTranslations('languages')
const activeLanguage = useLanguageStore((s) => s.activeLanguage)
const user = useAuthStore((s) => s.user)
const nativeLanguageName = user?.native_language
- ? tTargetLang(user.native_language)
+ ? tLang(user.native_language)
: ''
const { slug } = use(params)
diff --git a/frontend/src/app/(app)/grammar/page.tsx b/frontend/src/app/(app)/grammar/page.tsx
index 6c774d0be..1ec926f31 100644
--- a/frontend/src/app/(app)/grammar/page.tsx
+++ b/frontend/src/app/(app)/grammar/page.tsx
@@ -121,7 +121,7 @@ export default function GrammarIndexPage() {
diff --git a/frontend/src/app/(app)/layout.tsx b/frontend/src/app/(app)/layout.tsx
index 7dc83d92e..aa0d6a670 100644
--- a/frontend/src/app/(app)/layout.tsx
+++ b/frontend/src/app/(app)/layout.tsx
@@ -379,7 +379,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
- v1.9.20
+ v1.9.25
setContactOpen(true)}
@@ -405,7 +405,9 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
setMobileMenuOpen((o) => !o)}
className="text-fl-muted-2 hover:text-fl-fg p-1 font-mono transition-colors"
- aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
+ aria-label={
+ mobileMenuOpen ? tCommon('closeMenu') : tCommon('openMenu')
+ }
>
{mobileMenuOpen ? '✕' : '☰'}
@@ -573,7 +575,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
)}
- v1.9.20
+ v1.9.25
{
diff --git a/frontend/src/app/(app)/phrasebook/page.tsx b/frontend/src/app/(app)/phrasebook/page.tsx
index 7364d7ab4..fb331ead5 100644
--- a/frontend/src/app/(app)/phrasebook/page.tsx
+++ b/frontend/src/app/(app)/phrasebook/page.tsx
@@ -38,10 +38,10 @@ function CategoryCard({
}) {
const t = useTranslations('phrasebook')
const tCommon = useTranslations('common')
- const tTargetLang = useTranslations('targetLanguages')
+ const tLang = useTranslations('languages')
const user = useAuthStore((s) => s.user)
const nativeLanguageName = user?.native_language
- ? tTargetLang(user.native_language)
+ ? tLang(user.native_language)
: ''
const [nativeHelpOpen, setNativeHelpOpen] = useState(
cat.level === 'A1' || cat.level === 'A2'
@@ -280,6 +280,7 @@ function CategoryCard({
}
function CopyButton({ text }: { text: string }) {
+ const t = useTranslations('phrasebook')
const [copied, setCopied] = useState(false)
async function handleCopy() {
@@ -296,8 +297,8 @@ function CopyButton({ text }: { text: string }) {
{copied ? '\u2713' : '\u{1f4cb}'}
diff --git a/frontend/src/app/(app)/progress/page.tsx b/frontend/src/app/(app)/progress/page.tsx
index ab532057f..6fbcb34e4 100644
--- a/frontend/src/app/(app)/progress/page.tsx
+++ b/frontend/src/app/(app)/progress/page.tsx
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { PageLoading } from '@/components/ui/page-loading'
import { apiFetch } from '@/lib/api'
import { useLanguageStore } from '@/store/language'
@@ -156,6 +156,7 @@ function UnitCompetencyBlock({
// ── Page ──────────────────────────────────────────────────────────────────────
export default function ProgressPage() {
+ const locale = useLocale()
const t = useTranslations('progress')
const tVocab = useTranslations('vocabulary')
const activeLanguage = useLanguageStore((s) => s.activeLanguage)
@@ -259,8 +260,11 @@ export default function ProgressPage() {
{summary && (
{[
- { label: t('xp'), value: summary.total_xp.toLocaleString() },
- { label: t('streak'), value: `${summary.current_streak}d 🔥` },
+ {
+ label: t('xp'),
+ value: summary.total_xp.toLocaleString(locale),
+ },
+ { label: t('streak'), value: `${summary.current_streak} 🔥` },
{ label: t('lessons'), value: summary.total_lessons },
{
label: t('accuracy'),
diff --git a/frontend/src/app/(app)/settings/languages/page.tsx b/frontend/src/app/(app)/settings/languages/page.tsx
index 797410cba..6c7b27666 100644
--- a/frontend/src/app/(app)/settings/languages/page.tsx
+++ b/frontend/src/app/(app)/settings/languages/page.tsx
@@ -4,7 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { useLanguageStore } from '@/store/language'
import {
getLanguageByCode,
@@ -16,6 +16,7 @@ import { PageLoading } from '@/components/ui/page-loading'
import type { UserLanguageInfo } from '@/store/language'
export default function MyLanguagesPage() {
+ const locale = useLocale()
const t = useTranslations('languages')
const tTarget = useTranslations('targetLanguages')
const tSettings = useTranslations('settings')
@@ -207,7 +208,8 @@ export default function MyLanguagesPage() {
{progress && (
<>
- {t('xpLabel')}: {progress.total_xp.toLocaleString()}
+ {t('xpLabel')}:{' '}
+ {progress.total_xp.toLocaleString(locale)}
{t('streakLabel')}: {progress.current_streak}d
diff --git a/frontend/src/app/(app)/vocabulary/[setId]/page.tsx b/frontend/src/app/(app)/vocabulary/[setId]/page.tsx
index e13d141ca..6a90fd8ed 100644
--- a/frontend/src/app/(app)/vocabulary/[setId]/page.tsx
+++ b/frontend/src/app/(app)/vocabulary/[setId]/page.tsx
@@ -32,7 +32,7 @@ export default function VocabularySetPage({
const router = useRouter()
const t = useTranslations('vocabulary')
const tCommon = useTranslations('common')
- const tTargetLang = useTranslations('targetLanguages')
+ const tLang = useTranslations('languages')
const activeLanguage = useLanguageStore((s) => s.activeLanguage)
const user = useAuthStore((s) => s.user)
const [vocabSet, setVocabSet] = useState(null)
@@ -47,7 +47,7 @@ export default function VocabularySetPage({
const [loadingNativeHelp, setLoadingNativeHelp] = useState(false)
const [nativeHelpError, setNativeHelpError] = useState(false)
const nativeLanguageName = user?.native_language
- ? tTargetLang(user.native_language)
+ ? tLang(user.native_language)
: ''
useEffect(() => {
@@ -115,7 +115,9 @@ export default function VocabularySetPage({
if (!res.ok) {
const d = await res.json().catch(() => ({}))
throw new Error(
- (d as { detail?: string }).detail ?? `Error ${res.status}`
+ (d as { detail?: string }).detail === 'No active study plan found'
+ ? 'No active study plan found'
+ : t('addFailed')
)
}
const data = (await res.json()) as { created: number }
@@ -125,7 +127,7 @@ export default function VocabularySetPage({
setError(
msg === 'No active study plan found'
? tCommon('noActivePlan')
- : msg || 'Failed to add flashcards.'
+ : t('addFailed')
)
} finally {
setAdding(false)
diff --git a/frontend/src/app/(app)/vocabulary/page.tsx b/frontend/src/app/(app)/vocabulary/page.tsx
index 8da9aa33c..ef98ebe9e 100644
--- a/frontend/src/app/(app)/vocabulary/page.tsx
+++ b/frontend/src/app/(app)/vocabulary/page.tsx
@@ -83,7 +83,7 @@ export default function VocabularyIndexPage() {
- {vocabSets.length} {t('sets')} · {totalWords} {t('words')}
+ {t('sets', { count: vocabSets.length })} · {totalWords} {t('words')}
{usedLevels.length > 0 && (
<>
{' · '}
@@ -142,7 +142,7 @@ export default function VocabularyIndexPage() {
- {sets.length} set{sets.length !== 1 ? 's' : ''} ·{' '}
+ {t('sets', { count: sets.length })} ·{' '}
{sets.reduce((a, s) => a + s.words.length, 0)} {t('words')}
diff --git a/frontend/src/app/(auth)/forgot-password/page.tsx b/frontend/src/app/(auth)/forgot-password/page.tsx
index ebe3b51ee..7f4594949 100644
--- a/frontend/src/app/(auth)/forgot-password/page.tsx
+++ b/frontend/src/app/(auth)/forgot-password/page.tsx
@@ -26,8 +26,8 @@ export default function ForgotPasswordPage() {
})
if (!res.ok) throw new Error(t('error'))
setSent(true)
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('error'))
+ } catch {
+ setError(t('error'))
} finally {
setLoading(false)
}
diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx
index b092a14c2..5b8336632 100644
--- a/frontend/src/app/(auth)/login/page.tsx
+++ b/frontend/src/app/(auth)/login/page.tsx
@@ -71,7 +71,12 @@ function LoginForm() {
if (meRes.ok) setUser(mapUser(await meRes.json()))
router.push('/dashboard')
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('loginFailed'))
+ setError(
+ err instanceof Error &&
+ (err.message === t('error') || err.message === t('invalidEmail'))
+ ? err.message
+ : t('loginFailed')
+ )
} finally {
setLoading(false)
}
diff --git a/frontend/src/app/(auth)/onboarding/page.tsx b/frontend/src/app/(auth)/onboarding/page.tsx
index ba841744a..aa85a0ac2 100644
--- a/frontend/src/app/(auth)/onboarding/page.tsx
+++ b/frontend/src/app/(auth)/onboarding/page.tsx
@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Image from 'next/image'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { Loader2 } from 'lucide-react'
import { apiFetch } from '@/lib/api'
import { splitYearlyCta, type BillingInterval } from '@/lib/billing-copy'
@@ -38,6 +38,7 @@ export default function OnboardingPage() {
const t = useTranslations('onboarding')
const tCommon = useTranslations('common')
const tLang = useTranslations('targetLanguages')
+ const locale = useLocale()
const router = useRouter()
const searchParams = useSearchParams()
const setUser = useAuthStore((s) => s.setUser)
@@ -125,8 +126,8 @@ export default function OnboardingPage() {
} else {
router.push('/dashboard')
}
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('saveFailed'))
+ } catch {
+ setError(t('saveFailed'))
} finally {
setLoading(false)
}
@@ -155,13 +156,12 @@ export default function OnboardingPage() {
body: JSON.stringify({ plan: interval }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail ?? t('trialError'))
+ throw new Error(t('trialError'))
}
const { url } = await res.json()
window.location.assign(url)
- } catch (err: unknown) {
- setCheckoutError(err instanceof Error ? err.message : t('trialError'))
+ } catch {
+ setCheckoutError(t('trialError'))
setCheckoutLoading(null)
}
}
@@ -171,7 +171,7 @@ export default function OnboardingPage() {
const trialEligible = !user?.trial_used
const yearlyCta = splitYearlyCta(
t(trialEligible ? 'trialCtaYearly' : 'trialCtaYearlyTrialUsed', {
- price: String(priceYearly),
+ price: new Intl.NumberFormat(locale).format(priceYearly),
})
)
@@ -395,7 +395,9 @@ export default function OnboardingPage() {
? 'trialCtaMonthly'
: 'trialCtaMonthlyTrialUsed',
{
- price: String(priceMonthly),
+ price: new Intl.NumberFormat(locale).format(
+ priceMonthly
+ ),
}
)
) : (
diff --git a/frontend/src/app/(auth)/register/page.tsx b/frontend/src/app/(auth)/register/page.tsx
index 82c9a178f..23991ef39 100644
--- a/frontend/src/app/(auth)/register/page.tsx
+++ b/frontend/src/app/(auth)/register/page.tsx
@@ -22,6 +22,11 @@ const LANGUAGES = [
'nl',
'ro',
'ru',
+ 'tr',
+ 'sv',
+ 'da',
+ 'fi',
+ 'hr',
] as const
type SelectedPlan = 'monthly' | 'yearly'
@@ -123,7 +128,20 @@ function RegisterForm() {
selectedPlan ? `/onboarding?plan=${selectedPlan}` : '/onboarding'
)
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('error'))
+ const knownErrors = [
+ t('error'),
+ t('usernameTaken'),
+ t('emailTaken'),
+ t('registrationClosed'),
+ t('invalidInvite'),
+ t('invalidEmail'),
+ t('invalidPassword'),
+ ]
+ setError(
+ err instanceof Error && knownErrors.includes(err.message)
+ ? err.message
+ : t('error')
+ )
} finally {
setLoading(false)
}
diff --git a/frontend/src/app/(auth)/reset-password/page.tsx b/frontend/src/app/(auth)/reset-password/page.tsx
index c53306f60..22d46023c 100644
--- a/frontend/src/app/(auth)/reset-password/page.tsx
+++ b/frontend/src/app/(auth)/reset-password/page.tsx
@@ -9,6 +9,8 @@ import { apiFetch } from '@/lib/api'
function ResetPasswordContent() {
const t = useTranslations('auth.resetPassword')
+ const tCommon = useTranslations('common')
+ const tRegister = useTranslations('auth.register')
const router = useRouter()
const searchParams = useSearchParams()
const token = searchParams.get('token') ?? ''
@@ -25,10 +27,15 @@ function ResetPasswordContent() {
setError(t('mismatch'))
return
}
- if (password.length < 8) {
+ const passwordLength = [...password].length
+ if (passwordLength < 10) {
setError(t('tooShort'))
return
}
+ if (passwordLength > 25) {
+ setError(tRegister('invalidPassword'))
+ return
+ }
setLoading(true)
try {
const res = await apiFetch('/api/auth/reset-password', {
@@ -37,13 +44,24 @@ function ResetPasswordContent() {
body: JSON.stringify({ token, new_password: password }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail || t('error'))
+ throw new Error(
+ res.status === 400 || res.status === 404
+ ? t('error')
+ : res.status === 422
+ ? tRegister('invalidPassword')
+ : tCommon('errorMessage')
+ )
}
setDone(true)
setTimeout(() => router.push('/login'), 2000)
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : t('error'))
+ setError(
+ err instanceof Error &&
+ (err.message === t('error') ||
+ err.message === tRegister('invalidPassword'))
+ ? err.message
+ : tCommon('errorMessage')
+ )
} finally {
setLoading(false)
}
@@ -78,7 +96,10 @@ function ResetPasswordContent() {
)}
{error && (
-
+
✕ {error}
)}
@@ -89,8 +110,15 @@ function ResetPasswordContent() {
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
+ aria-describedby="reset-password-requirements"
className="bg-fl-bg border-fl-border text-fl-fg placeholder:text-fl-muted-4 focus:border-fl-border-2 w-full border px-4 py-3 font-mono text-xs transition-colors focus:outline-none"
/>
+
+ {tRegister('invalidPassword')}
+
{process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
diff --git a/frontend/src/components/LanguageBubbles.tsx b/frontend/src/components/LanguageBubbles.tsx
index 0bb3b83b5..0a1b02064 100644
--- a/frontend/src/components/LanguageBubbles.tsx
+++ b/frontend/src/components/LanguageBubbles.tsx
@@ -14,6 +14,7 @@ function circlePosition(index: number, total: number, radius: number) {
export function LanguageBubbles() {
const t = useTranslations('landing')
+ const tTarget = useTranslations('targetLanguages')
const positions = useMemo(
() =>
SUPPORTED_TARGET_LANGUAGES.map((_, i) =>
@@ -54,7 +55,7 @@ export function LanguageBubbles() {
s.user)
const stripeEnabled = useConfigStore((s) => s.stripeEnabled)
@@ -66,7 +67,9 @@ export function PaywallBanner({
const [error, setError] = useState(null)
const paymentRecovery = needsPaymentRecovery(user)
const yearlyCta = splitYearlyCta(
- t('planYearly', { price: String(priceYearly) })
+ t('planYearly', {
+ price: new Intl.NumberFormat(locale).format(priceYearly),
+ })
)
if (!stripeEnabled || isSubscribed(user, stripeEnabled)) return null
@@ -85,13 +88,12 @@ export function PaywallBanner({
body: JSON.stringify({ plan: interval }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail ?? t('checkoutError'))
+ throw new Error(t('checkoutError'))
}
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setError(err instanceof Error ? err.message : t('checkoutError'))
+ } catch {
+ setError(t('checkoutError'))
setLoading(null)
}
}
@@ -104,8 +106,8 @@ export function PaywallBanner({
if (!res.ok) throw new Error(t('portalError'))
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setError(err instanceof Error ? err.message : t('portalError'))
+ } catch {
+ setError(t('portalError'))
setPortalLoading(false)
}
}
@@ -140,11 +142,11 @@ export function PaywallBanner({
{t('paywallLabel')}
- {t(paymentRecovery ? 'premiumBannerPastDueTitle' : context.title)}
+ {t(paymentRecovery ? 'pastDueTitle' : context.title)}
{paymentRecovery
- ? t('premiumBannerPastDueDesc')
+ ? t('pastDueDesc')
: t(
context.desc ??
(trialEligible ? 'paywallDesc' : 'paywallDescTrialUsed'),
@@ -187,7 +189,9 @@ export function PaywallBanner({
>
{loading === 'monthly'
? '...'
- : t('planMonthly', { price: String(priceMonthly) })}
+ : t('planMonthly', {
+ price: new Intl.NumberFormat(locale).format(priceMonthly),
+ })}
)}
diff --git a/frontend/src/components/billing/PricingSection.tsx b/frontend/src/components/billing/PricingSection.tsx
index 8f4c7a7da..82ad43705 100644
--- a/frontend/src/components/billing/PricingSection.tsx
+++ b/frontend/src/components/billing/PricingSection.tsx
@@ -2,7 +2,7 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { Circle, CircleDot, Diamond, Check, Minus } from 'lucide-react'
import { PageLoading } from '@/components/ui/page-loading'
import { getLandingSubscriptionState } from '@/lib/landing-subscription'
@@ -34,6 +34,7 @@ export default function PricingSection({
}: PricingSectionProps) {
const tBilling = useTranslations('billing')
const tLanding = useTranslations('landing')
+ const locale = useLocale()
const [subscribed, setSubscribed] = useState
(
hasSession ? null : false
)
@@ -87,15 +88,12 @@ export default function PricingSection({
body: JSON.stringify({ plan }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail ?? tBilling('checkoutError'))
+ throw new Error(tBilling('checkoutError'))
}
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setCheckoutError(
- err instanceof Error ? err.message : tBilling('checkoutError')
- )
+ } catch {
+ setCheckoutError(tBilling('checkoutError'))
setCheckoutLoading(null)
}
}
@@ -223,12 +221,18 @@ export default function PricingSection({
<>
{tBilling('priceOriginal', {
- price: plan.originalPrice,
+ price: new Intl.NumberFormat(locale).format(
+ plan.originalPrice
+ ),
period: plan.priceLabel,
})}
- {tBilling('priceAmount', { amount: plan.price })}
+ {tBilling('priceAmount', {
+ amount: new Intl.NumberFormat(locale).format(
+ plan.price
+ ),
+ })}
{tBilling('pricePerPeriod', {
period: plan.priceLabel,
diff --git a/frontend/src/components/billing/SubscriptionPlanButtons.tsx b/frontend/src/components/billing/SubscriptionPlanButtons.tsx
index 7b189a62e..039c573c1 100644
--- a/frontend/src/components/billing/SubscriptionPlanButtons.tsx
+++ b/frontend/src/components/billing/SubscriptionPlanButtons.tsx
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { apiFetch } from '@/lib/api'
import { splitYearlyCta, type BillingInterval } from '@/lib/billing-copy'
import { useConfigStore } from '@/store/config'
@@ -14,12 +14,15 @@ export function SubscriptionPlanButtons({
className = '',
}: SubscriptionPlanButtonsProps) {
const tBilling = useTranslations('billing')
+ const locale = useLocale()
const priceMonthly = useConfigStore((s) => s.priceMonthly)
const priceYearly = useConfigStore((s) => s.priceYearly)
const [loading, setLoading] = useState(null)
const [error, setError] = useState(null)
const yearlyCta = splitYearlyCta(
- tBilling('planYearly', { price: String(priceYearly) })
+ tBilling('planYearly', {
+ price: new Intl.NumberFormat(locale).format(priceYearly),
+ })
)
async function startCheckout(plan: BillingInterval) {
@@ -33,13 +36,12 @@ export function SubscriptionPlanButtons({
body: JSON.stringify({ plan }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail ?? tBilling('checkoutError'))
+ throw new Error(tBilling('checkoutError'))
}
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setError(err instanceof Error ? err.message : tBilling('checkoutError'))
+ } catch {
+ setError(tBilling('checkoutError'))
setLoading(null)
}
}
@@ -74,7 +76,9 @@ export function SubscriptionPlanButtons({
>
{loading === 'monthly'
? '...'
- : tBilling('planMonthly', { price: String(priceMonthly) })}
+ : tBilling('planMonthly', {
+ price: new Intl.NumberFormat(locale).format(priceMonthly),
+ })}
{error &&
{error}
}
diff --git a/frontend/src/components/conversation/ConversationMode.tsx b/frontend/src/components/conversation/ConversationMode.tsx
index ec42a2cc1..d7882ba65 100644
--- a/frontend/src/components/conversation/ConversationMode.tsx
+++ b/frontend/src/components/conversation/ConversationMode.tsx
@@ -3,7 +3,7 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react'
import { useMicVAD } from '@ricky0123/vad-react'
import { useRouter } from 'next/navigation'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { useAuthStore } from '@/store/auth'
import { resolveVadRedemptionMs } from '@/lib/conversation-vad'
import { useConfigStore } from '@/store/config'
@@ -162,13 +162,16 @@ function QuotaPill({
function TrialPremiumCta() {
const t = useTranslations('billing')
const tConversation = useTranslations('conversation')
+ const locale = useLocale()
const router = useRouter()
const priceMonthly = useConfigStore((s) => s.priceMonthly)
const priceYearly = useConfigStore((s) => s.priceYearly)
const [loading, setLoading] = useState
(null)
const [error, setError] = useState(null)
const yearlyCta = splitYearlyCta(
- t('planYearly', { price: String(priceYearly) })
+ t('planYearly', {
+ price: new Intl.NumberFormat(locale).format(priceYearly),
+ })
)
async function handleCheckout(interval: BillingInterval) {
@@ -181,13 +184,12 @@ function TrialPremiumCta() {
body: JSON.stringify({ plan: interval }),
})
if (!res.ok) {
- const data = await res.json().catch(() => ({}))
- throw new Error(data.detail ?? t('checkoutError'))
+ throw new Error(t('checkoutError'))
}
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setError(err instanceof Error ? err.message : t('checkoutError'))
+ } catch {
+ setError(t('checkoutError'))
setLoading(null)
}
}
@@ -229,7 +231,9 @@ function TrialPremiumCta() {
>
{loading === 'monthly'
? '...'
- : t('planMonthly', { price: String(priceMonthly) })}
+ : t('planMonthly', {
+ price: new Intl.NumberFormat(locale).format(priceMonthly),
+ })}
{error && (
@@ -841,24 +845,23 @@ export default function ConversationMode({
if (trialMode) void refreshCurrentUser()
break
- case 'error':
+ case 'error': {
convLogger.error('ws error message', {
code: msg.code,
message: msg.message,
})
- setErrorMsg(
- msg.code === 'services_disabled'
- ? t('errorServicesDisabled')
- : msg.code === 'quota_exceeded_sessions'
- ? t('quotaExceededSessions')
- : msg.code === 'quota_exceeded_time'
- ? t('quotaExceededTime')
- : msg.code === 'quota_exceeded_tokens'
- ? t('quotaExceededTokens')
- : msg.code === 'no_active_plan'
- ? tCommon('noActivePlan')
- : (msg.message ?? t('errorConnection'))
- )
+ const errorMessages: Record = {
+ auth_failed: t('errorUnauthorized'),
+ services_disabled: t('errorServicesDisabled'),
+ quota_exceeded_sessions: t('quotaExceededSessions'),
+ quota_exceeded_time: t('quotaExceededTime'),
+ quota_exceeded_tokens: t('quotaExceededTokens'),
+ no_active_plan: tCommon('noActivePlan'),
+ stt_failed: t('errorTranscription'),
+ llm_failed: t('errorResponse'),
+ tts_failed: t('errorSpeech'),
+ }
+ setErrorMsg(errorMessages[msg.code] ?? tCommon('errorMessage'))
setStatus('error')
assistantTurnActiveRef.current = false
if (
@@ -874,6 +877,7 @@ export default function ConversationMode({
finalizeSession()
}
break
+ }
case 'memory_updated':
showMemoryToast()
@@ -888,7 +892,7 @@ export default function ConversationMode({
if (!isCurrent()) return
if (!cleanEndRef.current) {
convLogger.error('ws onerror')
- setErrorMsg(`${t('errorConnection')} [onerror → ${url}]`)
+ setErrorMsg(t('errorConnection'))
setStatus('error')
finalizeSession()
}
@@ -901,9 +905,7 @@ export default function ConversationMode({
if (ev.code === 1008) {
setErrorMsg(t('errorUnauthorized'))
} else if (ev.code !== 1000) {
- setErrorMsg(
- `${t('errorConnection')} [code ${ev.code}: ${ev.reason || 'no reason'} → ${url}]`
- )
+ setErrorMsg(t('errorConnection'))
}
setStatus('error')
finalizeSession()
@@ -911,9 +913,9 @@ export default function ConversationMode({
cleanEndRef.current = false
}
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
[
t,
+ tCommon,
targetLanguage,
voiceTrialToken,
trialMode,
diff --git a/frontend/src/components/conversation/TranscriptBubble.tsx b/frontend/src/components/conversation/TranscriptBubble.tsx
index 5be928897..c7a6389e6 100644
--- a/frontend/src/components/conversation/TranscriptBubble.tsx
+++ b/frontend/src/components/conversation/TranscriptBubble.tsx
@@ -44,7 +44,7 @@ export default function TranscriptBubble({
{!isUser ? (
- ADMIN
+ {t('roleAdmin')}
)
}
diff --git a/frontend/src/components/reviews/ReviewPrompt.tsx b/frontend/src/components/reviews/ReviewPrompt.tsx
index cfbab19c7..9bf31817b 100644
--- a/frontend/src/components/reviews/ReviewPrompt.tsx
+++ b/frontend/src/components/reviews/ReviewPrompt.tsx
@@ -60,6 +60,7 @@ export function ReviewForm({
onSubmit,
}: ReviewFormProps) {
const t = useTranslations('reviewPrompt')
+ const tStars = useTranslations('landingReviews')
const [rating, setRating] = useState(initialReview?.rating ?? 0)
const [comment, setComment] = useState(initialReview?.comment ?? '')
const [submitting, setSubmitting] = useState(false)
@@ -104,7 +105,7 @@ export function ReviewForm({
type="button"
role="radio"
aria-checked={rating === value}
- aria-label={`${value} ${t(value === 1 ? 'star' : 'stars')}`}
+ aria-label={tStars('starsLabel', { rating: value })}
onClick={() => setRating(value)}
className={`border px-3 py-2 transition-colors ${
value <= rating
@@ -169,13 +170,17 @@ export function ReviewPrompt({
onSubmitted,
}: ReviewPromptProps) {
const t = useTranslations('reviewPrompt')
- const [checking, setChecking] = useState(false)
+ const [checking, setChecking] = useState(true)
const [hasReview, setHasReview] = useState(false)
const [error, setError] = useState('')
const [statusCheckFailed, setStatusCheckFailed] = useState(false)
useEffect(() => {
- if (!open) return
+ if (!open) {
+ setChecking(true)
+ setHasReview(false)
+ return
+ }
let cancelled = false
setChecking(true)
setError('')
diff --git a/frontend/src/components/settings/BillingSection.tsx b/frontend/src/components/settings/BillingSection.tsx
index e5fd4f09a..2844329d1 100644
--- a/frontend/src/components/settings/BillingSection.tsx
+++ b/frontend/src/components/settings/BillingSection.tsx
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
-import { useTranslations } from 'next-intl'
+import { useLocale, useTranslations } from 'next-intl'
import { apiFetch } from '@/lib/api'
import { useAuthStore, isSubscribed, needsPaymentRecovery } from '@/store/auth'
import { useConfigStore } from '@/store/config'
@@ -9,6 +9,7 @@ import { SubscriptionPlanButtons } from '@/components/billing/SubscriptionPlanBu
export function BillingSection() {
const tBilling = useTranslations('billing')
+ const locale = useLocale()
const user = useAuthStore((s) => s.user)
const stripeEnabled = useConfigStore((s) => s.stripeEnabled)
const [portalLoading, setPortalLoading] = useState(false)
@@ -26,10 +27,8 @@ export function BillingSection() {
if (!res.ok) throw new Error(tBilling('portalError'))
const { url } = await res.json()
window.location.assign(url)
- } catch (err) {
- setPortalError(
- err instanceof Error ? err.message : tBilling('portalError')
- )
+ } catch {
+ setPortalError(tBilling('portalError'))
setPortalLoading(false)
}
}
@@ -105,7 +104,7 @@ export function BillingSection() {
: tBilling('nextBilling')}
- {new Date(user.subscription_ends_at).toLocaleDateString()}
+ {new Date(user.subscription_ends_at).toLocaleDateString(locale)}
)}
diff --git a/frontend/src/components/settings/ConversationSection.tsx b/frontend/src/components/settings/ConversationSection.tsx
index da8021062..42b943f88 100644
--- a/frontend/src/components/settings/ConversationSection.tsx
+++ b/frontend/src/components/settings/ConversationSection.tsx
@@ -50,10 +50,10 @@ export function ConversationSection({ title }: { title?: string } = {}) {
const updated = await res.json()
setUser(mapUser(updated, user))
setConvMessage({ type: 'ok', text: t('conversationSaved') })
- } catch (err: unknown) {
+ } catch {
setConvMessage({
type: 'err',
- text: err instanceof Error ? err.message : t('saveFailed'),
+ text: t('saveFailed'),
})
} finally {
setSavingConv(false)
diff --git a/frontend/src/components/settings/ProfileSection.tsx b/frontend/src/components/settings/ProfileSection.tsx
index c3b850972..85b400488 100644
--- a/frontend/src/components/settings/ProfileSection.tsx
+++ b/frontend/src/components/settings/ProfileSection.tsx
@@ -20,6 +20,11 @@ const LANGUAGES = [
'nl',
'ro',
'ru',
+ 'tr',
+ 'sv',
+ 'da',
+ 'fi',
+ 'hr',
] as const
function resizeImage(file: File, maxPx: number): Promise {
@@ -163,16 +168,16 @@ export function ProfileSection({ title }: { title?: string } = {}) {
setPassword('')
setConfirmPassword('')
- if (uiLocale !== user?.ui_locale) {
- const opts = `path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax${window.location.protocol === 'https:' ? '; Secure' : ''}`
- document.cookie = `NEXT_LOCALE=${uiLocale}; ${opts}`
- document.cookie = `LOCALE_DETECTED=1; ${opts}`
+ const opts = `path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax${window.location.protocol === 'https:' ? '; Secure' : ''}`
+ document.cookie = `NEXT_LOCALE=${uiLocale}; ${opts}`
+ document.cookie = `LOCALE_DETECTED=1; ${opts}`
+ if (uiLocale !== currentLocale) {
window.location.reload()
}
- } catch (err: unknown) {
+ } catch {
setMessage({
type: 'err',
- text: err instanceof Error ? err.message : t('saveFailed'),
+ text: t('saveFailed'),
})
} finally {
setSaving(false)
diff --git a/frontend/src/components/ui/WordTooltip.tsx b/frontend/src/components/ui/WordTooltip.tsx
index 8990fc84b..6d5d32f83 100644
--- a/frontend/src/components/ui/WordTooltip.tsx
+++ b/frontend/src/components/ui/WordTooltip.tsx
@@ -1,6 +1,7 @@
'use client'
import { useState, useCallback, useEffect, useRef } from 'react'
+import { useTranslations } from 'next-intl'
import { apiFetch } from '@/lib/api'
// ---------------------------------------------------------------------------
@@ -38,6 +39,7 @@ export function WordTooltip({
wordSaveError: string
}
}) {
+ const tCommon = useTranslations('common')
return (
✕
diff --git a/frontend/src/components/ui/landing-nav.tsx b/frontend/src/components/ui/landing-nav.tsx
index 66a076237..4100dce4b 100644
--- a/frontend/src/components/ui/landing-nav.tsx
+++ b/frontend/src/components/ui/landing-nav.tsx
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { Menu, X } from 'lucide-react'
+import { useTranslations } from 'next-intl'
import { hasActiveLandingSubscription } from '@/lib/landing-subscription'
interface LandingNavProps {
@@ -29,6 +30,7 @@ export function LandingNav({
signIn,
dashboard,
}: LandingNavProps) {
+ const tCommon = useTranslations('common')
const [open, setOpen] = useState(false)
const [showPricing, setShowPricing] = useState(stripeEnabled && !hasSession)
@@ -118,7 +120,7 @@ export function LandingNav({
setOpen(!open)}
className="text-fl-muted-2 hover:text-fl-fg md:hidden"
- aria-label="Toggle menu"
+ aria-label={open ? tCommon('closeMenu') : tCommon('openMenu')}
>
{open ? : }
diff --git a/frontend/src/components/ui/loading-bar.tsx b/frontend/src/components/ui/loading-bar.tsx
index 4790acdb0..b0e407c68 100644
--- a/frontend/src/components/ui/loading-bar.tsx
+++ b/frontend/src/components/ui/loading-bar.tsx
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useRef, useState } from 'react'
+import { useTranslations } from 'next-intl'
import { useLoadingStore } from '@/store/loading'
const MIN_VISIBLE_MS = 600
@@ -9,6 +10,7 @@ const COMPLETE_DURATION_MS = 400
type Phase = 'hidden' | 'loading' | 'completing'
export function LoadingBar() {
+ const tCommon = useTranslations('common')
const count = useLoadingStore((s) => s.count)
const complete = useLoadingStore((s) => s.complete)
const finishComplete = useLoadingStore((s) => s.finishComplete)
@@ -46,7 +48,7 @@ export function LoadingBar() {
@@ -46,6 +47,7 @@ function SheetContent({
side?: 'top' | 'right' | 'bottom' | 'left'
showCloseButton?: boolean
}) {
+ const tCommon = useTranslations('common')
return (
@@ -71,7 +73,7 @@ function SheetContent({
}
>
- Close
+ {tCommon('close')}
)}
diff --git a/frontend/src/components/whats-new/WhatsNew.tsx b/frontend/src/components/whats-new/WhatsNew.tsx
index d0cd8b146..249c3b422 100644
--- a/frontend/src/components/whats-new/WhatsNew.tsx
+++ b/frontend/src/components/whats-new/WhatsNew.tsx
@@ -5,7 +5,7 @@ import Image from 'next/image'
import { useTranslations, useMessages } from 'next-intl'
import { CircleDot } from 'lucide-react'
-const WHATS_NEW_VERSION = 'v1.9.20'
+const WHATS_NEW_VERSION = 'v1.9.25'
const STORAGE_KEY = `fl_whats_new_seen_${WHATS_NEW_VERSION}`
const TOUR_KEY = 'fl_tour_done'
diff --git a/frontend/src/lib/locales.ts b/frontend/src/lib/locales.ts
index d5822cb9c..88e673ff9 100644
--- a/frontend/src/lib/locales.ts
+++ b/frontend/src/lib/locales.ts
@@ -13,6 +13,11 @@ export const SUPPORTED_LOCALES = [
'nl',
'ro',
'ru',
+ 'tr',
+ 'sv',
+ 'da',
+ 'fi',
+ 'hr',
] as const
export type Locale = (typeof SUPPORTED_LOCALES)[number]
diff --git a/frontend/src/lib/target-languages.ts b/frontend/src/lib/target-languages.ts
index 038523a39..397952676 100644
--- a/frontend/src/lib/target-languages.ts
+++ b/frontend/src/lib/target-languages.ts
@@ -172,7 +172,7 @@ export function getTargetLanguageTextClass(code: string): string {
export const DEFAULT_TARGET_LANGUAGE = 'en-GB'
-const LOCALES_CAPITALIZE_LANGUAGE = new Set(['en', 'de', 'nl'])
+const LOCALES_CAPITALIZE_LANGUAGE = new Set(['en', 'de', 'nl', 'tr'])
export function formatLanguageName(name: string, locale: string): string {
const lang = locale.split('-')[0]
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
index bd6b485f6..8c5ad5a2c 100644
--- a/frontend/src/middleware.ts
+++ b/frontend/src/middleware.ts
@@ -43,6 +43,7 @@ function detectLocale(req: NextRequest): Locale {
const lang = langTag.trim().split(/[-_]/)[0].toLowerCase()
return { lang, q: isNaN(q) ? 0 : q }
})
+ .filter(({ q }) => q > 0 && q <= 1)
.sort((a, b) => b.q - a.q)
for (const { lang } of parsed) {
diff --git a/frontend/tests/app/admin-overview.test.tsx b/frontend/tests/app/admin-overview.test.tsx
index c470cb15e..2a26ff898 100644
--- a/frontend/tests/app/admin-overview.test.tsx
+++ b/frontend/tests/app/admin-overview.test.tsx
@@ -2,8 +2,10 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import React from 'react'
+const translate = vi.hoisted(() => (key: string) => key)
+
vi.mock('next-intl', () => ({
- useTranslations: () => (key: string) => key,
+ useTranslations: () => translate,
}))
vi.mock('next/link', () => ({
@@ -60,10 +62,8 @@ describe('AdminOverviewPage', () => {
render( )
- await waitFor(() =>
- expect(mockApiFetch).toHaveBeenCalledWith('/api/admin/stats')
- )
- expect(screen.getByText('12')).toBeDefined()
+ await waitFor(() => expect(screen.getByText('12')).toBeDefined())
+ expect(mockApiFetch).toHaveBeenCalledWith('/api/admin/stats')
expect(screen.getByText('10')).toBeDefined()
expect(screen.getByText('4 / 3')).toBeDefined()
expect(screen.getByText('5')).toBeDefined()
diff --git a/frontend/tests/app/admin-query-params.test.tsx b/frontend/tests/app/admin-query-params.test.tsx
index 14a262367..9416942b1 100644
--- a/frontend/tests/app/admin-query-params.test.tsx
+++ b/frontend/tests/app/admin-query-params.test.tsx
@@ -9,6 +9,7 @@ const { mockApiFetch, mockSearchParams, navigationState } = vi.hoisted(() => ({
}))
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => (key: string) => key,
}))
diff --git a/frontend/tests/app/admin-reviews.test.tsx b/frontend/tests/app/admin-reviews.test.tsx
index faf5a6061..1b7f099c6 100644
--- a/frontend/tests/app/admin-reviews.test.tsx
+++ b/frontend/tests/app/admin-reviews.test.tsx
@@ -4,10 +4,15 @@ import React from 'react'
import AdminReviewsPage from '@/app/(app)/admin/reviews/page'
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations:
- () => (key: string, values?: Record) => {
+ (namespace: string) => (key: string, values?: Record) => {
+ if (namespace === 'targetLanguages' && key === 'de-DE') return 'German'
if (key === 'learningLanguage') return `Learning ${values?.language}`
- if (key === 'starsLabel') return `${values?.rating} stars`
+ if (key === 'starsLabel')
+ return namespace === 'landingReviews'
+ ? `${values?.rating} out of 5 stars`
+ : `${values?.rating} stars`
if (key === 'total') return `${values?.total} total`
const labels: Record = {
approve: 'Approve',
@@ -71,6 +76,7 @@ describe('AdminReviewsPage', () => {
expect(await screen.findByText('Reviewer')).toBeDefined()
expect(screen.getByText('Strong experience')).toBeDefined()
expect(screen.getByText(/Learning German/)).toBeDefined()
+ expect(screen.getByLabelText('5 out of 5 stars')).toBeInTheDocument()
})
it('calls approve action', async () => {
diff --git a/frontend/tests/app/admin-system-banner.test.tsx b/frontend/tests/app/admin-system-banner.test.tsx
index 9c7a4e272..e9134b465 100644
--- a/frontend/tests/app/admin-system-banner.test.tsx
+++ b/frontend/tests/app/admin-system-banner.test.tsx
@@ -28,7 +28,7 @@ vi.mock('next/navigation', () => ({
usePathname: () => '/admin/system',
}))
-const locales = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ru', 'nl', 'pl', 'ro']
+const locales = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ru', 'nl', 'pl', 'ro', 'tr', 'sv', 'da', 'fi', 'hr']
const generatedTranslations = Object.fromEntries(
locales.map((locale) => [
locale,
@@ -114,9 +114,167 @@ describe('Admin system dashboard banner', () => {
expect(JSON.parse(saveOptions.body).translations.es.title).toBe(
'Título corregido'
)
+ expect(JSON.parse(saveOptions.body).translations.tr.title).toBe('tr title')
+ expect(JSON.parse(saveOptions.body).translations.sv.title).toBe('sv title')
+ expect(JSON.parse(saveOptions.body).translations.da.title).toBe('da title')
+ expect(JSON.parse(saveOptions.body).translations.fi.title).toBe('fi title')
+ expect(JSON.parse(saveOptions.body).translations.hr.title).toBe('hr title')
expect(await screen.findByText('dashboardBanner.saveSuccess')).toBeVisible()
})
+ it('uses the selected translation as source when changing the source locale', async () => {
+ mockApiFetch.mockReset()
+ mockApiFetch
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ source_locale: 'en',
+ is_active: true,
+ revision: 1,
+ translations: generatedTranslations,
+ updated_at: '2026-08-06T10:00:00Z',
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
+ )
+ )
+ .mockResolvedValueOnce(
+ new Response(JSON.stringify({ translations: generatedTranslations }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ )
+
+ render( )
+ expect(await screen.findByText('15/15 complete')).toBeInTheDocument()
+ expect(screen.getAllByLabelText('dashboardBanner.fieldTitle')[0]).toHaveValue(
+ 'en title'
+ )
+
+ fireEvent.change(screen.getByLabelText('dashboardBanner.sourceLocale'), {
+ target: { value: 'hr' },
+ })
+ expect(screen.getAllByLabelText('dashboardBanner.fieldTitle')[0]).toHaveValue(
+ 'hr title'
+ )
+ fireEvent.click(screen.getByText('dashboardBanner.translate'))
+
+ await waitFor(() => expect(mockApiFetch).toHaveBeenCalledTimes(2))
+ expect(JSON.parse(mockApiFetch.mock.calls[1][1].body)).toEqual({
+ source_locale: 'hr',
+ title: 'hr title',
+ subtitle: 'hr subtitle',
+ description: 'hr description',
+ })
+ })
+
+ it('loads a legacy announcement and requires missing locales before saving it', async () => {
+ const { tr, sv, da, fi, hr, ...legacyTranslations } = generatedTranslations
+ mockApiFetch.mockReset()
+ mockApiFetch
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ source_locale: 'en',
+ is_active: true,
+ revision: 4,
+ translations: legacyTranslations,
+ updated_at: '2026-08-06T10:00:00Z',
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
+ )
+ )
+ .mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ source_locale: 'en',
+ is_active: true,
+ revision: 5,
+ translations: generatedTranslations,
+ updated_at: '2026-08-06T10:00:00Z',
+ }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } }
+ )
+ )
+
+ render( )
+
+ expect(await screen.findByText('10/15 complete')).toBeVisible()
+ expect(screen.getByText('dashboardBanner.save')).toBeDisabled()
+
+ fireEvent.change(screen.getByLabelText('dashboardBanner.editTranslation'), {
+ target: { value: 'tr' },
+ })
+ const [title, subtitle, description] = [
+ 'dashboardBanner.fieldTitle',
+ 'dashboardBanner.fieldSubtitle',
+ 'dashboardBanner.fieldDescription',
+ ].map((label) => screen.getAllByLabelText(label)[1])
+ fireEvent.change(title, { target: { value: tr.title } })
+ fireEvent.change(subtitle, { target: { value: tr.subtitle } })
+ fireEvent.change(description, { target: { value: tr.description } })
+
+ expect(screen.getByText('11/15 complete')).toBeVisible()
+ expect(screen.getByText('dashboardBanner.save')).toBeDisabled()
+ fireEvent.change(screen.getByLabelText('dashboardBanner.editTranslation'), {
+ target: { value: 'sv' },
+ })
+ const [swedishTitle, swedishSubtitle, swedishDescription] = [
+ 'dashboardBanner.fieldTitle',
+ 'dashboardBanner.fieldSubtitle',
+ 'dashboardBanner.fieldDescription',
+ ].map((label) => screen.getAllByLabelText(label)[1])
+ fireEvent.change(swedishTitle, { target: { value: sv.title } })
+ fireEvent.change(swedishSubtitle, { target: { value: sv.subtitle } })
+ fireEvent.change(swedishDescription, {
+ target: { value: sv.description },
+ })
+ expect(screen.getByText('12/15 complete')).toBeVisible()
+ expect(screen.getByText('dashboardBanner.save')).toBeDisabled()
+ fireEvent.change(screen.getByLabelText('dashboardBanner.editTranslation'), {
+ target: { value: 'da' },
+ })
+ const [danishTitle, danishSubtitle, danishDescription] = [
+ 'dashboardBanner.fieldTitle',
+ 'dashboardBanner.fieldSubtitle',
+ 'dashboardBanner.fieldDescription',
+ ].map((label) => screen.getAllByLabelText(label)[1])
+ fireEvent.change(danishTitle, { target: { value: da.title } })
+ fireEvent.change(danishSubtitle, { target: { value: da.subtitle } })
+ fireEvent.change(danishDescription, { target: { value: da.description } })
+ expect(screen.getByText('13/15 complete')).toBeVisible()
+ expect(screen.getByText('dashboardBanner.save')).toBeDisabled()
+ fireEvent.change(screen.getByLabelText('dashboardBanner.editTranslation'), {
+ target: { value: 'fi' },
+ })
+ const [finnishTitle, finnishSubtitle, finnishDescription] = [
+ 'dashboardBanner.fieldTitle',
+ 'dashboardBanner.fieldSubtitle',
+ 'dashboardBanner.fieldDescription',
+ ].map((label) => screen.getAllByLabelText(label)[1])
+ fireEvent.change(finnishTitle, { target: { value: fi.title } })
+ fireEvent.change(finnishSubtitle, { target: { value: fi.subtitle } })
+ fireEvent.change(finnishDescription, { target: { value: fi.description } })
+ expect(screen.getByText('14/15 complete')).toBeVisible()
+ expect(screen.getByText('dashboardBanner.save')).toBeDisabled()
+ fireEvent.change(screen.getByLabelText('dashboardBanner.editTranslation'), {
+ target: { value: 'hr' },
+ })
+ const [croatianTitle, croatianSubtitle, croatianDescription] = [
+ 'dashboardBanner.fieldTitle',
+ 'dashboardBanner.fieldSubtitle',
+ 'dashboardBanner.fieldDescription',
+ ].map((label) => screen.getAllByLabelText(label)[1])
+ fireEvent.change(croatianTitle, { target: { value: hr.title } })
+ fireEvent.change(croatianSubtitle, { target: { value: hr.subtitle } })
+ fireEvent.change(croatianDescription, { target: { value: hr.description } })
+ expect(screen.getByText('15/15 complete')).toBeVisible()
+ fireEvent.click(screen.getByText('dashboardBanner.save'))
+ await waitFor(() => expect(mockApiFetch).toHaveBeenCalledTimes(2))
+ const saved = JSON.parse(mockApiFetch.mock.calls[1][1].body)
+ expect(saved.translations).toEqual(generatedTranslations)
+ expect(saved.source_locale).toBe('en')
+ })
+
it('keeps source editor content when translation fails', async () => {
mockApiFetch.mockResolvedValueOnce(new Response(null, { status: 500 }))
render( )
diff --git a/frontend/tests/app/admin-user-detail.test.tsx b/frontend/tests/app/admin-user-detail.test.tsx
index f830acfa3..e90f4bfae 100644
--- a/frontend/tests/app/admin-user-detail.test.tsx
+++ b/frontend/tests/app/admin-user-detail.test.tsx
@@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => (key: string) => key,
}))
diff --git a/frontend/tests/app/chat-memory-stream.test.tsx b/frontend/tests/app/chat-memory-stream.test.tsx
index ce6d69166..39ae62d8e 100644
--- a/frontend/tests/app/chat-memory-stream.test.tsx
+++ b/frontend/tests/app/chat-memory-stream.test.tsx
@@ -280,4 +280,29 @@ describe('chat memory stream', () => {
expect(await screen.findByText('memorySavedToast')).toBeInTheDocument()
expect(await screen.findByText(/errorMessage/)).toBeInTheDocument()
})
+
+ it.each([
+ [429, 'Monthly token limit reached (10/10 tokens).', 'quotaExceededTokens'],
+ [429, 'Rate limit exceeded: 30 per 1 minute', 'errorMessage'],
+ [502, 'Provider unavailable', 'errorMessage'],
+ ])('localizes HTTP %i errors instead of showing backend details', async (status, detail, label) => {
+ mocks.apiFetch.mockImplementation((path: string) =>
+ Promise.resolve(
+ path === '/api/chat'
+ ? new Response(JSON.stringify({ detail }), {
+ status,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ : jsonResponse([])
+ )
+ )
+ render( )
+
+ const input = await screen.findByPlaceholderText('placeholder')
+ fireEvent.change(input, { target: { value: 'Hello' } })
+ fireEvent.click(screen.getByRole('button', { name: 'send' }))
+
+ expect(await screen.findByText(new RegExp(label))).toBeInTheDocument()
+ expect(screen.queryByText(detail)).toBeNull()
+ })
})
diff --git a/frontend/tests/app/dashboard-next-step.test.tsx b/frontend/tests/app/dashboard-next-step.test.tsx
index 7317451da..a28a16580 100644
--- a/frontend/tests/app/dashboard-next-step.test.tsx
+++ b/frontend/tests/app/dashboard-next-step.test.tsx
@@ -8,6 +8,7 @@ const { mockApiFetch, mockPush } = vi.hoisted(() => ({
}))
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => (key: string) => key,
}))
diff --git a/frontend/tests/app/feedback-unread-label.test.tsx b/frontend/tests/app/feedback-unread-label.test.tsx
index bd142e9e7..b141b5ce2 100644
--- a/frontend/tests/app/feedback-unread-label.test.tsx
+++ b/frontend/tests/app/feedback-unread-label.test.tsx
@@ -1,12 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-const { mockApiFetch } = vi.hoisted(() => ({
+const { mockApiFetch, translate } = vi.hoisted(() => ({
mockApiFetch: vi.fn(),
+ translate: (key: string) => key,
}))
vi.mock('next-intl', () => ({
- useTranslations: () => (key: string) => key,
+ useLocale: () => 'en',
+ useTranslations: () => translate,
}))
vi.mock('@/lib/api', () => ({
@@ -82,4 +84,43 @@ describe('FeedbackPage unread labels', () => {
expect(screen.getAllByText('statusPending').length).toBeGreaterThan(0)
expect(screen.getByText('Add focused practice')).toBeDefined()
})
+
+ it('updates the translated vote button label when a vote is toggled', async () => {
+ const list = {
+ items: [
+ {
+ id: 10,
+ type: 'feature',
+ title: 'Add focused practice',
+ description: 'A suggestion.',
+ status: 'pending',
+ author: {
+ id: 2,
+ username: 'other',
+ display_name: 'Other User',
+ },
+ vote_count: 0,
+ voted_by_me: false,
+ unread_by_me: false,
+ comment_count: 0,
+ created_at: '2026-07-04T10:00:00',
+ },
+ ],
+ total: 1,
+ skip: 0,
+ limit: 20,
+ }
+ mockApiFetch
+ .mockResolvedValueOnce(jsonResponse(list))
+ .mockResolvedValueOnce(jsonResponse(list))
+ .mockResolvedValueOnce(jsonResponse({ voted: true, vote_count: 1 }))
+
+ render( )
+ const voteButton = await screen.findByRole('button', { name: 'voteAction' })
+ expect(voteButton).toHaveAttribute('title', 'voteAction')
+ fireEvent.click(voteButton)
+ expect(
+ await screen.findByRole('button', { name: 'removeVoteAction' })
+ ).toHaveAttribute('title', 'removeVoteAction')
+ })
})
diff --git a/frontend/tests/app/flashcards-review.test.tsx b/frontend/tests/app/flashcards-review.test.tsx
index e30eebb67..e5b3f4b66 100644
--- a/frontend/tests/app/flashcards-review.test.tsx
+++ b/frontend/tests/app/flashcards-review.test.tsx
@@ -12,6 +12,7 @@ vi.mock('@/lib/api', () => ({
}))
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => (key: string) => key,
}))
diff --git a/frontend/tests/app/landing.test.tsx b/frontend/tests/app/landing.test.tsx
index bb4426712..910be2654 100644
--- a/frontend/tests/app/landing.test.tsx
+++ b/frontend/tests/app/landing.test.tsx
@@ -24,6 +24,7 @@ vi.mock('next/link', () => ({
}))
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => (key: string) => key,
}))
diff --git a/frontend/tests/app/onboarding-goals-subtitle.test.tsx b/frontend/tests/app/onboarding-goals-subtitle.test.tsx
index 1a25a1370..b0cc4518c 100644
--- a/frontend/tests/app/onboarding-goals-subtitle.test.tsx
+++ b/frontend/tests/app/onboarding-goals-subtitle.test.tsx
@@ -7,6 +7,9 @@ import { useAuthStore } from '@/store/auth'
import { useConfigStore } from '@/store/config'
import { useLanguageStore } from '@/store/language'
import de from '../../../messages/de.json'
+import da from '../../../messages/da.json'
+import fi from '../../../messages/fi.json'
+import hr from '../../../messages/hr.json'
import en from '../../../messages/en.json'
import es from '../../../messages/es.json'
import fr from '../../../messages/fr.json'
@@ -16,6 +19,8 @@ import pl from '../../../messages/pl.json'
import pt from '../../../messages/pt.json'
import ro from '../../../messages/ro.json'
import ru from '../../../messages/ru.json'
+import sv from '../../../messages/sv.json'
+import tr from '../../../messages/tr.json'
const { searchParams, apiFetch } = vi.hoisted(() => ({
searchParams: new URLSearchParams(),
@@ -31,20 +36,25 @@ vi.mock('@/lib/api', () => ({ apiFetch }))
import OnboardingPage from '@/app/(auth)/onboarding/page'
-const catalogs = { de, en, es, fr, it: itMessages, nl, pl, pt, ro, ru }
+const catalogs = { da, de, en, es, fi, fr, hr, it: itMessages, nl, pl, pt, ro, ru, sv, tr }
type Locale = keyof typeof catalogs
const spanishSubtitles: Record = {
+ da: 'Sprog: Spansk. Hvad vil du bruge det til? Vælg alle relevante mål.',
de: 'Sprache: Spanisch. Wofür möchtest du sie nutzen? Wähle alle zutreffenden Ziele aus.',
en: 'Language: Spanish. What do you want to use it for? Select all that apply.',
es: 'Idioma: español. ¿Para qué quieres utilizarlo? Selecciona todos los objetivos que correspondan.',
+ fi: 'Kieli: Espanja. Mihin haluat käyttää sitä? Valitse kaikki sopivat.',
fr: "Langue : espagnol. Dans quel but souhaitez-vous l'utiliser ? Sélectionnez tous les objectifs qui vous correspondent.",
+ hr: 'Jezik: Španjolski. Za što ga želiš koristiti? Odaberi sve što vrijedi.',
it: 'Lingua: spagnolo. Per cosa vuoi usarla? Seleziona tutti gli obiettivi che fanno al caso tuo.',
nl: 'Taal: Spaans. Waarvoor wil je deze taal gebruiken? Selecteer alle doelen die van toepassing zijn.',
pl: 'Język: hiszpański. Do czego chcesz go używać? Zaznacz wszystkie cele, które Ci odpowiadają.',
pt: 'Idioma: espanhol. Para que você quer usá-lo? Selecione todos os objetivos que se aplicam.',
ro: 'Limbă: spaniolă. Pentru ce vrei să o folosești? Selectează toate obiectivele care ți se potrivesc.',
ru: 'Язык: испанский. Для чего ты хочешь его использовать? Выбери все подходящие цели.',
+ sv: 'Språk: Spanska. Vad vill du använda det till? Välj alla som passar.',
+ tr: 'Dil: İspanyolca. Bu dili ne için kullanmak istiyorsunuz? Uygun olanların hepsini seçin.',
}
const onIntlError = vi.fn()
diff --git a/frontend/tests/app/reset-password-errors.test.tsx b/frontend/tests/app/reset-password-errors.test.tsx
new file mode 100644
index 000000000..eb99cbeb1
--- /dev/null
+++ b/frontend/tests/app/reset-password-errors.test.tsx
@@ -0,0 +1,100 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import React from 'react'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import ResetPasswordPage from '@/app/(auth)/reset-password/page'
+
+const apiFetch = vi.hoisted(() => vi.fn())
+
+vi.mock('@/lib/api', () => ({ apiFetch }))
+vi.mock('next-intl', () => ({
+ useTranslations: (namespace: string) => (key: string) => `${namespace}.${key}`,
+}))
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ push: vi.fn() }),
+ useSearchParams: () => new URLSearchParams('token=valid'),
+}))
+vi.mock('next/link', () => ({
+ default: ({ href, children }: React.PropsWithChildren<{ href: string }>) => (
+ {children}
+ ),
+}))
+
+describe('reset password errors', () => {
+ beforeEach(() => apiFetch.mockReset())
+
+ it.each([
+ [400, 'Invalid or expired reset token', 'auth.resetPassword.error'],
+ [422, 'Password validation failed', 'auth.register.invalidPassword'],
+ [503, 'Service unavailable', 'common.errorMessage'],
+ ])('localizes HTTP %i without exposing the backend detail', async (status, detail, message) => {
+ apiFetch.mockResolvedValue(
+ new Response(JSON.stringify({ detail }), {
+ status,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ )
+ render( )
+
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.newPassword'), {
+ target: { value: 'ValidPwd12!' },
+ })
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.confirmPassword'), {
+ target: { value: 'ValidPwd12!' },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'auth.resetPassword.submit' }))
+
+ expect(await screen.findByRole('alert')).toHaveTextContent(message)
+ expect(screen.queryByText(detail)).toBeNull()
+ })
+
+ it.each([
+ ['Valid12!', 'auth.resetPassword.tooShort'],
+ ['Valid123!', 'auth.resetPassword.tooShort'],
+ [`A1!${'😀'.repeat(6)}`, 'auth.resetPassword.tooShort'],
+ [`A1!${'a'.repeat(23)}`, 'auth.register.invalidPassword'],
+ ])('rejects an invalid password length before submitting: %s', async (password, message) => {
+ render( )
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.newPassword'), {
+ target: { value: password },
+ })
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.confirmPassword'), {
+ target: { value: password },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'auth.resetPassword.submit' }))
+
+ expect(await screen.findByRole('alert')).toHaveTextContent(message)
+ expect(apiFetch).not.toHaveBeenCalled()
+ })
+
+ it.each(['Abcdefg1!?', `A1!${'a'.repeat(22)}`, `A1!${'😀'.repeat(22)}`])(
+ 'submits a password within the backend length limits: %s',
+ async (password) => {
+ apiFetch.mockResolvedValue(new Response(null, { status: 503 }))
+ render( )
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.newPassword'), {
+ target: { value: password },
+ })
+ fireEvent.change(screen.getByPlaceholderText('auth.resetPassword.confirmPassword'), {
+ target: { value: password },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'auth.resetPassword.submit' }))
+
+ expect(await screen.findByRole('alert')).toHaveTextContent('common.errorMessage')
+ expect(apiFetch).toHaveBeenCalledWith(
+ '/api/auth/reset-password',
+ expect.objectContaining({
+ body: JSON.stringify({ token: 'valid', new_password: password }),
+ })
+ )
+ }
+ )
+
+ it('shows the translated password requirements before submission', () => {
+ render( )
+
+ expect(screen.getByPlaceholderText('auth.resetPassword.newPassword')).toHaveAccessibleDescription(
+ 'auth.register.invalidPassword'
+ )
+ })
+})
diff --git a/frontend/tests/components/BillingPaywall.test.tsx b/frontend/tests/components/BillingPaywall.test.tsx
index 11959da84..226e501bb 100644
--- a/frontend/tests/components/BillingPaywall.test.tsx
+++ b/frontend/tests/components/BillingPaywall.test.tsx
@@ -2,8 +2,10 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import React from 'react'
+const localeState = vi.hoisted(() => ({ value: 'en' }))
+
vi.mock('next-intl', () => ({
- useLocale: () => 'en',
+ useLocale: () => localeState.value,
useTranslations: () => (key: string) => key,
}))
@@ -116,6 +118,7 @@ function resetStores() {
}
beforeEach(() => {
+ localeState.value = 'en'
mockApiFetch.mockReset()
mockLandingSubscriptionState.mockReset()
resetStores()
@@ -126,6 +129,20 @@ beforeEach(() => {
})
describe('billing paywall UI', () => {
+ it('formats the subscription end date with the selected interface locale', () => {
+ localeState.value = 'fi'
+ useAuthStore.setState({
+ user: user({
+ subscription_status: 'canceled',
+ subscription_ends_at: '2099-09-24T12:00:00Z',
+ }),
+ })
+
+ render( )
+
+ expect(screen.getByText('24.9.2099')).toBeInTheDocument()
+ })
+
it('opens Customer Portal for past_due users in Settings instead of showing plan buttons', async () => {
useAuthStore.setState({
accessToken: 'token',
@@ -370,8 +387,8 @@ describe('billing paywall UI', () => {
render( )
- expect(screen.getByText('premiumBannerPastDueTitle')).toBeDefined()
- expect(screen.getByText('premiumBannerPastDueDesc')).toBeDefined()
+ expect(screen.getByText('pastDueTitle')).toBeDefined()
+ expect(screen.getByText('pastDueDesc')).toBeDefined()
expect(screen.getByText('updatePayment')).toBeDefined()
expect(screen.queryByText('planMonthly')).toBeNull()
expect(screen.queryByText('paywallNoCharge')).toBeNull()
diff --git a/frontend/tests/components/ConversationMode.test.tsx b/frontend/tests/components/ConversationMode.test.tsx
index 8a69ffc11..c8d3d0980 100644
--- a/frontend/tests/components/ConversationMode.test.tsx
+++ b/frontend/tests/components/ConversationMode.test.tsx
@@ -20,6 +20,7 @@ vi.mock('@ricky0123/vad-react', () => ({
},
}))
vi.mock('next-intl', () => ({
+ useLocale: () => 'en',
useTranslations: () => Object.assign((key: string) => key, { raw: () => [] }),
}))
vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
@@ -179,6 +180,44 @@ describe('ConversationMode session lifecycle', () => {
expect(ws.close).not.toHaveBeenCalled()
})
+ it.each([
+ ['stt_failed', 'errorTranscription'],
+ ['llm_failed', 'errorResponse'],
+ ['tts_failed', 'errorSpeech'],
+ ['auth_failed', 'errorUnauthorized'],
+ ['services_disabled', 'errorServicesDisabled'],
+ ['quota_exceeded_sessions', 'quotaExceededSessions'],
+ ['quota_exceeded_time', 'quotaExceededTime'],
+ ['quota_exceeded_tokens', 'quotaExceededTokens'],
+ ['no_active_plan', 'noActivePlan'],
+ ['unknown_server_error', 'errorMessage'],
+ ])('localizes %s without displaying the backend message', async (code, key) => {
+ render( )
+ const ws = await start()
+
+ act(() => ws.message({
+ type: 'error',
+ code,
+ message: 'Untranslated backend failure',
+ }))
+
+ expect(screen.getByText(new RegExp(key))).toBeInTheDocument()
+ expect(screen.queryByText(/Untranslated backend failure/)).toBeNull()
+ })
+
+ it.each(['onerror', 'onclose'])('localizes %s without displaying transport diagnostics', async (event) => {
+ render( )
+ const ws = await start()
+
+ act(() => {
+ if (event === 'onerror') ws.onerror?.()
+ else ws.onclose?.({ code: 1011, reason: 'Untranslated close reason' })
+ })
+
+ expect(screen.getByText('✕ errorConnection')).toBeInTheDocument()
+ expect(screen.queryByText(/Untranslated close reason|\[onerror|\[code/)).toBeNull()
+ })
+
it('clears visual speech and discards the unfinished segment on misfire', async () => {
render( )
const ws = await start()
diff --git a/frontend/tests/components/LanguageBubbles.test.tsx b/frontend/tests/components/LanguageBubbles.test.tsx
index d4566ff56..be3b046d5 100644
--- a/frontend/tests/components/LanguageBubbles.test.tsx
+++ b/frontend/tests/components/LanguageBubbles.test.tsx
@@ -5,7 +5,8 @@ import { LanguageBubbles } from '@/components/LanguageBubbles'
import { SUPPORTED_TARGET_LANGUAGES } from '@/lib/target-languages'
vi.mock('next-intl', () => ({
- useTranslations: () => (key: string) => key,
+ useTranslations: (namespace: string) => (key: string) =>
+ namespace === 'targetLanguages' ? `Localized ${key}` : key,
}))
vi.mock('next/image', () => ({
@@ -29,6 +30,7 @@ describe('LanguageBubbles', () => {
expect(screen.getAllByRole('img')).toHaveLength(
SUPPORTED_TARGET_LANGUAGES.length
)
+ expect(screen.getByAltText('Localized de-DE')).toBeInTheDocument()
})
it('positions bubbles dynamically from the supported language count', () => {
diff --git a/frontend/tests/components/ProfileSection.test.tsx b/frontend/tests/components/ProfileSection.test.tsx
index 6e1cf8e91..f498e96d7 100644
--- a/frontend/tests/components/ProfileSection.test.tsx
+++ b/frontend/tests/components/ProfileSection.test.tsx
@@ -464,6 +464,75 @@ describe('ProfileSection', () => {
expect(reloadSpy).not.toHaveBeenCalled()
})
+ it('applies a saved locale when the interface is displaying another language', async () => {
+ const reloadSpy = vi.fn()
+ vi.stubGlobal('location', {
+ ...window.location,
+ reload: reloadSpy,
+ protocol: 'http:',
+ })
+ useAuthStore.setState({ user: { ...defaultUser, ui_locale: 'fi' } })
+ document.cookie = 'NEXT_LOCALE=en; path=/'
+ mockApiFetch.mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({ id: 1, ui_locale: 'fi' }),
+ })
+
+ render( )
+ expect(selectAfterLabelText('uiLocale')).toHaveValue('fi')
+ fireEvent.click(screen.getByText('saveChanges'))
+
+ await waitFor(() => expect(reloadSpy).toHaveBeenCalledTimes(1))
+ expect(document.cookie).toContain('NEXT_LOCALE=fi')
+ expect(document.cookie).toContain('LOCALE_DETECTED=1')
+ })
+
+ it('synchronizes cookies without reloading when choosing the displayed locale', async () => {
+ const reloadSpy = vi.fn()
+ vi.stubGlobal('location', {
+ ...window.location,
+ reload: reloadSpy,
+ protocol: 'http:',
+ })
+ useAuthStore.setState({ user: { ...defaultUser, ui_locale: 'fi' } })
+ document.cookie = 'NEXT_LOCALE=fi; path=/'
+ mockApiFetch.mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({ id: 1, ui_locale: 'en' }),
+ })
+
+ render( )
+ fireEvent.change(selectAfterLabelText('uiLocale'), {
+ target: { value: 'en' },
+ })
+ fireEvent.click(screen.getByText('saveChanges'))
+
+ await waitFor(() => expect(screen.getByText(/saved/)).toBeDefined())
+ expect(document.cookie).toContain('NEXT_LOCALE=en')
+ expect(reloadSpy).not.toHaveBeenCalled()
+ })
+
+ it('keeps the current locale when saving the profile fails', async () => {
+ const reloadSpy = vi.fn()
+ vi.stubGlobal('location', {
+ ...window.location,
+ reload: reloadSpy,
+ protocol: 'http:',
+ })
+ document.cookie = 'NEXT_LOCALE=en; path=/'
+ mockApiFetch.mockResolvedValue({ ok: false })
+
+ render( )
+ fireEvent.change(selectAfterLabelText('uiLocale'), {
+ target: { value: 'hr' },
+ })
+ fireEvent.click(screen.getByText('saveChanges'))
+
+ await waitFor(() => expect(screen.getByText(/saveFailed/)).toBeDefined())
+ expect(document.cookie).toContain('NEXT_LOCALE=en')
+ expect(reloadSpy).not.toHaveBeenCalled()
+ })
+
// ── API errors ──────────────────────────────────────────────
it('shows error message on API failure', async () => {
@@ -478,14 +547,14 @@ describe('ProfileSection', () => {
})
})
- it('shows error message on network failure', async () => {
+ it('shows a localized error on network failure', async () => {
mockApiFetch.mockRejectedValue(new Error('Network error'))
render( )
fireEvent.click(screen.getByText('saveChanges'))
- // The component uses err.message (the thrown Error's message) as the displayed text.
await waitFor(() => {
- expect(screen.getByText(/Network error/)).toBeDefined()
+ expect(screen.getByText(/saveFailed/)).toBeDefined()
})
+ expect(screen.queryByText(/Network error/)).toBeNull()
})
it('message type is error for failures', async () => {
@@ -804,6 +873,11 @@ describe('ProfileSection', () => {
expect(options).toContain('nl')
expect(options).toContain('ro')
expect(options).toContain('ru')
+ expect(options).toContain('tr')
+ expect(options).toContain('sv')
+ expect(options).toContain('da')
+ expect(options).toContain('fi')
+ expect(options).toContain('hr')
})
it('renders all ui locale options', () => {
@@ -820,6 +894,11 @@ describe('ProfileSection', () => {
expect(options).toContain('nl')
expect(options).toContain('ro')
expect(options).toContain('ru')
+ expect(options).toContain('tr')
+ expect(options).toContain('sv')
+ expect(options).toContain('da')
+ expect(options).toContain('fi')
+ expect(options).toContain('hr')
})
// ── Avatar upload buttons ───────────────────────────────────
diff --git a/frontend/tests/components/ReviewPrompt.test.tsx b/frontend/tests/components/ReviewPrompt.test.tsx
index 79072e7ad..f81a1eb75 100644
--- a/frontend/tests/components/ReviewPrompt.test.tsx
+++ b/frontend/tests/components/ReviewPrompt.test.tsx
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import {
ReviewPrompt,
getReviewPromptDismissal,
@@ -25,7 +25,10 @@ const translations: Record = {
submit: 'Submit',
}
-const translate = (key: string) => translations[key] ?? key
+const translate = (key: string, values?: { rating: number }) =>
+ key === 'starsLabel'
+ ? `${values?.rating} out of 5 stars`
+ : (translations[key] ?? key)
vi.mock('next-intl', () => ({
useTranslations: () => translate,
@@ -53,11 +56,22 @@ describe('ReviewPrompt', () => {
it('submits rating-only reviews', async () => {
const onClose = vi.fn()
mockCreateReview.mockResolvedValue({ id: 1, rating: 5 })
+ let resolveReview!: (value: { has_review: boolean; review: null }) => void
+ mockFetchMyReview.mockReturnValueOnce(
+ new Promise<{ has_review: boolean; review: null }>((resolve) => {
+ resolveReview = resolve
+ })
+ )
render( )
+ expect(screen.getByText('checking')).toBeInTheDocument()
+ expect(screen.queryByText('Rating required')).toBeNull()
+ await act(async () => {
+ resolveReview({ has_review: false, review: null })
+ })
await screen.findByText('Rating required')
- fireEvent.click(screen.getByLabelText('5 stars'))
+ fireEvent.click(screen.getByLabelText('5 out of 5 stars'))
await waitFor(() =>
- expect(screen.getByLabelText('5 stars')).toHaveAttribute(
+ expect(screen.getByLabelText('5 out of 5 stars')).toHaveAttribute(
'aria-checked',
'true'
)
@@ -74,11 +88,20 @@ describe('ReviewPrompt', () => {
it('submits rating plus comment', async () => {
mockCreateReview.mockResolvedValue({ id: 1, rating: 4 })
+ let resolveReview!: (value: { has_review: boolean; review: null }) => void
+ mockFetchMyReview.mockReturnValueOnce(
+ new Promise<{ has_review: boolean; review: null }>((resolve) => {
+ resolveReview = resolve
+ })
+ )
render( {}} />)
+ await act(async () => {
+ resolveReview({ has_review: false, review: null })
+ })
await screen.findByText('Rating required')
- fireEvent.click(screen.getByLabelText('4 stars'))
+ fireEvent.click(screen.getByLabelText('4 out of 5 stars'))
await waitFor(() =>
- expect(screen.getByLabelText('4 stars')).toHaveAttribute(
+ expect(screen.getByLabelText('4 out of 5 stars')).toHaveAttribute(
'aria-checked',
'true'
)
diff --git a/frontend/tests/components/WordTooltip.test.tsx b/frontend/tests/components/WordTooltip.test.tsx
index 0d0752e79..16f6752de 100644
--- a/frontend/tests/components/WordTooltip.test.tsx
+++ b/frontend/tests/components/WordTooltip.test.tsx
@@ -5,6 +5,10 @@ import { WordTooltip, useWordSave } from '@/components/ui/WordTooltip'
const mockApiFetch = vi.hoisted(() => vi.fn())
vi.mock('@/lib/api', () => ({ apiFetch: mockApiFetch }))
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string) =>
+ key === 'close' ? 'Cerrar' : key,
+}))
const labels = {
saveWord: 'Save',
@@ -40,6 +44,7 @@ describe('WordTooltip component', () => {
/>
)
expect(screen.getByText('Save')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Cerrar' })).toBeInTheDocument()
})
it('renders wordSaved with green styling in saved state', () => {
diff --git a/frontend/tests/i18n/admin-messages.test.ts b/frontend/tests/i18n/admin-messages.test.ts
index 2ad663ed4..42b64cdea 100644
--- a/frontend/tests/i18n/admin-messages.test.ts
+++ b/frontend/tests/i18n/admin-messages.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
+import { createTranslator } from 'next-intl'
import de from '../../../messages/de.json'
+import da from '../../../messages/da.json'
+import fi from '../../../messages/fi.json'
+import hr from '../../../messages/hr.json'
import en from '../../../messages/en.json'
import es from '../../../messages/es.json'
import fr from '../../../messages/fr.json'
@@ -9,8 +13,10 @@ import pl from '../../../messages/pl.json'
import pt from '../../../messages/pt.json'
import ro from '../../../messages/ro.json'
import ru from '../../../messages/ru.json'
+import sv from '../../../messages/sv.json'
+import tr from '../../../messages/tr.json'
-const locales = { de, es, fr, it: itMessages, nl, pl, pt, ro, ru }
+const locales = { da, de, es, fi, fr, hr, it: itMessages, nl, pl, pt, ro, ru, sv, tr }
describe('admin i18n messages', () => {
it('keeps admin namespace keys in sync across locales', () => {
@@ -21,3 +27,32 @@ describe('admin i18n messages', () => {
}
})
})
+
+describe('shared interface messages', () => {
+ it('renders generic errors and vocabulary counts in all fifteen locales', () => {
+ for (const [locale, messages] of Object.entries({ en, ...locales })) {
+ const errors: string[] = []
+ const t = createTranslator({
+ locale,
+ messages,
+ onError: (error) => errors.push(error.message),
+ })
+
+ expect(t('common.errorMessage'), locale).not.toBe('common.errorMessage')
+ expect(t('billing.pastDueTitle'), locale).not.toBe('billing.pastDueTitle')
+ expect(t('billing.pastDueDesc'), locale).not.toBe('billing.pastDueDesc')
+ expect(t('vocabulary.sets', { count: 1 }), locale).toContain('1')
+ expect(t('vocabulary.sets', { count: 2 }), locale).toContain('2')
+ expect(errors, locale).toEqual([])
+ }
+ })
+
+ it.each([
+ ['ru', ru, '2 из 5 звезд'],
+ ['pl', pl, '2 z 5 gwiazdek'],
+ ['hr', hr, 'Ocjena: 2 od 5'],
+ ])('formats rating labels in %s', (locale, messages, expected) => {
+ const t = createTranslator({ locale, messages })
+ expect(t('landingReviews.starsLabel', { rating: 2 })).toBe(expected)
+ })
+})
diff --git a/frontend/tests/i18n/hr-plurals.test.tsx b/frontend/tests/i18n/hr-plurals.test.tsx
new file mode 100644
index 000000000..306035b57
--- /dev/null
+++ b/frontend/tests/i18n/hr-plurals.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from '@testing-library/react'
+import { NextIntlClientProvider, useTranslations } from 'next-intl'
+import { describe, expect, it } from 'vitest'
+
+import hr from '../../../messages/hr.json'
+
+function PluralExamples({ count }: { count: number }) {
+ const grammar = useTranslations('grammar')
+ const billing = useTranslations('billing')
+ const vocabulary = useTranslations('vocabulary')
+ const feedback = useTranslations('feedback')
+ const common = useTranslations('common')
+
+ return (
+ <>
+ {grammar('topicsFound', { count })}
+ {billing('trialDays', { days: count })}
+ {vocabulary('sets', { count })}
+ {feedback('comments', { count })}
+ {common('errorMessage')}
+ >
+ )
+}
+
+describe('Croatian plural messages', () => {
+ it.each([
+ [1, 'Pronađena 1 tema', '1 dan probnog razdoblja', '1 zbirka', '1 komentar'],
+ [2, 'Pronađene 2 teme', '2 dana probnog razdoblja', '2 zbirke', '2 komentara'],
+ [5, 'Pronađeno 5 tema', '5 dana probnog razdoblja', '5 zbirki', '5 komentara'],
+ [11, 'Pronađeno 11 tema', '11 dana probnog razdoblja', '11 zbirki', '11 komentara'],
+ [21, 'Pronađena 21 tema', '21 dan probnog razdoblja', '21 zbirka', '21 komentar'],
+ [22, 'Pronađene 22 teme', '22 dana probnog razdoblja', '22 zbirke', '22 komentara'],
+ [25, 'Pronađeno 25 tema', '25 dana probnog razdoblja', '25 zbirki', '25 komentara'],
+ [31, 'Pronađena 31 tema', '31 dan probnog razdoblja', '31 zbirka', '31 komentar'],
+ ])('formats %i with the correct Croatian forms', (count, topics, days, sets, comments) => {
+ render(
+
+
+
+ )
+
+ expect(screen.getByText(topics)).toBeInTheDocument()
+ expect(screen.getByText(days)).toBeInTheDocument()
+ expect(screen.getByText(sets)).toBeInTheDocument()
+ expect(screen.getByText(comments)).toBeInTheDocument()
+ expect(screen.getByText(hr.common.errorMessage)).toBeInTheDocument()
+ })
+})
diff --git a/frontend/tests/i18n/ru-plurals.test.ts b/frontend/tests/i18n/ru-plurals.test.ts
new file mode 100644
index 000000000..5986add13
--- /dev/null
+++ b/frontend/tests/i18n/ru-plurals.test.ts
@@ -0,0 +1,27 @@
+import { createTranslator } from 'next-intl'
+import { describe, expect, it } from 'vitest'
+
+import ru from '../../../messages/ru.json'
+
+describe('Russian plural messages', () => {
+ const t = createTranslator({ locale: 'ru', messages: ru })
+
+ it.each([
+ [1, 'день', 'Найдена 1 тема', 'Показана 1 ситуация', '1 набор'],
+ [2, 'дня', 'Найдены 2 темы', 'Показаны 2 ситуации', '2 набора'],
+ [5, 'дней', 'Найдено 5 тем', 'Показано 5 ситуаций', '5 наборов'],
+ [21, 'день', 'Найдена 21 тема', 'Показана 21 ситуация', '21 набор'],
+ [22, 'дня', 'Найдены 22 темы', 'Показаны 22 ситуации', '22 набора'],
+ [25, 'дней', 'Найдено 25 тем', 'Показано 25 ситуаций', '25 наборов'],
+ ])('formats %i with the correct forms', (count, day, topics, situations, sets) => {
+ expect(t('dashboard.freemiumTrialTitle', { days: count })).toBe(
+ `${count} ${day} пробного периода`
+ )
+ expect(t('freemium.trialDaysLeft', { days: count })).toBe(
+ `${count} ${day} бесплатного пробного периода`
+ )
+ expect(t('grammar.topicsFound', { count })).toBe(topics)
+ expect(t('phrasebook.situationsShown', { count })).toBe(situations)
+ expect(t('vocabulary.sets', { count })).toBe(sets)
+ })
+})
diff --git a/frontend/tests/i18n/voice-messages.test.ts b/frontend/tests/i18n/voice-messages.test.ts
new file mode 100644
index 000000000..f032ddae1
--- /dev/null
+++ b/frontend/tests/i18n/voice-messages.test.ts
@@ -0,0 +1,45 @@
+import { createTranslator } from 'next-intl'
+import { describe, expect, it } from 'vitest'
+
+import { SUPPORTED_LOCALES } from '@/lib/locales'
+import en from '../../../messages/en.json'
+
+const messageKeys = [
+ 'assessment.voiceTrialLabel',
+ 'assessment.voiceTrialTitle',
+ 'assessment.voiceTrialDesc',
+ 'assessment.voiceTrialStart',
+ 'assessment.voiceTrialSkip',
+ 'conversation.trialBanner',
+ 'conversation.trialCtaLabel',
+ 'conversation.trialCtaTitle',
+ 'conversation.trialCtaDesc',
+ 'conversation.errorTranscription',
+ 'conversation.errorResponse',
+ 'conversation.errorSpeech',
+] as const
+
+const english = createTranslator({ locale: 'en', messages: en })
+
+describe.each(SUPPORTED_LOCALES)('voice messages in %s', (locale) => {
+ it('renders translated trial and error messages without English fallback', async () => {
+ const messages = (await import(`../../../messages/${locale}.json`)).default
+ const t = createTranslator({
+ locale,
+ messages,
+ onError: (error) => {
+ throw error
+ },
+ })
+
+ for (const key of messageKeys) {
+ const rendered = t(key, { minutes: 5 })
+ expect(rendered.trim()).not.toBe('')
+ expect(rendered).not.toBe(key)
+ expect(rendered).not.toContain('{minutes}')
+ if (locale !== 'en') {
+ expect(rendered).not.toBe(english(key, { minutes: 5 }))
+ }
+ }
+ })
+})
diff --git a/frontend/tests/middleware.test.ts b/frontend/tests/middleware.test.ts
index 37dac6b4d..d3849921f 100644
--- a/frontend/tests/middleware.test.ts
+++ b/frontend/tests/middleware.test.ts
@@ -94,6 +94,42 @@ describe('middleware', () => {
expect(setCookie).toContain('NEXT_LOCALE=es')
})
+ it('detects Swedish browser language from sv-SE', () => {
+ const req = createRequest('/login', {
+ acceptLanguage: 'sv-SE,sv;q=0.9,en;q=0.8',
+ })
+
+ const res = middleware(req)
+ expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=sv')
+ })
+
+ it('detects Danish browser language from da-DK', () => {
+ const req = createRequest('/login', {
+ acceptLanguage: 'da-DK,da;q=0.9,en;q=0.8',
+ })
+
+ const res = middleware(req)
+ expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=da')
+ })
+
+ it('detects Finnish browser language from fi-FI', () => {
+ const req = createRequest('/login', {
+ acceptLanguage: 'fi-FI,fi;q=0.9,en;q=0.8',
+ })
+
+ const res = middleware(req)
+ expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=fi')
+ })
+
+ it('detects Croatian browser language from hr-HR', () => {
+ const req = createRequest('/login', {
+ acceptLanguage: 'hr-HR,hr;q=0.9,en;q=0.8',
+ })
+
+ const res = middleware(req)
+ expect(res.headers.get('set-cookie')).toContain('NEXT_LOCALE=hr')
+ })
+
it('falls back to en when no locale detected', () => {
const req = createRequest('/login')
const res = middleware(req)
diff --git a/messages/da.json b/messages/da.json
new file mode 100644
index 000000000..9f27e1d03
--- /dev/null
+++ b/messages/da.json
@@ -0,0 +1,1708 @@
+{
+ "landing": {
+ "microDemo": {
+ "title": "Sådan hjælper Lingu dig",
+ "exampleLabel": "Eksempel på engelskøvelse",
+ "questionLabel": "Lingu · Spørgsmål",
+ "answerLabel": "Dig · Svar",
+ "correctionLabel": "Lingu · Rettelse",
+ "explanation": "Når du taler om i går, skal du bruge 'went', datidsformen af 'go'."
+ },
+ "hero": "Lær sprog med en samtalepartner, du kan tale med",
+ "heroSub": "Tal. Lyt. Bliv bedre.",
+ "signIn": "Log ind",
+ "dashboard": "Fortsæt",
+ "feature1Title": "Læringsplan",
+ "feature1Desc": "Niveauvurdering og ugeplan fra A1 til C2. Lektioner låses op i rækkefølge og i dit eget tempo.",
+ "feature2Title": "AI-sproglærer i chatten",
+ "feature2Desc": "Øv dig med lektioner, øvelser og flashcards med hjælp fra din lærer. Få feedback undervejs.",
+ "feature3Title": "Stemmesamtale",
+ "feature3Desc": "Tal med din lærer i realtid. Stemmestyret, med hurtige svar og uden at vente på tur.",
+ "feature4Title": "Lytning og læsning",
+ "feature4Desc": "AI-genererede øvelser på dit CEFR-niveau. Lyt til et lydklip, besvar fem forståelsesspørgsmål, og lås op for transskriptionen – eller læs en tekst og svar direkte. Nye øvelser, når du har lyst.",
+ "feature5Title": "Flashcards",
+ "feature5Desc": "Træn ordforråd med gentagelser fordelt over tid. SM-2-algoritmen planlægger hvert kort på det rette tidspunkt, så du husker ordene længere.",
+ "feature6Title": "Fremskridt og stimer",
+ "feature6Desc": "XP, daglige stimer, færdighedsscorer og kompetencer for hver enhed. Følg din udvikling uge for uge, og test dit niveau efter hvert CEFR-trin.",
+ "footer": "Open source · Host selv gratis, eller abonnér på den hostede tjeneste",
+ "navFeatures": "Funktioner",
+ "navReviews": "Anmeldelser",
+ "navPricing": "Priser",
+ "navFAQ": "Ofte stillede spørgsmål",
+ "howItWorks": "Sådan fungerer det",
+ "faqTitle": "Ofte stillede spørgsmål",
+ "terms": "Vilkår",
+ "privacy": "Privatliv",
+ "contact": "Kontakt",
+ "aboutMe": "Om mig",
+ "github": "GitHub",
+ "footerProduct": "Produkt",
+ "footerLegal": "Juridisk",
+ "languageGreetings": {
+ "en-US": "Hello!",
+ "en-GB": "Hello!",
+ "de-DE": "Hallo!",
+ "es-ES": "¡Hola!",
+ "fr-FR": "Bonjour !",
+ "it-IT": "Ciao!",
+ "pt-PT": "Olá!",
+ "ja-JP": "こんにちは!",
+ "ko-KR": "안녕하세요!",
+ "zh-CN": "你好!"
+ }
+ },
+ "common": {
+ "loading": "Indlæser...",
+ "error": "Fejl",
+ "errorMessage": "Noget gik galt. Prøv igen.",
+ "save": "Gem ændringer",
+ "saving": "Gemmer...",
+ "cancel": "Annuller",
+ "back": "Tilbage",
+ "done": "Færdig",
+ "next": "Næste",
+ "submit": "Send",
+ "start": "Start",
+ "retry": "Prøv igen",
+ "or": "eller",
+ "appName": "FreeLingo",
+ "days": "dage",
+ "logout": "Log ud",
+ "logoutConfirmTitle": "Log ud",
+ "logoutConfirmMessage": "Er du sikker på, at du vil logge ud?",
+ "initializing": "Starter...",
+ "clearFilters": "Ryd filtre",
+ "all": "Alle",
+ "score": "Score",
+ "complete": "Fuldfør",
+ "checking": "Kontrollerer...",
+ "backToDashboard": "Tilbage til oversigten",
+ "tagline": "Sprogindlæring med AI",
+ "close": "Luk",
+ "delete": "Slet",
+ "openMenu": "Åbn menuen",
+ "closeMenu": "Luk menuen",
+ "verifyEmailBanner": "Bekræft din e-mailadresse for at færdiggøre din konto.",
+ "verifyEmailSent": "E-mailen er sendt. Tjek din indbakke.",
+ "resendVerification": "Send bekræftelsesmail igen",
+ "saveWord": "Gem",
+ "wordSaved": "Gemt",
+ "wordAlreadySaved": "Allerede gemt",
+ "wordSaveError": "Noget gik galt",
+ "selectWordHint": "Markér et ord for at gemme det i dit ordforråd",
+ "noActivePlan": "Ingen aktiv læringsplan fundet",
+ "memorySavedToast": "Lingu gemte en ny hukommelse. Du kan se den i Indstillinger.",
+ "removeLanguageConfirmTitle": "Fjern sprog",
+ "removeLanguageConfirmMessage": "Alle sprogspecifikke data (læringsplan, fremskridt, flashcards og samtaler) slettes permanent. Dine kontohukommelser bevares. Handlingen kan ikke fortrydes.",
+ "removeLanguageConfirmLabel": "Fjern",
+ "nativeHelpTitle": "Hjælp på {language}",
+ "nativeHelpLoading": "Forbereder hjælp på {language}...",
+ "nativeHelpShow": "Vis hjælp på {language}",
+ "nativeHelpKeyPoints": "Vigtige punkter",
+ "nativeHelpCommonTraps": "Almindelige faldgruber",
+ "nativeHelpMiniGlossary": "Kort ordliste",
+ "nativeHelpUsageTips": "Tips til brug",
+ "nativeHelpRegisterNotes": "Sprogligt register",
+ "nativeHelpPhraseNotes": "Noter om fraser",
+ "nativeHelpStudyTips": "Studietips",
+ "nativeHelpWordNotes": "Noter om ord",
+ "nativeHelpPractice": "Øvelse"
+ },
+ "nav": {
+ "home": "Hjem",
+ "myPlan": "Min plan",
+ "progress": "Fremskridt",
+ "flashcards": "Flashcards",
+ "tutor": "Chat med læreren",
+ "assessment": "Niveauvurdering",
+ "resources": "Ressourcer",
+ "grammar": "Grammatik",
+ "vocabulary": "Ordforråd",
+ "phrasebook": "Parlør",
+ "settings": "Indstillinger",
+ "faq": "Ofte stillede spørgsmål",
+ "admin": "Administration",
+ "conversation": "Samtale",
+ "listening": "Lytning",
+ "reading": "Læsning",
+ "contact": "Kontakt",
+ "feedback": "Feedback",
+ "reviews": "Anmeldelser",
+ "switchLanguage": "Skift sprog"
+ },
+ "auth": {
+ "login": {
+ "title": "Log ind",
+ "subtitle": "Velkommen tilbage",
+ "email": "E-mailadresse",
+ "password": "Adgangskode",
+ "submit": "Log ind",
+ "noAccount": "Har du ikke en konto?",
+ "register": "Opret dig",
+ "error": "Forkert e-mailadresse eller adgangskode",
+ "emailRequired": "Indtast din e-mailadresse",
+ "passwordRequired": "Indtast din adgangskode",
+ "loginFailed": "Login mislykkedes",
+ "signingIn": "Logger ind...",
+ "showPassword": "Vis adgangskode",
+ "hidePassword": "Skjul adgangskode",
+ "accountCreated": "Kontoen er oprettet. Du kan nu logge ind",
+ "invalidEmail": "Indtast en gyldig e-mailadresse",
+ "forgotPassword": "Har du glemt din adgangskode?"
+ },
+ "register": {
+ "title": "Opret konto",
+ "subtitle": "Begynd at lære",
+ "username": "Brugernavn",
+ "email": "E-mailadresse",
+ "password": "Adgangskode",
+ "displayName": "Visningsnavn",
+ "nativeLanguage": "Modersmål",
+ "inviteToken": "Invitationskode",
+ "inviteTokenPlaceholder": "Indtast invitationskode",
+ "submit": "Opret konto",
+ "hasAccount": "Har du allerede en konto?",
+ "login": "Log ind",
+ "registrationClosed": "Registrering er lukket. Du skal bruge en invitationskode.",
+ "error": "Registreringen mislykkedes",
+ "passwordMismatch": "Adgangskoderne stemmer ikke overens",
+ "confirmPassword": "Bekræft adgangskode",
+ "showPassword": "Vis adgangskode",
+ "hidePassword": "Skjul adgangskode",
+ "displayNamePlaceholder": "Samme som brugernavnet, hvis feltet er tomt",
+ "inviteActive": "Invitationen er aktiv",
+ "creatingAccount": "Opretter...",
+ "invalidEmail": "Indtast en gyldig e-mailadresse",
+ "usernameTaken": "Brugernavnet er allerede i brug",
+ "emailTaken": "E-mailadressen er allerede registreret",
+ "invalidInvite": "Ugyldig eller udløbet invitationskode",
+ "termsAccept": "Jeg har læst og accepterer",
+ "termsLink": "Brugervilkårene",
+ "andWord": "og",
+ "privacyLink": "Privatlivspolitikken",
+ "termsRequired": "Du skal acceptere vilkårene for at fortsætte",
+ "invalidPassword": "Adgangskoden skal være på 10–25 tegn og indeholde mindst ét stort bogstav, ét tal og ét symbol",
+ "invalidUsernameChars": "Brugernavnet må kun indeholde bogstaver, tal, bindestreger, understreger og punktummer"
+ },
+ "verifyEmail": {
+ "title": "Bekræft e-mailadresse",
+ "loading": "Bekræfter...",
+ "success": "Din e-mailadresse er bekræftet. Du kan nu logge ind.",
+ "error": "Ugyldigt eller udløbet bekræftelseslink. Anmod om et nyt fra din konto.",
+ "goToLogin": "Gå til login"
+ },
+ "forgotPassword": {
+ "title": "Glemt adgangskode",
+ "description": "Indtast e-mailadressen til din konto, så sender vi dig et link til nulstilling.",
+ "emailPlaceholder": "din@email.dk",
+ "submit": "Send link til nulstilling",
+ "sending": "Sender...",
+ "sent": "Hvis e-mailadressen er registreret, har vi sendt et link til nulstilling. Tjek din indbakke.",
+ "backToLogin": "Tilbage til login",
+ "error": "Noget gik galt. Prøv igen."
+ },
+ "resetPassword": {
+ "title": "Nulstil adgangskode",
+ "newPassword": "Ny adgangskode",
+ "confirmPassword": "Bekræft ny adgangskode",
+ "submit": "Angiv ny adgangskode",
+ "saving": "Gemmer...",
+ "success": "Adgangskoden er opdateret. Du sendes videre til login...",
+ "mismatch": "Adgangskoderne stemmer ikke overens",
+ "tooShort": "Adgangskoden skal være på mindst 10 tegn",
+ "missingToken": "Ugyldigt link til nulstilling. Anmod om at nulstille adgangskoden igen.",
+ "error": "Ugyldigt eller udløbet link til nulstilling",
+ "backToLogin": "Tilbage til login"
+ }
+ },
+ "settings": {
+ "title": "Indstillinger",
+ "sectionAccount": "Konto",
+ "sectionProfile": "Profil",
+ "avatarChange": "Skift billede",
+ "avatarRemove": "Fjern",
+ "avatarUploading": "Uploader...",
+ "avatarTypeError": "Kun JPEG- og PNG-billeder er tilladt",
+ "avatarSizeError": "Billedet er for stort (højst 2 MB)",
+ "displayName": "Visningsnavn",
+ "email": "E-mailadresse",
+ "nativeLanguage": "Modersmål",
+ "uiLocale": "Grænsefladesprog",
+ "uiLocaleHint": "Ændrer sproget i menuer og på knapper. Siden skal genindlæses.",
+ "newPassword": "Ny adgangskode",
+ "newPasswordPlaceholder": "Lad feltet være tomt for at beholde den nuværende",
+ "confirmPassword": "Bekræft adgangskode",
+ "confirmPasswordPlaceholder": "Gentag den nye adgangskode",
+ "bio": "Om dig (AI-kontekst)",
+ "bioPlaceholder": "F.eks.: Jeg er softwareudvikler og rejser ofte med arbejdet...",
+ "bioHint": "AI-læreren bruger dette til at tilpasse samtaler.",
+ "saveChanges": "Gem ændringer",
+ "saving": "Gemmer...",
+ "saved": "Profilen er opdateret.",
+ "passwordMismatch": "Adgangskoderne stemmer ikke overens",
+ "saveFailed": "Kunne ikke gemme",
+ "logout": "Log ud",
+ "sectionAppearance": "Udseende",
+ "theme": "Tema",
+ "darkActive": "Mørk tilstand aktiv",
+ "lightActive": "Lys tilstand aktiv",
+ "systemActive": "Automatisk (system)",
+ "themeSystem": "Auto",
+ "themeDark": "Mørk",
+ "themeLight": "Lys",
+ "switchToLight": "Skift til lys tilstand",
+ "switchToDark": "Skift til mørk tilstand",
+ "sectionConversation": "Samtale",
+ "conversationMaxDuration": "Maksimal samtalelængde",
+ "conversationInactivityTimeout": "Tidsgrænse ved inaktivitet",
+ "conversationSpeechPause": "Pause før din tur slutter",
+ "conversationSpeechPauseHint": "Hvor længe du kan holde pause midt i en sætning, før din tur sendes. Automatisk tilpasser sig dit niveau.",
+ "speechPauseAuto": "Auto",
+ "speechPauseSec1": "1 sekund",
+ "speechPauseSec2": "2 sekunder",
+ "speechPauseSec3": "3 sekunder",
+ "min15": "15 minutter",
+ "min30": "30 minutter",
+ "min1": "1 minut",
+ "min3": "3 minutter",
+ "min5": "5 minutter",
+ "saveConversation": "Gem",
+ "conversationSaved": "Samtaleindstillingerne er gemt.",
+ "sectionLegal": "Juridisk",
+ "termsOfService": "Brugervilkår",
+ "privacyPolicy": "Privatlivspolitik",
+ "contactLink": "Kontakt",
+ "deleteAccount": "Slet konto",
+ "deleteAccountTitle": "Slet konto",
+ "deleteAccountMessage": "Dette sletter din konto og alle dine data permanent. Handlingen kan ikke fortrydes.",
+ "deleteAccountConfirm": "Slet konto",
+ "sectionUsageLimits": "Forbrugsgrænser",
+ "sectionVoice": "Stemme",
+ "voiceHint": "Vælg den AI-stemme, du foretrækker, på listen, og klik derefter på afspil for at høre den.",
+ "voicePlay": "Afspil",
+ "voiceStop": "Stop",
+ "quotaSessions": "Samtaler pr. uge",
+ "quotaMinutesDay": "Minutter pr. dag",
+ "quotaMinutesWeek": "Minutter pr. uge",
+ "quotaTokens": "Tokens pr. måned",
+ "quotaUnlimited": "Ubegrænset",
+ "quotaLoading": "Indlæser...",
+ "quotaHint": "Forbruget nulstilles hver uge (samtaler og minutter) og hver måned (tokens).",
+ "sectionMemory": "Hukommelse",
+ "memoryEmpty": "Ingen hukommelser gemt endnu. AI'en vil huske vigtige detaljer om dig fra jeres samtaler og gemme dem her.",
+ "memoryClearAll": "Ryd alle hukommelser",
+ "memoryClearAllTitle": "Ryd alle hukommelser",
+ "memoryClearAllMessage": "Alle gemte hukommelser slettes permanent. Handlingen kan ikke fortrydes.",
+ "memoryClearAllConfirm": "Ryd alle",
+ "memoryManage": "Administrer hukommelser",
+ "memoryBreadcrumb": "Hukommelsesindstillinger",
+ "memoryDescription": "Hukommelser deles på tværs af alle de sprog, du lærer. Lingu kan gemme nyttige detaljer under samtaler, og du kan tilføje eller fjerne dem her.",
+ "memoryInputLabel": "Tilføj en hukommelse",
+ "memoryInputPlaceholder": "For eksempel: Jeg foretrækker korte forklaringer.",
+ "memoryInputHint": "Højst {max} tegn",
+ "memoryAdd": "Tilføj",
+ "memoryAdding": "Tilføjer...",
+ "memoryAddSuccess": "Hukommelsen er tilføjet.",
+ "memoryAddError": "Hukommelsen kunne ikke tilføjes. Prøv igen.",
+ "memoryDuplicateError": "Den hukommelse findes allerede.",
+ "memoryLoadError": "Hukommelserne kunne ikke indlæses.",
+ "memoryDeleteLabel": "Slet hukommelse: {content}",
+ "memoryDeleteSuccess": "Hukommelsen er slettet.",
+ "memoryDeleteError": "Hukommelsen kunne ikke slettes.",
+ "memoryClearing": "Rydder...",
+ "memoryClearSuccess": "Alle hukommelser er slettet.",
+ "memoryClearError": "Hukommelserne kunne ikke ryddes.",
+ "memorySourceChat": "Chat",
+ "memorySourceVoice": "Stemme",
+ "memorySourceManual": "Manuelt",
+ "memorySourceUnknown": "Hukommelse",
+ "sectionLanguages": "Sprog",
+ "languagesManage": "Mine sprog",
+ "cardProfileAccess": "Profil og adgang",
+ "cardSessionSecurity": "Session og sikkerhed",
+ "cardTheme": "Tema",
+ "cardConversationTiming": "Samtaletid",
+ "cardTutorVoice": "Lærerens stemme",
+ "cardCurrentUsage": "Aktuelt forbrug",
+ "cardProductReview": "Produktanmeldelse",
+ "cardLegalDocuments": "Juridiske dokumenter",
+ "sectionReview": "Anmeldelse",
+ "reviewDescription": "Fortæl om din oplevelse med FreeLingo. Hvis du redigerer en anmeldelse, som allerede er godkendt, skal den vurderes igen, før den vises offentligt.",
+ "reviewPending": "Afventer godkendelse",
+ "reviewUpdate": "Opdater",
+ "reviewSaved": "Anmeldelsen er gemt.",
+ "reviewDelete": "Slet anmeldelse",
+ "reviewDeleteTitle": "Slet anmeldelse",
+ "reviewDeleteMessage": "Vil du slette din anmeldelse? Handlingen kan ikke fortrydes.",
+ "reviewDeleteConfirm": "Slet",
+ "reviewDeleted": "Anmeldelsen er slettet.",
+ "reviewDeleteError": "Vi kunne ikke slette din anmeldelse. Prøv igen."
+ },
+ "legal": {
+ "terms": {
+ "pageTitle": "Brugervilkår",
+ "updated": "Senest opdateret: august 2026",
+ "s1Title": "1. Om FreeLingo",
+ "s1Body": "FreeLingo er en open source-platform til sprogindlæring med AI, som kan bruges på to måder: (1) ved selvhosting, hvor du kører tjenesten på din egen infrastruktur uden omkostninger, og (2) som en hostet tjeneste drevet af FreeLingo-teamet med et gratis abonnement og betalte abonnementer. Ved at bruge tjenesten på en af disse måder accepterer du disse brugervilkår.",
+ "s2Title": "2. Tilladt brug",
+ "s2Intro": "Du må kun bruge FreeLingo til personlig sprogindlæring. Du accepterer ikke at:",
+ "s2i1": "Forsøge at få uautoriseret adgang til nogen del af systemet.",
+ "s2i2": "Uploade skadeligt indhold eller forsøge at forstyrre tjenesten.",
+ "s2i3": "Dele dine loginoplysninger med tredjeparter.",
+ "s2i4": "Bruge automatiserede scripts eller bots til at misbruge AI-lærerens funktioner.",
+ "s3Title": "3. Brugerkonti",
+ "s3Body": "Du skal være mindst 16 år for at oprette en konto og bruge FreeLingo. Ved at registrere dig bekræfter du, at du opfylder alderskravet. Hvis vi bliver opmærksomme på, at en konto tilhører en person under 16 år, sletter vi den og alle tilknyttede data uden varsel. Du er ansvarlig for at holde dine loginoplysninger fortrolige og for al aktivitet på din konto. Instansens operatør – dig selv ved selvhosting eller FreeLingo-teamet for den hostede tjeneste – kan suspendere eller lukke konti, som overtræder disse vilkår.",
+ "s4Title": "4. Indhold og AI-interaktioner",
+ "s4Body": "Samtaler med AI-læreren gemmes i databasen, så du kan fortsætte din læring. I den hostede tjeneste behandles AI-chat og stemmefunktioner via OpenAI's API, og dit indhold er underlagt OpenAI's brugspolitikker. AI kan give upræcise svar – brug altid din egen dømmekraft. FreeLingo er ikke ansvarlig for indhold genereret af de underliggende sprogmodeller. Indhold genereret af AI-læreren (øvelser, rettelser, forklaringer og andet materiale) tilhører dig til personlig brug. FreeLingo gør ikke krav på ejerskab over dine samtaler eller det indhold, der genereres på baggrund af dem.",
+ "s5Title": "5. Tjenestens former",
+ "s5Body": "Selvhostede installationer er gratis, og operatøren har fuld kontrol over sine data og sin infrastruktur. Den hostede tjeneste, som drives af FreeLingo-teamet, tilbyder et gratis abonnement med forbrugsgrænser og betalte abonnementer med ubegrænset adgang, enten månedligt eller årligt. Det gratis abonnement omfatter en prøveperiode på syv dage med fuld adgang (uden krav om betalingsmetode) og derefter begrænsede daglige kvoter for lektioner og AI-chat samt ugentlige kvoter for lytteøvelser, læseøvelser og stemmesamtaler. Betalte abonnementer fjerner alle forbrugsgrænser for disse funktioner. I den hostede tjeneste opbevares dine data på servere drevet af FreeLingo-teamet. Dine aktuelle forbrugsgrænser vises på din oversigt.",
+ "s6Title": "6. Abonnement og betaling",
+ "s6Body": "Det gratis abonnement giver begrænset adgang til AI-funktioner med en prøveperiode på syv dage med fuld adgang for nye brugere (uden krav om betalingsmetode). Betalte abonnementer tilbydes månedligt eller årligt og fornyes automatisk, medmindre de opsiges inden da. Du kan til enhver tid opsige dit abonnement under Indstillinger; opsigelsen træder i kraft ved udgangen af den aktuelle periode. Operatøren kan også stoppe fornyelser eller abonnementer, hvis tjenesten ændres eller lukkes. Hvis tjenesten ophører, før en betalt periode slutter, gælder enhver tilbagebetaling, som forbrugerlovgivningen kræver. Fortrydelsesret og gældende priser vises og håndteres i overensstemmelse med gældende lovgivning.",
+ "s7Title": "7. Tilgængelighed",
+ "s7Body": "Den hostede tjeneste leveres uden garanti for kontinuitet eller tilgængelighed. Operatøren kan til enhver tid ændre, suspendere eller lukke den af tekniske, økonomiske eller andre årsager. Planlagte lukninger annonceres via appen eller e-mail, når det er rimeligt muligt. Efter den annoncerede dato kan konti og driftsdata blive slettet uden mulighed for gendannelse. FreeLingo tilbyder ingen sikkerhedskopieringstjeneste eller automatisk eksportfunktion.",
+ "s8Title": "8. Ansvarsbegrænsning",
+ "s8Body": "FreeLingo og dets bidragydere er ikke ansvarlige for direkte, indirekte, hændelige eller følgeskader, der opstår som følge af din brug af eller manglende mulighed for at bruge tjenesten. I det omfang gældende lovgivning tillader det, kan det samlede ansvar ikke overstige det beløb, du har betalt i de tre måneder før kravet. Du accepterer at holde FreeLingo og dets bidragydere skadesløse for krav, skader eller udgifter fra tredjeparter, der opstår som følge af din brug af tjenesten i strid med disse vilkår.",
+ "s9Title": "9. Lovvalg",
+ "s9Body": "Disse vilkår er underlagt spansk lovgivning. Tvister, der udspringer af vilkårene, er underlagt de spanske domstoles enekompetence, medmindre ufravigelige forbrugerbeskyttelsesregler i dit bopælsland bestemmer andet.",
+ "s10Title": "10. Ændringer i vilkårene",
+ "s10Body": "Vilkårene kan opdateres til enhver tid. Væsentlige ændringer meddeles via tjenestens brugerflade eller e-mail, når det er passende. Fortsat brug efter en ændring betyder, at du accepterer de reviderede vilkår.",
+ "s11Title": "11. Ugyldige bestemmelser",
+ "s11Body": "Hvis en kompetent domstol finder en bestemmelse i disse vilkår ugyldig eller uden retskraft, begrænses eller fjernes den i mindst muligt omfang, mens de øvrige bestemmelser fortsat gælder fuldt ud.",
+ "linkPrivacy": "Privatlivspolitik",
+ "linkBack": "Tilbage til registrering",
+ "linkBackSettings": "Tilbage til Indstillinger",
+ "linkContact": "Hvis du har spørgsmål om vilkårene, kan du bruge kontaktformularen på freelingo.app, så længe den er tilgængelig, eller kontaktvejen angivet i FreeLingos GitHub-arkiv."
+ },
+ "privacy": {
+ "pageTitle": "Privatlivspolitik",
+ "updated": "Senest opdateret: august 2026",
+ "s1Title": "1. Overblik",
+ "s1Body": "FreeLingo tilbydes som en selvhostet app og som en hostet tjeneste. Ved selvhosting opbevares alle data udelukkende på operatørens server, og FreeLingo-projektets ophavspersoner har ikke adgang til dem. For den hostede tjeneste er FreeLingo-teamet dataansvarlig, og denne politik beskriver, hvordan dine personoplysninger indsamles, bruges og beskyttes.",
+ "s2Title": "2. Oplysninger, vi indsamler",
+ "s2Intro": "FreeLingo henvender sig ikke til børn under 16 år. Vi indsamler ikke bevidst personoplysninger fra personer under 16 år. Hvis du er under 16 år, må du ikke oprette en konto eller angive personoplysninger. Når du opretter en konto og bruger FreeLingo, gemmes følgende oplysninger:",
+ "s2i1": "Brugernavn, visningsnavn og e-mailadresse.",
+ "s2i2": "Hashet adgangskode (bcrypt – din adgangskode i klartekst gemmes aldrig).",
+ "s2i3": "Valg af modersmål og læringsindstillinger.",
+ "s2i4": "Valgfrit profilbillede.",
+ "s2i5": "Historik over chats og samtaler med AI-læreren.",
+ "s2i6": "Flashcards, læringsfremskridt og data til gentagelser fordelt over tid.",
+ "s2i7": "Resultater fra niveauvurderinger og fremskridt i læringsplanen.",
+ "s2i8": "Abonnementsstatus, plantype og Stripe-kunde-id (for betalende abonnenter).",
+ "s2i9": "Globale kontohukommelser og præferencer, som AI-læreren lærer under samtaler, eller som du selv tilføjer for at tilpasse fremtidige interaktioner på tværs af læringssprog.",
+ "s2i10": "Funktionsforslag, fejlrapporter, kommentarer og stemmer, som du indsender via feedbacktavlen. Kun dit brugernavn vises offentligt; alle brugere kan se offentliggjorte forslag og rapporter.",
+ "s3Title": "3. Sådan bruger vi dine oplysninger",
+ "s3Intro": "Dine oplysninger bruges udelukkende til at give dig FreeLingos læringsoplevelse:",
+ "s3i1": "Til at bekræfte din identitet og opretholde din session.",
+ "s3i2": "Til at tilpasse din læringsplan og dine samtaler med læreren.",
+ "s3i3": "Til at følge dine læringsfremskridt over tid.",
+ "s3i4": "Til at generere kontekstafhængige svar fra AI-læreren.",
+ "s3i5": "Til at huske dine præferencer og din situation, så læreren kan tilpasse fremtidige interaktioner.",
+ "s3i6": "Til at vise og administrere brugernes funktionsforslag og fejlrapporter.",
+ "s3Footer": "Dine oplysninger bliver aldrig solgt, udlejet eller delt med tredjeparters markedsføringstjenester.",
+ "s4Title": "4. Eksterne AI-tjenester",
+ "s4Body": "I den hostede tjeneste leveres AI-chat, talesyntese (TTS) og transskription af tale (STT) via OpenAI's API. Indholdet af dine samtaler og lyd sendes til OpenAI til behandling i henhold til OpenAI's privatlivspolitik (openai.com/privacy). Betalinger håndteres af Stripe, Inc. (USA); Stripe behandler betalingsoplysninger direkte, og vi gemmer ikke kortoplysninger på vores servere – dine transaktionsoplysninger er omfattet af Stripes privatlivspolitik (stripe.com/privacy). Ved selvhosting vælger operatøren AI-udbyder; som standard bruges en lokal Ollama-instans, og ingen data forlader serveren.",
+ "s5Title": "5. Cookies og sessionstokens",
+ "s5Body": "FreeLingo bruger to tekniske cookies, som er strengt nødvendige for tjenestens funktion: refresh_token (httpOnly, udløber efter 30 dage) til at bekræfte sessionen og NEXT_LOCALE (udløber efter et år) til at huske dit sprogvalg. Der bruges ingen sporings- eller reklamecookies. Til anonym trafikmåling bruger vi Umami – et selvhostet open source-analyseværktøj, der ikke bruger cookies, ikke indsamler personoplysninger og gemmer alle data på vores egne servere.",
+ "s6Title": "6. Dine rettigheder (GDPR)",
+ "s6Body": "Hvis du befinder dig i Det Europæiske Økonomiske Samarbejdsområde, har du efter GDPR ret til indsigt, berigtigelse, sletning, indsigelse og dataportabilitet. Du kan selv slette din konto under Indstillinger. Andre anmodninger behandles manuelt via kontakten i afsnit 8; FreeLingo tilbyder ikke et automatisk eksportværktøj. Vi behandler personoplysninger for at levere tjenesten (GDPR artikel 6, stk. 1, litra b). OpenAI og Stripe kan behandle oplysninger i USA under de sikkerhedsforanstaltninger, der gælder for internationale overførsler. Du kan klage til den spanske databeskyttelsesmyndighed (aepd.es) eller din lokale tilsynsmyndighed.",
+ "s7Title": "7. Opbevaring og sletning af oplysninger",
+ "s7Body": "Driftsdata gemmes, så længe din konto findes, og den hostede tjeneste er aktiv. Du kan slette hukommelser, sprog eller hele din konto under Indstillinger. Operatøren kan også slette alle konti og oplysninger, hvis tjenesten lukkes, efter et rimeligt varsel, når det er muligt. Sletning kan ikke fortrydes. Når systemer og kopier under operatørens kontrol er slettet, opbevarer FreeLingo ingen personoplysninger. Stripe kan opbevare betalings- og transaktionsoplysninger i henhold til sine egne regler og lovmæssige forpligtelser.",
+ "s8Title": "8. Kontakt",
+ "s8Body": "Hvis du har spørgsmål om privatliv i den hostede tjeneste, kan du bruge kontaktformularen på freelingo.app, så længe den er tilgængelig. Efter en lukning kan du bruge kontaktvejen angivet i FreeLingos GitHub-arkiv, men du må ikke skrive personoplysninger i offentlige issues. Ved selvhosting skal du kontakte instansens administrator.",
+ "s9Title": "9. Ugyldige bestemmelser",
+ "s9Body": "Hvis en bestemmelse i denne politik viser sig at være ugyldig eller ikke kan håndhæves, begrænses eller fjernes den i mindst muligt omfang, og de øvrige bestemmelser gælder fortsat fuldt ud.",
+ "linkTerms": "Brugervilkår",
+ "linkBack": "Tilbage til registrering",
+ "linkBackSettings": "Tilbage til Indstillinger"
+ }
+ },
+ "dashboard": {
+ "welcomeBack": "Velkommen tilbage",
+ "streak": "Stime",
+ "xp": "XP",
+ "level": "Niveau",
+ "skills": "Færdigheder",
+ "today": "I dag",
+ "noSkills": "Gennemfør niveauvurderingen for at følge dine færdigheder.",
+ "noLessons": "Ingen lektioner planlagt i dag.",
+ "lessonDone": "færdig",
+ "startLesson": "Start",
+ "loadingProgress": "Indlæser fremskridt...",
+ "noLevel": "Intet niveau angivet endnu.",
+ "completeAssessment": "Tag niveauvurderingen",
+ "allCaughtUp": "Din plan, i dit tempo",
+ "levelTestReady": "Din niveautest er klar.",
+ "levelTestReadyDesc": "Du er nået til slutningen af din plan. Tag niveautesten for at bekræfte dine fremskridt.",
+ "levelTestCompleted": "Niveautesten er gennemført.",
+ "levelTestScoreLine": "Slutresultat: {score}. Dit resultat er gemt i Min plan.",
+ "startWithAssessment": "Start med en niveauvurdering.",
+ "takeAssessmentArrow": "Tag niveauvurderingen →",
+ "goToMyPlan": "Min plan →",
+ "quickActions": "Hurtige handlinger",
+ "dayProgress": "Dag {current} af {total}",
+ "skipDay": "Spring i dag over",
+ "pendingLessons": "afventende lektioner",
+ "nextStep": "Næste trin",
+ "assessmentCreatesPlan": "Niveauvurderingen finder dit niveau og opretter en personlig plan for dit aktive sprog.",
+ "lessonReady": "Dit næste trin, i dit tempo",
+ "pendingStillAvailable": "Du har {count} tidligere lektioner, der venter i din plan.",
+ "noPendingToday": "Se i din plan, hvordan du kan fortsætte.",
+ "planDayGoal": "Dagens mål i din plan",
+ "lessonsCompleted": "Lektioner",
+ "accuracy": "Træfsikkerhed",
+ "exerciseStats": "{correct}/{total} øvelser",
+ "noExercisesYet": "Ingen øvelser endnu",
+ "planProgress": "Fremskridt i planen",
+ "currentDay": "Aktuel dag",
+ "daysRemaining": "Dage tilbage",
+ "completedToday": "{completed}/{total} færdige",
+ "vocabularyProgress": "Ordforråd {level}",
+ "vocabularyWords": "{mastered}/{total} ord",
+ "recentPerformance": "Seneste resultater",
+ "recentPerformanceDescription": "Baseret på dine seneste svar og repetitionstræning.",
+ "performanceNeedsPractice": "Kræver øvelse",
+ "performanceInProgress": "I gang",
+ "performanceStrong": "Stærk",
+ "premiumBannerTitle": "Lås op for samtale, lytning og læsning",
+ "premiumBannerDesc": "Begynd med en gratis prøveperiode, og øv dig med Lingu via tekst, stemme og øvelser tilpasset dit niveau.",
+ "premiumBannerDescTrialUsed": "Aktivér Premium for at låse op for tekstchat med Lingu, stemmesamtaler, læsning og lytning.",
+ "premiumBannerCta": "Gratis prøveperiode",
+ "premiumBannerCtaTrialUsed": "Se abonnementer",
+ "premiumBannerPastDueTitle": "Betaling afventer",
+ "premiumBannerPastDueDesc": "Opdater din betalingsmetode for at få Premium-adgang til chat, stemmesamtaler, læsning og lytning igen.",
+ "premiumBannerPastDueCta": "Gendan Premium",
+ "freemiumTrialTitle": "{days, plural, one {# dag med gratis prøveperiode} other {# dage med gratis prøveperiode}}",
+ "freemiumTrialDesc": "Få ubegrænset adgang til chat, stemme, lytning og læsning. Prøv alle Premium-funktioner gratis.",
+ "announcementDismiss": "Luk meddelelsen",
+ "announcementDismissError": "Meddelelsen kunne ikke lukkes. Prøv igen."
+ },
+ "assessment": {
+ "title": "Niveauvurdering",
+ "subtitle": "Placering efter CEFR",
+ "intro": "Vi stiller dig nogle spørgsmål for at vurdere dit engelskniveau.",
+ "startWriting": "Start skrivevurdering",
+ "startQuiz": "Start quiz",
+ "writing": {
+ "title": "Skriveprøve",
+ "submit": "Send tekst",
+ "placeholder": "Skriv dit svar her...",
+ "evaluating": "Vurderer..."
+ },
+ "quiz": {
+ "question": "Spørgsmål",
+ "of": "af",
+ "next": "Næste",
+ "submit": "Send quiz",
+ "submitting": "Sender..."
+ },
+ "result": {
+ "title": "Dit niveau",
+ "xpEarned": "Optjente XP",
+ "skillsTitle": "Færdigheder",
+ "goToPlan": "Se min plan",
+ "generatePlan": "Opret læringsplan"
+ },
+ "currentLevel": "Aktuelt niveau",
+ "assessedOn": "Vurderet den",
+ "alreadyHasPlan": "Du har allerede en aktiv læringsplan. Hvis du tager vurderingen igen, oprettes en ny.",
+ "retake": "Tag niveauvurderingen igen",
+ "evaluating": "Vurderer...",
+ "resultStep": "Resultat",
+ "cefrLevel": "CEFR-niveau",
+ "overrideLevel": "Ændr startniveau",
+ "strengths": "Styrker",
+ "needsWork": "Kræver øvelse",
+ "createPlan": "Opret plan →",
+ "creating": "Opretter...",
+ "loading": "Indlæser...",
+ "levelTest": {
+ "title": "Niveautest",
+ "loading": "Indlæser spørgsmål...",
+ "loadingTest": "Indlæser test...",
+ "submittingTest": "Sender svar...",
+ "quizTitle": "Niveautest for {level}",
+ "resultsTitle": "Niveautest for {level} – Resultater",
+ "finalScore": "Slutresultat",
+ "correctCount": "{correct} / {total} rigtige",
+ "correctAnswer": "✓ Rigtigt",
+ "incorrectAnswer": "✗ Forkert – korrekt svar: {answer}",
+ "nextLevelFallback": "NÆSTE NIVEAU",
+ "advanceLabel": "KLAR TIL {level}",
+ "advanceMessage": "Du har vist gode færdigheder på niveau {level}. Tag niveauvurderingen igen for at oprette din plan for {next}.",
+ "extendLabel": "FORLÆNGELSE PÅ FIRE UGER",
+ "extendMessage": "Vi har fundet områder, der kræver øvelse: {areas}. Vi anbefaler fire ekstra uger med målrettet træning.",
+ "repeatLabel": "GENTAG {level}",
+ "repeatMessage": "Resultat under 55 %. Flere vigtige færdigheder på niveau {level} skal styrkes.",
+ "reviewPlan": "Gennemgå min plan →",
+ "goToPlan": "Gå til planen",
+ "invalidPlan": "Ugyldigt plan-id. Åbn niveautesten fra din planside.",
+ "noQuestions": "Ingen spørgsmål modtaget fra serveren.",
+ "loadFailed": "Niveautesten kunne ikke indlæses.",
+ "submitFailed": "Svarene kunne ikke sendes.",
+ "question": "Spørgsmål",
+ "of": "af",
+ "confirm": "Bekræft svar",
+ "nextQuestion": "Næste spørgsmål",
+ "submit": "Send test",
+ "result": {
+ "title": "Testen er færdig",
+ "recommendation": "Anbefaling",
+ "advance": "Klar til at gå videre til næste niveau",
+ "extend": "Fortsæt på dit nuværende niveau",
+ "repeat": "Det anbefales at gentage dit nuværende niveau",
+ "back": "Tilbage til planen"
+ }
+ },
+ "suggestedLevel": "Foreslået niveau {aiLevel}; planen bruger {selectedLevel}",
+ "step1": "Trin 1 / 3: Dit niveau",
+ "step2": "Trin 2 / 3: Spørgsmål {questionNumber} / {totalQuestions}",
+ "dontKnow": "Jeg ved det ikke",
+ "step3": "Trin 3 / 3: Dit program",
+ "studiedBefore": "Har du læst {language} før?",
+ "studiedBeforeHint": "Det hjælper os med at starte quizzen på det rigtige niveau.",
+ "beginnerOption": "Jeg er helt nybegynder",
+ "beginnerOptionHint": "Jeg starter på A1 og behøver ikke en quiz.",
+ "hasExperienceOption": "Ja, jeg har lidt erfaring",
+ "hasExperienceOptionHint": "Jeg tager en kort, adaptiv quiz (cirka 12 spørgsmål).",
+ "skills": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordforråd",
+ "reading": "Læsning"
+ },
+ "howManyWeeks": "Hvor mange uger til niveau {cefr_level}?",
+ "nWeeks": "{count} uger",
+ "intensity": {
+ "intensive": "Intensiv",
+ "standard": "Standard",
+ "relaxed": "Rolig",
+ "veryRelaxed": "Meget rolig"
+ },
+ "approxLessons": "Cirka {count} lektioner",
+ "daysPerWeek": "{count} dage/uge",
+ "mainGoals": "Primære mål (vælg alle relevante)",
+ "goals": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordforråd",
+ "reading": "Læsning",
+ "writing": "Skrivning",
+ "conversation": "Samtale",
+ "listening": "Lytning"
+ },
+ "summaryLevel": "Niveau",
+ "summaryDuration": "Varighed",
+ "summaryGoals": "Mål",
+ "noneSelected": "ingen valgt",
+ "buildingPlan": "Opretter plan...",
+ "startWarningTitle": "Før du begynder",
+ "startWarningMessage": "Hvis du forlader niveauvurderingen, før du er færdig, mister du dine fremskridt og skal starte forfra.",
+ "startWarningMessageLevelTest": "Hvis du forlader niveautesten, før du er færdig, mister du dine fremskridt og skal tage den igen.",
+ "startWarningConfirm": "Start",
+ "startMyPlan": "Start min plan",
+ "voiceTrialLabel": "Prøv stemmen",
+ "voiceTrialTitle": "Prøv en rigtig samtale nu",
+ "voiceTrialDesc": "Tal med Lingu i {minutes} minutter gratis. Din lærer tilpasser sig det niveau, du lige har opnået.",
+ "voiceTrialStart": "Start gratis samtale",
+ "voiceTrialSkip": "Spring over indtil videre"
+ },
+ "plan": {
+ "unitLocked": "Enheden er låst",
+ "unitAvailable": "Enheden er tilgængelig",
+ "lessonPending": "Afventende lektion",
+ "title": "Min plan",
+ "subtitle": "Læringsplan",
+ "noPlan": "Ingen aktiv læringsplan fundet.",
+ "startAssessment": "Tag niveauvurderingen først",
+ "week": "Uge",
+ "currentLesson": "Aktuel lektion",
+ "currentUnit": "Aktuel enhed",
+ "startLesson": "Start lektion",
+ "completed": "Gennemført",
+ "generating": "Opretter lektion...",
+ "generatingError": "Lektionen kunne ikke oprettes",
+ "loading": "Indlæser plan...",
+ "learningRoadmap": "Læringsforløb",
+ "level": "Niveau",
+ "duration": "Varighed",
+ "durationDetail": "{weeks} uger · {days} dage/uge",
+ "unitsLabel": "Enheder",
+ "noUnitsForLevel": "Ingen enheder tilgængelige for niveau {level}",
+ "takeAssessmentArrow": "Tag niveauvurderingen →",
+ "unitLabel": "Enhed",
+ "grammarCovered": "Gennemgået grammatik",
+ "noLessons": "Ingen lektioner planlagt for denne enhed endnu.",
+ "weekDay": "Uge {week}, dag {day}",
+ "lessonTypes": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordforråd",
+ "reading": "Læsning",
+ "writing": "Skrivning",
+ "listening": "Lytning",
+ "conversation": "Samtale",
+ "review": "Gentagelse",
+ "level_test": "Niveautest"
+ },
+ "levelComplete": "Niveau {level} gennemført. Tag niveautesten",
+ "levelCompleteDesc": "Du er nået til slutningen af din plan for niveau {level}. Tag niveautesten for at bekræfte dine fremskridt og låse op for næste niveau.",
+ "levelCompleteHint": "Testen tager cirka 20 minutter og dækker grammatik, ordforråd og læsning.",
+ "beginLevelTest": "Start niveautesten",
+ "nLessons": "{count, plural, one {# lektion} other {# lektioner}}",
+ "nGrammar": "{count} grammatiklektioner",
+ "levelTestLabel": "Niveautest",
+ "unitAriaLabel": "Enhed {index}: {title}",
+ "noUnitsDesc": "Indholdet til dette niveau er ved at blive forberedt. Kom tilbage snart.",
+ "completionTestTitle": "Afsluttende test for niveau {level}",
+ "levelTestResult": "Resultat af niveautest",
+ "testScore": "Score:",
+ "pendingLessons": "Afventende lektioner",
+ "pendingReassurance": "Du kan fortsætte med dem, når det passer dig.",
+ "resume": "Fortsæt",
+ "lessonsHeader": "Lektioner · {count}",
+ "reviewLesson": "Gentagelse",
+ "noPlanLabel": "KOM I GANG",
+ "noPlanTitle": "Opret din læringsplan",
+ "noPlanDesc": "Tag en kort niveauvurdering for at finde dit niveau og få et personligt læringsforløb.",
+ "noPlanHint": "Tager cirka 10 minutter"
+ },
+ "lesson": {
+ "loading": "Indlæser lektion...",
+ "notFound": "Lektionen blev ikke fundet",
+ "backToPlan": "Tilbage til planen",
+ "progress": "Fremskridt",
+ "exercise": "Øvelse",
+ "of": "af",
+ "checkAnswer": "Tjek svar",
+ "nextExercise": "Næste øvelse",
+ "submitWriting": "Send tekst",
+ "yourAnswer": "Dit svar...",
+ "correct": "Rigtigt",
+ "incorrect": "Forkert",
+ "feedback": "Feedback",
+ "corrections": "Rettelser",
+ "completeLesson": "Gennemfør lektion",
+ "completing": "Gennemfører...",
+ "relatedGrammar": "Relateret grammatik",
+ "explanation": "Forklaring",
+ "exercises": "Øvelser",
+ "vocabulary": "Ordforråd",
+ "label": "Lektion",
+ "submitAnswer": "Send svar",
+ "lessonDone": "Godt arbejde. Lektionen er gennemført.",
+ "exerciseSummary": "Vurderede øvelser: {completed} af {total}",
+ "score": "Score",
+ "showNativeExplanation": "Vis på",
+ "hint": "Tip",
+ "showNativeHint": "Vis tip på",
+ "regenerateExercise": "Opret øvelsen igen",
+ "regeneratingExercise": "Opretter igen...",
+ "regenerateError": "Øvelsen kunne ikke oprettes igen. Prøv igen.",
+ "regenerateNotNeeded": "Denne øvelse behøver ikke at blive oprettet igen.",
+ "examples": "Eksempler",
+ "commonTraps": "Almindelige faldgruber",
+ "miniGlossary": "Kort ordliste",
+ "pronunciationExercise": "Udtale",
+ "exerciseTypeMultipleChoice": "Multiple choice",
+ "exerciseTypeFillBlank": "Udfyld det manglende ord",
+ "exerciseTypeFreeWrite": "Fri skrivning",
+ "exerciseTypePronunciation": "Udtale",
+ "tapToListen": "Tryk for at lytte",
+ "recordNow": "Optag dit svar",
+ "exit": "Afslut",
+ "exitConfirmTitle": "Forlad lektionen?",
+ "exitConfirmMessage": "Dine fremskridt i denne lektion går tabt.",
+ "dayComplete": "Dagen er klaret!",
+ "dayCompleteMsg": "Du har gennemført denne dag i din plan. Fortsæt, når det passer dig."
+ },
+ "flashcards": {
+ "title": "Flashcards",
+ "due": "Klar til repetition",
+ "total": "I alt",
+ "noDue": "Ingen kort skal repeteres lige nu",
+ "allCaughtUp": "Du er helt ajour!",
+ "generate": "Opret flashcards",
+ "generating": "Opretter...",
+ "topic": "Emne",
+ "topicPlaceholder": "f.eks. ordforråd til rejser",
+ "count": "Antal",
+ "level": "Niveau",
+ "submit": "Opret",
+ "again": "Igen",
+ "hard": "Svært",
+ "good": "Godt",
+ "easy": "Let",
+ "front": "Ord",
+ "back": "Definition",
+ "loading": "Indlæser flashcards...",
+ "sectionGenerate": "Opret nye kort",
+ "speakingMode": "Tale",
+ "standardMode": "Standard",
+ "sayWord": "Sig ordet højt",
+ "refresh": "Opdater",
+ "generateBtn": "Opret",
+ "cards": "kort",
+ "myVocabularyBtn": "Mit ordforråd",
+ "myVocabulary": "Mit ordforråd",
+ "myVocabularyEmpty": "Ingen gemte ord endnu. Markér et ord, mens du læser, for at gemme det her.",
+ "myVocabularyNoResults": "Ingen gemte ord passer til din søgning.",
+ "vocabularySearch": "Søg efter ord...",
+ "vocabularyPageInfo": "Side {page} af {pages}",
+ "vocabularyPrev": "Forrige",
+ "vocabularyNext": "Næste",
+ "backToFlashcards": "Flashcards",
+ "noCardsHint": "Brug + Opret for at lave dine første AI-genererede kort",
+ "tapToHide": "tryk for at skjule",
+ "tapToReveal": "tryk for at vise",
+ "interval": "Interval",
+ "repetitions": "Gentagelser"
+ },
+ "chat": {
+ "title": "AI-lærer",
+ "subtitle": "{language} – Spørg om hvad som helst",
+ "placeholder": "Skriv en besked...",
+ "send": "Send",
+ "sending": "Sender...",
+ "noConversation": "Vælg eller start en samtale",
+ "newConversation": "Ny",
+ "toggleSidebarShow": "Vis chats",
+ "toggleSidebarHide": "Skjul chats",
+ "conversations": "Samtaler",
+ "voiceSession": "Stemmesamtale",
+ "loading": "Indlæser...",
+ "thinking": "Tænker...",
+ "takingLonger": "Det tager længere tid end forventet...",
+ "errorMessage": "Noget gik galt. Prøv igen.",
+ "deleteTitle": "Slet chat",
+ "deleteMessage": "Denne samtale slettes permanent.",
+ "deleteConfirm": "Slet",
+ "enterToSend": "Tryk Enter for at sende",
+ "continueInVoice": "Fortsæt med stemmen",
+ "memoryUpdated": "Hukommelsen er opdateret"
+ },
+ "progress": {
+ "title": "Fremskridt",
+ "subtitle": "Følg dine færdigheder",
+ "xp": "XP",
+ "streak": "Stime",
+ "lessons": "Lektioner",
+ "accuracy": "Træfsikkerhed",
+ "noPlan": "Ingen aktiv læringsplan.",
+ "startPlan": "Start niveauvurderingen",
+ "competencies": "Kompetencer for hver enhed",
+ "vocabularySection": "Ordforråd",
+ "skills": "Træfsikkerhed pr. færdighed",
+ "mastered": "Behersket",
+ "inProgress": "I gang",
+ "notStarted": "Ikke startet",
+ "loading": "Indlæser fremskridt...",
+ "days": "dage",
+ "noData": "Gennemfør nogle lektioner for at se dine fremskridt her.",
+ "exploreVocab": "Udforsk ordsamlinger",
+ "competenciesSection": "Grammatikkompetencer for {level}",
+ "noCompetencies": "Gennemfør lektioner for at begynde at følge dine kompetencer.",
+ "goToMyPlan": "→ Gå til Min plan",
+ "addToFlashcards": "Tilføj ord fra en samling til dine flashcards for at følge dine fremskridt med hvert ord.",
+ "noActivePlanDesc": "Tag niveauvurderingen for at begynde at følge dine færdigheder.",
+ "vocabularyHeader": "Ordforråd for {level}",
+ "currentLevelOnly": "Kun aktuelt niveau",
+ "allLevels": "Alle niveauer"
+ },
+ "grammar": {
+ "title": "Grammatikoversigt",
+ "subtitle": "Opslagsværk",
+ "searchPlaceholder": "Søg efter grammatikemner...",
+ "allLevels": "Alle niveauer",
+ "allCategories": "Alle",
+ "noResults": "Ingen emner fundet.",
+ "structure": "Opbygning",
+ "keyRules": "Vigtige regler",
+ "examples": "Eksempler",
+ "commonMistakes": "Almindelige fejl",
+ "relatedTopics": "Relaterede emner",
+ "backToGrammar": "Grammatikoversigt",
+ "explanation": "Forklaring",
+ "backLink": "Tilbage til grammatik",
+ "notFound": "Emnet blev ikke fundet.",
+ "topicsFound": "{count, plural, one {# emne fundet} other {# emner fundet}}"
+ },
+ "vocabulary": {
+ "title": "Ordforråd",
+ "subtitle": "Ordforråd",
+ "all": "Alle",
+ "searchPlaceholder": "Søg efter ordsamlinger...",
+ "sets": "{count, plural, one {# samling} other {# samlinger}}",
+ "words": "ord",
+ "addAll": "Tilføj alle {count} som flashcards",
+ "adding": "Tilføjer...",
+ "added": "{count} flashcards tilføjet",
+ "addError": "Flashcards kunne ikke tilføjes",
+ "frequency": "hyppighed",
+ "backToVocabulary": "Ordforråd",
+ "notFound": "Samlingen blev ikke fundet.",
+ "vocabularySet": "Ordsamling",
+ "cardsAdded": "{count} kort er tilføjet til dine flashcards.",
+ "goToFlashcards": "Gå til flashcards →",
+ "addFailed": "Flashcards kunne ikke tilføjes.",
+ "noResults": "Ingen ordsamlinger passer til din søgning"
+ },
+ "phrasebook": {
+ "title": "Parlør",
+ "subtitle": "Fraser",
+ "all": "Alle",
+ "allRegisters": "Alle stilniveauer",
+ "formal": "Formelt",
+ "neutral": "Neutralt",
+ "informal": "Uformelt",
+ "copied": "Kopieret!",
+ "copy": "Kopiér",
+ "copyPhrase": "Kopiér frase",
+ "noResults": "Ingen fraser passer til dine filtre",
+ "level": "Niveau",
+ "register": "Stilniveau",
+ "statsLine": "{situationCount} situationer · {phraseCount} fraser · {range}",
+ "situationsShown": "{count, plural, one {# situation vises} other {# situationer vises}}",
+ "searchPlaceholder": "Søg efter fraser..."
+ },
+ "admin": {
+ "title": "Administration",
+ "overview": "Oversigt",
+ "users": "Brugere",
+ "feedback": "Feedback",
+ "reviews": "Anmeldelser",
+ "system": "System",
+ "invites": "Invitationslinks",
+ "createUser": "Opret bruger",
+ "createInvite": "Opret invitation",
+ "deactivate": "Deaktiver",
+ "activate": "Aktiver",
+ "loading": "Indlæser...",
+ "loadError": "Brugerdata kunne ikke indlæses",
+ "userNotFound": "Brugeren blev ikke fundet",
+ "role": "Rolle",
+ "status": "Status",
+ "active": "Aktiv",
+ "inactive": "Inaktiv",
+ "inviteCreated": "Invitationslink oprettet",
+ "copyLink": "Kopiér link",
+ "inviteCopied": "Invitationslink kopieret",
+ "deleteUser": "Slet bruger",
+ "deleteUserMessage": "Vil du slette denne bruger? Handlingen kan ikke fortrydes.",
+ "deleteConfirm": "Slet",
+ "noUsers": "Ingen brugere fundet",
+ "inviteLink": "Invitationslink (gyldigt i 48 timer)",
+ "total": "i alt",
+ "delete": "Slet",
+ "inviteBtn": "Invitationslink",
+ "createUserBtn": "Opret bruger",
+ "prevPage": "← Forrige",
+ "nextPage": "Næste →",
+ "roleUser": "Bruger",
+ "roleAdmin": "Administrator",
+ "fieldUsername": "Brugernavn",
+ "invalidUsernameChars": "Brugernavnet må kun indeholde bogstaver, tal, bindestreger, understreger og punktummer",
+ "fieldEmail": "E-mailadresse",
+ "fieldPassword": "Adgangskode",
+ "fieldDisplayName": "Visningsnavn",
+ "fieldNativeLanguage": "Modersmål",
+ "fieldTargetLanguage": "Sprog, der skal læres",
+ "fieldRole": "Kontotype",
+ "submitCreate": "Opret",
+ "searchPlaceholder": "Søg efter brugernavn eller e-mailadresse...",
+ "statsTitle": "Forbrugsstatistik",
+ "statsCefr": "Aktuelt niveau",
+ "statsUnit": "Aktuel enhed",
+ "statsPlanWeeks": "Planens varighed",
+ "sectionXpProgress": "XP og fremskridt",
+ "statsXp": "XP i alt",
+ "statsStreak": "Stime",
+ "statsActiveDays": "Aktive dage",
+ "statsLessons": "Gennemførte lektioner",
+ "statsExercises": "Korrekte øvelser",
+ "statsMessages": "Sendte beskeder",
+ "statsTokensTotal": "Brugte tokens (i alt)",
+ "statsTokensChat": "Tokens til læreren",
+ "statsTokensConversation": "Tokens til stemme",
+ "statsTokensNote": "Kun tilgængeligt, når AI-udbyderen sender disse oplysninger",
+ "statsNoData": "Ingen fremskridtsdata",
+ "viewStats": "Se statistik",
+ "weeks": "uger",
+ "statsTestScore": "Slutresultat på test",
+ "statsDays": "dage",
+ "sectionQuota": "Samtalekvoter",
+ "quotaWeeklySessions": "Grænse for samtaler pr. uge",
+ "quotaDailyMinutes": "Grænse for minutter pr. dag",
+ "quotaWeeklyMinutes": "Grænse for minutter pr. uge",
+ "quotaMonthlyTokens": "Grænse for tokens pr. måned",
+ "quotaUnlimitedLabel": "Ubegrænset (0)",
+ "quotaUsedSessions": "Brugt i denne uge",
+ "quotaUsedMinutes": "Brugt i dag",
+ "quotaUsedWeeklyMinutes": "Brugt i denne uge (min)",
+ "quotaSave": "Gem kvoter",
+ "quotaSaved": "Kvoterne er gemt.",
+ "emailVerified": "E-mailadresse bekræftet",
+ "emailNotVerified": "E-mailadresse ikke bekræftet",
+ "verifyEmail": "Bekræft e-mailadresse",
+ "unverifyEmail": "Markér som ubekræftet",
+ "emailVerifiedSuccess": "E-mailadressen er markeret som bekræftet.",
+ "emailUnverifiedSuccess": "E-mailadressen er markeret som ubekræftet.",
+ "fieldSubscription": "Abonnement",
+ "fieldEndsRenews": "Udløber / Fornyes",
+ "fieldStripeCustomer": "Stripe-kunde",
+ "maintenanceTitle": "Vedligeholdelsestilstand",
+ "maintenanceOn": "TIL",
+ "maintenanceEnable": "Aktivér vedligeholdelse",
+ "maintenanceDisable": "Deaktivér vedligeholdelse",
+ "maintenanceDesc": "Når tilstanden er aktiv, er alle funktioner, der kræver abonnement (chat med læreren, stemmesamtaler samt lytte- og læseøvelser), utilgængelige for alle brugere.",
+ "cannotDeactivateSelf": "Du kan ikke deaktivere din egen konto",
+ "cannotDeleteSelf": "Du kan ikke slette din egen konto",
+ "sectionLanguages": "Sprog",
+ "sectionActivity": "Aktivitet",
+ "statsActive": "Aktiv",
+ "manageUsers": "Administrer brugere",
+ "manageUsersDesc": "Gennemgå konti, abonnementer, roller og adgangsstatus.",
+ "createUserDesc": "Opret en administreret brugerkonto med et første læringssprog.",
+ "reviewFeedback": "Gennemgå feedback",
+ "reviewFeedbackDesc": "Håndter funktionsforslag og fejlrapporter fra brugerne.",
+ "reviewReviews": "Gennemgå anmeldelser",
+ "reviewReviewsDesc": "Godkend verificerede anmeldelser til visning på forsiden.",
+ "maintenanceOnDesc": "Vedligeholdelsestilstand er aktiv for læringsværktøjer, der kræver abonnement.",
+ "maintenanceOffDesc": "Vedligeholdelsestilstand er deaktiveret. Læringsværktøjer, der kræver abonnement, er tilgængelige.",
+ "openSystemControls": "Systemindstillinger",
+ "adminRequired": "Administratoradgang påkrævet",
+ "usersLoadError": "Brugerne kunne ikke indlæses",
+ "createUserError": "Brugeren kunne ikke oprettes",
+ "updateUserError": "Brugeren kunne ikke opdateres",
+ "deleteUserError": "Brugeren kunne ikke slettes",
+ "inviteError": "Invitationslinket kunne ikke oprettes",
+ "copyInviteError": "Invitationslinket kunne ikke kopieres",
+ "maintenanceError": "Vedligeholdelsestilstand kunne ikke ændres",
+ "maintenanceOff": "FRA",
+ "inviteExpiry": "Engangslink. Udløber efter 48 timer.",
+ "searchAction": "Søg efter brugere",
+ "roleFilter": "Filtrér efter rolle",
+ "statusFilter": "Filtrér efter status",
+ "subscriptionFilter": "Filtrér efter abonnement",
+ "allRoles": "Alle roller",
+ "allStatuses": "Alle statusser",
+ "allSubscriptions": "Alle abonnementer",
+ "clearFilters": "Ryd",
+ "userColumn": "Bruger",
+ "actions": "Handlinger",
+ "createUserSheetDesc": "Kontoen oprettes med det samme. Der sendes en bekræftelsesmail, hvis e-mail er aktiveret.",
+ "activateUser": "Aktivér bruger",
+ "deactivateUser": "Deaktivér bruger",
+ "activateUserMessage": "Vil du aktivere denne brugerkonto?",
+ "deactivateUserMessage": "Vil du deaktivere denne brugerkonto? Brugeren kan ikke bruge appen, før kontoen aktiveres igen.",
+ "tabProfile": "Profil",
+ "tabLanguages": "Sprog",
+ "tabActivity": "Aktivitet",
+ "tabQuota": "Kvoter",
+ "tabSubscription": "Abonnement",
+ "sectionCommunication": "Kommunikation",
+ "quotaCurrentUsage": "Aktuelt forbrug",
+ "quotaConfiguredLimits": "Indstillede grænser",
+ "unitMinutes": "min",
+ "unitSessions": "samtaler",
+ "unitTokens": "tokens",
+ "quotaZeroMeansUnlimited": "Sæt grænsen til 0 for ubegrænset adgang.",
+ "quotaValidationError": "Alle kvoter skal være heltal på 0 eller derover.",
+ "quotaSaveError": "Kvoterne kunne ikke gemmes.",
+ "subscriptionOverride": "Manuel tilsidesættelse af abonnement",
+ "subscriptionOverrideDesc": "Dette ændrer brugerens adgangsperiode direkte og kan tilsidesætte status, som håndteres af Stripe.",
+ "subscriptionUpdateError": "Abonnementet kunne ikke opdateres.",
+ "saving": "Gemmer...",
+ "verifyEmailConfirmMessage": "Vil du markere denne e-mailadresse som bekræftet?",
+ "unverifyEmailConfirmMessage": "Vil du markere denne e-mailadresse som ubekræftet?",
+ "subscriptionConfirmTitle": "Skift abonnement",
+ "subscriptionConfirmMessage": "Vil du anvende abonnementet {plan}?",
+ "subscriptionConfirm": "Anvend",
+ "feedbackStatusError": "Feedbackstatus kunne ikke ændres.",
+ "feedbackDeleteError": "Feedbackindlægget kunne ikke slettes.",
+ "allFeedback": "Al feedback",
+ "feedbackTotal": "Feedback i alt",
+ "feedbackQueue": "Feedbackkø",
+ "feedbackSearchPlaceholder": "Søg efter titel, beskrivelse eller forfatter...",
+ "feedbackSearchAction": "Søg efter feedback",
+ "feedbackTypeFilter": "Filtrér efter feedbacktype",
+ "noFilteredFeedback": "Ingen feedback passer til de valgte filtre.",
+ "feedbackItem": "Indlæg",
+ "type": "Type",
+ "signals": "Signaler",
+ "feedbackStatusAction": "Skift feedbackstatus",
+ "adminStatsError": "Administrationsstatistikken kunne ikke indlæses.",
+ "activeUsers": "Aktive brugere",
+ "paidAccess": "Aktiv / prøveperiode",
+ "pendingFeedback": "Afventende feedback",
+ "pendingReviews": "Anmeldelser, der afventer godkendelse",
+ "operationalAlerts": "Driftsadvarsler",
+ "pendingBugs": "Afventende fejlrapporter",
+ "pendingBugsDesc": "Fejlrapporter, der afventer gennemgang af en administrator.",
+ "pastDueSubscriptions": "Abonnementer med forfalden betaling",
+ "pastDueSubscriptionsDesc": "Konti med betalingsproblemer, som muligvis skal gennemgås.",
+ "dashboardBanner": {
+ "title": "Meddelelse på oversigten",
+ "description": "Opret, oversæt, gennemgå og offentliggør en meddelelse på alle brugeres oversigt.",
+ "loading": "Indlæser meddelelse...",
+ "loadError": "Meddelelsen på oversigten kunne ikke indlæses.",
+ "sourceLocale": "Kildesprog",
+ "activeLabel": "Offentliggør meddelelse",
+ "activeHint": "Inaktive meddelelser gemmes, men vises ikke for brugerne.",
+ "fieldTitle": "Titel",
+ "fieldSubtitle": "Undertitel",
+ "fieldDescription": "Beskrivelse",
+ "translate": "Oversæt",
+ "translateSuccess": "Oversættelserne er oprettet. Gennemgå dem, før du gemmer.",
+ "translateError": "Oversættelsen mislykkedes. Dit nuværende indhold blev bevaret.",
+ "editTranslation": "Oversættelse, der skal redigeres",
+ "pending": "afventer",
+ "completion": "{complete}/{total} sprog færdige",
+ "save": "Gem meddelelse",
+ "saveSuccess": "Meddelelsen på oversigten er gemt.",
+ "saveError": "Meddelelsen på oversigten kunne ikke gemmes.",
+ "revision": "Version {revision}",
+ "updated": "Opdateret {date}",
+ "locales": {
+ "en": "Engelsk",
+ "es": "Spansk",
+ "fr": "Fransk",
+ "pt": "Portugisisk",
+ "de": "Tysk",
+ "it": "Italiensk",
+ "ru": "Russisk",
+ "nl": "Nederlandsk",
+ "pl": "Polsk",
+ "ro": "Rumænsk",
+ "tr": "Tyrkisk",
+ "sv": "Svensk",
+ "da": "Dansk",
+ "fi": "Finsk",
+ "hr": "Kroatisk"
+ }
+ }
+ },
+ "faq": {
+ "title": "Ofte stillede spørgsmål",
+ "subtitle": "Ofte stillede spørgsmål og svar",
+ "q_start": "Hvor skal jeg starte?",
+ "a_start": "Start med niveauvurderingen . Den finder dit engelskniveau (A1–C2) samt dine styrker og svagheder. Med resultatet kan du oprette en personlig læringsplan , som fylder din oversigt med daglige lektioner. Uden niveauvurderingen er oversigten tom.",
+ "q_workflow": "Hvordan fungerer hele forløbet?",
+ "workflowStep1": "Opret en konto, og vælg dit modersmål",
+ "workflowStep2": "Tag niveauvurderingen → dit CEFR-niveau findes",
+ "workflowStep3": "Opret din læringsplan fra oversigten",
+ "workflowStep4": "Gennemfør dine daglige lektioner (START-knappen på oversigten)",
+ "workflowStep5": "Brug flashcards til at træne ordforråd ved siden af",
+ "workflowStep6": "Øv frie samtaler med AI-læreren",
+ "q_language": "Hvilket sprog kommer jeg til at lære?",
+ "a_language": "FreeLingo understøtter i øjeblikket flere sprog (amerikansk engelsk, britisk engelsk, fransk, tysk, italiensk, japansk, koreansk, kinesisk fra Fastlandskina, portugisisk og spansk), og vi tilføjer flere med tiden. Når et nyt sprog bliver tilgængeligt, finder du det under Indstillinger → Mine sprog – tilføj det, og begynd at lære med det samme.",
+ "q_assessment": "Hvad er niveauvurderingen?",
+ "a_assessment": "En AI-genereret test med 15 spørgsmål om grammatik, ordforråd, læseforståelse og omskrivning af sætninger med sværhedsgrader fra A1 til C1. Når du er færdig, får du dit CEFR-niveau, en score og en analyse af dine styrker og svagheder. Resultatet bruges i læringsplanen og de genererede flashcards.",
+ "q_studyPlan": "Hvad er læringsplanen, og hvordan opretter jeg en?",
+ "a_studyPlan": "Efter niveauvurderingen går du til oversigten og klikker på Opret læringsplan . Der oprettes automatisk en plan uge for uge med daglige lektioner tilpasset dit CEFR-niveau og dine mål. Hver lektion indeholder multiple choice- og skriveøvelser, som vurderes af AI.",
+ "q_resources": "Hvad er Ressourcer?",
+ "a_resources": "Under Ressourcer finder du opslagsmateriale, som er uafhængigt af din læringsplan: grammatik efter niveau, ordsamlinger og nyttige fraser. Brug det når som helst til at repetere, slå forklaringer op eller udvide dit ordforråd uden at vente på en daglig lektion.",
+ "q_flashcards": "Hvad bruges flashcards til?",
+ "a_flashcards": "Et system til at øve ord med SM-2 -algoritmen til gentagelser fordelt over tid. Opret kort om ethvert emne (f.eks. \"arbejde\", \"fraseverber\" eller \"mad\") med definition, eksempelsætning og oversættelse til dit modersmål. Systemet beregner, hvornår du skal se hvert kort igen, lige før du risikerer at glemme det. Helt uafhængigt af læringsplanen.",
+ "q_vocabulary": "Hvordan tilføjer jeg ord til mit ordforråd?",
+ "a_vocabulary": "Du kan gemme ord på to måder: (1) Markér et ord med musen, mens du læser en lektion, laver en lytteøvelse eller chatter med læreren, og klik på Gem i menuen, der vises. AI genererer så en definition, eksempelsætning og oversættelse til dit modersmål. (2) Gå til Ordforråd , åbn en ordsamling til dit niveau, og klik på Tilføj alle for at importere hele samlingen. Dine gemte ord vises under Mit ordforråd i Flashcards.",
+ "q_tutor": "Hvordan fungerer AI-læreren?",
+ "a_tutor": "En åben chat, hvor svarene vises løbende. Læreren kender dit CEFR-niveau og dine svage områder og tilpasser sine svar. Stil spørgsmål om grammatik, bed om forklaringer, øv dig i at skrive engelsk, eller tal bare sammen. Samtalerne gemmes og kan findes i sidepanelet, også næste gang du logger ind.",
+ "q_voice": "Hvordan fungerer stemmesamtaler?",
+ "a_voice": "Med Stemmesamtale kan du tale med AI-læreren i realtid. Tryk på Start – appen lytter automatisk med stemmeaktivitetsregistrering (VAD), og læreren svarer med syntetisk tale. Du kan afbryde læreren når som helst. Samtalen slutter, når tidsgrænsen nås, eller når du trykker på Stop. Sørg for, at din browser har adgang til mikrofonen.",
+ "q_listening": "Hvordan fungerer lytteøvelser?",
+ "a_listening": "Under Lytning oprettes korte lydklip på dit CEFR-niveau med AI-stemmer. Lyt, og svar derefter på fem forståelsesspørgsmål. Når du sender dine svar, får du din score (0–5), XP, de rigtige svar og hele transskriptionen. Du kan oprette nye øvelser, når du vil.",
+ "q_reading": "Hvordan fungerer læseøvelser?",
+ "a_reading": "Under Læsning oprettes tekster på dit CEFR-niveau – beskeder, e-mails, artikler, blogindlæg og mere. Læs teksten, og besvar fem multiple choice-spørgsmål. Når du sender dine svar, får du din score (0–5) og XP. Du kan oprette nye øvelser, når du vil.",
+ "q_providers": "Hvilke AI-udbydere understøtter FreeLingo?",
+ "a_providers_intro": "Konfigureres i .env med LLM_PROVIDER:",
+ "provider_ollama": "Lokal og gratis. Kræver, at Ollama kører på værtsmaskinen. Anbefalet model: gemma4:e4b. Kan være langsom uden GPU.",
+ "provider_openai": "OpenAI API (GPT-4o m.fl.). Hurtig og pålidelig. Kræver OPENAI_API_KEY.",
+ "provider_anthropic": "Anthropic Claude. Kræver ANTHROPIC_API_KEY.",
+ "provider_deepseek": "DeepSeek API. Kræver DEEPSEEK_API_KEY.",
+ "q_invite": "Kan jeg invitere andre?",
+ "a_invite": "Ja. Under Administration → Brugere kan administratoren oprette brugere direkte eller generere engangslinks, der udløber efter 48 timer. Offentlig registrering er som standard slået fra (ALLOW_REGISTRATION=false).",
+ "q_password": "Hvordan ændrer jeg min adgangskode eller mit modersmål?",
+ "a_password": "Gå til Indstillinger . Der kan du ændre visningsnavn, modersmål og adgangskode.",
+ "q_uiLanguage": "Hvordan ændrer jeg platformens sprog?",
+ "a_uiLanguage": "Gå til Indstillinger , og find menuen Grænsefladesprog under Profil. Vælg det ønskede sprog, så skifter hele brugerfladen med det samme.",
+ "q_feedback": "Hvordan foreslår jeg en ny funktion eller rapporterer en fejl?",
+ "a_feedback": "Brug Feedback i sidepanelet. Du kan foreslå nye funktioner eller rapportere fejl. Stem på andres idéer, skriv kommentarer, og følg status for hvert indlæg. Alle indlæg er offentlige og gennemgås af administratoren."
+ },
+ "cookieBanner": {
+ "message": "Vi bruger kun nødvendige cookies for at holde dig logget ind. Vi bruger ikke sporings- eller reklamecookies.",
+ "learnMore": "Privatlivspolitik",
+ "accept": "Forstået"
+ },
+ "error": {
+ "label": "Uventet fejl",
+ "title": "Noget gik galt",
+ "body": "Der opstod en uventet fejl. Du kan prøve igen eller gå tilbage til oversigten.",
+ "retry": "Prøv igen",
+ "dashboard": "Gå til oversigten",
+ "digest": "Fejlreference"
+ },
+ "notFound": {
+ "title": "Siden blev ikke fundet",
+ "body": "Siden, du leder efter, findes ikke eller er blevet flyttet.",
+ "dashboard": "Gå til oversigten",
+ "home": "Hjem"
+ },
+ "errors": {
+ "notFound": "Siden blev ikke fundet",
+ "serverError": "Noget gik galt",
+ "unauthorized": "Log ind",
+ "backHome": "Tilbage til forsiden"
+ },
+ "conversation": {
+ "title": "Samtale",
+ "subtitle": "Stemmeøvelse",
+ "start": "Start samtale",
+ "stop": "Stop",
+ "startNew": "Start ny samtale",
+ "statusLoading": "Indlæser stemmemodel...",
+ "statusReady": "Klar",
+ "statusWarming": "Indlæser modeller...",
+ "statusConnecting": "Opretter forbindelse...",
+ "statusListening": "Lytter...",
+ "statusDetecting": "Registrerer tale...",
+ "statusSpeaking": "Taler...",
+ "statusError": "Fejl",
+ "errorServicesDisabled": "Stemmetjenester er ikke aktiveret på denne server.",
+ "errorConnection": "Forbindelsesfejl. Prøv igen.",
+ "errorTranscription": "Din tale kunne ikke omdannes til tekst. Prøv at sige det igen.",
+ "errorResponse": "Lingu kunne ikke svare. Prøv igen.",
+ "errorSpeech": "Lingus svar kunne ikke læses op. Prøv igen.",
+ "errorUnauthorized": "Sessionen er udløbet. Genindlæs siden.",
+ "errorMic": "Adgang til mikrofonen blev afvist.",
+ "errorVadInit": "Talegenkendelsen kunne ikke startes. Kontrollér, at din browser understøtter WebAssembly.",
+ "warningTimeout": "Samtalen slutter om {seconds} sekunder",
+ "sessionEnded": "Samtalen er afsluttet",
+ "tapToStart": "Tal, når du er klar",
+ "you": "Dig",
+ "assistant": "Lingu",
+ "quotaSessions": "Samtaler i denne uge",
+ "quotaMinutes": "Minutter i dag",
+ "quotaUnlimited": "Ubegrænset",
+ "quotaExceededSessions": "Du har brugt alle dine samtaler i denne uge.",
+ "quotaExceededTime": "Du har brugt al din daglige øvetid.",
+ "quotaExceededTokens": "Du har brugt alle dine AI-tokens for denne måned.",
+ "freemiumVoiceRemaining": "Gratis stemme: {remaining}/{limit} min i denne uge",
+ "quotaTokens": "Tokens i denne måned",
+ "memoryUpdated": "Hukommelsen er opdateret",
+ "trialBanner": "Gratis samtale til niveauvurdering · {minutes} min",
+ "trialCtaLabel": "Fortsæt samtalen",
+ "trialCtaTitle": "Fortsæt med at øve dig med Lingu",
+ "trialCtaDesc": "Lås op for fulde stemmesamtaler, lærerchat, lytte- og læseøvelser med Premium.",
+ "startersHint": "Eller begynd med et emne",
+ "starters": [
+ "Kunst og kreativitet",
+ "Bøger og læsning",
+ "Barndomsminder",
+ "By eller land",
+ "Madlavning og opskrifter",
+ "Drømme og ambitioner",
+ "Familie og relationer",
+ "Mode og stil",
+ "Yndlingsfilm",
+ "Festivaler og traditioner",
+ "Mad fra hele verden",
+ "Sundhed og velvære",
+ "Hobbyer og håndværk",
+ "Sprogindlæring",
+ "At lære nye færdigheder",
+ "Penge og budget",
+ "Musik og koncerter",
+ "Kæledyr og dyr",
+ "Offentlig transport",
+ "Videnskab og rummet",
+ "Indkøbsvaner",
+ "Sociale medier",
+ "Sport og motion",
+ "Teknologi og gadgets",
+ "Miljøet",
+ "Rejsemål",
+ "Tv-programmer og serier",
+ "Weekendplaner",
+ "Arbejde og karrieremål",
+ "At arbejde hjemmefra"
+ ]
+ },
+ "voiceRecorder": {
+ "record": "Optag",
+ "stop": "Stop",
+ "processing": "Behandler",
+ "error": "Fejl",
+ "ariaRecord": "Start optagelse",
+ "ariaStop": "Stop optagelse"
+ },
+ "audioPlayer": {
+ "listen": "Lyt",
+ "stop": "Stop",
+ "ariaListen": "Lyt til udtalen",
+ "ariaStop": "Stop lyd"
+ },
+ "languages": {
+ "en": "Engelsk",
+ "es": "Spansk",
+ "fr": "Fransk",
+ "pt": "Portugisisk",
+ "de": "Tysk",
+ "it": "Italiensk",
+ "pl": "Polsk",
+ "nl": "Nederlandsk",
+ "ro": "Rumænsk",
+ "ru": "Russisk",
+ "tr": "Tyrkisk",
+ "sv": "Svensk",
+ "da": "Dansk",
+ "fi": "Finsk",
+ "hr": "Kroatisk",
+ "myLanguages": "Mine sprog",
+ "addLanguage": "Tilføj nyt sprog",
+ "selectLanguage": "Vælg det sprog, du vil lære",
+ "activeLanguage": "Aktivt",
+ "switchTo": "Skift til dette",
+ "switching": "Skifter til {language}...",
+ "switched": "Skiftet til {language} ({level})",
+ "removeLanguage": "Fjern sprog",
+ "removeConfirmTitle": "Fjern {language}?",
+ "removeConfirmMessage": "Alle fremskridt, lektioner, flashcards og samtaler knyttet til dette sprog slettes permanent. Dine kontohukommelser bevares. Handlingen kan ikke fortrydes.",
+ "removeConfirmButton": "Fjern",
+ "deleteError": "{language} kunne ikke fjernes. Prøv igen.",
+ "noLanguages": "Du har ikke tilføjet nogen sprog.",
+ "progressLabel": "Fremskridt",
+ "levelLabel": "Niveau",
+ "xpLabel": "XP i alt",
+ "streakLabel": "Stime",
+ "lessonsLabel": "Lektioner",
+ "flashcardsLabel": "Flashcards",
+ "viewDetails": "Se detaljer",
+ "supportedLanguages": "Tilgængelige sprog"
+ },
+ "targetLanguages": {
+ "en-US": "Amerikansk engelsk",
+ "en-GB": "Britisk engelsk",
+ "es-ES": "Spansk",
+ "it-IT": "Italiensk",
+ "pt-PT": "Portugisisk",
+ "fr-FR": "Fransk",
+ "de-DE": "Tysk",
+ "ja-JP": "Japansk",
+ "ko-KR": "Koreansk",
+ "zh-CN": "Kinesisk (Fastlandskina)",
+ "en": "Engelsk",
+ "es": "Spansk",
+ "it": "Italiensk",
+ "pt": "Portugisisk",
+ "fr": "Fransk",
+ "de": "Tysk",
+ "ja": "Japansk",
+ "ko": "Koreansk",
+ "zh": "Kinesisk",
+ "es-ES-description": "Spansk, som det tales i Spanien.",
+ "it-IT-description": "Italiensk, som det tales i Italien.",
+ "pt-PT-description": "Portugisisk, som det tales i Portugal.",
+ "en-US-description": "Engelsk, som det tales i USA.",
+ "en-GB-description": "Engelsk, som det tales i Storbritannien.",
+ "fr-FR-description": "Fransk, som det tales i Frankrig.",
+ "de-DE-description": "Tysk, som det tales i Tyskland.",
+ "ja-JP-description": "Japansk, som det tales i Japan.",
+ "ko-KR-description": "Koreansk, som det tales i Sydkorea.",
+ "zh-CN-description": "Mandarinkinesisk, som det tales i Fastlandskina med forenklede tegn."
+ },
+ "onboarding": {
+ "title": "Vælg dit sprog",
+ "subtitle": "Hvilket sprog vil du lære?",
+ "chooseVariant": "Vælg variant",
+ "continue": "Fortsæt",
+ "saveFailed": "Indstillingerne kunne ikke gemmes",
+ "goals": {
+ "title": "Dine læringsmål",
+ "subtitle": "Sprog: {language}. Hvad vil du bruge det til? Vælg alle relevante mål.",
+ "skip": "Spring over",
+ "travel": "Rejser og turisme",
+ "work": "Arbejde og karriere",
+ "academic": "Studier og uddannelse",
+ "daily": "Samtaler i hverdagen",
+ "media": "Film, musik og serier",
+ "emigration": "At bo i udlandet",
+ "exams": "Prøver og certifikater",
+ "social": "Socialt samvær"
+ },
+ "trialHeadline": "Gratis prøveperiode",
+ "trialHeadlineTrialUsed": "Premium",
+ "trialTitle": "Start med {days} gratis dage",
+ "trialTitleTrialUsed": "Aktivér FreeLingo Premium",
+ "trialDesc": "Få fuld adgang til Lingu, stemmeøvelser, lytning og læsning inden din første betaling.",
+ "trialDescTrialUsed": "Vælg et abonnement for at låse op for Lingu, stemmeøvelser, lytning og læsning.",
+ "trialCtaMonthly": "Prøv gratis, derefter €{price}/måned",
+ "trialCtaMonthlyTrialUsed": "Fleksibelt månedsabonnement · €{price}/måned",
+ "trialCtaYearly": "Prøv gratis, derefter €{price}/år · 2 måneder gratis",
+ "trialCtaYearlyTrialUsed": "Mest for pengene · €{price}/år · 2 måneder gratis",
+ "trialSkip": "Fortsæt med gratis funktioner",
+ "trialNoCharge": "Ingen betaling i dag. Kort påkrævet, opsig når som helst.",
+ "trialNoChargeTrialUsed": "Se beløbet, før du abonnerer. Opsig når som helst.",
+ "trialError": "Betalingen kunne ikke startes. Prøv igen.",
+ "freemiumTrialTitle": "Din gratis prøveperiode er begyndt",
+ "freemiumTrialDesc": "Få ubegrænset adgang til alle funktioner i syv dage. Intet kort påkrævet. Opgrader til Premium, når du vil, fra oversigten.",
+ "freemiumChatLabel": "Chat med AI-lærer",
+ "freemiumVoiceLabel": "Stemmesamtaler",
+ "freemiumListeningLabel": "Lytteøvelser",
+ "freemiumReadingLabel": "Læseøvelser",
+ "goToDashboard": "Gå til oversigten",
+ "newLanguageHeadline": "Hvilket nyt sprog vil du lære?",
+ "newLanguageSubtitle": "Der oprettes en ny læringsplan for dette sprog."
+ },
+ "tour": {
+ "skip": "Spring over",
+ "prev": "Tilbage",
+ "next": "Næste",
+ "done": "Kom i gang",
+ "step1": {
+ "label": "Velkommen",
+ "title": "Velkommen til FreeLingo",
+ "desc": "FreeLingo er din AI-drevne sproglærer. Øv dig i dit eget tempo, når du vil, gennem samtaler, lektioner og smarte flashcards."
+ },
+ "step2": {
+ "label": "Tekstchat",
+ "title": "Chat med din lærer",
+ "desc": "Før skriftlige samtaler med din AI-lærer. Stil spørgsmål, få rettelser, og øv grammatik og ordforråd på en naturlig måde."
+ },
+ "step3": {
+ "label": "Stemme",
+ "title": "Tal i realtid",
+ "desc": "Brug stemmesamtaler til at øve talesprog med transskription i realtid og hurtig feedback på din udtale."
+ },
+ "step4": {
+ "label": "Flashcards",
+ "title": "Glem aldrig et ord",
+ "desc": "Flashcards bruger SM-2-algoritmen til gentagelser fordelt over tid, så du repeterer ord, lige før du er ved at glemme dem – og husker dem bedre."
+ },
+ "step5": {
+ "label": "Læringsplan",
+ "title": "Din personlige plan",
+ "desc": "Ud fra din niveauvurdering opretter FreeLingo en struktureret læringsplan med daglige lektioner tilpasset dine mål og fremskridt."
+ },
+ "step6": {
+ "label": "Øvelse",
+ "title": "Lytning og læsning",
+ "desc": "Test din forståelse med AI-genererede øvelser. Lyt til et lydklip, og svar på fem spørgsmål, eller læs en tekst på dit CEFR-niveau – få XP for hvert rigtigt svar."
+ },
+ "step7": {
+ "label": "Klar",
+ "title": "Nu er du klar!",
+ "desc": "Start med dagens lektion, åbn en chat, eller øv lytning og læsning – FreeLingo tilpasser sig den måde, du bedst kan lide at lære på."
+ }
+ },
+ "billing": {
+ "paywallLabel": "Premium påkrævet",
+ "paywallTitle": "Lås op for FreeLingo Premium",
+ "paywallDesc": "Begynd din gratis prøveperiode på {days} dage, og lås op for alle Premium-øvelser før din første betaling.",
+ "paywallDescTrialUsed": "Lås op for alle Premium-øvelser, og gennemgå beløbet, før du bekræfter.",
+ "paywallChatTitle": "Øv dig med Lingu uden grænser",
+ "paywallChatDesc": "Stil spørgsmål, få rettelser, og hav naturlige tekstsamtaler med din AI-lærer.",
+ "paywallLessonsTitle": "Gennemfør ubegrænset antal lektioner",
+ "paywallLessonsDesc": "Grammatik, ordforråd, læsning og skrivning – gennemfør så mange lektioner, du vil hver dag.",
+ "paywallConversationTitle": "Tal med Lingu i realtid",
+ "paywallConversationDesc": "Øv dig i at tale med transskription, hurtige svar og feedback, der styrker din talefærdighed.",
+ "paywallListeningTitle": "Træn din lytteforståelse på dit niveau",
+ "paywallListeningDesc": "Lyt til AI-genererede øvelser tilpasset dine fremskridt, og tjek med det samme, hvor meget du forstod.",
+ "paywallReadingTitle": "Læs tekster tilpasset dine fremskridt",
+ "paywallReadingDesc": "Bliv bedre til at læse med tekster på dit niveau, spørgsmål og øjeblikkelig feedback.",
+ "paywallNoCharge": "Ingen betaling i dag · Kort påkrævet · Opsig når som helst",
+ "paywallNoChargeTrialUsed": "Sikker betaling · Opsig når som helst",
+ "paywallSkip": "Ikke nu",
+ "trialDays": "{days, plural, one {# prøvedag} other {# prøvedage}}",
+ "planMonthly": "Fleksibelt månedsabonnement · €{price}/måned →",
+ "planYearly": "Mest for pengene · €{price}/år · 2 måneder gratis →",
+ "checkoutError": "Betalingen kunne ikke startes. Prøv igen.",
+ "pricingTitle": "Enkle, gennemskuelige priser",
+ "pricingDesc": "{days} dage gratis · Ingen betaling i dag · Opsig når som helst",
+ "pricingDescTrialUsed": "Vælg månedligt eller årligt · Opsig når som helst",
+ "planMonthlyName": "Månedligt",
+ "planYearlyName": "Årligt",
+ "bestValue": "Mest for pengene",
+ "month": "måned",
+ "year": "år",
+ "planFeature": {
+ "feature1": "Ubegrænset adgang – ingen daglige eller ugentlige grænser",
+ "feature2": "2 måneder gratis"
+ },
+ "planFreeName": "Gratis",
+ "trialBadge": "Gratis prøveperiode",
+ "trialBadgeTrialUsed": "Premium",
+ "ctaRegister": "Start gratis prøveperiode",
+ "ctaRegisterTrialUsed": "Vælg abonnement",
+ "freeFeature": {
+ "f1": "Niveautest og niveauvurdering",
+ "f2": "Lektioner og læringsplan",
+ "f3": "Opslagsværk for grammatik og ordforråd",
+ "f4": "Oversigt og opfølgning på fremskridt",
+ "f5": "Flashcards og gentagelser fordelt over tid",
+ "l1": "Chat med AI-lærer",
+ "l2": "Stemmesamtaler",
+ "l3": "Lytteøvelser",
+ "l4": "Læseøvelser",
+ "l5": "7 dages prøveperiode med fuld adgang"
+ },
+ "successLabel": "Betaling bekræftet",
+ "successTitle": "Velkommen til FreeLingo Premium!",
+ "successDesc": "Dit abonnement er nu aktivt. Få ubegrænset adgang til alle funktioner.",
+ "successCheckingLabel": "Bekræfter abonnement",
+ "successCheckingTitle": "Kontrollerer dit abonnement",
+ "successCheckingDesc": "Det tager normalt kun få sekunder.",
+ "successPendingLabel": "Næsten klar",
+ "successPendingTitle": "Dit abonnement bekræftes",
+ "successPendingDesc": "Du bør snart få adgang til Premium. Hvis ikke, skal du tjekke dine betalingsindstillinger.",
+ "successErrorLabel": "Bekræftelse afventer",
+ "successErrorTitle": "Vi kunne endnu ikke bekræfte dit abonnement",
+ "successErrorDesc": "Gå til oversigten, og opdater om lidt, eller tjek dine betalingsindstillinger.",
+ "successRedirect": "Sender dig til oversigten om {seconds} sekunder...",
+ "successCta": "Gå til oversigten",
+ "canceledLabel": "Betalingen blev annulleret",
+ "canceledTitle": "Der er ikke trukket penge",
+ "canceledDesc": "Der er ikke trukket penge. Dine fremskridt er bevaret, og du kan vælge et abonnement senere under Indstillinger.",
+ "canceledCtaDashboard": "Gå til oversigten",
+ "canceledCtaSettings": "Se abonnementer",
+ "canceledNoCharge": "Der blev ikke trukket penge · Vælg abonnement, når du vil",
+ "section": "Abonnement",
+ "status": "Status",
+ "statusActive": "Aktivt",
+ "statusCanceling": "Opsiges",
+ "statusCanceled": "Opsagt",
+ "statusTrialing": "Prøveperiode",
+ "statusPastDue": "Forfalden betaling",
+ "statusUnpaid": "Ubetalt",
+ "statusPaused": "Sat på pause",
+ "statusIncomplete": "Betaling afventer",
+ "statusIncompleteExpired": "Betalingen er udløbet",
+ "statusNone": "Intet abonnement",
+ "nextBilling": "Næste betaling",
+ "accessUntil": "Adgang indtil",
+ "manage": "Administrer abonnement",
+ "updatePayment": "Opdater betaling",
+ "pastDueTitle": "Betaling afventer",
+ "pastDueDesc": "Opdater din betalingsmetode for at få adgang til Premium igen.",
+ "subscribe": "Abonnér",
+ "portalError": "Betalingsportalen kunne ikke åbnes. Prøv igen.",
+ "openSourceTitle": "Open source",
+ "openSourceDesc": "Gratis at hoste på din egen server",
+ "openSourceCta": "Se på GitHub",
+ "planFreeBadge": "Gratis for altid",
+ "priceAmount": "€{amount}",
+ "pricePerPeriod": "/ {period}",
+ "priceOriginal": "€{price} / {period}",
+ "planFreeDesc": "7 dages prøveperiode med fuld adgang, derefter gratis med daglige og ugentlige grænser",
+ "planFreeCta": "Start",
+ "limitedLabel": "Begrænset"
+ },
+ "freemium": {
+ "chatLabel": "Chat",
+ "lessonsLabel": "Lektioner",
+ "listeningLabel": "Lytning",
+ "readingLabel": "Læsning",
+ "voiceLabel": "Stemme (min)",
+ "freeLimit": "Få Premium",
+ "trialDaysLeft": "{days, plural, one {# dag tilbage af gratis prøveperiode} other {# dage tilbage af gratis prøveperiode}}",
+ "trialUnlimited": "Ubegrænset adgang",
+ "requiresSubscription": "Kun Premium"
+ },
+ "whatsNew": {
+ "title": "Nyheder",
+ "version": "v1.9.25",
+ "cta": "Forstået",
+ "entry1": {
+ "label": "Fem nye grænsefladesprog",
+ "desc": "FreeLingo er tilgængeligt på dansk, finsk, kroatisk, svensk og tyrkisk. Vælg dit grænsefladesprog under Indstillinger."
+ },
+ "entry2": {
+ "label": "En mere smidig oplevelse",
+ "desc": "Vi har også rettet mindre fejl og forbedret nogle detaljer, så du får endnu mere glæde af din daglige læring med FreeLingo."
+ }
+ },
+ "contact": {
+ "title": "Kontakt",
+ "labelEmail": "E-mailadresse",
+ "placeholderEmail": "din@email.dk",
+ "labelSubject": "Emne",
+ "placeholderSubject": "Kort beskrivelse af dit spørgsmål",
+ "labelDescription": "Besked",
+ "placeholderDescription": "Beskriv dit spørgsmål eller din feedback...",
+ "send": "Send",
+ "sending": "Sender...",
+ "sent": "Beskeden er sendt",
+ "errorGeneric": "Kunne ikke sende. Prøv igen."
+ },
+ "listening": {
+ "title": "Lytning",
+ "topic": "Emne",
+ "listenLabel": "Lyt",
+ "questionsLabel": "Spørgsmål",
+ "submit": "Send svar",
+ "generating": "Opretter øvelse...",
+ "generatingDesc": "AI opretter en ny øvelse. Det tager normalt cirka 30 sekunder.",
+ "generatingLong": "Det tager længere tid end forventet...",
+ "noExercises": "Ingen øvelser tilgængelige på dit niveau.",
+ "generate": "Opret øvelse",
+ "audioError": "Lyden kunne ikke indlæses",
+ "alreadyAttempted": "Du har allerede gennemført denne øvelse.",
+ "errorLoading": "Øvelsen kunne ikke indlæses. Prøv igen.",
+ "errorSubmit": "Svarene kunne ikke sendes. Prøv igen.",
+ "resultsLabel": "Resultater",
+ "transcript": "Transskription",
+ "review": "Gennemgå svar",
+ "nextExercise": "Næste øvelse",
+ "viewHistory": "Historik",
+ "history": "Historik",
+ "historyTitle": "Historik for lytteøvelser",
+ "historyEmpty": "Ingen gennemførte øvelser endnu.",
+ "practiceMore": "Øv dig",
+ "moreInHistory": "+{count} mere",
+ "practiceAgain": "Øv igen",
+ "replayNoXp": "Gentag (ingen XP)"
+ },
+ "reading": {
+ "title": "Læsning",
+ "topic": "Emne",
+ "textLabel": "Tekst",
+ "questionsLabel": "Spørgsmål",
+ "submit": "Send svar",
+ "generating": "Opretter øvelse...",
+ "generatingDesc": "AI opretter en ny øvelse. Det tager normalt cirka 30 sekunder.",
+ "generatingLong": "Det tager længere tid end forventet...",
+ "noExercises": "Ingen øvelser tilgængelige på dit niveau.",
+ "generate": "Opret øvelse",
+ "alreadyAttempted": "Du har allerede gennemført denne øvelse.",
+ "errorLoading": "Øvelsen kunne ikke indlæses. Prøv igen.",
+ "errorSubmit": "Svarene kunne ikke sendes. Prøv igen.",
+ "resultsLabel": "Resultater",
+ "review": "Gennemgå svar",
+ "nextExercise": "Næste øvelse",
+ "viewHistory": "Historik",
+ "history": "Historik",
+ "historyTitle": "Historik for læseøvelser",
+ "historyEmpty": "Ingen gennemførte øvelser endnu.",
+ "practiceMore": "Øv dig",
+ "moreInHistory": "+{count} mere",
+ "practiceAgain": "Øv igen",
+ "replayNoXp": "Gentag (ingen XP)",
+ "selectWordHint": "Markér et ord for at gemme det i dit ordforråd"
+ },
+ "feedback": {
+ "title": "Feedback",
+ "subtitle": "Funktionsforslag og fejlrapporter",
+ "tabFeatures": "Forslag",
+ "tabBugs": "Fejlrapporter",
+ "newFeature": "+ Nyt forslag",
+ "newBug": "+ Rapportér en fejl",
+ "sortBy": "Sortér efter",
+ "sortVotes": "Flest stemmer",
+ "sortDate": "Nyeste",
+ "filterStatus": "Status",
+ "filterAll": "Alle",
+ "noEntries": "Intet her endnu. Vær den første til at skrive et indlæg.",
+ "votes": "{count} stemmer",
+ "vote": "1 stemme",
+ "voteAction": "Stem",
+ "removeVoteAction": "Fjern stemme",
+ "comments": "{count} kommentarer",
+ "comment": "1 kommentar",
+ "by": "af",
+ "statusPending": "Afventer",
+ "statusPlanned": "Planlagt",
+ "statusInProgress": "I gang",
+ "statusDone": "Færdig",
+ "statusDeclined": "Afvist",
+ "unread": "Ulæst",
+ "modalCreateTitleFeature": "Nyt forslag",
+ "modalCreateTitleBug": "Rapportér en fejl",
+ "labelTitle": "Titel",
+ "placeholderTitleFeature": "Beskriv dit forslag i én sætning",
+ "placeholderTitleBug": "Kort beskrivelse af fejlen",
+ "labelDescription": "Detaljer",
+ "placeholderDescriptionFeature": "Forklar funktionen, og hvorfor den ville være nyttig",
+ "placeholderDescriptionBug": "Trin til at genskabe fejlen, forventet resultat, og hvad der skete i stedet",
+ "submit": "Send",
+ "submitting": "Sender...",
+ "cancel": "Annuller",
+ "deleteEntry": "Slet",
+ "deleteEntryConfirmTitle": "Slet indlæg",
+ "deleteEntryConfirmMessage": "Er du sikker på, at du vil slette dette indlæg? Handlingen kan ikke fortrydes.",
+ "deleteEntryConfirm": "Slet",
+ "addComment": "Tilføj en kommentar",
+ "commentPlaceholder": "Skriv en kommentar...",
+ "postComment": "Udgiv",
+ "postingComment": "Udgiver...",
+ "deleteComment": "Slet",
+ "deleteCommentConfirmTitle": "Slet kommentar",
+ "deleteCommentConfirmMessage": "Vil du slette denne kommentar?",
+ "deleteCommentConfirm": "Slet",
+ "loading": "Indlæser...",
+ "backToList": "← Tilbage",
+ "errorLoad": "Kunne ikke indlæse. Prøv igen.",
+ "errorSubmit": "Kunne ikke sende. Prøv igen.",
+ "prevPage": "← Forrige",
+ "nextPage": "Næste →"
+ },
+ "reviewPrompt": {
+ "statusError": "Vi kunne ikke kontrollere status for din anmeldelse. Prøv igen senere.",
+ "ratingRequiredError": "Vælg en bedømmelse, før du sender.",
+ "saveError": "Vi kunne ikke gemme din anmeldelse. Prøv igen.",
+ "eyebrow": "Anmeld FreeLingo",
+ "title": "Hvordan var din oplevelse?",
+ "closeLabel": "Luk anmeldelsesvinduet",
+ "checking": "Kontrollerer anmeldelsesstatus...",
+ "ratingLabel": "Bedømmelse påkrævet",
+ "ratingGroupLabel": "Bedømmelse",
+ "star": "stjerne",
+ "stars": "stjerner",
+ "commentLabel": "Kommentar valgfri",
+ "commentPlaceholder": "Fortæl os, hvad der hjalp dig med at lære.",
+ "cancel": "Annuller",
+ "submit": "Send"
+ },
+ "landingReviews": {
+ "eyebrow": "Historier fra brugerne",
+ "title": "Læring føles mere virkelig, når du selv fortæller om den",
+ "subtitle": "Anmeldelser fra brugere, der allerede øver sig med Lingu på FreeLingo.",
+ "averageLabel": "{average}/5 · {count, plural, one {# anmeldelse i alt} other {# anmeldelser i alt}}",
+ "learningLanguage": "Lærer {language}",
+ "starsLabel": "{rating} ud af 5 stjerner",
+ "ratingOnly": "Bekræftet FreeLingo-bedømmelse."
+ },
+ "adminReviews": {
+ "title": "Anmeldelser",
+ "eyebrow": "Administration",
+ "totalFiltered": "Filtrerede i alt",
+ "approvedOnPage": "Godkendte på siden",
+ "pendingOnPage": "Afventende på siden",
+ "filters": "Filtre",
+ "approvalFilter": "Filtrér efter godkendelse",
+ "allReviews": "Alle anmeldelser",
+ "pending": "Afventer",
+ "approved": "Godkendte",
+ "ratingFilter": "Filtrér efter bedømmelse",
+ "allRatings": "Alle bedømmelser",
+ "starsOption": "{rating} stjerner",
+ "clear": "Ryd",
+ "loadError": "Anmeldelserne kunne ikke indlæses.",
+ "updateError": "Anmeldelsen kunne ikke opdateres.",
+ "deleteError": "Anmeldelsen kunne ikke slettes.",
+ "loading": "Indlæser anmeldelser...",
+ "queue": "Anmeldelseskø",
+ "total": "{total} i alt",
+ "empty": "Ingen anmeldelser fundet.",
+ "starsLabel": "{rating} stjerner",
+ "learningLanguage": "Lærer {language}",
+ "ratingOnly": "Anmeldelse med kun bedømmelse.",
+ "unapprove": "Fjern godkendelse",
+ "approve": "Godkend",
+ "delete": "Slet",
+ "prev": "Forrige",
+ "next": "Næste",
+ "deleteTitle": "Slet anmeldelse",
+ "deleteMessage": "Vil du slette denne anmeldelse? Handlingen kan ikke fortrydes."
+ },
+ "maintenance": {
+ "label": "Vedligeholdelse",
+ "title": "Vedligeholdelse i gang",
+ "description": "Dette afsnit er midlertidigt utilgængeligt, mens vi udfører vedligeholdelse. Kom tilbage senere."
+ }
+}
diff --git a/messages/de.json b/messages/de.json
index aa2f62af4..ed39a16b1 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Laden...",
"error": "Fehler",
+ "errorMessage": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.",
"save": "Änderungen speichern",
"saving": "Wird gespeichert...",
"cancel": "Abbrechen",
@@ -78,6 +79,9 @@
"backToDashboard": "Zurück zum Dashboard",
"tagline": "KI-gestütztes Sprachenlernen",
"close": "Schließen",
+ "delete": "Löschen",
+ "openMenu": "Menü öffnen",
+ "closeMenu": "Menü schließen",
"verifyEmailBanner": "Bitte bestätige deine E-Mail-Adresse, um dein Konto einzurichten.",
"verifyEmailSent": "E-Mail gesendet. Prüfe deinen Posteingang.",
"resendVerification": "Bestätigungs-E-Mail erneut senden",
@@ -205,7 +209,7 @@
"saving": "Speichern...",
"success": "Passwort aktualisiert. Weiterleitung...",
"mismatch": "Passwörter stimmen nicht überein",
- "tooShort": "Das Passwort muss mindestens 8 Zeichen lang sein",
+ "tooShort": "Das Passwort muss mindestens 10 Zeichen lang sein",
"missingToken": "Ungültiger Link. Bitte fordere eine neue Zurücksetzung an.",
"error": "Ungültiger oder abgelaufener Link",
"backToLogin": "Zurück zum Login"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Wenn Sie den Stufentest vor dem Abschluss verlassen, geht Ihr Fortschritt verloren und Sie müssen ihn wiederholen.",
"startWarningConfirm": "Beginnen",
"startMyPlan": "Meinen Plan starten",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Sprachgespräch ausprobieren",
+ "voiceTrialTitle": "Probiere jetzt ein echtes Gespräch aus",
+ "voiceTrialDesc": "Sprich {minutes} Minuten lang kostenlos mit Lingu. Dein Tutor passt sich an die Stufe an, die du gerade freigeschaltet hast.",
+ "voiceTrialStart": "Kostenloses Gespräch starten",
+ "voiceTrialSkip": "Vorerst überspringen"
},
"plan": {
"unitLocked": "Einheit gesperrt",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Chats anzeigen",
"toggleSidebarHide": "Chats ausblenden",
"conversations": "Gespräche",
+ "voiceSession": "Sprachsitzung",
"loading": "Laden...",
"thinking": "Denke nach...",
"takingLonger": "Dauert länger als erwartet...",
@@ -823,7 +828,7 @@
"subtitle": "Vokabular",
"all": "Alle",
"searchPlaceholder": "Sammlungen suchen...",
- "sets": "Sammlungen",
+ "sets": "{count, plural, one {# Sammlung} other {# Sammlungen}}",
"words": "Wörter",
"addAll": "Alle {count} als Karteikarten hinzufügen",
"adding": "Wird hinzugefügt...",
@@ -848,6 +853,7 @@
"informal": "Informell",
"copied": "Kopiert!",
"copy": "Kopieren",
+ "copyPhrase": "Satz kopieren",
"noResults": "Keine Phrasen gefunden",
"level": "Niveau",
"register": "Register",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Übersetzungen erstellt. Prüfe sie vor dem Speichern.",
"translateError": "Übersetzung fehlgeschlagen. Der aktuelle Inhalt wurde beibehalten.",
"editTranslation": "Übersetzung bearbeiten",
+ "pending": "ausstehend",
"completion": "{complete}/{total} Sprachen vollständig",
"save": "Ankündigung speichern",
"saveSuccess": "Dashboard-Ankündigung gespeichert.",
@@ -1062,7 +1069,12 @@
"ru": "Russisch",
"nl": "Niederländisch",
"pl": "Polnisch",
- "ro": "Rumänisch"
+ "ro": "Rumänisch",
+ "tr": "Türkisch",
+ "sv": "Schwedisch",
+ "da": "Dänisch",
+ "fi": "Finnisch",
+ "hr": "Kroatisch"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Fehler",
"errorServicesDisabled": "Sprachdienste sind auf diesem Server nicht aktiviert.",
"errorConnection": "Verbindungsfehler. Bitte versuche es erneut.",
+ "errorTranscription": "Deine Spracheingabe konnte nicht transkribiert werden. Bitte sprich noch einmal.",
+ "errorResponse": "Lingu konnte nicht antworten. Bitte versuche es erneut.",
+ "errorSpeech": "Die Audioausgabe für Lingus Antwort konnte nicht erstellt werden. Bitte versuche es erneut.",
"errorUnauthorized": "Sitzung abgelaufen. Bitte neu laden.",
"errorMic": "Mikrofonzugriff verweigert.",
"errorVadInit": "Fehler beim Initialisieren der Spracherkennung. Überprüfe, ob dein Browser WebAssembly unterstützt.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Kostenlose Stimme: {remaining}/{limit} Min diese Woche",
"quotaTokens": "Token diesen Monat",
"memoryUpdated": "Erinnerung gespeichert",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Kostenloses Gespräch nach der Einstufung · {minutes} Min.",
+ "trialCtaLabel": "Sprich weiter",
+ "trialCtaTitle": "Übe weiter mit Lingu",
+ "trialCtaDesc": "Schalte mit Premium vollständige Sprachgespräche, den Tutor-Chat sowie Hör- und Leseübungen frei.",
"startersHint": "Oder mit einem Thema beginnen",
"starters": [
"Kunst und Kreativität",
@@ -1234,6 +1249,11 @@
"nl": "Niederländisch",
"ro": "Rumänisch",
"ru": "Russisch",
+ "tr": "Türkisch",
+ "sv": "Schwedisch",
+ "da": "Dänisch",
+ "fi": "Finnisch",
+ "hr": "Kroatisch",
"myLanguages": "Meine Sprachen",
"addLanguage": "Sprache hinzufügen",
"selectLanguage": "Wähle die Sprache, die du lernen möchtest",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Neuigkeiten",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Verstanden",
"entry1": {
- "label": "Ausgewogenere Lernpläne",
- "desc": "Neue Lernpläne verteilen die Lektionen gleichmäßiger auf alle Einheiten und bieten mehr Abwechslung bei den Übungen – auch bei kürzeren Plänen."
+ "label": "Fünf neue Oberflächensprachen",
+ "desc": "FreeLingo ist auf Dänisch, Finnisch, Kroatisch, Schwedisch und Türkisch verfügbar. Wähle deine Oberflächensprache in den Einstellungen."
},
"entry2": {
- "label": "Dein Test zum Planabschluss",
- "desc": "Wenn du den letzten Tag erreicht und alle offenen Lektionen abgeschlossen hast, kannst du den Abschlusstest über das Dashboard oder deinen Lernplan starten und sehen, wie viel du gelernt hast."
- },
- "entry3": {
- "label": "Klarer wissen, wie es weitergeht",
- "desc": "Testergebnisse und Empfehlungen gibt es jetzt in deiner Sprache, mit klareren Hinweisen für deinen nächsten Schritt oder zum Wiederholen des Gelernten."
- },
- "entry4": {
"label": "Ein flüssigeres Erlebnis",
"desc": "Außerdem haben wir kleine Fehler behoben und an einigen Details gefeilt, damit dir das tägliche Lernen mit FreeLingo noch mehr Freude macht."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Noch nichts hier. Sei der Erste!",
"votes": "{count} Stimmen",
"vote": "1 Stimme",
+ "voteAction": "Abstimmen",
+ "removeVoteAction": "Stimme zurückziehen",
"comments": "{count} Kommentare",
"comment": "1 Kommentar",
"by": "von",
@@ -1691,4 +1705,4 @@
"title": "Wartungsarbeiten",
"description": "Dieser Bereich ist wegen Wartungsarbeiten vorübergehend nicht verfügbar. Bitte versuche es später erneut."
}
-}
\ No newline at end of file
+}
diff --git a/messages/en.json b/messages/en.json
index ce3eaf3d2..151da6ffc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Loading...",
"error": "Error",
+ "errorMessage": "Something went wrong. Please try again.",
"save": "Save changes",
"saving": "Saving...",
"cancel": "Cancel",
@@ -78,6 +79,9 @@
"backToDashboard": "Back to Dashboard",
"tagline": "AI language learning",
"close": "Close",
+ "delete": "Delete",
+ "openMenu": "Open menu",
+ "closeMenu": "Close menu",
"verifyEmailBanner": "Please verify your email address to complete your account setup.",
"verifyEmailSent": "Email sent. Check your inbox.",
"resendVerification": "Resend verification email",
@@ -205,7 +209,7 @@
"saving": "Saving...",
"success": "Password updated. Redirecting to login...",
"mismatch": "Passwords do not match",
- "tooShort": "Password must be at least 8 characters",
+ "tooShort": "Password must be at least 10 characters",
"missingToken": "Invalid reset link. Please request a new password reset.",
"error": "Invalid or expired reset link",
"backToLogin": "Back to login"
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Show chats",
"toggleSidebarHide": "Hide chats",
"conversations": "Conversations",
+ "voiceSession": "Voice session",
"loading": "Loading...",
"thinking": "Thinking...",
"takingLonger": "Taking longer than expected...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabulary",
"all": "All",
"searchPlaceholder": "Search sets...",
- "sets": "sets",
+ "sets": "{count, plural, one {# set} other {# sets}}",
"words": "words",
"addAll": "Add All {count} to Flashcards",
"adding": "Adding...",
@@ -848,6 +853,7 @@
"informal": "Informal",
"copied": "Copied!",
"copy": "Copy",
+ "copyPhrase": "Copy phrase",
"noResults": "No phrases match your filters",
"level": "Level",
"register": "Register",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Translations generated. Review them before saving.",
"translateError": "Translation failed. Your current content was kept.",
"editTranslation": "Translation to edit",
+ "pending": "pending",
"completion": "{complete}/{total} languages complete",
"save": "Save announcement",
"saveSuccess": "Dashboard announcement saved.",
@@ -1062,7 +1069,12 @@
"ru": "Russian",
"nl": "Dutch",
"pl": "Polish",
- "ro": "Romanian"
+ "ro": "Romanian",
+ "tr": "Turkish",
+ "sv": "Swedish",
+ "da": "Danish",
+ "fi": "Finnish",
+ "hr": "Croatian"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Error",
"errorServicesDisabled": "Voice services are not enabled on this server.",
"errorConnection": "Connection error. Please try again.",
+ "errorTranscription": "Your speech could not be transcribed. Please try speaking again.",
+ "errorResponse": "Lingu could not respond. Please try again.",
+ "errorSpeech": "Could not generate audio for Lingu's reply. Please try again.",
"errorUnauthorized": "Session expired. Please refresh.",
"errorMic": "Microphone access denied.",
"errorVadInit": "Failed to initialize voice recognition. Make sure your browser supports WebAssembly.",
@@ -1234,6 +1249,11 @@
"nl": "Dutch",
"ro": "Romanian",
"ru": "Russian",
+ "tr": "Turkish",
+ "sv": "Swedish",
+ "da": "Danish",
+ "fi": "Finnish",
+ "hr": "Croatian",
"myLanguages": "My Languages",
"addLanguage": "Add new language",
"selectLanguage": "Select the language you want to learn",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "What's New",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Got it",
"entry1": {
- "label": "Better-balanced study plans",
- "desc": "New plans spread lessons more evenly across all units and make room for a wider mix of activities, even in shorter plans."
+ "label": "Five new interface languages",
+ "desc": "FreeLingo is available in Croatian, Danish, Finnish, Swedish and Turkish. Choose your interface language in Settings."
},
"entry2": {
- "label": "A test to round off your plan",
- "desc": "Once you reach the final day and finish any outstanding lessons, you can take the end-of-level test from the dashboard or My Plan and see how much you've learned."
- },
- "entry3": {
- "label": "Clearer next steps",
- "desc": "Test results and recommendations are now available in your language, with clearer guidance on taking the next step or reviewing what you've learned."
- },
- "entry4": {
"label": "A smoother experience",
"desc": "We've also fixed minor bugs and polished a few details to make your everyday learning with FreeLingo more enjoyable."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nothing here yet. Be the first to submit one.",
"votes": "{count} votes",
"vote": "1 vote",
+ "voteAction": "Vote",
+ "removeVoteAction": "Remove vote",
"comments": "{count} comments",
"comment": "1 comment",
"by": "by",
@@ -1691,4 +1705,4 @@
"title": "Under Maintenance",
"description": "This section is temporarily unavailable while we perform maintenance. Please check back later."
}
-}
\ No newline at end of file
+}
diff --git a/messages/es.json b/messages/es.json
index b27a12406..512c89033 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Cargando...",
"error": "Error",
+ "errorMessage": "Algo salió mal. Por favor inténtalo de nuevo.",
"save": "Guardar cambios",
"saving": "Guardando...",
"cancel": "Cancelar",
@@ -78,6 +79,9 @@
"backToDashboard": "Volver al inicio",
"tagline": "aprendizaje de idiomas con IA",
"close": "Cerrar",
+ "delete": "Eliminar",
+ "openMenu": "Abrir menú",
+ "closeMenu": "Cerrar menú",
"verifyEmailBanner": "Por favor, verifica tu dirección de correo para completar la configuración de tu cuenta.",
"verifyEmailSent": "Correo enviado. Revisa tu bandeja de entrada.",
"resendVerification": "Reenviar correo de verificación",
@@ -205,7 +209,7 @@
"saving": "Guardando...",
"success": "Contraseña actualizada. Redirigiendo al inicio de sesión...",
"mismatch": "Las contraseñas no coinciden",
- "tooShort": "La contraseña debe tener al menos 8 caracteres",
+ "tooShort": "La contraseña debe tener al menos 10 caracteres",
"missingToken": "Enlace inválido. Por favor, solicita un nuevo restablecimiento.",
"error": "Enlace inválido o caducado",
"backToLogin": "Volver al inicio de sesión"
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Mostrar chats",
"toggleSidebarHide": "Ocultar chats",
"conversations": "Conversaciones",
+ "voiceSession": "Sesión de voz",
"loading": "Cargando...",
"thinking": "Pensando...",
"takingLonger": "Está tardando más de lo esperado...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabulario",
"all": "Todos",
"searchPlaceholder": "Buscar conjuntos...",
- "sets": "conjuntos",
+ "sets": "{count, plural, one {# conjunto} other {# conjuntos}}",
"words": "palabras",
"addAll": "Añadir las {count} a tarjetas",
"adding": "Añadiendo...",
@@ -848,6 +853,7 @@
"informal": "Informal",
"copied": "¡Copiado!",
"copy": "Copiar",
+ "copyPhrase": "Copiar frase",
"noResults": "Ninguna frase coincide con los filtros",
"level": "Nivel",
"register": "Registro",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Traducciones generadas. Revísalas antes de guardar.",
"translateError": "La traducción falló. Se ha conservado el contenido actual.",
"editTranslation": "Traducción que editar",
+ "pending": "pendiente",
"completion": "{complete}/{total} idiomas completos",
"save": "Guardar anuncio",
"saveSuccess": "Anuncio del panel guardado.",
@@ -1062,7 +1069,12 @@
"ru": "Ruso",
"nl": "Neerlandés",
"pl": "Polaco",
- "ro": "Rumano"
+ "ro": "Rumano",
+ "tr": "Turco",
+ "sv": "Sueco",
+ "da": "Danés",
+ "fi": "Finés",
+ "hr": "Croata"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Error",
"errorServicesDisabled": "Los servicios de voz no están habilitados en este servidor.",
"errorConnection": "Error de conexión. Por favor, inténtalo de nuevo.",
+ "errorTranscription": "No se ha podido transcribir lo que has dicho. Intenta hablar de nuevo.",
+ "errorResponse": "Lingu no ha podido responder. Inténtalo de nuevo.",
+ "errorSpeech": "No se ha podido generar el audio de la respuesta de Lingu. Inténtalo de nuevo.",
"errorUnauthorized": "Sesión expirada. Por favor, actualiza la página.",
"errorMic": "Acceso al micrófono denegado.",
"errorVadInit": "Error al inicializar el reconocimiento de voz. Comprueba que el navegador es compatible con WebAssembly.",
@@ -1234,6 +1249,11 @@
"nl": "Neerlandés",
"ro": "Rumano",
"ru": "Ruso",
+ "tr": "Turco",
+ "sv": "Sueco",
+ "da": "Danés",
+ "fi": "Finés",
+ "hr": "Croata",
"myLanguages": "Mis idiomas",
"addLanguage": "Añadir idioma",
"selectLanguage": "Selecciona el idioma que quieres aprender",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Novedades",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Entendido",
"entry1": {
- "label": "Planes mejor equilibrados",
- "desc": "Los nuevos planes reparten mejor las lecciones entre todas las unidades y dan más espacio a distintos tipos de actividades, también en los planes cortos."
+ "label": "Cinco nuevos idiomas de interfaz",
+ "desc": "FreeLingo está disponible en croata, danés, finés, sueco y turco. Elige tu idioma de interfaz en Ajustes."
},
"entry2": {
- "label": "Tu test al terminar el plan",
- "desc": "Al llegar al último día y completar las lecciones pendientes, podrás hacer el test de fin de nivel desde el panel o Mi plan y ver cuánto has aprendido."
- },
- "entry3": {
- "label": "Más claridad para seguir aprendiendo",
- "desc": "Los resultados del test y sus recomendaciones ya están en tu idioma, con indicaciones más claras para dar el siguiente paso o repasar lo aprendido."
- },
- "entry4": {
"label": "Una experiencia más fluida",
"desc": "También hemos corregido pequeños errores y pulido algunos detalles para que disfrutes más de tu día a día con FreeLingo."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nada aquí todavía. ¡Sé el primero en enviar algo!",
"votes": "{count} votos",
"vote": "1 voto",
+ "voteAction": "Votar",
+ "removeVoteAction": "Retirar voto",
"comments": "{count} comentarios",
"comment": "1 comentario",
"by": "por",
@@ -1691,4 +1705,4 @@
"title": "En mantenimiento",
"description": "Esta sección no está disponible temporalmente por tareas de mantenimiento. Vuelve más tarde."
}
-}
\ No newline at end of file
+}
diff --git a/messages/fi.json b/messages/fi.json
new file mode 100644
index 000000000..f704997d6
--- /dev/null
+++ b/messages/fi.json
@@ -0,0 +1,1708 @@
+{
+ "landing": {
+ "microDemo": {
+ "title": "Näin Lingu auttaa sinua",
+ "exampleLabel": "Esimerkki englannin harjoittelusta",
+ "questionLabel": "Lingu · Kysymys",
+ "answerLabel": "Sinä · Vastaus",
+ "correctionLabel": "Lingu · Korjaus",
+ "explanation": "Kun puhut eilisestä, käytä muotoa 'went', joka on verbin 'go' mennyt aikamuoto."
+ },
+ "hero": "Opi kieliä keskustelukumppanin kanssa, jolle voit puhua",
+ "heroSub": "Puhu. Kuuntele. Kehity.",
+ "signIn": "Kirjaudu sisään",
+ "dashboard": "Jatka",
+ "feature1Title": "Opintosuunnitelma",
+ "feature1Desc": "Tasoarvio ja viikkosuunnitelma A1-tasolta C2-tasolle. Oppitunnit avautuvat järjestyksessä omaan tahtiisi.",
+ "feature2Title": "AI-kieliopettaja chatissa",
+ "feature2Desc": "Harjoittele opettajasi ohjaamilla oppitunneilla, tehtävillä ja muistikorteilla. Saat palautetta joka vaiheessa.",
+ "feature3Title": "Äänikeskustelu",
+ "feature3Desc": "Puhu opettajasi kanssa reaaliajassa. Puheohjattu keskustelu, nopeat vastaukset eikä vuoroasi tarvitse odottaa.",
+ "feature4Title": "Kuuntelu ja lukeminen",
+ "feature4Desc": "Taitotasoasi vastaavia tekoälyn luomia harjoituksia. Kuuntele katkelma, vastaa viiteen ymmärtämiskysymykseen ja avaa litterointi – tai lue teksti ja vastaa kysymyksiin. Uusia harjoituksia aina kun haluat.",
+ "feature5Title": "Muistikortit",
+ "feature5Desc": "Kertaa sanastoa aikavälikertauksen avulla. SM-2-algoritmi ajoittaa jokaisen kortin sopivaan hetkeen, jotta muistaisit sanat pidempään.",
+ "feature6Title": "Edistyminen ja putket",
+ "feature6Desc": "XP-pisteet, päivittäiset putket, taitotulokset ja osaaminen yksiköittäin. Seuraa kehitystäsi viikosta toiseen ja testaa tasosi kunkin kielitaitotason lopussa.",
+ "footer": "Avoin lähdekoodi · Ylläpidä itse ilmaiseksi tai tilaa ylläpidetty palvelu",
+ "navFeatures": "Ominaisuudet",
+ "navReviews": "Arvostelut",
+ "navPricing": "Hinnat",
+ "navFAQ": "Usein kysytyt kysymykset",
+ "howItWorks": "Näin se toimii",
+ "faqTitle": "Usein kysytyt kysymykset",
+ "terms": "Ehdot",
+ "privacy": "Tietosuoja",
+ "contact": "Yhteystiedot",
+ "aboutMe": "Tietoa minusta",
+ "github": "GitHub",
+ "footerProduct": "Tuote",
+ "footerLegal": "Oikeudelliset tiedot",
+ "languageGreetings": {
+ "en-US": "Hello!",
+ "en-GB": "Hello!",
+ "de-DE": "Hallo!",
+ "es-ES": "¡Hola!",
+ "fr-FR": "Bonjour !",
+ "it-IT": "Ciao!",
+ "pt-PT": "Olá!",
+ "ja-JP": "こんにちは!",
+ "ko-KR": "안녕하세요!",
+ "zh-CN": "你好!"
+ }
+ },
+ "common": {
+ "loading": "Ladataan...",
+ "error": "Virhe",
+ "errorMessage": "Jokin meni pieleen. Yritä uudelleen.",
+ "save": "Tallenna muutokset",
+ "saving": "Tallennetaan...",
+ "cancel": "Peruuta",
+ "back": "Takaisin",
+ "done": "Valmis",
+ "next": "Seuraava",
+ "submit": "Lähetä",
+ "start": "Aloita",
+ "retry": "Yritä uudelleen",
+ "or": "tai",
+ "appName": "FreeLingo",
+ "days": "päivää",
+ "logout": "Kirjaudu ulos",
+ "logoutConfirmTitle": "Kirjaudu ulos",
+ "logoutConfirmMessage": "Haluatko varmasti kirjautua ulos?",
+ "initializing": "Käynnistetään...",
+ "clearFilters": "Tyhjennä suodattimet",
+ "all": "Kaikki",
+ "score": "Pisteet",
+ "complete": "Suorita loppuun",
+ "checking": "Tarkistetaan...",
+ "backToDashboard": "Takaisin koontinäyttöön",
+ "tagline": "Kielten oppimista tekoälyn avulla",
+ "close": "Sulje",
+ "delete": "Poista",
+ "openMenu": "Avaa valikko",
+ "closeMenu": "Sulje valikko",
+ "verifyEmailBanner": "Vahvista sähköpostiosoitteesi viimeistelläksesi tilisi.",
+ "verifyEmailSent": "Sähköposti lähetetty. Tarkista saapuneet viestit.",
+ "resendVerification": "Lähetä vahvistusviesti uudelleen",
+ "saveWord": "Tallenna",
+ "wordSaved": "Tallennettu",
+ "wordAlreadySaved": "Jo tallennettu",
+ "wordSaveError": "Jokin meni pieleen",
+ "selectWordHint": "Valitse sana tallentaaksesi sen sanastoosi",
+ "noActivePlan": "Aktiivista opintosuunnitelmaa ei löytynyt",
+ "memorySavedToast": "Lingu tallensi uuden muiston. Voit tarkastella sitä Asetuksissa.",
+ "removeLanguageConfirmTitle": "Poista kieli",
+ "removeLanguageConfirmMessage": "Kaikki kielikohtaiset tiedot (opintosuunnitelma, edistyminen, muistikortit ja keskustelut) poistetaan pysyvästi. Tilisi muistot säilyvät. Toimintoa ei voi perua.",
+ "removeLanguageConfirmLabel": "Poista",
+ "nativeHelpTitle": "Ohje: {language}",
+ "nativeHelpLoading": "Valmistellaan ohjetta: {language}...",
+ "nativeHelpShow": "Näytä ohje: {language}",
+ "nativeHelpKeyPoints": "Tärkeimmät asiat",
+ "nativeHelpCommonTraps": "Yleiset sudenkuopat",
+ "nativeHelpMiniGlossary": "Pieni sanasto",
+ "nativeHelpUsageTips": "Käyttövinkit",
+ "nativeHelpRegisterNotes": "Kielirekisteri",
+ "nativeHelpPhraseNotes": "Ilmausten huomioita",
+ "nativeHelpStudyTips": "Opiskeluvinkit",
+ "nativeHelpWordNotes": "Sanojen huomioita",
+ "nativeHelpPractice": "Harjoitus"
+ },
+ "nav": {
+ "home": "Etusivu",
+ "myPlan": "Oma suunnitelma",
+ "progress": "Edistyminen",
+ "flashcards": "Muistikortit",
+ "tutor": "Keskustele opettajan kanssa",
+ "assessment": "Tasoarvio",
+ "resources": "Materiaalit",
+ "grammar": "Kielioppi",
+ "vocabulary": "Sanasto",
+ "phrasebook": "Fraasiopas",
+ "settings": "Asetukset",
+ "faq": "Usein kysytyt kysymykset",
+ "admin": "Ylläpito",
+ "conversation": "Keskustelu",
+ "listening": "Kuuntelu",
+ "reading": "Lukeminen",
+ "contact": "Yhteystiedot",
+ "feedback": "Palaute",
+ "reviews": "Arvostelut",
+ "switchLanguage": "Vaihda kieli"
+ },
+ "auth": {
+ "login": {
+ "title": "Kirjaudu sisään",
+ "subtitle": "Tervetuloa takaisin",
+ "email": "Sähköpostiosoite",
+ "password": "Salasana",
+ "submit": "Kirjaudu sisään",
+ "noAccount": "Eikö sinulla ole tiliä?",
+ "register": "Rekisteröidy",
+ "error": "Virheellinen sähköpostiosoite tai salasana",
+ "emailRequired": "Anna sähköpostiosoitteesi",
+ "passwordRequired": "Anna salasanasi",
+ "loginFailed": "Kirjautuminen epäonnistui",
+ "signingIn": "Kirjaudutaan sisään...",
+ "showPassword": "Näytä salasana",
+ "hidePassword": "Piilota salasana",
+ "accountCreated": "Tili luotu. Voit nyt kirjautua sisään",
+ "invalidEmail": "Anna kelvollinen sähköpostiosoite",
+ "forgotPassword": "Unohditko salasanasi?"
+ },
+ "register": {
+ "title": "Luo tili",
+ "subtitle": "Aloita oppiminen",
+ "username": "Käyttäjänimi",
+ "email": "Sähköpostiosoite",
+ "password": "Salasana",
+ "displayName": "Näyttönimi",
+ "nativeLanguage": "Äidinkieli",
+ "inviteToken": "Kutsukoodi",
+ "inviteTokenPlaceholder": "Anna kutsukoodi",
+ "submit": "Luo tili",
+ "hasAccount": "Onko sinulla jo tili?",
+ "login": "Kirjaudu sisään",
+ "registrationClosed": "Rekisteröityminen on suljettu. Tarvitset kutsukoodin.",
+ "error": "Rekisteröityminen epäonnistui",
+ "passwordMismatch": "Salasanat eivät täsmää",
+ "confirmPassword": "Vahvista salasana",
+ "showPassword": "Näytä salasana",
+ "hidePassword": "Piilota salasana",
+ "displayNamePlaceholder": "Sama kuin käyttäjänimi, jos jätetään tyhjäksi",
+ "inviteActive": "Kutsu on voimassa",
+ "creatingAccount": "Luodaan tiliä...",
+ "invalidEmail": "Anna kelvollinen sähköpostiosoite",
+ "usernameTaken": "Käyttäjänimi on jo käytössä",
+ "emailTaken": "Sähköpostiosoite on jo rekisteröity",
+ "invalidInvite": "Virheellinen tai vanhentunut kutsukoodi",
+ "termsAccept": "Olen lukenut ja hyväksyn",
+ "termsLink": "Käyttöehdot",
+ "andWord": "ja",
+ "privacyLink": "Tietosuojakäytännön",
+ "termsRequired": "Sinun on hyväksyttävä ehdot jatkaaksesi",
+ "invalidPassword": "Salasanassa on oltava 10–25 merkkiä sekä vähintään yksi iso kirjain, yksi numero ja yksi erikoismerkki",
+ "invalidUsernameChars": "Käyttäjänimi saa sisältää vain kirjaimia, numeroita, yhdysmerkkejä, alaviivoja ja pisteitä"
+ },
+ "verifyEmail": {
+ "title": "Sähköpostin vahvistus",
+ "loading": "Vahvistetaan...",
+ "success": "Sähköpostiosoitteesi on vahvistettu. Voit nyt kirjautua sisään.",
+ "error": "Virheellinen tai vanhentunut vahvistuslinkki. Pyydä uusi tililtäsi.",
+ "goToLogin": "Siirry kirjautumiseen"
+ },
+ "forgotPassword": {
+ "title": "Unohtunut salasana",
+ "description": "Anna tiliisi liittyvä sähköpostiosoite, niin lähetämme palautuslinkin.",
+ "emailPlaceholder": "sinun@sahkoposti.fi",
+ "submit": "Lähetä palautuslinkki",
+ "sending": "Lähetetään...",
+ "sent": "Jos sähköpostiosoite on rekisteröity, palautuslinkki on lähetetty. Tarkista saapuneet viestit.",
+ "backToLogin": "Takaisin kirjautumiseen",
+ "error": "Jokin meni pieleen. Yritä uudelleen."
+ },
+ "resetPassword": {
+ "title": "Palauta salasana",
+ "newPassword": "Uusi salasana",
+ "confirmPassword": "Vahvista uusi salasana",
+ "submit": "Aseta uusi salasana",
+ "saving": "Tallennetaan...",
+ "success": "Salasana päivitetty. Siirryt kirjautumiseen...",
+ "mismatch": "Salasanat eivät täsmää",
+ "tooShort": "Salasanassa on oltava vähintään 10 merkkiä",
+ "missingToken": "Virheellinen palautuslinkki. Pyydä uutta salasanan palautusta.",
+ "error": "Virheellinen tai vanhentunut palautuslinkki",
+ "backToLogin": "Takaisin kirjautumiseen"
+ }
+ },
+ "settings": {
+ "title": "Asetukset",
+ "sectionAccount": "Tili",
+ "sectionProfile": "Profiili",
+ "avatarChange": "Vaihda kuva",
+ "avatarRemove": "Poista",
+ "avatarUploading": "Ladataan kuvaa...",
+ "avatarTypeError": "Vain JPEG- ja PNG-kuvat ovat sallittuja",
+ "avatarSizeError": "Kuva on liian suuri (enintään 2 Mt)",
+ "displayName": "Näyttönimi",
+ "email": "Sähköpostiosoite",
+ "nativeLanguage": "Äidinkieli",
+ "uiLocale": "Käyttöliittymän kieli",
+ "uiLocaleHint": "Vaihtaa valikoiden ja painikkeiden kielen. Sivu on ladattava uudelleen.",
+ "newPassword": "Uusi salasana",
+ "newPasswordPlaceholder": "Jätä tyhjäksi säilyttääksesi nykyisen",
+ "confirmPassword": "Vahvista salasana",
+ "confirmPasswordPlaceholder": "Kirjoita uusi salasana uudelleen",
+ "bio": "Tietoa sinusta (tekoälyn taustatiedot)",
+ "bioPlaceholder": "Esimerkiksi: Olen ohjelmistokehittäjä ja matkustan usein työni vuoksi...",
+ "bioHint": "AI-opettaja käyttää tätä keskustelujen mukauttamiseen.",
+ "saveChanges": "Tallenna muutokset",
+ "saving": "Tallennetaan...",
+ "saved": "Profiili päivitetty.",
+ "passwordMismatch": "Salasanat eivät täsmää",
+ "saveFailed": "Tallennus epäonnistui",
+ "logout": "Kirjaudu ulos",
+ "sectionAppearance": "Ulkoasu",
+ "theme": "Teema",
+ "darkActive": "Tumma tila käytössä",
+ "lightActive": "Vaalea tila käytössä",
+ "systemActive": "Automaattinen (järjestelmä)",
+ "themeSystem": "Automaattinen",
+ "themeDark": "Tumma",
+ "themeLight": "Vaalea",
+ "switchToLight": "Vaihda vaaleaan tilaan",
+ "switchToDark": "Vaihda tummaan tilaan",
+ "sectionConversation": "Keskustelu",
+ "conversationMaxDuration": "Keskustelun enimmäiskesto",
+ "conversationInactivityTimeout": "Toimettomuuden aikaraja",
+ "conversationSpeechPause": "Tauko ennen puheenvuoron loppua",
+ "conversationSpeechPauseHint": "Kuinka pitkän tauon voit pitää kesken lauseen ennen kuin puheenvuorosi lähetetään. Automaattinen tila mukautuu tasoosi.",
+ "speechPauseAuto": "Automaattinen",
+ "speechPauseSec1": "1 sekunti",
+ "speechPauseSec2": "2 sekuntia",
+ "speechPauseSec3": "3 sekuntia",
+ "min15": "15 minuuttia",
+ "min30": "30 minuuttia",
+ "min1": "1 minuutti",
+ "min3": "3 minuuttia",
+ "min5": "5 minuuttia",
+ "saveConversation": "Tallenna",
+ "conversationSaved": "Keskusteluasetukset tallennettu.",
+ "sectionLegal": "Oikeudelliset tiedot",
+ "termsOfService": "Käyttöehdot",
+ "privacyPolicy": "Tietosuojakäytäntö",
+ "contactLink": "Yhteystiedot",
+ "deleteAccount": "Poista tili",
+ "deleteAccountTitle": "Poista tili",
+ "deleteAccountMessage": "Tämä poistaa tilisi ja kaikki tietosi pysyvästi. Toimintoa ei voi perua.",
+ "deleteAccountConfirm": "Poista tili",
+ "sectionUsageLimits": "Käyttörajat",
+ "sectionVoice": "Ääni",
+ "voiceHint": "Valitse haluamasi tekoälyääni valikosta ja kuuntele esimerkki painamalla toistopainiketta.",
+ "voicePlay": "Toista",
+ "voiceStop": "Pysäytä",
+ "quotaSessions": "Keskustelua / viikko",
+ "quotaMinutesDay": "Minuuttia / päivä",
+ "quotaMinutesWeek": "Minuuttia / viikko",
+ "quotaTokens": "Tokenia / kuukausi",
+ "quotaUnlimited": "Rajaton",
+ "quotaLoading": "Ladataan...",
+ "quotaHint": "Käyttömäärät nollautuvat viikoittain (keskustelut ja minuutit) ja kuukausittain (tokenit).",
+ "sectionMemory": "Muistot",
+ "memoryEmpty": "Muistoja ei ole vielä tallennettu. Tekoäly muistaa tärkeitä asioita sinusta keskustelujen aikana ja tallentaa ne tänne.",
+ "memoryClearAll": "Tyhjennä kaikki muistot",
+ "memoryClearAllTitle": "Tyhjennä kaikki muistot",
+ "memoryClearAllMessage": "Kaikki tallennetut muistot poistetaan pysyvästi. Toimintoa ei voi perua.",
+ "memoryClearAllConfirm": "Tyhjennä kaikki",
+ "memoryManage": "Hallitse muistoja",
+ "memoryBreadcrumb": "Muistiasetukset",
+ "memoryDescription": "Muistot ovat yhteisiä kaikille opiskelemillesi kielille. Lingu voi tallentaa hyödyllisiä tietoja keskustelujen aikana, ja voit lisätä tai poistaa niitä täällä.",
+ "memoryInputLabel": "Lisää muisto",
+ "memoryInputPlaceholder": "Esimerkiksi: Pidän lyhyistä selityksistä.",
+ "memoryInputHint": "Enintään {max} merkkiä",
+ "memoryAdd": "Lisää",
+ "memoryAdding": "Lisätään...",
+ "memoryAddSuccess": "Muisto lisätty.",
+ "memoryAddError": "Muistoa ei voitu lisätä. Yritä uudelleen.",
+ "memoryDuplicateError": "Tämä muisto on jo olemassa.",
+ "memoryLoadError": "Muistoja ei voitu ladata.",
+ "memoryDeleteLabel": "Poista muisto: {content}",
+ "memoryDeleteSuccess": "Muisto poistettu.",
+ "memoryDeleteError": "Muistoa ei voitu poistaa.",
+ "memoryClearing": "Tyhjennetään...",
+ "memoryClearSuccess": "Kaikki muistot poistettu.",
+ "memoryClearError": "Muistoja ei voitu tyhjentää.",
+ "memorySourceChat": "Chatti",
+ "memorySourceVoice": "Ääni",
+ "memorySourceManual": "Manuaalinen",
+ "memorySourceUnknown": "Muisto",
+ "sectionLanguages": "Kielet",
+ "languagesManage": "Omat kielet",
+ "cardProfileAccess": "Profiili ja käyttöoikeudet",
+ "cardSessionSecurity": "Istunto ja turvallisuus",
+ "cardTheme": "Teema",
+ "cardConversationTiming": "Keskustelun ajoitus",
+ "cardTutorVoice": "Opettajan ääni",
+ "cardCurrentUsage": "Nykyinen käyttö",
+ "cardProductReview": "Tuotearvostelu",
+ "cardLegalDocuments": "Oikeudelliset asiakirjat",
+ "sectionReview": "Arvostelu",
+ "reviewDescription": "Kerro kokemuksestasi FreeLingossa. Jos muokkaat jo hyväksyttyä arvostelua, se tarkistetaan uudelleen ennen julkista näyttämistä.",
+ "reviewPending": "Odottaa hyväksyntää",
+ "reviewUpdate": "Päivitä",
+ "reviewSaved": "Arvostelu tallennettu.",
+ "reviewDelete": "Poista arvostelu",
+ "reviewDeleteTitle": "Poista arvostelu",
+ "reviewDeleteMessage": "Haluatko poistaa arvostelusi? Toimintoa ei voi perua.",
+ "reviewDeleteConfirm": "Poista",
+ "reviewDeleted": "Arvostelu poistettu.",
+ "reviewDeleteError": "Arvosteluasi ei voitu poistaa. Yritä uudelleen."
+ },
+ "legal": {
+ "terms": {
+ "pageTitle": "Käyttöehdot",
+ "updated": "Päivitetty viimeksi: elokuu 2026",
+ "s1Title": "1. Tietoa FreeLingosta",
+ "s1Body": "FreeLingo on avoimen lähdekoodin tekoälypohjainen kieltenoppimisalusta, jota voi käyttää kahdella tavalla: (1) itse ylläpidettynä omassa infrastruktuurissasi maksutta tai (2) FreeLingo-tiimin ylläpitämänä palveluna, jossa on ilmainen suunnitelma ja maksullisia tilauksia. Käyttämällä palvelua kummalla tahansa tavalla hyväksyt nämä käyttöehdot.",
+ "s2Title": "2. Sallittu käyttö",
+ "s2Intro": "Saat käyttää FreeLingoa vain henkilökohtaiseen kielten oppimiseen. Sitoudut olemaan:",
+ "s2i1": "Yrittämättä päästä luvatta mihinkään järjestelmän osaan.",
+ "s2i2": "Lataamatta haitallista sisältöä tai häiritsemättä palvelun toimintaa.",
+ "s2i3": "Jakamatta tilisi kirjautumistietoja kolmansille osapuolille.",
+ "s2i4": "Käyttämättä automatisoituja komentosarjoja tai botteja AI-opettajan toimintojen väärinkäyttöön.",
+ "s3Title": "3. Käyttäjätilit",
+ "s3Body": "Sinun on oltava vähintään 16-vuotias luodaksesi tilin ja käyttääksesi FreeLingoa. Rekisteröitymällä vahvistat täyttäväsi ikävaatimuksen. Jos saamme tietää tilin kuuluvan alle 16-vuotiaalle, poistamme tilin ja siihen liittyvät tiedot ilmoittamatta siitä etukäteen. Vastaat kirjautumistietojesi salassapidosta ja kaikesta tililläsi tapahtuvasta toiminnasta. Palvelimen ylläpitäjä – itse ylläpidetyssä asennuksessa sinä tai ylläpidetyssä palvelussa FreeLingo-tiimi – voi keskeyttää tai sulkea tilin, joka rikkoo näitä ehtoja.",
+ "s4Title": "4. Sisältö ja tekoälyvuorovaikutus",
+ "s4Body": "Keskustelut AI-opettajan kanssa tallennetaan tietokantaan, jotta opiskelu voi jatkua myöhemmin. Ylläpidetyssä palvelussa AI-chat ja äänitoiminnot käsitellään OpenAI:n rajapinnan kautta, ja sisältöösi sovelletaan OpenAI:n käyttökäytäntöjä. Tekoäly voi antaa virheellisiä vastauksia – käytä aina omaa harkintaasi. FreeLingo ei vastaa taustalla olevien kielimallien tuottamasta sisällöstä. AI-opettajan luoma sisältö (harjoitukset, korjaukset, selitykset ja muu materiaali) kuuluu sinulle henkilökohtaiseen käyttöösi. FreeLingo ei vaadi omistusoikeutta keskusteluihisi tai niistä tuotettuun sisältöön.",
+ "s5Title": "5. Palvelun käyttötavat",
+ "s5Body": "Itse ylläpidetyt asennukset ovat maksuttomia, ja ylläpitäjä hallitsee täysin tietojaan ja infrastruktuuriaan. FreeLingo-tiimin ylläpitämässä palvelussa on käyttörajoituksia sisältävä ilmainen suunnitelma sekä kuukausittaisia ja vuosittaisia maksullisia tilauksia rajattomaan käyttöön. Ilmaiseen suunnitelmaan sisältyy seitsemän päivän kokeilujakso täydellä käyttöoikeudella (maksutapaa ei vaadita), jonka jälkeen oppitunneille ja AI-chatille on päivittäiset sekä kuuntelu- ja lukuharjoituksille ja äänikeskusteluille viikoittaiset kiintiöt. Maksullisissa tilauksissa nämä käyttörajat poistuvat. Ylläpidetyssä palvelussa tietosi tallennetaan FreeLingo-tiimin ylläpitämille palvelimille. Tilisi nykyiset käyttörajat näkyvät koontinäytössä.",
+ "s6Title": "6. Tilaus ja laskutus",
+ "s6Body": "Ilmainen suunnitelma tarjoaa rajoitetun pääsyn tekoälytoimintoihin ja uusille käyttäjille seitsemän päivän kokeilujakson täydellä käyttöoikeudella (maksutapaa ei vaadita). Maksulliset tilaukset ovat kuukausittaisia tai vuosittaisia ja uusiutuvat automaattisesti, ellei niitä peruta etukäteen. Voit perua tilauksesi milloin tahansa Asetuksissa; peruutus tulee voimaan nykyisen kauden päättyessä. Ylläpitäjä voi myös päättää tilausten uusiutumisen tai tilaukset, jos palvelu muuttuu tai suljetaan. Jos palvelu päättyy ennen maksetun kauden loppua, kuluttajalainsäädännön vaatima hyvitys maksetaan. Peruuttamisoikeudet ja voimassa olevat hinnat näytetään ja käsitellään sovellettavan lain mukaisesti.",
+ "s7Title": "7. Saatavuus",
+ "s7Body": "Ylläpidetty palvelu tarjotaan ilman takuuta jatkuvuudesta tai saatavuudesta. Ylläpitäjä voi milloin tahansa muuttaa palvelua, keskeyttää sen tai sulkea sen teknisistä, taloudellisista tai muista syistä. Suunnitelluista sulkemisista ilmoitetaan sovelluksessa tai sähköpostitse, kun se on kohtuudella mahdollista. Ilmoitetun päivän jälkeen tilit ja palvelun tiedot voidaan poistaa ilman palautusmahdollisuutta. FreeLingo ei tarjoa varmuuskopiointipalvelua eikä automaattista vientityökalua.",
+ "s8Title": "8. Vastuunrajoitus",
+ "s8Body": "FreeLingo ja sen kehittäjät eivät vastaa suorista, epäsuorista, satunnaisista tai välillisistä vahingoista, jotka aiheutuvat palvelun käytöstä tai siitä, ettei sitä voi käyttää. Sovellettavan lain sallimissa rajoissa kokonaisvastuu on enintään vaatimusta edeltävien kolmen kuukauden aikana maksamasi summa. Sitoudut pitämään FreeLingon ja sen kehittäjät vahingoittumattomina kolmansien osapuolten vaatimuksista, vahingoista tai kuluista, jotka aiheutuvat ehtojen vastaisesta palvelun käytöstäsi.",
+ "s9Title": "9. Sovellettava laki",
+ "s9Body": "Näihin ehtoihin sovelletaan Espanjan lakia. Ehdoista johtuvat riidat kuuluvat yksinomaan Espanjan tuomioistuinten ratkaistaviksi, ellei asuinmaasi pakottava kuluttajansuojalainsäädäntö määrää toisin.",
+ "s10Title": "10. Ehtojen muutokset",
+ "s10Body": "Näitä ehtoja voidaan päivittää milloin tahansa. Olennaisista muutoksista ilmoitetaan palvelun käyttöliittymässä tai tarvittaessa sähköpostitse. Palvelun käytön jatkaminen muutoksen jälkeen merkitsee päivitettyjen ehtojen hyväksymistä.",
+ "s11Title": "11. Osittainen pätemättömyys",
+ "s11Body": "Jos toimivaltainen tuomioistuin katsoo jonkin näiden ehtojen määräyksen pätemättömäksi tai täytäntöönpanokelvottomaksi, määräystä rajoitetaan tai se poistetaan vain välttämättömässä laajuudessa ja muut määräykset pysyvät täysin voimassa.",
+ "linkPrivacy": "Tietosuojakäytäntö",
+ "linkBack": "Takaisin rekisteröitymiseen",
+ "linkBackSettings": "Takaisin Asetuksiin",
+ "linkContact": "Jos sinulla on kysyttävää ehdoista, käytä osoitteessa freelingo.app olevaa yhteydenottolomaketta sen ollessa saatavilla tai FreeLingon GitHub-arkistossa ilmoitettua yhteystapaa."
+ },
+ "privacy": {
+ "pageTitle": "Tietosuojakäytäntö",
+ "updated": "Päivitetty viimeksi: elokuu 2026",
+ "s1Title": "1. Yleiskatsaus",
+ "s1Body": "FreeLingo on saatavilla itse ylläpidettävänä sovelluksena ja ylläpidettynä palveluna. Itse ylläpidettävässä asennuksessa kaikki tiedot tallennetaan yksinomaan ylläpitäjän palvelimelle eikä FreeLingo-projektin tekijöillä ole pääsyä niihin. Ylläpidetyssä palvelussa FreeLingo-tiimi toimii rekisterinpitäjänä. Tässä käytännössä kuvataan, miten henkilötietojasi kerätään, käytetään ja suojataan.",
+ "s2Title": "2. Keräämämme tiedot",
+ "s2Intro": "FreeLingoa ei ole suunnattu alle 16-vuotiaille. Emme tietoisesti kerää henkilötietoja alle 16-vuotiailta. Jos olet alle 16-vuotias, älä luo tiliä tai anna henkilötietoja. Kun luot tilin ja käytät FreeLingoa, seuraavat tiedot tallennetaan:",
+ "s2i1": "Käyttäjänimi, näyttönimi ja sähköpostiosoite.",
+ "s2i2": "Tiivisteenä tallennettu salasana (bcrypt – salasanaasi ei koskaan tallenneta selväkielisenä).",
+ "s2i3": "Äidinkielivalinta ja opiskeluasetukset.",
+ "s2i4": "Valinnainen profiilikuva.",
+ "s2i5": "Chat- ja keskusteluhistoria AI-opettajan kanssa.",
+ "s2i6": "Muistikortit, opiskelun edistyminen ja aikavälikertauksen tiedot.",
+ "s2i7": "Tasoarvioiden tulokset ja opintosuunnitelman eteneminen.",
+ "s2i8": "Tilaustila, suunnitelman tyyppi ja Stripen asiakastunnus (maksullisen palvelun tilaajille).",
+ "s2i9": "Kaikille kielille yhteiset tilin muistot ja mieltymykset, jotka AI-opettaja oppii keskusteluissa tai jotka lisäät itse tulevien keskustelujen mukauttamiseksi.",
+ "s2i10": "Ominaisuusehdotukset, virheraportit, kommentit ja äänet, jotka lähetät palautetaulun kautta. Vain käyttäjänimesi näkyy julkisesti; kaikki käyttäjät näkevät julkaistut ehdotukset ja raportit.",
+ "s3Title": "3. Tietojesi käyttö",
+ "s3Intro": "Tietojasi käytetään ainoastaan FreeLingon oppimiskokemuksen tarjoamiseen:",
+ "s3i1": "Henkilöllisyytesi varmistamiseen ja istuntosi ylläpitämiseen.",
+ "s3i2": "Opintosuunnitelmasi ja keskustelujesi mukauttamiseen.",
+ "s3i3": "Oppimisesi edistymisen seuraamiseen.",
+ "s3i4": "Tilanteeseen sopivien AI-opettajan vastausten tuottamiseen.",
+ "s3i5": "Mieltymystesi ja tilanteesi muistamiseen, jotta opettaja voi mukauttaa tulevia keskusteluja.",
+ "s3i6": "Yhteisön ominaisuusehdotusten ja virheraporttien näyttämiseen ja hallintaan.",
+ "s3Footer": "Tietojasi ei koskaan myydä, vuokrata tai jaeta kolmansien osapuolten markkinointipalveluille.",
+ "s4Title": "4. Ulkoiset tekoälypalvelut",
+ "s4Body": "Ylläpidetyssä palvelussa AI-chat, puhesynteesi (TTS) ja puheen litterointi (STT) toteutetaan OpenAI:n rajapinnan kautta. Keskustelujesi sisältö ja ääni lähetetään OpenAI:lle käsiteltäväksi OpenAI:n tietosuojakäytännön mukaisesti (openai.com/privacy). Maksut käsittelee Stripe, Inc. (Yhdysvallat); Stripe käsittelee maksutiedot suoraan emmekä tallenna maksukorttitietoja palvelimillemme – maksutapahtumiasi koskee Stripen tietosuojakäytäntö (stripe.com/privacy). Itse ylläpidettävässä asennuksessa ylläpitäjä valitsee tekoälypalvelun; oletuksena käytetään paikallista Ollama-asennusta eivätkä tiedot poistu palvelimelta.",
+ "s5Title": "5. Evästeet ja istuntotunnukset",
+ "s5Body": "FreeLingo käyttää kahta palvelun toiminnalle välttämätöntä teknistä evästettä: refresh_token (httpOnly, voimassa 30 päivää) istunnon tunnistamiseen ja NEXT_LOCALE (voimassa vuoden) kielivalintasi muistamiseen. Seuranta- tai mainosevästeitä ei käytetä. Nimettömään kävijämäärien mittaamiseen käytämme Umamia – itse ylläpidettävää avoimen lähdekoodin analytiikkatyökalua, joka ei käytä evästeitä, ei kerää henkilötietoja ja säilyttää kaikki tiedot omilla palvelimillamme.",
+ "s6Title": "6. Oikeutesi (GDPR)",
+ "s6Body": "Jos olet Euroopan talousalueella, sinulla on GDPR:n mukaiset oikeudet saada pääsy tietoihisi, oikaista ja poistaa niitä, vastustaa käsittelyä ja siirtää tietoja. Voit poistaa tilisi itse Asetuksissa. Muut pyynnöt käsitellään manuaalisesti kohdassa 8 mainittua yhteystapaa käyttäen; FreeLingo ei tarjoa automaattista vientityökalua. Käsittelemme tietoja palvelun tarjoamiseksi (GDPR:n 6 artiklan 1 kohdan b alakohta). OpenAI ja Stripe voivat käsitellä tietoja Yhdysvalloissa kansainvälisiin siirtoihin sovellettavien suojatoimien mukaisesti. Voit valittaa Espanjan tietosuojaviranomaiselle (aepd.es) tai oman maasi valvontaviranomaiselle.",
+ "s7Title": "7. Tietojen säilyttäminen ja poistaminen",
+ "s7Body": "Palvelun tietoja säilytetään niin kauan kuin tilisi on olemassa ja ylläpidetty palvelu toimii. Voit poistaa muistoja, kieliä tai koko tilisi Asetuksissa. Ylläpitäjä voi myös poistaa kaikki tilit ja niiden tiedot palvelun sulkemisen yhteydessä ilmoitettuaan siitä kohtuullisesti mahdollisuuksien mukaan. Poistamista ei voi perua. Poistettuaan hallinnassaan olevat järjestelmät ja kopiot FreeLingo ei säilytä henkilötietoja. Stripe voi säilyttää maksu- ja tapahtumatietoja omien käytäntöjensä ja lakisääteisten velvoitteidensa mukaisesti.",
+ "s8Title": "8. Yhteydenotto",
+ "s8Body": "Ylläpidetyn palvelun tietosuojaan liittyvissä kysymyksissä käytä osoitteessa freelingo.app olevaa yhteydenottolomaketta sen ollessa käytettävissä. Palvelun sulkemisen jälkeen käytä FreeLingon GitHub-arkistossa ilmoitettua yhteystapaa äläkä julkaise henkilötietoja julkisissa tiketeissä. Itse ylläpidettävässä asennuksessa ota yhteyttä sen ylläpitäjään.",
+ "s9Title": "9. Osittainen pätemättömyys",
+ "s9Body": "Jos tämän käytännön jokin määräys todetaan pätemättömäksi tai täytäntöönpanokelvottomaksi, sitä rajoitetaan tai se poistetaan vain välttämättömässä laajuudessa, ja muut määräykset pysyvät täysin voimassa.",
+ "linkTerms": "Käyttöehdot",
+ "linkBack": "Takaisin rekisteröitymiseen",
+ "linkBackSettings": "Takaisin Asetuksiin"
+ }
+ },
+ "dashboard": {
+ "welcomeBack": "Tervetuloa takaisin",
+ "streak": "Putki",
+ "xp": "XP",
+ "level": "Taso",
+ "skills": "Taidot",
+ "today": "Tänään",
+ "noSkills": "Suorita tasoarvio seurataksesi taitojasi.",
+ "noLessons": "Tälle päivälle ei ole suunniteltu oppitunteja.",
+ "lessonDone": "valmis",
+ "startLesson": "Aloita",
+ "loadingProgress": "Ladataan edistymistä...",
+ "noLevel": "Tasoa ei ole vielä määritetty.",
+ "completeAssessment": "Tee tasoarvio",
+ "allCaughtUp": "Suunnitelmasi, sinun tahtiisi",
+ "levelTestReady": "Tasotestisi on valmis.",
+ "levelTestReadyDesc": "Olet saavuttanut suunnitelmasi lopun. Tee tasotesti vahvistaaksesi edistymisesi.",
+ "levelTestCompleted": "Tasotesti suoritettu.",
+ "levelTestScoreLine": "Lopputulos: {score}. Tuloksesi on tallennettu Omaan suunnitelmaan.",
+ "startWithAssessment": "Aloita tasoarviolla.",
+ "takeAssessmentArrow": "Tee tasoarvio →",
+ "goToMyPlan": "Oma suunnitelma →",
+ "quickActions": "Pikatoiminnot",
+ "dayProgress": "Päivä {current}/{total}",
+ "skipDay": "Ohita tämä päivä",
+ "pendingLessons": "odottavaa oppituntia",
+ "nextStep": "Seuraava vaihe",
+ "assessmentCreatesPlan": "Tasoarvio selvittää tasosi ja luo henkilökohtaisen suunnitelman aktiiviselle kielellesi.",
+ "lessonReady": "Seuraava vaihe omaan tahtiisi",
+ "pendingStillAvailable": "Suunnitelmassasi odottaa {count} aiempaa oppituntia.",
+ "noPendingToday": "Katso suunnitelmastasi, miten voit jatkaa.",
+ "planDayGoal": "Tämän päivän tavoite suunnitelmassasi",
+ "lessonsCompleted": "Oppitunnit",
+ "accuracy": "Oikein vastatut",
+ "exerciseStats": "{correct}/{total} tehtävää",
+ "noExercisesYet": "Ei vielä tehtäviä",
+ "planProgress": "Suunnitelman edistyminen",
+ "currentDay": "Nykyinen päivä",
+ "daysRemaining": "Päiviä jäljellä",
+ "completedToday": "{completed}/{total} valmiina",
+ "vocabularyProgress": "Sanasto {level}",
+ "vocabularyWords": "{mastered}/{total} sanaa",
+ "recentPerformance": "Viimeaikaiset tulokset",
+ "recentPerformanceDescription": "Perustuu viimeisimpiin vastauksiisi ja kertauksiisi.",
+ "performanceNeedsPractice": "Vaatii harjoittelua",
+ "performanceInProgress": "Kesken",
+ "performanceStrong": "Vahva",
+ "premiumBannerTitle": "Avaa keskustelu-, kuuntelu- ja lukuharjoitukset",
+ "premiumBannerDesc": "Aloita ilmaisella kokeilulla ja harjoittele Lingun kanssa tekstillä, puheella ja tasoosi sopivilla tehtävillä.",
+ "premiumBannerDescTrialUsed": "Ota Premium käyttöön avataksesi tekstikeskustelut Lingun kanssa sekä ääni-, luku- ja kuunteluharjoitukset.",
+ "premiumBannerCta": "Ilmainen kokeilu",
+ "premiumBannerCtaTrialUsed": "Näytä tilaukset",
+ "premiumBannerPastDueTitle": "Maksu odottaa",
+ "premiumBannerPastDueDesc": "Päivitä maksutapasi saadaksesi Premium-keskustelut ja ääni-, luku- ja kuunteluharjoitukset jälleen käyttöösi.",
+ "premiumBannerPastDueCta": "Palauta Premium",
+ "freemiumTrialTitle": "{days, plural, one {# päivä ilmaista kokeilua} other {# päivää ilmaista kokeilua}}",
+ "freemiumTrialDesc": "Käytä rajattomasti chat-, ääni-, kuuntelu- ja lukutoimintoja. Kokeile kaikkia Premium-ominaisuuksia ilmaiseksi.",
+ "announcementDismiss": "Sulje ilmoitus",
+ "announcementDismissError": "Ilmoitusta ei voitu sulkea. Yritä uudelleen."
+ },
+ "assessment": {
+ "title": "Tasoarvio",
+ "subtitle": "CEFR-tason määritys",
+ "intro": "Esitämme muutaman kysymyksen englannin taitotasosi selvittämiseksi.",
+ "startWriting": "Aloita kirjoitusarvio",
+ "startQuiz": "Aloita kysely",
+ "writing": {
+ "title": "Kirjoitusnäyte",
+ "submit": "Lähetä teksti",
+ "placeholder": "Kirjoita vastauksesi tähän...",
+ "evaluating": "Arvioidaan..."
+ },
+ "quiz": {
+ "question": "Kysymys",
+ "of": "/",
+ "next": "Seuraava",
+ "submit": "Lähetä kysely",
+ "submitting": "Lähetetään..."
+ },
+ "result": {
+ "title": "Tasosi",
+ "xpEarned": "Ansaitut XP-pisteet",
+ "skillsTitle": "Taidot",
+ "goToPlan": "Näytä oma suunnitelma",
+ "generatePlan": "Luo opintosuunnitelma"
+ },
+ "currentLevel": "Nykyinen taso",
+ "assessedOn": "Arvioitu",
+ "alreadyHasPlan": "Sinulla on jo aktiivinen opintosuunnitelma. Uusi arvio luo uuden suunnitelman.",
+ "retake": "Tee tasoarvio uudelleen",
+ "evaluating": "Arvioidaan...",
+ "resultStep": "Tulos",
+ "cefrLevel": "CEFR-taso",
+ "overrideLevel": "Vaihda lähtötasoa",
+ "strengths": "Vahvuudet",
+ "needsWork": "Kehitettävää",
+ "createPlan": "Luo suunnitelma →",
+ "creating": "Luodaan...",
+ "loading": "Ladataan...",
+ "levelTest": {
+ "title": "Tasotesti",
+ "loading": "Ladataan kysymyksiä...",
+ "loadingTest": "Ladataan testiä...",
+ "submittingTest": "Lähetetään vastauksia...",
+ "quizTitle": "Tason {level} tasotesti",
+ "resultsTitle": "Tason {level} tasotesti – tulokset",
+ "finalScore": "Lopputulos",
+ "correctCount": "{correct} / {total} oikein",
+ "correctAnswer": "✓ Oikein",
+ "incorrectAnswer": "✗ Väärin – oikea vastaus: {answer}",
+ "nextLevelFallback": "SEURAAVA TASO",
+ "advanceLabel": "VALMIS TASOLLE {level}",
+ "advanceMessage": "Osoitit hallitsevasi tason {level} hyvin. Tee tasoarvio uudelleen luodaksesi suunnitelman tasolle {next}.",
+ "extendLabel": "NELJÄN VIIKON JATKO",
+ "extendMessage": "Löysimme kehittämiskohteita: {areas}. Suosittelemme neljää lisäviikkoa kohdennettua harjoittelua.",
+ "repeatLabel": "KERTAA TASO {level}",
+ "repeatMessage": "Tulos on alle 55 %. Useita tason {level} perustaitoja on vahvistettava.",
+ "reviewPlan": "Tarkastele suunnitelmaani →",
+ "goToPlan": "Siirry suunnitelmaan",
+ "invalidPlan": "Virheellinen suunnitelman tunnus. Avaa tasotesti suunnitelmasivulta.",
+ "noQuestions": "Palvelin ei lähettänyt kysymyksiä.",
+ "loadFailed": "Tasotestiä ei voitu ladata.",
+ "submitFailed": "Vastausten lähetys epäonnistui.",
+ "question": "Kysymys",
+ "of": "/",
+ "confirm": "Vahvista vastaus",
+ "nextQuestion": "Seuraava kysymys",
+ "submit": "Lähetä testi",
+ "result": {
+ "title": "Testi valmis",
+ "recommendation": "Suositus",
+ "advance": "Valmis siirtymään seuraavalle tasolle",
+ "extend": "Jatka nykyisellä tasolla",
+ "repeat": "Nykyisen tason kertausta suositellaan",
+ "back": "Takaisin suunnitelmaan"
+ }
+ },
+ "suggestedLevel": "Ehdotettu taso {aiLevel}; suunnitelmassa käytetään tasoa {selectedLevel}",
+ "step1": "Vaihe 1 / 3: Tasosi",
+ "step2": "Vaihe 2 / 3: Kysymys {questionNumber} / {totalQuestions}",
+ "dontKnow": "En tiedä",
+ "step3": "Vaihe 3 / 3: Ohjelmasi",
+ "studiedBefore": "Oletko opiskellut tätä kieltä aiemmin ({language})?",
+ "studiedBeforeHint": "Tämä auttaa aloittamaan kyselyn oikealta tasolta.",
+ "beginnerOption": "Olen aivan aloittelija",
+ "beginnerOptionHint": "Aloitan A1-tasolta, en tarvitse kyselyä.",
+ "hasExperienceOption": "Kyllä, minulla on jonkin verran kokemusta",
+ "hasExperienceOptionHint": "Teen lyhyen mukautuvan kyselyn (noin 12 kysymystä).",
+ "skills": {
+ "grammar": "Kielioppi",
+ "vocabulary": "Sanasto",
+ "reading": "Lukeminen"
+ },
+ "howManyWeeks": "Kuinka monta viikkoa tasolle {cefr_level}?",
+ "nWeeks": "{count} viikkoa",
+ "intensity": {
+ "intensive": "Intensiivinen",
+ "standard": "Tavallinen",
+ "relaxed": "Rento",
+ "veryRelaxed": "Hyvin rento"
+ },
+ "approxLessons": "Noin {count} oppituntia",
+ "daysPerWeek": "{count} päivää viikossa",
+ "mainGoals": "Päätavoitteet (valitse kaikki sopivat)",
+ "goals": {
+ "grammar": "Kielioppi",
+ "vocabulary": "Sanasto",
+ "reading": "Lukeminen",
+ "writing": "Kirjoittaminen",
+ "conversation": "Keskustelu",
+ "listening": "Kuuntelu"
+ },
+ "summaryLevel": "Taso",
+ "summaryDuration": "Kesto",
+ "summaryGoals": "Tavoitteet",
+ "noneSelected": "ei valintoja",
+ "buildingPlan": "Luodaan suunnitelmaa...",
+ "startWarningTitle": "Ennen aloittamista",
+ "startWarningMessage": "Jos poistut tasoarviosta ennen sen suorittamista, edistymisesi katoaa ja sinun on aloitettava alusta.",
+ "startWarningMessageLevelTest": "Jos poistut tasotestistä ennen sen suorittamista, edistymisesi katoaa ja sinun on tehtävä testi uudelleen.",
+ "startWarningConfirm": "Aloita",
+ "startMyPlan": "Aloita oma suunnitelma",
+ "voiceTrialLabel": "Äänikokeilu",
+ "voiceTrialTitle": "Kokeile oikeaa keskustelua nyt",
+ "voiceTrialDesc": "Puhu Lingun kanssa {minutes} minuuttia ilmaiseksi. Opettajasi mukautuu juuri saavuttamaasi tasoon.",
+ "voiceTrialStart": "Aloita ilmainen keskustelu",
+ "voiceTrialSkip": "Ohita toistaiseksi"
+ },
+ "plan": {
+ "unitLocked": "Yksikkö on lukittu",
+ "unitAvailable": "Yksikkö on käytettävissä",
+ "lessonPending": "Odottava oppitunti",
+ "title": "Oma suunnitelma",
+ "subtitle": "Opintosuunnitelma",
+ "noPlan": "Aktiivista opintosuunnitelmaa ei löytynyt.",
+ "startAssessment": "Tee ensin tasoarvio",
+ "week": "Viikko",
+ "currentLesson": "Nykyinen oppitunti",
+ "currentUnit": "Nykyinen yksikkö",
+ "startLesson": "Aloita oppitunti",
+ "completed": "Suoritettu",
+ "generating": "Luodaan oppituntia...",
+ "generatingError": "Oppituntia ei voitu luoda",
+ "loading": "Ladataan suunnitelmaa...",
+ "learningRoadmap": "Oppimispolku",
+ "level": "Taso",
+ "duration": "Kesto",
+ "durationDetail": "{weeks} viikkoa · {days} päivää viikossa",
+ "unitsLabel": "Yksiköt",
+ "noUnitsForLevel": "Tasolle {level} ei ole yksiköitä",
+ "takeAssessmentArrow": "Tee tasoarvio →",
+ "unitLabel": "Yksikkö",
+ "grammarCovered": "Käsitelty kielioppi",
+ "noLessons": "Tälle yksikölle ei ole vielä suunniteltu oppitunteja.",
+ "weekDay": "Viikko {week}, päivä {day}",
+ "lessonTypes": {
+ "grammar": "Kielioppi",
+ "vocabulary": "Sanasto",
+ "reading": "Lukeminen",
+ "writing": "Kirjoittaminen",
+ "listening": "Kuuntelu",
+ "conversation": "Keskustelu",
+ "review": "Kertaus",
+ "level_test": "Tasotesti"
+ },
+ "levelComplete": "Taso {level} suoritettu. Tee tasotesti",
+ "levelCompleteDesc": "Olet saavuttanut tason {level} suunnitelman lopun. Tee tasotesti vahvistaaksesi edistymisesi ja avataksesi seuraavan tason.",
+ "levelCompleteHint": "Testi kestää noin 20 minuuttia ja sisältää kielioppia, sanastoa ja lukemista.",
+ "beginLevelTest": "Aloita tasotesti",
+ "nLessons": "{count, plural, one {# oppitunti} other {# oppituntia}}",
+ "nGrammar": "{count} kielioppitehtävää",
+ "levelTestLabel": "Tasotesti",
+ "unitAriaLabel": "Yksikkö {index}: {title}",
+ "noUnitsDesc": "Tämän tason sisältöä valmistellaan. Palaa pian takaisin.",
+ "completionTestTitle": "Tason {level} loppukoe",
+ "levelTestResult": "Tasotestin tulos",
+ "testScore": "Pisteet:",
+ "pendingLessons": "Odottavat oppitunnit",
+ "pendingReassurance": "Voit jatkaa niitä, kun sinulle sopii.",
+ "resume": "Jatka",
+ "lessonsHeader": "Oppitunnit · {count}",
+ "reviewLesson": "Kertaus",
+ "noPlanLabel": "ALOITA",
+ "noPlanTitle": "Luo opintosuunnitelmasi",
+ "noPlanDesc": "Tee lyhyt tasoarvio selvittääksesi tasosi ja saadaksesi henkilökohtaisen oppimispolun.",
+ "noPlanHint": "Kestää noin 10 minuuttia"
+ },
+ "lesson": {
+ "loading": "Ladataan oppituntia...",
+ "notFound": "Oppituntia ei löytynyt",
+ "backToPlan": "Takaisin suunnitelmaan",
+ "progress": "Edistyminen",
+ "exercise": "Tehtävä",
+ "of": "/",
+ "checkAnswer": "Tarkista vastaus",
+ "nextExercise": "Seuraava tehtävä",
+ "submitWriting": "Lähetä kirjoitus",
+ "yourAnswer": "Vastauksesi...",
+ "correct": "Oikein",
+ "incorrect": "Väärin",
+ "feedback": "Palaute",
+ "corrections": "Korjaukset",
+ "completeLesson": "Suorita oppitunti",
+ "completing": "Suoritetaan...",
+ "relatedGrammar": "Aiheeseen liittyvä kielioppi",
+ "explanation": "Selitys",
+ "exercises": "Tehtävät",
+ "vocabulary": "Sanasto",
+ "label": "Oppitunti",
+ "submitAnswer": "Lähetä vastaus",
+ "lessonDone": "Hyvää työtä. Oppitunti suoritettu.",
+ "exerciseSummary": "Arvioidut tehtävät: {completed}/{total}",
+ "score": "Pisteet",
+ "showNativeExplanation": "Näytä kielellä",
+ "hint": "Vihje",
+ "showNativeHint": "Näytä vihje kielellä",
+ "regenerateExercise": "Luo tehtävä uudelleen",
+ "regeneratingExercise": "Luodaan uudelleen...",
+ "regenerateError": "Tehtävää ei voitu luoda uudelleen. Yritä uudelleen.",
+ "regenerateNotNeeded": "Tätä tehtävää ei tarvitse luoda uudelleen.",
+ "examples": "Esimerkit",
+ "commonTraps": "Yleiset sudenkuopat",
+ "miniGlossary": "Pieni sanasto",
+ "pronunciationExercise": "Ääntäminen",
+ "exerciseTypeMultipleChoice": "Monivalinta",
+ "exerciseTypeFillBlank": "Täydennä aukko",
+ "exerciseTypeFreeWrite": "Vapaa kirjoitus",
+ "exerciseTypePronunciation": "Ääntäminen",
+ "tapToListen": "Kuuntele napauttamalla",
+ "recordNow": "Nauhoita vastauksesi",
+ "exit": "Poistu",
+ "exitConfirmTitle": "Poistutaanko oppitunnilta?",
+ "exitConfirmMessage": "Tämän oppitunnin edistyminen menetetään.",
+ "dayComplete": "Päivä valmis!",
+ "dayCompleteMsg": "Olet suorittanut tämän päivän suunnitelmastasi. Jatka, kun sinulle sopii."
+ },
+ "flashcards": {
+ "title": "Muistikortit",
+ "due": "Kerrattavana",
+ "total": "Yhteensä",
+ "noDue": "Kortteja ei tarvitse kerrata juuri nyt",
+ "allCaughtUp": "Olet ajan tasalla!",
+ "generate": "Luo muistikortteja",
+ "generating": "Luodaan...",
+ "topic": "Aihe",
+ "topicPlaceholder": "esim. matkasanasto",
+ "count": "Määrä",
+ "level": "Taso",
+ "submit": "Luo",
+ "again": "Uudelleen",
+ "hard": "Vaikea",
+ "good": "Hyvä",
+ "easy": "Helppo",
+ "front": "Sana",
+ "back": "Määritelmä",
+ "loading": "Ladataan muistikortteja...",
+ "sectionGenerate": "Luo uusia kortteja",
+ "speakingMode": "Puhuminen",
+ "standardMode": "Tavallinen",
+ "sayWord": "Sano sana ääneen",
+ "refresh": "Päivitä",
+ "generateBtn": "Luo",
+ "cards": "korttia",
+ "myVocabularyBtn": "Oma sanasto",
+ "myVocabulary": "Oma sanasto",
+ "myVocabularyEmpty": "Ei vielä tallennettuja sanoja. Valitse sana lukiessasi tallentaaksesi sen tänne.",
+ "myVocabularyNoResults": "Tallennettuja sanoja ei löytynyt haullasi.",
+ "vocabularySearch": "Hae sanoja...",
+ "vocabularyPageInfo": "Sivu {page}/{pages}",
+ "vocabularyPrev": "Edellinen",
+ "vocabularyNext": "Seuraava",
+ "backToFlashcards": "Muistikortit",
+ "noCardsHint": "Luo ensimmäiset tekoälyn tuottamat korttisi valitsemalla + Luo",
+ "tapToHide": "piilota napauttamalla",
+ "tapToReveal": "näytä napauttamalla",
+ "interval": "Aikaväli",
+ "repetitions": "Kertaukset"
+ },
+ "chat": {
+ "title": "AI-opettaja",
+ "subtitle": "{language} – kysy mitä vain",
+ "placeholder": "Kirjoita viesti...",
+ "send": "Lähetä",
+ "sending": "Lähetetään...",
+ "noConversation": "Valitse tai aloita keskustelu",
+ "newConversation": "Uusi",
+ "toggleSidebarShow": "Näytä chatit",
+ "toggleSidebarHide": "Piilota chatit",
+ "conversations": "Keskustelut",
+ "voiceSession": "Äänikeskustelu",
+ "loading": "Ladataan...",
+ "thinking": "Miettii...",
+ "takingLonger": "Kestää odotettua kauemmin...",
+ "errorMessage": "Jokin meni pieleen. Yritä uudelleen.",
+ "deleteTitle": "Poista chatti",
+ "deleteMessage": "Tämä keskustelu poistetaan pysyvästi.",
+ "deleteConfirm": "Poista",
+ "enterToSend": "Lähetä Enterillä",
+ "continueInVoice": "Jatka äänellä",
+ "memoryUpdated": "Muisto päivitetty"
+ },
+ "progress": {
+ "title": "Edistyminen",
+ "subtitle": "Taitojen seuranta",
+ "xp": "XP",
+ "streak": "Putki",
+ "lessons": "Oppitunnit",
+ "accuracy": "Oikein vastatut",
+ "noPlan": "Ei aktiivista opintosuunnitelmaa.",
+ "startPlan": "Aloita tasoarvio",
+ "competencies": "Yksiköiden osaaminen",
+ "vocabularySection": "Sanasto",
+ "skills": "Taitojen osumatarkkuus",
+ "mastered": "Hallussa",
+ "inProgress": "Kesken",
+ "notStarted": "Ei aloitettu",
+ "loading": "Ladataan edistymistä...",
+ "days": "päivää",
+ "noData": "Suorita muutama oppitunti nähdäksesi edistymisesi täällä.",
+ "exploreVocab": "Tutustu sanakokoelmiin",
+ "competenciesSection": "Tason {level} kielioppiosaaminen",
+ "noCompetencies": "Suorita oppitunteja aloittaaksesi osaamisesi seurannan.",
+ "goToMyPlan": "→ Siirry omaan suunnitelmaan",
+ "addToFlashcards": "Lisää sanoja kokoelmasta muistikortteihisi seurataksesi yksittäisten sanojen hallintaa.",
+ "noActivePlanDesc": "Tee tasoarvio aloittaaksesi taitojesi seurannan.",
+ "vocabularyHeader": "Tason {level} sanasto",
+ "currentLevelOnly": "Vain nykyinen taso",
+ "allLevels": "Kaikki tasot"
+ },
+ "grammar": {
+ "title": "Kieliopin hakemisto",
+ "subtitle": "Hakemisto",
+ "searchPlaceholder": "Hae kielioppiaiheita...",
+ "allLevels": "Kaikki tasot",
+ "allCategories": "Kaikki",
+ "noResults": "Aiheita ei löytynyt.",
+ "structure": "Rakenne",
+ "keyRules": "Tärkeimmät säännöt",
+ "examples": "Esimerkit",
+ "commonMistakes": "Yleiset virheet",
+ "relatedTopics": "Aiheeseen liittyvät aiheet",
+ "backToGrammar": "Kieliopin hakemisto",
+ "explanation": "Selitys",
+ "backLink": "Takaisin kielioppiin",
+ "notFound": "Aihetta ei löytynyt.",
+ "topicsFound": "{count, plural, one {# aihe löytyi} other {# aihetta löytyi}}"
+ },
+ "vocabulary": {
+ "title": "Sanasto",
+ "subtitle": "Sanasto",
+ "all": "Kaikki",
+ "searchPlaceholder": "Hae sanakokoelmia...",
+ "sets": "{count, plural, one {# kokoelma} other {# kokoelmaa}}",
+ "words": "sanaa",
+ "addAll": "Lisää kaikki {count} muistikortteihin",
+ "adding": "Lisätään...",
+ "added": "{count} muistikorttia lisätty",
+ "addError": "Muistikortteja ei voitu lisätä",
+ "frequency": "yleisyys",
+ "backToVocabulary": "Sanasto",
+ "notFound": "Kokoelmaa ei löytynyt.",
+ "vocabularySet": "Sanakokoelma",
+ "cardsAdded": "{count} korttia lisättiin muistikortteihisi.",
+ "goToFlashcards": "Siirry muistikortteihin →",
+ "addFailed": "Muistikortteja ei voitu lisätä.",
+ "noResults": "Hakua vastaavia sanakokoelmia ei löytynyt"
+ },
+ "phrasebook": {
+ "title": "Fraasiopas",
+ "subtitle": "Ilmaukset",
+ "all": "Kaikki",
+ "allRegisters": "Kaikki tyylit",
+ "formal": "Muodollinen",
+ "neutral": "Neutraali",
+ "informal": "Epämuodollinen",
+ "copied": "Kopioitu!",
+ "copy": "Kopioi",
+ "copyPhrase": "Kopioi ilmaus",
+ "noResults": "Suodattimiasi vastaavia ilmauksia ei löytynyt",
+ "level": "Taso",
+ "register": "Tyyli",
+ "statsLine": "{situationCount} tilannetta · {phraseCount} ilmausta · {range}",
+ "situationsShown": "{count, plural, one {# tilanne näkyvissä} other {# tilannetta näkyvissä}}",
+ "searchPlaceholder": "Hae ilmauksia..."
+ },
+ "admin": {
+ "title": "Ylläpito",
+ "overview": "Yleiskatsaus",
+ "users": "Käyttäjät",
+ "feedback": "Palaute",
+ "reviews": "Arvostelut",
+ "system": "Järjestelmä",
+ "invites": "Kutsulinkit",
+ "createUser": "Luo käyttäjä",
+ "createInvite": "Luo kutsu",
+ "deactivate": "Poista käytöstä",
+ "activate": "Ota käyttöön",
+ "loading": "Ladataan...",
+ "loadError": "Käyttäjätietoja ei voitu ladata",
+ "userNotFound": "Käyttäjää ei löytynyt",
+ "role": "Rooli",
+ "status": "Tila",
+ "active": "Aktiivinen",
+ "inactive": "Ei aktiivinen",
+ "inviteCreated": "Kutsulinkki luotu",
+ "copyLink": "Kopioi linkki",
+ "inviteCopied": "Kutsulinkki kopioitu",
+ "deleteUser": "Poista käyttäjä",
+ "deleteUserMessage": "Poistetaanko tämä käyttäjä? Toimintoa ei voi perua.",
+ "deleteConfirm": "Poista",
+ "noUsers": "Käyttäjiä ei löytynyt",
+ "inviteLink": "Kutsulinkki (voimassa 48 tuntia)",
+ "total": "yhteensä",
+ "delete": "Poista",
+ "inviteBtn": "Kutsulinkki",
+ "createUserBtn": "Luo käyttäjä",
+ "prevPage": "← Edellinen",
+ "nextPage": "Seuraava →",
+ "roleUser": "Käyttäjä",
+ "roleAdmin": "Ylläpitäjä",
+ "fieldUsername": "Käyttäjänimi",
+ "invalidUsernameChars": "Käyttäjänimi saa sisältää vain kirjaimia, numeroita, yhdysmerkkejä, alaviivoja ja pisteitä",
+ "fieldEmail": "Sähköpostiosoite",
+ "fieldPassword": "Salasana",
+ "fieldDisplayName": "Näyttönimi",
+ "fieldNativeLanguage": "Äidinkieli",
+ "fieldTargetLanguage": "Opiskeltava kieli",
+ "fieldRole": "Tilin tyyppi",
+ "submitCreate": "Luo",
+ "searchPlaceholder": "Hae käyttäjänimellä tai sähköpostiosoitteella...",
+ "statsTitle": "Käyttötilastot",
+ "statsCefr": "Nykyinen taso",
+ "statsUnit": "Nykyinen yksikkö",
+ "statsPlanWeeks": "Suunnitelman kesto",
+ "sectionXpProgress": "XP ja edistyminen",
+ "statsXp": "XP yhteensä",
+ "statsStreak": "Putki",
+ "statsActiveDays": "Aktiiviset päivät",
+ "statsLessons": "Suoritetut oppitunnit",
+ "statsExercises": "Oikein vastatut tehtävät",
+ "statsMessages": "Lähetetyt viestit",
+ "statsTokensTotal": "Käytetyt tokenit (yhteensä)",
+ "statsTokensChat": "Opettajan tokenit",
+ "statsTokensConversation": "Äänitoimintojen tokenit",
+ "statsTokensNote": "Saatavilla vain, jos tekoälypalvelu lähettää nämä tiedot",
+ "statsNoData": "Ei edistymistietoja",
+ "viewStats": "Näytä tilastot",
+ "weeks": "viikkoa",
+ "statsTestScore": "Lopputestin pisteet",
+ "statsDays": "päivää",
+ "sectionQuota": "Keskustelukiintiöt",
+ "quotaWeeklySessions": "Keskustelujen viikkoraja",
+ "quotaDailyMinutes": "Minuuttien päiväkohtainen raja",
+ "quotaWeeklyMinutes": "Minuuttien viikkoraja",
+ "quotaMonthlyTokens": "Tokenien kuukausiraja",
+ "quotaUnlimitedLabel": "Rajaton (0)",
+ "quotaUsedSessions": "Käytetty tällä viikolla",
+ "quotaUsedMinutes": "Käytetty tänään",
+ "quotaUsedWeeklyMinutes": "Käytetty tällä viikolla (min)",
+ "quotaSave": "Tallenna kiintiöt",
+ "quotaSaved": "Kiintiöt tallennettu.",
+ "emailVerified": "Sähköposti vahvistettu",
+ "emailNotVerified": "Sähköpostia ei ole vahvistettu",
+ "verifyEmail": "Vahvista sähköposti",
+ "unverifyEmail": "Merkitse vahvistamattomaksi",
+ "emailVerifiedSuccess": "Sähköposti merkitty vahvistetuksi.",
+ "emailUnverifiedSuccess": "Sähköposti merkitty vahvistamattomaksi.",
+ "fieldSubscription": "Tilaus",
+ "fieldEndsRenews": "Päättyy / Uusiutuu",
+ "fieldStripeCustomer": "Stripe-asiakas",
+ "maintenanceTitle": "Huoltotila",
+ "maintenanceOn": "KÄYTÖSSÄ",
+ "maintenanceEnable": "Ota huoltotila käyttöön",
+ "maintenanceDisable": "Poista huoltotila käytöstä",
+ "maintenanceDesc": "Kun huoltotila on käytössä, tilausta vaativat toiminnot (opettajachatti, äänikeskustelut sekä kuuntelu- ja lukuharjoitukset) eivät ole kenenkään käytettävissä.",
+ "cannotDeactivateSelf": "Et voi poistaa omaa tiliäsi käytöstä",
+ "cannotDeleteSelf": "Et voi poistaa omaa tiliäsi",
+ "sectionLanguages": "Kielet",
+ "sectionActivity": "Toiminta",
+ "statsActive": "Aktiivinen",
+ "manageUsers": "Hallitse käyttäjiä",
+ "manageUsersDesc": "Tarkastele tilejä, tilauksia, rooleja ja käyttöoikeuksia.",
+ "createUserDesc": "Luo ylläpidetty käyttäjätili, jolla on ensimmäinen opiskeltava kieli.",
+ "reviewFeedback": "Tarkastele palautetta",
+ "reviewFeedbackDesc": "Käsittele oppijoiden ominaisuusehdotukset ja virheraportit.",
+ "reviewReviews": "Tarkastele arvosteluja",
+ "reviewReviewsDesc": "Hyväksy vahvistetut käyttäjäarvostelut etusivulla näytettäviksi.",
+ "maintenanceOnDesc": "Huoltotila on käytössä tilausrajoitteisissa oppimistyökaluissa.",
+ "maintenanceOffDesc": "Huoltotila on pois käytöstä. Tilausrajoitteiset oppimistyökalut ovat saatavilla.",
+ "openSystemControls": "Järjestelmäasetukset",
+ "adminRequired": "Ylläpitäjän oikeudet vaaditaan",
+ "usersLoadError": "Käyttäjiä ei voitu ladata",
+ "createUserError": "Käyttäjää ei voitu luoda",
+ "updateUserError": "Käyttäjää ei voitu päivittää",
+ "deleteUserError": "Käyttäjää ei voitu poistaa",
+ "inviteError": "Kutsulinkkiä ei voitu luoda",
+ "copyInviteError": "Kutsulinkkiä ei voitu kopioida",
+ "maintenanceError": "Huoltotilaa ei voitu päivittää",
+ "maintenanceOff": "POIS",
+ "inviteExpiry": "Kertakäyttöinen linkki. Vanhenee 48 tunnin kuluttua.",
+ "searchAction": "Hae käyttäjiä",
+ "roleFilter": "Suodata roolin mukaan",
+ "statusFilter": "Suodata tilan mukaan",
+ "subscriptionFilter": "Suodata tilauksen mukaan",
+ "allRoles": "Kaikki roolit",
+ "allStatuses": "Kaikki tilat",
+ "allSubscriptions": "Kaikki tilaukset",
+ "clearFilters": "Tyhjennä",
+ "userColumn": "Käyttäjä",
+ "actions": "Toiminnot",
+ "createUserSheetDesc": "Tili luodaan heti. Sähköpostivahvistus lähetetään, jos sähköposti on käytössä.",
+ "activateUser": "Aktivoi käyttäjä",
+ "deactivateUser": "Poista käyttäjä käytöstä",
+ "activateUserMessage": "Aktivoidaanko tämä käyttäjätili?",
+ "deactivateUserMessage": "Poistetaanko tämä käyttäjätili käytöstä? Käyttäjä ei voi käyttää sovellusta, ennen kuin tili aktivoidaan uudelleen.",
+ "tabProfile": "Profiili",
+ "tabLanguages": "Kielet",
+ "tabActivity": "Toiminta",
+ "tabQuota": "Kiintiöt",
+ "tabSubscription": "Tilaus",
+ "sectionCommunication": "Viestintä",
+ "quotaCurrentUsage": "Nykyinen käyttö",
+ "quotaConfiguredLimits": "Asetetut rajat",
+ "unitMinutes": "min",
+ "unitSessions": "keskustelua",
+ "unitTokens": "tokenia",
+ "quotaZeroMeansUnlimited": "Aseta rajaksi 0, jos et halua rajoitusta.",
+ "quotaValidationError": "Kaikkien kiintiöiden on oltava kokonaislukuja, joiden arvo on vähintään 0.",
+ "quotaSaveError": "Kiintiöitä ei voitu tallentaa.",
+ "subscriptionOverride": "Tilauksen manuaalinen ohitus",
+ "subscriptionOverrideDesc": "Tämä muuttaa käyttäjän käyttöoikeuden kestoa suoraan ja voi ohittaa Stripen hallinnoiman tilan.",
+ "subscriptionUpdateError": "Tilausta ei voitu päivittää.",
+ "saving": "Tallennetaan...",
+ "verifyEmailConfirmMessage": "Merkitäänkö tämä sähköpostiosoite vahvistetuksi?",
+ "unverifyEmailConfirmMessage": "Merkitäänkö tämä sähköpostiosoite vahvistamattomaksi?",
+ "subscriptionConfirmTitle": "Vaihda tilausta",
+ "subscriptionConfirmMessage": "Otetaanko tilaus {plan} käyttöön?",
+ "subscriptionConfirm": "Ota käyttöön",
+ "feedbackStatusError": "Palautteen tilaa ei voitu muuttaa.",
+ "feedbackDeleteError": "Palautetta ei voitu poistaa.",
+ "allFeedback": "Kaikki palaute",
+ "feedbackTotal": "Palautteet yhteensä",
+ "feedbackQueue": "Palautteiden käsittelyjono",
+ "feedbackSearchPlaceholder": "Hae otsikolla, kuvauksella tai kirjoittajalla...",
+ "feedbackSearchAction": "Hae palautetta",
+ "feedbackTypeFilter": "Suodata palautetyypin mukaan",
+ "noFilteredFeedback": "Nykyisiä suodattimia vastaavaa palautetta ei ole.",
+ "feedbackItem": "Palaute",
+ "type": "Tyyppi",
+ "signals": "Tiedot",
+ "feedbackStatusAction": "Vaihda palautteen tilaa",
+ "adminStatsError": "Ylläpitotilastoja ei voitu ladata.",
+ "activeUsers": "Aktiiviset käyttäjät",
+ "paidAccess": "Aktiivinen / kokeilu",
+ "pendingFeedback": "Odottava palaute",
+ "pendingReviews": "Hyväksyntää odottavat arvostelut",
+ "operationalAlerts": "Toimintahälytykset",
+ "pendingBugs": "Odottavat virheraportit",
+ "pendingBugsDesc": "Ylläpitäjän tarkistusta odottavat virheraportit.",
+ "pastDueSubscriptions": "Erääntyneet tilaukset",
+ "pastDueSubscriptionsDesc": "Tilit, joilla on mahdollisesti tarkistusta vaativia maksuongelmia.",
+ "dashboardBanner": {
+ "title": "Koontinäytön ilmoitus",
+ "description": "Luo, käännä, tarkista ja julkaise ilmoitus jokaisen oppijan koontinäytössä.",
+ "loading": "Ladataan ilmoitusta...",
+ "loadError": "Koontinäytön ilmoitusta ei voitu ladata.",
+ "sourceLocale": "Lähdekieli",
+ "activeLabel": "Julkaise ilmoitus",
+ "activeHint": "Passiiviset ilmoitukset tallennetaan, mutta niitä ei näytetä oppijoille.",
+ "fieldTitle": "Otsikko",
+ "fieldSubtitle": "Alaotsikko",
+ "fieldDescription": "Kuvaus",
+ "translate": "Käännä",
+ "translateSuccess": "Käännökset luotu. Tarkista ne ennen tallentamista.",
+ "translateError": "Kääntäminen epäonnistui. Nykyinen sisältö säilytettiin.",
+ "editTranslation": "Muokattava käännös",
+ "pending": "odottaa",
+ "completion": "{complete}/{total} kieltä valmiina",
+ "save": "Tallenna ilmoitus",
+ "saveSuccess": "Koontinäytön ilmoitus tallennettu.",
+ "saveError": "Koontinäytön ilmoitusta ei voitu tallentaa.",
+ "revision": "Versio {revision}",
+ "updated": "Päivitetty {date}",
+ "locales": {
+ "en": "Englanti",
+ "es": "Espanja",
+ "fr": "Ranska",
+ "pt": "Portugali",
+ "de": "Saksa",
+ "it": "Italia",
+ "ru": "Venäjä",
+ "nl": "Hollanti",
+ "pl": "Puola",
+ "ro": "Romania",
+ "tr": "Turkki",
+ "sv": "Ruotsi",
+ "da": "Tanska",
+ "fi": "Suomi",
+ "hr": "Kroatia"
+ }
+ }
+ },
+ "faq": {
+ "title": "Usein kysytyt kysymykset",
+ "subtitle": "Kysymyksiä ja vastauksia",
+ "q_start": "Mistä aloitan?",
+ "a_start": "Aloita tasoarviosta . Se selvittää englannin taitotasosi (A1–C2) sekä vahvuutesi ja heikkoutesi. Tuloksen perusteella voit luoda henkilökohtaisen opintosuunnitelman , joka tuo päivittäiset oppitunnit koontinäyttöön. Ilman tasoarviota koontinäyttö on tyhjä.",
+ "q_workflow": "Miten opiskelu etenee alusta loppuun?",
+ "workflowStep1": "Rekisteröidy ja valitse äidinkielesi",
+ "workflowStep2": "Tee tasoarvio → CEFR-tasosi selvitetään",
+ "workflowStep3": "Luo opintosuunnitelma koontinäytöstä",
+ "workflowStep4": "Suorita päivittäiset oppitunnit (koontinäytön ALOITA-painike)",
+ "workflowStep5": "Harjoittele sanastoa muistikorteilla samalla",
+ "workflowStep6": "Harjoittele vapaata keskustelua AI-opettajan kanssa",
+ "q_language": "Mitä kieltä voin oppia?",
+ "a_language": "FreeLingo tukee tällä hetkellä useita kieliä (amerikanenglanti, brittienglanti, ranska, saksa, italia, japani, korea, Manner-Kiinan kiina, portugali ja espanja), ja lisäämme kieliä ajan mittaan. Kun uusi kieli tulee saataville, löydät sen kohdasta Asetukset → Omat kielet – lisää se ja aloita opiskelu heti.",
+ "q_assessment": "Mikä tasoarvio on?",
+ "a_assessment": "Tekoälyn luoma 15 kysymyksen testi, joka kattaa kieliopin, sanaston, luetun ymmärtämisen ja lauseiden muuntelun tasoilla A1–C1. Lopuksi saat CEFR-tasosi, pistemäärän sekä arvion vahvuuksistasi ja heikkouksistasi. Tulos vaikuttaa opintosuunnitelmaasi ja luotuihin muistikortteihin.",
+ "q_studyPlan": "Mikä opintosuunnitelma on ja miten luon sellaisen?",
+ "a_studyPlan": "Siirry tasoarvion jälkeen koontinäyttöön ja valitse Luo opintosuunnitelma . Tavoitteidesi ja CEFR-tasosi mukainen, päivittäiset oppitunnit sisältävä viikkosuunnitelma luodaan automaattisesti. Oppitunneilla on monivalinta- ja kirjoitustehtäviä, joita tekoäly arvioi.",
+ "q_resources": "Mitä Materiaalit-osiossa on?",
+ "a_resources": "Materiaalit sisältää opintosuunnitelmasta riippumattomia lähteitä: kielioppia tasoittain, sanakokoelmia ja hyödyllisiä ilmauksia. Voit kerrata, hakea selityksiä tai laajentaa sanastoasi milloin tahansa odottamatta päivittäistä oppituntia.",
+ "q_flashcards": "Mihin muistikortteja käytetään?",
+ "a_flashcards": "Sanaston harjoitteluun SM-2 -aikavälikertausalgoritmin avulla. Luo kortteja mistä tahansa aiheesta (esim. \"liike-elämä\", \"fraasiverbit\" tai \"ruoka\"). Kortti sisältää määritelmän, esimerkkilauseen ja äidinkielisen käännöksen. Järjestelmä laskee, milloin kortti kannattaa näyttää uudelleen juuri ennen kuin unohdat sen. Muistikortit eivät riipu opintosuunnitelmasta.",
+ "q_vocabulary": "Miten lisään sanoja sanastooni?",
+ "a_vocabulary": "Voit tallentaa sanoja kahdella tavalla: (1) Valitse sana hiirellä oppituntia tai kuunteluharjoitusta lukiessasi tai chatissa ja valitse ilmestyvästä valikosta Tallenna . Tekoäly luo sanalle määritelmän, esimerkkilauseen ja äidinkielisen käännöksen. (2) Siirry Sanastoon , avaa tasoasi vastaava sanakokoelma ja valitse Lisää kaikki tuodaksesi koko kokoelman. Tallennetut sanat näkyvät muistikorttien Omassa sanastossa .",
+ "q_tutor": "Miten AI-opettaja toimii?",
+ "a_tutor": "Vapaa chat, jossa vastaukset näkyvät sitä mukaa kun ne syntyvät. Opettaja tuntee CEFR-tasosi ja harjoittelua vaativat alueet ja mukauttaa vastauksensa sen mukaan. Kysy kieliopista, pyydä selityksiä, harjoittele englanniksi kirjoittamista tai vain keskustele. Keskustelut tallennetaan ja ne löytyvät sivupalkista myös seuraavilla käyttökerroilla.",
+ "q_voice": "Miten äänikeskustelu toimii?",
+ "a_voice": "Äänikeskustelussa voit puhua AI-opettajan kanssa reaaliajassa. Aloita painamalla Aloita – sovellus kuuntelee automaattisesti puheentunnistuksen (VAD) avulla, ja opettaja vastaa puhesynteesillä. Voit keskeyttää opettajan milloin tahansa. Keskustelu päättyy aikarajaan tai kun painat Pysäytä. Varmista, että selaimella on mikrofonin käyttöoikeus.",
+ "q_listening": "Miten kuunteluharjoitukset toimivat?",
+ "a_listening": "Kuuntelu sisältää tasosi mukaisia lyhyitä, tekoälyäänellä tuotettuja äänikatkelmia. Kuuntele ja vastaa viiteen ymmärtämiskysymykseen. Saat lähetettyäsi pistemäärän (0–5), XP-pisteet, oikeat vastaukset ja koko litteroinnin. Voit luoda uusia harjoituksia halutessasi.",
+ "q_reading": "Miten lukuharjoitukset toimivat?",
+ "a_reading": "Lukeminen sisältää tasosi mukaisia tekstejä – ilmoituksia, sähköposteja, artikkeleita, blogikirjoituksia ja muuta. Lue teksti ja vastaa viiteen monivalintakysymykseen. Saat lähetettyäsi pistemäärän (0–5) ja XP-pisteet. Voit luoda uusia harjoituksia halutessasi.",
+ "q_providers": "Mitä tekoälypalveluita FreeLingo tukee?",
+ "a_providers_intro": "Määritä tiedostossa .env asetuksella LLM_PROVIDER:",
+ "provider_ollama": "Paikallinen ja maksuton. Ollaman on oltava käynnissä palvelimella. Suositeltu malli: gemma4:e4b. Ilman GPU:ta toiminta voi olla hidasta.",
+ "provider_openai": "OpenAI API (GPT-4o ym.). Nopea ja luotettava. Vaatii OPENAI_API_KEY-avaimen.",
+ "provider_anthropic": "Anthropic Claude. Vaatii ANTHROPIC_API_KEY-avaimen.",
+ "provider_deepseek": "DeepSeek API. Vaatii DEEPSEEK_API_KEY-avaimen.",
+ "q_invite": "Voinko kutsua muita?",
+ "a_invite": "Kyllä. Ylläpito → Käyttäjät -osiossa ylläpitäjä voi luoda käyttäjiä suoraan tai tehdä kertakäyttöisiä kutsulinkkejä, jotka vanhenevat 48 tunnissa. Julkinen rekisteröityminen on oletuksena pois käytöstä (ALLOW_REGISTRATION=false).",
+ "q_password": "Miten vaihdan salasanani tai äidinkieleni?",
+ "a_password": "Siirry Asetuksiin . Voit päivittää näyttönimesi, äidinkielesi ja salasanasi siellä.",
+ "q_uiLanguage": "Miten vaihdan palvelun kielen?",
+ "a_uiLanguage": "Siirry Asetuksiin ja etsi Profiili-osiosta Käyttöliittymän kieli . Valitse kieli, niin koko käyttöliittymä vaihtuu heti.",
+ "q_feedback": "Miten ehdotan uutta ominaisuutta tai ilmoitan virheestä?",
+ "a_feedback": "Käytä sivupalkin Palaute -osiota. Voit ehdottaa uusia ominaisuuksia tai raportoida virheistä. Äänestä muiden ideoita, jätä kommentteja ja seuraa pyyntöjen tilaa. Kaikki ilmoitukset ovat julkisia ja ylläpitäjä tarkistaa ne."
+ },
+ "cookieBanner": {
+ "message": "Käytämme vain välttämättömiä evästeitä pitääksemme sinut kirjautuneena. Seuranta- tai mainosevästeitä ei käytetä.",
+ "learnMore": "Tietosuojakäytäntö",
+ "accept": "Selvä"
+ },
+ "error": {
+ "label": "Odottamaton virhe",
+ "title": "Jokin meni pieleen",
+ "body": "Tapahtui odottamaton virhe. Voit yrittää uudelleen tai palata koontinäyttöön.",
+ "retry": "Yritä uudelleen",
+ "dashboard": "Siirry koontinäyttöön",
+ "digest": "Virheen tunnus"
+ },
+ "notFound": {
+ "title": "Sivua ei löytynyt",
+ "body": "Etsimääsi sivua ei ole tai se on siirretty.",
+ "dashboard": "Siirry koontinäyttöön",
+ "home": "Etusivu"
+ },
+ "errors": {
+ "notFound": "Sivua ei löytynyt",
+ "serverError": "Jokin meni pieleen",
+ "unauthorized": "Kirjaudu sisään",
+ "backHome": "Takaisin etusivulle"
+ },
+ "conversation": {
+ "title": "Keskustelu",
+ "subtitle": "Puheharjoitus",
+ "start": "Aloita keskustelu",
+ "stop": "Pysäytä",
+ "startNew": "Aloita uusi keskustelu",
+ "statusLoading": "Ladataan äänimallia...",
+ "statusReady": "Valmis",
+ "statusWarming": "Ladataan malleja...",
+ "statusConnecting": "Yhdistetään...",
+ "statusListening": "Kuunnellaan...",
+ "statusDetecting": "Tunnistetaan puhetta...",
+ "statusSpeaking": "Puhuu...",
+ "statusError": "Virhe",
+ "errorServicesDisabled": "Äänipalvelut eivät ole käytössä tällä palvelimella.",
+ "errorConnection": "Yhteysvirhe. Yritä uudelleen.",
+ "errorTranscription": "Puhettasi ei voitu muuntaa tekstiksi. Yritä puhua uudelleen.",
+ "errorResponse": "Lingu ei voinut vastata. Yritä uudelleen.",
+ "errorSpeech": "Lingun vastausta ei voitu muuntaa puheeksi. Yritä uudelleen.",
+ "errorUnauthorized": "Istunto on vanhentunut. Päivitä sivu.",
+ "errorMic": "Mikrofonin käyttö estetty.",
+ "errorVadInit": "Puheentunnistusta ei voitu käynnistää. Varmista, että selaimesi tukee WebAssemblyä.",
+ "warningTimeout": "Keskustelu päättyy {seconds} sekunnin kuluttua",
+ "sessionEnded": "Keskustelu päättynyt",
+ "tapToStart": "Puhu, kun olet valmis",
+ "you": "Sinä",
+ "assistant": "Lingu",
+ "quotaSessions": "Keskustelut tällä viikolla",
+ "quotaMinutes": "Minuutit tänään",
+ "quotaUnlimited": "Rajaton",
+ "quotaExceededSessions": "Olet käyttänyt kaikki tämän viikon keskustelusi.",
+ "quotaExceededTime": "Olet käyttänyt kaiken päivittäisen harjoitusaikasi.",
+ "quotaExceededTokens": "Olet käyttänyt kaikki tämän kuun AI-tokenisi.",
+ "freemiumVoiceRemaining": "Ilmainen ääni: {remaining}/{limit} min tällä viikolla",
+ "quotaTokens": "Tokenit tässä kuussa",
+ "memoryUpdated": "Muisto päivitetty",
+ "trialBanner": "Ilmainen tasoarviokeskustelu · {minutes} min",
+ "trialCtaLabel": "Jatka puhumista",
+ "trialCtaTitle": "Jatka harjoittelua Lingun kanssa",
+ "trialCtaDesc": "Avaa täydet äänikeskustelut, opettajachatti sekä kuuntelu- ja lukuharjoitukset Premiumilla.",
+ "startersHint": "Tai aloita aiheesta",
+ "starters": [
+ "Taide ja luovuus",
+ "Kirjat ja lukeminen",
+ "Lapsuusmuistot",
+ "Kaupunki vai maaseutu",
+ "Ruoanlaitto ja reseptit",
+ "Unelmat ja tavoitteet",
+ "Perhe ja ihmissuhteet",
+ "Muoti ja tyyli",
+ "Lempielokuvat",
+ "Juhlat ja perinteet",
+ "Ruoka eri puolilta maailmaa",
+ "Terveys ja hyvinvointi",
+ "Harrastukset ja käsityöt",
+ "Kielten oppiminen",
+ "Uusien taitojen oppiminen",
+ "Raha ja budjetointi",
+ "Musiikki ja konsertit",
+ "Lemmikit ja eläimet",
+ "Julkinen liikenne",
+ "Tiede ja avaruus",
+ "Ostostottumukset",
+ "Sosiaalinen media",
+ "Urheilu ja liikunta",
+ "Teknologia ja laitteet",
+ "Ympäristö",
+ "Matkakohteet",
+ "TV-ohjelmat ja sarjat",
+ "Viikonlopun suunnitelmat",
+ "Työ ja uratavoitteet",
+ "Etätyö"
+ ]
+ },
+ "voiceRecorder": {
+ "record": "Nauhoita",
+ "stop": "Pysäytä",
+ "processing": "Käsitellään",
+ "error": "Virhe",
+ "ariaRecord": "Aloita nauhoitus",
+ "ariaStop": "Lopeta nauhoitus"
+ },
+ "audioPlayer": {
+ "listen": "Kuuntele",
+ "stop": "Pysäytä",
+ "ariaListen": "Kuuntele ääntämistä",
+ "ariaStop": "Pysäytä ääni"
+ },
+ "languages": {
+ "en": "Englanti",
+ "es": "Espanja",
+ "fr": "Ranska",
+ "pt": "Portugali",
+ "de": "Saksa",
+ "it": "Italia",
+ "pl": "Puola",
+ "nl": "Hollanti",
+ "ro": "Romania",
+ "ru": "Venäjä",
+ "tr": "Turkki",
+ "sv": "Ruotsi",
+ "da": "Tanska",
+ "fi": "Suomi",
+ "hr": "Kroatia",
+ "myLanguages": "Omat kielet",
+ "addLanguage": "Lisää uusi kieli",
+ "selectLanguage": "Valitse kieli, jota haluat opiskella",
+ "activeLanguage": "Aktiivinen",
+ "switchTo": "Vaihda tähän",
+ "switching": "Vaihdetaan kieltä: {language}...",
+ "switched": "Kieli vaihdettu: {language} ({level})",
+ "removeLanguage": "Poista kieli",
+ "removeConfirmTitle": "Poistetaanko kieli {language}?",
+ "removeConfirmMessage": "Kaikki tähän kieleen liittyvä edistyminen, oppitunnit, muistikortit ja keskustelut poistetaan pysyvästi. Tilisi muistot säilyvät. Toimintoa ei voi perua.",
+ "removeConfirmButton": "Poista",
+ "deleteError": "Kieltä {language} ei voitu poistaa. Yritä uudelleen.",
+ "noLanguages": "Et ole vielä lisännyt kieliä.",
+ "progressLabel": "Edistyminen",
+ "levelLabel": "Taso",
+ "xpLabel": "XP yhteensä",
+ "streakLabel": "Putki",
+ "lessonsLabel": "Oppitunnit",
+ "flashcardsLabel": "Muistikortit",
+ "viewDetails": "Näytä tiedot",
+ "supportedLanguages": "Saatavilla olevat kielet"
+ },
+ "targetLanguages": {
+ "en-US": "Amerikanenglanti",
+ "en-GB": "Brittienglanti",
+ "es-ES": "Espanja",
+ "it-IT": "Italia",
+ "pt-PT": "Portugali",
+ "fr-FR": "Ranska",
+ "de-DE": "Saksa",
+ "ja-JP": "Japani",
+ "ko-KR": "Korea",
+ "zh-CN": "Kiina (Manner-Kiina)",
+ "en": "Englanti",
+ "es": "Espanja",
+ "it": "Italia",
+ "pt": "Portugali",
+ "fr": "Ranska",
+ "de": "Saksa",
+ "ja": "Japani",
+ "ko": "Korea",
+ "zh": "Kiina",
+ "es-ES-description": "Espanjassa puhuttu espanja.",
+ "it-IT-description": "Italiassa puhuttu italia.",
+ "pt-PT-description": "Portugalissa puhuttu portugali.",
+ "en-US-description": "Yhdysvalloissa puhuttu englanti.",
+ "en-GB-description": "Yhdistyneessä kuningaskunnassa puhuttu englanti.",
+ "fr-FR-description": "Ranskassa puhuttu ranska.",
+ "de-DE-description": "Saksassa puhuttu saksa.",
+ "ja-JP-description": "Japanissa puhuttu japani.",
+ "ko-KR-description": "Etelä-Koreassa puhuttu korea.",
+ "zh-CN-description": "Manner-Kiinassa puhuttu mandariinikiina, jossa käytetään yksinkertaistettuja merkkejä."
+ },
+ "onboarding": {
+ "title": "Valitse kielesi",
+ "subtitle": "Mitä kieltä haluat oppia?",
+ "chooseVariant": "Valitse kielimuoto",
+ "continue": "Jatka",
+ "saveFailed": "Asetuksia ei voitu tallentaa",
+ "goals": {
+ "title": "Oppimistavoitteesi",
+ "subtitle": "Kieli: {language}. Mihin haluat käyttää sitä? Valitse kaikki sopivat.",
+ "skip": "Ohita",
+ "travel": "Matkailu",
+ "work": "Työelämä",
+ "academic": "Opinnot ja akateeminen elämä",
+ "daily": "Arjen keskustelut",
+ "media": "Elokuvat, musiikki ja sarjat",
+ "emigration": "Ulkomailla asuminen",
+ "exams": "Kokeet ja todistukset",
+ "social": "Sosiaalinen kanssakäyminen"
+ },
+ "trialHeadline": "Ilmainen kokeilu",
+ "trialHeadlineTrialUsed": "Premium",
+ "trialTitle": "Aloita {days} päivän ilmaisella kokeilulla",
+ "trialTitleTrialUsed": "Ota FreeLingo Premium käyttöön",
+ "trialDesc": "Saat Lingun, puhe-, kuuntelu- ja lukuharjoitukset kokonaan käyttöösi ennen ensimmäistä maksua.",
+ "trialDescTrialUsed": "Valitse tilaus avataksesi Lingun sekä puhe-, kuuntelu- ja lukuharjoitukset.",
+ "trialCtaMonthly": "Kokeile ilmaiseksi, sitten €{price}/kk",
+ "trialCtaMonthlyTrialUsed": "Joustava kuukausitilaus · €{price}/kk",
+ "trialCtaYearly": "Kokeile ilmaiseksi, sitten €{price}/vuosi · 2 kuukautta ilmaiseksi",
+ "trialCtaYearlyTrialUsed": "Paras vastine · €{price}/vuosi · 2 kuukautta ilmaiseksi",
+ "trialSkip": "Jatka ilmaisilla ominaisuuksilla",
+ "trialNoCharge": "Ei veloitusta tänään. Maksukortti vaaditaan. Peruuta milloin tahansa.",
+ "trialNoChargeTrialUsed": "Tarkista summa ennen tilaamista. Peruuta milloin tahansa.",
+ "trialError": "Maksua ei voitu aloittaa. Yritä uudelleen.",
+ "freemiumTrialTitle": "Ilmainen kokeilusi on alkanut",
+ "freemiumTrialDesc": "Käytä kaikkia ominaisuuksia rajattomasti seitsemän päivää. Maksukorttia ei tarvita. Voit päivittää Premiumiin milloin tahansa koontinäytöstä.",
+ "freemiumChatLabel": "AI-opettajachatti",
+ "freemiumVoiceLabel": "Äänikeskustelut",
+ "freemiumListeningLabel": "Kuunteluharjoitukset",
+ "freemiumReadingLabel": "Lukuharjoitukset",
+ "goToDashboard": "Siirry koontinäyttöön",
+ "newLanguageHeadline": "Mitä uutta kieltä haluat oppia?",
+ "newLanguageSubtitle": "Tälle kielelle luodaan uusi opintosuunnitelma."
+ },
+ "tour": {
+ "skip": "Ohita",
+ "prev": "Takaisin",
+ "next": "Seuraava",
+ "done": "Aloita",
+ "step1": {
+ "label": "Tervetuloa",
+ "title": "Tervetuloa FreeLingoon",
+ "desc": "FreeLingo on tekoälyä hyödyntävä kieltenopettajasi. Harjoittele omaan tahtiisi milloin tahansa keskusteluilla, oppitunneilla ja älykkäillä muistikorteilla."
+ },
+ "step2": {
+ "label": "Tekstichatti",
+ "title": "Keskustele opettajasi kanssa",
+ "desc": "Keskustele kirjoittamalla AI-opettajasi kanssa. Kysy, saat korjauksia ja harjoittele kielioppia ja sanastoa luonnollisesti."
+ },
+ "step3": {
+ "label": "Ääni",
+ "title": "Puhu reaaliajassa",
+ "desc": "Harjoittele puhumista äänikeskustelussa. Saat reaaliaikaisen litteroinnin ja välitöntä palautetta ääntämisestäsi."
+ },
+ "step4": {
+ "label": "Muistikortit",
+ "title": "Älä unohda sanoja",
+ "desc": "Muistikortit käyttävät SM-2-aikavälikertausta, joka näyttää sanat juuri ennen kuin olet unohtamassa ne – näin muistat ne pidempään."
+ },
+ "step5": {
+ "label": "Opintosuunnitelma",
+ "title": "Henkilökohtainen suunnitelmasi",
+ "desc": "Tasoarviosi perusteella FreeLingo laatii jäsennellyn opintosuunnitelman, jonka päivittäiset oppitunnit mukautuvat tavoitteisiisi ja edistymiseesi."
+ },
+ "step6": {
+ "label": "Harjoittelu",
+ "title": "Kuuntelu ja lukeminen",
+ "desc": "Testaa ymmärrystäsi tekoälyn luomilla harjoituksilla. Kuuntele katkelma ja vastaa viiteen kysymykseen tai lue tasoasi vastaava teksti – ansaitset XP-pisteitä jokaisesta oikeasta vastauksesta."
+ },
+ "step7": {
+ "label": "Valmis",
+ "title": "Kaikki on valmista!",
+ "desc": "Aloita päivän oppitunnilla, avaa chatti tai harjoittele kuuntelua ja lukemista – FreeLingo mukautuu tapaasi oppia."
+ }
+ },
+ "billing": {
+ "paywallLabel": "Premium vaaditaan",
+ "paywallTitle": "Avaa FreeLingo Premium",
+ "paywallDesc": "Aloita {days} päivän ilmainen kokeilu ja avaa kaikki Premium-harjoitukset ennen ensimmäistä maksua.",
+ "paywallDescTrialUsed": "Avaa kaikki Premium-harjoitukset ja tarkista summa ennen vahvistamista.",
+ "paywallChatTitle": "Harjoittele Lingun kanssa rajattomasti",
+ "paywallChatDesc": "Kysy, saat korjauksia ja käy luontevia tekstikeskusteluja AI-opettajasi kanssa.",
+ "paywallLessonsTitle": "Suorita rajattomasti oppitunteja",
+ "paywallLessonsDesc": "Kielioppia, sanastoa, lukemista ja kirjoittamista – suorita niin monta oppituntia päivässä kuin haluat.",
+ "paywallConversationTitle": "Puhu Lingun kanssa reaaliajassa",
+ "paywallConversationDesc": "Harjoittele puhumista litteroinnin, nopeiden vastausten ja sujuvuuttasi kehittävän palautteen avulla.",
+ "paywallListeningTitle": "Harjoittele kuuntelua omalla tasollasi",
+ "paywallListeningDesc": "Kuuntele edistymiseesi mukautettuja tekoälyn luomia harjoituksia ja tarkista heti, kuinka paljon ymmärsit.",
+ "paywallReadingTitle": "Lue edistymiseesi mukautettuja tekstejä",
+ "paywallReadingDesc": "Kehitä luetun ymmärtämistä tasollesi sopivilla teksteillä, kysymyksillä ja välittömällä palautteella.",
+ "paywallNoCharge": "Ei veloitusta tänään · Maksukortti vaaditaan · Peruuta milloin tahansa",
+ "paywallNoChargeTrialUsed": "Turvallinen maksu · Peruuta milloin tahansa",
+ "paywallSkip": "Ei nyt",
+ "trialDays": "{days, plural, one {# kokeilupäivä} other {# kokeilupäivää}}",
+ "planMonthly": "Joustava kuukausitilaus · €{price}/kk →",
+ "planYearly": "Paras vastine · €{price}/vuosi · 2 kuukautta ilmaiseksi →",
+ "checkoutError": "Maksua ei voitu aloittaa. Yritä uudelleen.",
+ "pricingTitle": "Selkeät hinnat",
+ "pricingDesc": "{days} päivää ilmaiseksi · Ei veloitusta tänään · Peruuta milloin tahansa",
+ "pricingDescTrialUsed": "Valitse kuukausi- tai vuositilaus · Peruuta milloin tahansa",
+ "planMonthlyName": "Kuukausittain",
+ "planYearlyName": "Vuosittain",
+ "bestValue": "Paras vastine",
+ "month": "kuukausi",
+ "year": "vuosi",
+ "planFeature": {
+ "feature1": "Rajaton käyttö – ei päivä- tai viikkorajoituksia",
+ "feature2": "2 kuukautta ilmaiseksi"
+ },
+ "planFreeName": "Ilmainen",
+ "trialBadge": "Ilmainen kokeilu",
+ "trialBadgeTrialUsed": "Premium",
+ "ctaRegister": "Aloita ilmainen kokeilu",
+ "ctaRegisterTrialUsed": "Valitse tilaus",
+ "freeFeature": {
+ "f1": "Tasotesti ja tasoarvio",
+ "f2": "Oppitunnit ja opintosuunnitelma",
+ "f3": "Kielioppi- ja sanastomateriaalit",
+ "f4": "Koontinäyttö ja edistymisen seuranta",
+ "f5": "Muistikortit ja aikavälikertaus",
+ "l1": "AI-opettajachatti",
+ "l2": "Äänikeskustelut",
+ "l3": "Kuunteluharjoitukset",
+ "l4": "Lukuharjoitukset",
+ "l5": "7 päivän täyden käyttöoikeuden kokeilu"
+ },
+ "successLabel": "Maksu vahvistettu",
+ "successTitle": "Tervetuloa FreeLingo Premiumiin!",
+ "successDesc": "Tilauksesi on nyt aktiivinen. Käytä kaikkia ominaisuuksia rajattomasti.",
+ "successCheckingLabel": "Vahvistetaan tilausta",
+ "successCheckingTitle": "Tarkistetaan tilaustasi",
+ "successCheckingDesc": "Tämä kestää yleensä vain muutaman sekunnin.",
+ "successPendingLabel": "Melkein valmis",
+ "successPendingTitle": "Tilaustasi vahvistetaan",
+ "successPendingDesc": "Premium-käyttöoikeutesi pitäisi aktivoitua pian. Ellei näin käy, tarkista laskutusasetuksesi.",
+ "successErrorLabel": "Vahvistus odottaa",
+ "successErrorTitle": "Emme voineet vielä vahvistaa tilaustasi",
+ "successErrorDesc": "Siirry koontinäyttöön ja päivitä sivu hetken kuluttua tai tarkista laskutusasetuksesi.",
+ "successRedirect": "Siirryt koontinäyttöön {seconds} sekunnin kuluttua...",
+ "successCta": "Siirry koontinäyttöön",
+ "canceledLabel": "Maksu peruutettu",
+ "canceledTitle": "Maksua ei veloitettu",
+ "canceledDesc": "Maksua ei veloitettu. Edistymisesi säilyy ja voit valita tilauksen myöhemmin Asetuksissa.",
+ "canceledCtaDashboard": "Siirry koontinäyttöön",
+ "canceledCtaSettings": "Näytä tilaukset",
+ "canceledNoCharge": "Maksua ei veloitettu · Valitse tilaus milloin tahansa",
+ "section": "Tilaus",
+ "status": "Tila",
+ "statusActive": "Aktiivinen",
+ "statusCanceling": "Päättymässä",
+ "statusCanceled": "Peruutettu",
+ "statusTrialing": "Kokeilu",
+ "statusPastDue": "Maksu myöhässä",
+ "statusUnpaid": "Maksamaton",
+ "statusPaused": "Keskeytetty",
+ "statusIncomplete": "Maksu odottaa",
+ "statusIncompleteExpired": "Maksu vanhentunut",
+ "statusNone": "Ei tilausta",
+ "nextBilling": "Seuraava laskutus",
+ "accessUntil": "Käyttöoikeus päättyy",
+ "manage": "Hallitse tilausta",
+ "updatePayment": "Päivitä maksutapa",
+ "pastDueTitle": "Maksu odottaa",
+ "pastDueDesc": "Päivitä maksutapasi palauttaaksesi Premium-käyttöoikeutesi.",
+ "subscribe": "Tilaa",
+ "portalError": "Laskutusportaalia ei voitu avata. Yritä uudelleen.",
+ "openSourceTitle": "Avoin lähdekoodi",
+ "openSourceDesc": "Ylläpidä itse omalla palvelimellasi maksutta",
+ "openSourceCta": "Näytä GitHubissa",
+ "planFreeBadge": "Ilmainen aina",
+ "priceAmount": "€{amount}",
+ "pricePerPeriod": "/ {period}",
+ "priceOriginal": "€{price} / {period}",
+ "planFreeDesc": "7 päivän kokeilu täydellä käyttöoikeudella, sen jälkeen ilmainen päivä- ja viikkorajoituksin",
+ "planFreeCta": "Aloita",
+ "limitedLabel": "Rajoitettu"
+ },
+ "freemium": {
+ "chatLabel": "Chatti",
+ "lessonsLabel": "Oppitunnit",
+ "listeningLabel": "Kuuntelu",
+ "readingLabel": "Lukeminen",
+ "voiceLabel": "Ääni (min)",
+ "freeLimit": "Hanki Premium",
+ "trialDaysLeft": "{days, plural, one {# päivä ilmaista kokeilua jäljellä} other {# päivää ilmaista kokeilua jäljellä}}",
+ "trialUnlimited": "Rajaton käyttö",
+ "requiresSubscription": "Vain Premium"
+ },
+ "whatsNew": {
+ "title": "Uutta",
+ "version": "v1.9.25",
+ "cta": "Selvä",
+ "entry1": {
+ "label": "Viisi uutta käyttöliittymän kieltä",
+ "desc": "FreeLingo on saatavilla kroatiaksi, ruotsiksi, suomeksi, tanskaksi ja turkiksi. Valitse käyttöliittymän kieli Asetuksista."
+ },
+ "entry2": {
+ "label": "Sujuvampi käyttökokemus",
+ "desc": "Korjasimme myös pieniä virheitä ja hioimme yksityiskohtia, jotta FreeLingolla oppiminen olisi joka päivä entistä mukavampaa."
+ }
+ },
+ "contact": {
+ "title": "Yhteystiedot",
+ "labelEmail": "Sähköpostiosoite",
+ "placeholderEmail": "sinun@sahkoposti.fi",
+ "labelSubject": "Aihe",
+ "placeholderSubject": "Lyhyt kuvaus kysymyksestäsi",
+ "labelDescription": "Viesti",
+ "placeholderDescription": "Kuvaile kysymyksesi tai palautteesi...",
+ "send": "Lähetä",
+ "sending": "Lähetetään...",
+ "sent": "Viesti lähetetty",
+ "errorGeneric": "Lähetys epäonnistui. Yritä uudelleen."
+ },
+ "listening": {
+ "title": "Kuuntelu",
+ "topic": "Aihe",
+ "listenLabel": "Kuuntele",
+ "questionsLabel": "Kysymykset",
+ "submit": "Lähetä vastaukset",
+ "generating": "Luodaan harjoitusta...",
+ "generatingDesc": "Tekoäly luo uutta harjoitusta. Tämä kestää yleensä noin 30 sekuntia.",
+ "generatingLong": "Kestää odotettua kauemmin...",
+ "noExercises": "Tasolle ei ole saatavilla harjoituksia.",
+ "generate": "Luo harjoitus",
+ "audioError": "Ääntä ei voitu ladata",
+ "alreadyAttempted": "Olet jo suorittanut tämän harjoituksen.",
+ "errorLoading": "Harjoitusta ei voitu ladata. Yritä uudelleen.",
+ "errorSubmit": "Vastauksia ei voitu lähettää. Yritä uudelleen.",
+ "resultsLabel": "Tulokset",
+ "transcript": "Litterointi",
+ "review": "Vastausten tarkistus",
+ "nextExercise": "Seuraava harjoitus",
+ "viewHistory": "Historia",
+ "history": "Historia",
+ "historyTitle": "Kuunteluhistoria",
+ "historyEmpty": "Ei vielä suoritettuja harjoituksia.",
+ "practiceMore": "Harjoittele",
+ "moreInHistory": "+{count} lisää",
+ "practiceAgain": "Harjoittele uudelleen",
+ "replayNoXp": "Toista (ei XP-pisteitä)"
+ },
+ "reading": {
+ "title": "Lukeminen",
+ "topic": "Aihe",
+ "textLabel": "Teksti",
+ "questionsLabel": "Kysymykset",
+ "submit": "Lähetä vastaukset",
+ "generating": "Luodaan harjoitusta...",
+ "generatingDesc": "Tekoäly luo uutta harjoitusta. Tämä kestää yleensä noin 30 sekuntia.",
+ "generatingLong": "Kestää odotettua kauemmin...",
+ "noExercises": "Tasolle ei ole saatavilla harjoituksia.",
+ "generate": "Luo harjoitus",
+ "alreadyAttempted": "Olet jo suorittanut tämän harjoituksen.",
+ "errorLoading": "Harjoitusta ei voitu ladata. Yritä uudelleen.",
+ "errorSubmit": "Vastauksia ei voitu lähettää. Yritä uudelleen.",
+ "resultsLabel": "Tulokset",
+ "review": "Vastausten tarkistus",
+ "nextExercise": "Seuraava harjoitus",
+ "viewHistory": "Historia",
+ "history": "Historia",
+ "historyTitle": "Lukuhistoria",
+ "historyEmpty": "Ei vielä suoritettuja harjoituksia.",
+ "practiceMore": "Harjoittele",
+ "moreInHistory": "+{count} lisää",
+ "practiceAgain": "Harjoittele uudelleen",
+ "replayNoXp": "Toista (ei XP-pisteitä)",
+ "selectWordHint": "Valitse sana tallentaaksesi sen sanastoosi"
+ },
+ "feedback": {
+ "title": "Palaute",
+ "subtitle": "Ominaisuusehdotukset ja virheraportit",
+ "tabFeatures": "Ehdotukset",
+ "tabBugs": "Virheraportit",
+ "newFeature": "+ Uusi ehdotus",
+ "newBug": "+ Ilmoita virheestä",
+ "sortBy": "Järjestä",
+ "sortVotes": "Eniten ääniä",
+ "sortDate": "Uusimmat",
+ "filterStatus": "Tila",
+ "filterAll": "Kaikki",
+ "noEntries": "Täällä ei ole vielä mitään. Lähetä ensimmäinen ehdotus.",
+ "votes": "{count} ääntä",
+ "vote": "1 ääni",
+ "voteAction": "Äänestä",
+ "removeVoteAction": "Peru ääni",
+ "comments": "{count} kommenttia",
+ "comment": "1 kommentti",
+ "by": "käyttäjältä",
+ "statusPending": "Odottaa",
+ "statusPlanned": "Suunniteltu",
+ "statusInProgress": "Työn alla",
+ "statusDone": "Valmis",
+ "statusDeclined": "Hylätty",
+ "unread": "Lukematon",
+ "modalCreateTitleFeature": "Uusi ehdotus",
+ "modalCreateTitleBug": "Ilmoita virheestä",
+ "labelTitle": "Otsikko",
+ "placeholderTitleFeature": "Kuvaa ehdotuksesi yhdellä lauseella",
+ "placeholderTitleBug": "Virheen lyhyt kuvaus",
+ "labelDescription": "Lisätiedot",
+ "placeholderDescriptionFeature": "Kerro ominaisuudesta ja miksi se olisi hyödyllinen",
+ "placeholderDescriptionBug": "Toistamisohjeet, odotettu tulos ja mitä tapahtui sen sijaan",
+ "submit": "Lähetä",
+ "submitting": "Lähetetään...",
+ "cancel": "Peruuta",
+ "deleteEntry": "Poista",
+ "deleteEntryConfirmTitle": "Poista palaute",
+ "deleteEntryConfirmMessage": "Haluatko varmasti poistaa tämän palautteen? Toimintoa ei voi perua.",
+ "deleteEntryConfirm": "Poista",
+ "addComment": "Lisää kommentti",
+ "commentPlaceholder": "Kirjoita kommentti...",
+ "postComment": "Julkaise",
+ "postingComment": "Julkaistaan...",
+ "deleteComment": "Poista",
+ "deleteCommentConfirmTitle": "Poista kommentti",
+ "deleteCommentConfirmMessage": "Poistetaanko tämä kommentti?",
+ "deleteCommentConfirm": "Poista",
+ "loading": "Ladataan...",
+ "backToList": "← Takaisin",
+ "errorLoad": "Lataus epäonnistui. Yritä uudelleen.",
+ "errorSubmit": "Lähetys epäonnistui. Yritä uudelleen.",
+ "prevPage": "← Edellinen",
+ "nextPage": "Seuraava →"
+ },
+ "reviewPrompt": {
+ "statusError": "Arvostelusi tilaa ei voitu tarkistaa. Yritä myöhemmin uudelleen.",
+ "ratingRequiredError": "Valitse arvosana ennen lähettämistä.",
+ "saveError": "Arvosteluasi ei voitu tallentaa. Yritä uudelleen.",
+ "eyebrow": "Arvostele FreeLingo",
+ "title": "Millainen kokemuksesi oli?",
+ "closeLabel": "Sulje arvostelukehote",
+ "checking": "Tarkistetaan arvostelun tilaa...",
+ "ratingLabel": "Arvosana vaaditaan",
+ "ratingGroupLabel": "Arvosana",
+ "star": "tähti",
+ "stars": "tähteä",
+ "commentLabel": "Kommentti vapaaehtoinen",
+ "commentPlaceholder": "Kerro, mikä auttoi sinua oppimaan.",
+ "cancel": "Peruuta",
+ "submit": "Lähetä"
+ },
+ "landingReviews": {
+ "eyebrow": "Oppijoiden tarinoita",
+ "title": "Oppiminen tuntuu todemmalta, kun kerrot siitä itse",
+ "subtitle": "Arvosteluja oppijoilta, jotka harjoittelevat jo Lingun kanssa FreeLingossa.",
+ "averageLabel": "{average}/5 · {count, plural, one {# arvostelu yhteensä} other {# arvostelua yhteensä}}",
+ "learningLanguage": "Opiskelee: {language}",
+ "starsLabel": "{rating}/5 tähteä",
+ "ratingOnly": "Vahvistettu FreeLingo-arvosana."
+ },
+ "adminReviews": {
+ "title": "Arvostelut",
+ "eyebrow": "Ylläpito",
+ "totalFiltered": "Suodatettuja yhteensä",
+ "approvedOnPage": "Hyväksyttyjä sivulla",
+ "pendingOnPage": "Odottavia sivulla",
+ "filters": "Suodattimet",
+ "approvalFilter": "Suodata hyväksynnän mukaan",
+ "allReviews": "Kaikki arvostelut",
+ "pending": "Odottaa",
+ "approved": "Hyväksytty",
+ "ratingFilter": "Suodata arvosanan mukaan",
+ "allRatings": "Kaikki arvosanat",
+ "starsOption": "{rating} tähteä",
+ "clear": "Tyhjennä",
+ "loadError": "Arvosteluja ei voitu ladata.",
+ "updateError": "Arvostelua ei voitu päivittää.",
+ "deleteError": "Arvostelua ei voitu poistaa.",
+ "loading": "Ladataan arvosteluja...",
+ "queue": "Arvostelujono",
+ "total": "{total} yhteensä",
+ "empty": "Arvosteluja ei löytynyt.",
+ "starsLabel": "{rating} tähteä",
+ "learningLanguage": "Opiskelee: {language}",
+ "ratingOnly": "Arvostelu, jossa on vain arvosana.",
+ "unapprove": "Poista hyväksyntä",
+ "approve": "Hyväksy",
+ "delete": "Poista",
+ "prev": "Edellinen",
+ "next": "Seuraava",
+ "deleteTitle": "Poista arvostelu",
+ "deleteMessage": "Poistetaanko tämä arvostelu? Toimintoa ei voi perua."
+ },
+ "maintenance": {
+ "label": "Huolto",
+ "title": "Huolto käynnissä",
+ "description": "Tämä osio on väliaikaisesti poissa käytöstä huollon vuoksi. Palaa myöhemmin."
+ }
+}
diff --git a/messages/fr.json b/messages/fr.json
index 1989d51ad..3977efdc3 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Chargement...",
"error": "Erreur",
+ "errorMessage": "Une erreur s'est produite. Veuillez réessayer.",
"save": "Sauvegarder",
"saving": "Sauvegarde...",
"cancel": "Annuler",
@@ -78,6 +79,9 @@
"backToDashboard": "Retour au tableau de bord",
"tagline": "apprentissage des langues par IA",
"close": "Fermer",
+ "delete": "Supprimer",
+ "openMenu": "Ouvrir le menu",
+ "closeMenu": "Fermer le menu",
"verifyEmailBanner": "Veuillez vérifier votre adresse e-mail pour finaliser la configuration de votre compte.",
"verifyEmailSent": "E-mail envoyé. Vérifiez votre boîte de réception.",
"resendVerification": "Renvoyer l'e-mail de vérification",
@@ -205,7 +209,7 @@
"saving": "Enregistrement...",
"success": "Mot de passe mis à jour. Redirection...",
"mismatch": "Les mots de passe ne correspondent pas",
- "tooShort": "Le mot de passe doit comporter au moins 8 caractères",
+ "tooShort": "Le mot de passe doit comporter au moins 10 caractères",
"missingToken": "Lien invalide. Veuillez demander une nouvelle réinitialisation.",
"error": "Lien invalide ou expiré",
"backToLogin": "Retour à la connexion"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Si vous quittez le test de niveau avant de le terminer, votre progression sera perdue et vous devrez le repasser.",
"startWarningConfirm": "Commencer",
"startMyPlan": "Démarrer mon plan",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Essai de conversation vocale",
+ "voiceTrialTitle": "Essayez une vraie conversation maintenant",
+ "voiceTrialDesc": "Parlez gratuitement avec Lingu pendant {minutes} minutes. Votre tuteur s'adaptera au niveau que vous venez de débloquer.",
+ "voiceTrialStart": "Commencer la conversation gratuite",
+ "voiceTrialSkip": "Passer pour le moment"
},
"plan": {
"unitLocked": "Unité verrouillée",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Afficher les chats",
"toggleSidebarHide": "Masquer les chats",
"conversations": "Conversations",
+ "voiceSession": "Conversation vocale",
"loading": "Chargement...",
"thinking": "Réflexion...",
"takingLonger": "Cela prend plus de temps que prévu...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabulaire",
"all": "Tous",
"searchPlaceholder": "Rechercher des ensembles...",
- "sets": "ensembles",
+ "sets": "{count, plural, one {# ensemble} other {# ensembles}}",
"words": "mots",
"addAll": "Ajouter les {count} en fiches",
"adding": "Ajout...",
@@ -848,6 +853,7 @@
"informal": "Informel",
"copied": "Copié !",
"copy": "Copier",
+ "copyPhrase": "Copier la phrase",
"noResults": "Aucune phrase ne correspond aux filtres",
"level": "Niveau",
"register": "Registre",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Traductions générées. Vérifiez-les avant d'enregistrer.",
"translateError": "La traduction a échoué. Votre contenu actuel a été conservé.",
"editTranslation": "Traduction à modifier",
+ "pending": "en attente",
"completion": "{complete}/{total} langues complètes",
"save": "Enregistrer l'annonce",
"saveSuccess": "Annonce du tableau de bord enregistrée.",
@@ -1062,7 +1069,12 @@
"ru": "Russe",
"nl": "Néerlandais",
"pl": "Polonais",
- "ro": "Roumain"
+ "ro": "Roumain",
+ "tr": "Turc",
+ "sv": "Suédois",
+ "da": "Danois",
+ "fi": "Finnois",
+ "hr": "Croate"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Erreur",
"errorServicesDisabled": "Les services vocaux ne sont pas activés sur ce serveur.",
"errorConnection": "Erreur de connexion. Veuillez réessayer.",
+ "errorTranscription": "Impossible de transcrire vos paroles. Veuillez parler à nouveau.",
+ "errorResponse": "Lingu n'a pas pu répondre. Veuillez réessayer.",
+ "errorSpeech": "Impossible de générer l'audio de la réponse de Lingu. Veuillez réessayer.",
"errorUnauthorized": "Session expirée. Veuillez actualiser.",
"errorMic": "Accès au microphone refusé.",
"errorVadInit": "Échec de l'initialisation de la reconnaissance vocale. Vérifiez que votre navigateur prend en charge WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Voix gratuite : {remaining}/{limit} min cette semaine",
"quotaTokens": "Tokens ce mois-ci",
"memoryUpdated": "Mémoire mise à jour",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Conversation gratuite après l'évaluation · {minutes} min",
+ "trialCtaLabel": "Continuez à parler",
+ "trialCtaTitle": "Continuez à pratiquer avec Lingu",
+ "trialCtaDesc": "Débloquez les conversations vocales complètes, le chat avec le tuteur et les exercices de compréhension orale et écrite avec Premium.",
"startersHint": "Ou commencez par un sujet",
"starters": [
"Art et créativité",
@@ -1234,6 +1249,11 @@
"nl": "Néerlandais",
"ro": "Roumain",
"ru": "Russe",
+ "tr": "Turc",
+ "sv": "Suédois",
+ "da": "Danois",
+ "fi": "Finnois",
+ "hr": "Croate",
"myLanguages": "Mes langues",
"addLanguage": "Ajouter une langue",
"selectLanguage": "Choisissez la langue que vous voulez apprendre",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Nouveautés",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Compris",
"entry1": {
- "label": "Des plans mieux équilibrés",
- "desc": "Les nouveaux plans répartissent mieux les leçons entre toutes les unités et font plus de place à des activités variées, même dans les plans courts."
+ "label": "Cinq nouvelles langues d'interface",
+ "desc": "FreeLingo est disponible en croate, danois, finnois, suédois et turc. Choisis la langue de l'interface dans les paramètres."
},
"entry2": {
- "label": "Un test pour terminer ton plan",
- "desc": "Une fois le dernier jour atteint et les leçons en attente terminées, tu peux passer le test de fin de niveau depuis le tableau de bord ou ton plan et voir tout ce que tu as appris."
- },
- "entry3": {
- "label": "La suite devient plus claire",
- "desc": "Les résultats du test et les recommandations sont maintenant dans ta langue, avec des indications plus claires pour passer à la suite ou revoir tes acquis."
- },
- "entry4": {
"label": "Une expérience plus fluide",
"desc": "Nous avons aussi corrigé de petits bugs et peaufiné quelques détails pour rendre ton apprentissage quotidien avec FreeLingo encore plus agréable."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Rien ici pour l'instant. Soyez le premier à soumettre !",
"votes": "{count} votes",
"vote": "1 vote",
+ "voteAction": "Voter",
+ "removeVoteAction": "Retirer mon vote",
"comments": "{count} commentaires",
"comment": "1 commentaire",
"by": "par",
@@ -1691,4 +1705,4 @@
"title": "En maintenance",
"description": "Cette section est temporairement indisponible pour cause de maintenance. Veuillez réessayer plus tard."
}
-}
\ No newline at end of file
+}
diff --git a/messages/hr.json b/messages/hr.json
new file mode 100644
index 000000000..e2919c9ab
--- /dev/null
+++ b/messages/hr.json
@@ -0,0 +1,1708 @@
+{
+ "landing": {
+ "microDemo": {
+ "title": "Ovako ti Lingu pomaže",
+ "exampleLabel": "Primjer vježbe engleskog jezika",
+ "questionLabel": "Lingu · Pitanje",
+ "answerLabel": "Ti · Odgovor",
+ "correctionLabel": "Lingu · Ispravak",
+ "explanation": "Kada govoriš o jučer, upotrijebi 'went', prošlo vrijeme glagola 'go'."
+ },
+ "hero": "Uči jezike uz sugovornika s kojim možeš razgovarati",
+ "heroSub": "Govori. Slušaj. Napreduj.",
+ "signIn": "Prijavi se",
+ "dashboard": "Nastavi",
+ "feature1Title": "Plan učenja",
+ "feature1Desc": "Procjena razine i tjedni plan od A1 do C2. Lekcije se otključavaju redom, tvojim tempom.",
+ "feature2Title": "AI učitelj u chatu",
+ "feature2Desc": "Vježbaj uz lekcije, zadatke i kartice pod vodstvom učitelja. Dobij povratne informacije na svakom koraku.",
+ "feature3Title": "Glasovni razgovor",
+ "feature3Desc": "Razgovaraj s učiteljem u stvarnom vremenu. Glasovno aktiviranje, trenutačni odgovori, bez čekanja na red.",
+ "feature4Title": "Slušanje i čitanje",
+ "feature4Desc": "Vježbe koje stvara AI na tvojoj razini prema CEFR-u. Poslušaj ulomak, odgovori na pet pitanja i otključaj prijepis – ili pročitaj tekst i odgovori na pitanja. Nove vježbe kad god poželiš.",
+ "feature5Title": "Kartice za učenje",
+ "feature5Desc": "Ponavljaj riječi u razmacima. Algoritam SM-2 zakazuje svaku karticu u najboljem trenutku za dugotrajnije pamćenje.",
+ "feature6Title": "Napredak i nizovi",
+ "feature6Desc": "XP, dnevni nizovi, rezultati vještina i znanja po jedinicama. Prati napredak iz tjedna u tjedan i provjeri razinu nakon svake CEFR etape.",
+ "footer": "Otvoreni kod · Besplatno samostalno hostanje ili pretplata na hostiranu uslugu",
+ "navFeatures": "Značajke",
+ "navReviews": "Recenzije",
+ "navPricing": "Cijene",
+ "navFAQ": "Česta pitanja",
+ "howItWorks": "Kako funkcionira",
+ "faqTitle": "Često postavljana pitanja",
+ "terms": "Uvjeti",
+ "privacy": "Privatnost",
+ "contact": "Kontakt",
+ "aboutMe": "O meni",
+ "github": "GitHub",
+ "footerProduct": "Proizvod",
+ "footerLegal": "Pravne informacije",
+ "languageGreetings": {
+ "en-US": "Hello!",
+ "en-GB": "Hello!",
+ "de-DE": "Hallo!",
+ "es-ES": "¡Hola!",
+ "fr-FR": "Bonjour !",
+ "it-IT": "Ciao!",
+ "pt-PT": "Olá!",
+ "ja-JP": "こんにちは!",
+ "ko-KR": "안녕하세요!",
+ "zh-CN": "你好!"
+ }
+ },
+ "common": {
+ "loading": "Učitavanje...",
+ "error": "Pogreška",
+ "errorMessage": "Nešto je pošlo po zlu. Pokušaj ponovno.",
+ "save": "Spremi promjene",
+ "saving": "Spremanje...",
+ "cancel": "Odustani",
+ "back": "Natrag",
+ "done": "Gotovo",
+ "next": "Dalje",
+ "submit": "Pošalji",
+ "start": "Započni",
+ "retry": "Pokušaj ponovno",
+ "or": "ili",
+ "appName": "FreeLingo",
+ "days": "dana",
+ "logout": "Odjavi se",
+ "logoutConfirmTitle": "Odjava",
+ "logoutConfirmMessage": "Jesi li siguran da se želiš odjaviti?",
+ "initializing": "Pokretanje...",
+ "clearFilters": "Očisti filtre",
+ "all": "Sve",
+ "score": "Rezultat",
+ "complete": "Dovrši",
+ "checking": "Provjera...",
+ "backToDashboard": "Natrag na nadzornu ploču",
+ "tagline": "Učenje jezika uz AI",
+ "close": "Zatvori",
+ "delete": "Izbriši",
+ "openMenu": "Otvori izbornik",
+ "closeMenu": "Zatvori izbornik",
+ "verifyEmailBanner": "Potvrdi svoju adresu e-pošte kako bi dovršio postavljanje računa.",
+ "verifyEmailSent": "E-poruka je poslana. Provjeri pristiglu poštu.",
+ "resendVerification": "Ponovno pošalji poruku za potvrdu",
+ "saveWord": "Spremi",
+ "wordSaved": "Spremljeno",
+ "wordAlreadySaved": "Već spremljeno",
+ "wordSaveError": "Nešto je pošlo po zlu",
+ "selectWordHint": "Označi riječ kako bi je spremio u svoj rječnik",
+ "noActivePlan": "Nije pronađen aktivan plan učenja",
+ "memorySavedToast": "Lingu je spremio novu uspomenu. Možeš je pregledati u Postavkama.",
+ "removeLanguageConfirmTitle": "Ukloni jezik",
+ "removeLanguageConfirmMessage": "Svi podaci povezani s tim jezikom (plan učenja, napredak, kartice i razgovori) bit će trajno izbrisani. Uspomene računa ostat će sačuvane. Ovu radnju nije moguće poništiti.",
+ "removeLanguageConfirmLabel": "Ukloni",
+ "nativeHelpTitle": "Pomoć: {language}",
+ "nativeHelpLoading": "Priprema pomoći: {language}...",
+ "nativeHelpShow": "Prikaži pomoć: {language}",
+ "nativeHelpKeyPoints": "Ključne točke",
+ "nativeHelpCommonTraps": "Česte zamke",
+ "nativeHelpMiniGlossary": "Kratki pojmovnik",
+ "nativeHelpUsageTips": "Savjeti za upotrebu",
+ "nativeHelpRegisterNotes": "Jezični registar",
+ "nativeHelpPhraseNotes": "Napomene o izrazima",
+ "nativeHelpStudyTips": "Savjeti za učenje",
+ "nativeHelpWordNotes": "Napomene o riječima",
+ "nativeHelpPractice": "Vježba"
+ },
+ "nav": {
+ "home": "Početna",
+ "myPlan": "Moj plan",
+ "progress": "Napredak",
+ "flashcards": "Kartice",
+ "tutor": "Razgovaraj s učiteljem",
+ "assessment": "Procjena",
+ "resources": "Materijali",
+ "grammar": "Gramatika",
+ "vocabulary": "Rječnik",
+ "phrasebook": "Zbirka izraza",
+ "settings": "Postavke",
+ "faq": "Česta pitanja",
+ "admin": "Administracija",
+ "conversation": "Razgovor",
+ "listening": "Slušanje",
+ "reading": "Čitanje",
+ "contact": "Kontakt",
+ "feedback": "Povratne informacije",
+ "reviews": "Recenzije",
+ "switchLanguage": "Promijeni jezik"
+ },
+ "auth": {
+ "login": {
+ "title": "Prijavi se",
+ "subtitle": "Dobrodošli natrag",
+ "email": "Adresa e-pošte",
+ "password": "Lozinka",
+ "submit": "Prijavi se",
+ "noAccount": "Nemaš račun?",
+ "register": "Registriraj se",
+ "error": "Neispravna adresa e-pošte ili lozinka",
+ "emailRequired": "Unesi adresu e-pošte",
+ "passwordRequired": "Unesi lozinku",
+ "loginFailed": "Prijava nije uspjela",
+ "signingIn": "Prijava u tijeku...",
+ "showPassword": "Prikaži lozinku",
+ "hidePassword": "Sakrij lozinku",
+ "accountCreated": "Račun je stvoren. Sada se možeš prijaviti",
+ "invalidEmail": "Unesi valjanu adresu e-pošte",
+ "forgotPassword": "Zaboravljena lozinka?"
+ },
+ "register": {
+ "title": "Stvori račun",
+ "subtitle": "Počni učiti",
+ "username": "Korisničko ime",
+ "email": "Adresa e-pošte",
+ "password": "Lozinka",
+ "displayName": "Ime za prikaz",
+ "nativeLanguage": "Materinski jezik",
+ "inviteToken": "Pozivni kod",
+ "inviteTokenPlaceholder": "Unesi pozivni kod",
+ "submit": "Stvori račun",
+ "hasAccount": "Već imaš račun?",
+ "login": "Prijavi se",
+ "registrationClosed": "Registracija je zatvorena. Potreban je pozivni kod.",
+ "error": "Registracija nije uspjela",
+ "passwordMismatch": "Lozinke se ne podudaraju",
+ "confirmPassword": "Potvrdi lozinku",
+ "showPassword": "Prikaži lozinku",
+ "hidePassword": "Sakrij lozinku",
+ "displayNamePlaceholder": "Isto kao korisničko ime ako je polje prazno",
+ "inviteActive": "Pozivnica je aktivna",
+ "creatingAccount": "Stvaranje računa...",
+ "invalidEmail": "Unesi valjanu adresu e-pošte",
+ "usernameTaken": "Korisničko ime već se koristi",
+ "emailTaken": "Adresa e-pošte već je registrirana",
+ "invalidInvite": "Neispravan ili istekao pozivni kod",
+ "termsAccept": "Potvrđujem da sam pročitao/la i prihvaćam",
+ "termsLink": "Uvjete korištenja",
+ "andWord": "i",
+ "privacyLink": "Pravila privatnosti",
+ "termsRequired": "Za nastavak moraš prihvatiti uvjete",
+ "invalidPassword": "Lozinka mora imati 10–25 znakova, barem jedno veliko slovo, jedan broj i jedan simbol",
+ "invalidUsernameChars": "Korisničko ime smije sadržavati samo slova, brojeve, crtice, podvlake i točke"
+ },
+ "verifyEmail": {
+ "title": "Potvrda e-pošte",
+ "loading": "Potvrđivanje...",
+ "success": "Adresa e-pošte potvrđena je. Sada se možeš prijaviti.",
+ "error": "Poveznica za potvrdu nije valjana ili je istekla. Zatraži novu u svojem računu.",
+ "goToLogin": "Idi na prijavu"
+ },
+ "forgotPassword": {
+ "title": "Zaboravljena lozinka",
+ "description": "Unesi adresu e-pošte povezanu s tvojim računom i poslat ćemo ti poveznicu za ponovno postavljanje lozinke.",
+ "emailPlaceholder": "tvoja@e-posta.hr",
+ "submit": "Pošalji poveznicu za novu lozinku",
+ "sending": "Slanje...",
+ "sent": "Ako je ta adresa e-pošte registrirana, poslali smo poveznicu za novu lozinku. Provjeri pristiglu poštu.",
+ "backToLogin": "Natrag na prijavu",
+ "error": "Nešto je pošlo po zlu. Pokušaj ponovno."
+ },
+ "resetPassword": {
+ "title": "Postavi novu lozinku",
+ "newPassword": "Nova lozinka",
+ "confirmPassword": "Potvrdi novu lozinku",
+ "submit": "Postavi novu lozinku",
+ "saving": "Spremanje...",
+ "success": "Lozinka je promijenjena. Preusmjeravamo te na prijavu...",
+ "mismatch": "Lozinke se ne podudaraju",
+ "tooShort": "Lozinka mora imati najmanje 10 znakova",
+ "missingToken": "Poveznica nije valjana. Zatraži novo postavljanje lozinke.",
+ "error": "Poveznica nije valjana ili je istekla",
+ "backToLogin": "Natrag na prijavu"
+ }
+ },
+ "settings": {
+ "title": "Postavke",
+ "sectionAccount": "Račun",
+ "sectionProfile": "Profil",
+ "avatarChange": "Promijeni sliku",
+ "avatarRemove": "Ukloni",
+ "avatarUploading": "Učitavanje slike...",
+ "avatarTypeError": "Dopuštene su samo JPEG i PNG slike",
+ "avatarSizeError": "Slika je prevelika (najviše 2 MB)",
+ "displayName": "Ime za prikaz",
+ "email": "Adresa e-pošte",
+ "nativeLanguage": "Materinski jezik",
+ "uiLocale": "Jezik sučelja",
+ "uiLocaleHint": "Mijenja jezik izbornika i gumba. Potrebno je ponovno učitati stranicu.",
+ "newPassword": "Nova lozinka",
+ "newPasswordPlaceholder": "Ostavi prazno da zadržiš trenutačnu",
+ "confirmPassword": "Potvrdi lozinku",
+ "confirmPasswordPlaceholder": "Ponovi novu lozinku",
+ "bio": "O tebi (kontekst za AI)",
+ "bioPlaceholder": "Npr. radim kao programer i često putujem zbog posla...",
+ "bioHint": "AI učitelj upotrebljava ove podatke kako bi prilagodio razgovore.",
+ "saveChanges": "Spremi promjene",
+ "saving": "Spremanje...",
+ "saved": "Profil je ažuriran.",
+ "passwordMismatch": "Lozinke se ne podudaraju",
+ "saveFailed": "Spremanje nije uspjelo",
+ "logout": "Odjavi se",
+ "sectionAppearance": "Izgled",
+ "theme": "Tema",
+ "darkActive": "Tamni način rada je aktivan",
+ "lightActive": "Svijetli način rada je aktivan",
+ "systemActive": "Automatski (sustav)",
+ "themeSystem": "Automatski",
+ "themeDark": "Tamno",
+ "themeLight": "Svijetlo",
+ "switchToLight": "Uključi svijetli način",
+ "switchToDark": "Uključi tamni način",
+ "sectionConversation": "Razgovor",
+ "conversationMaxDuration": "Najdulje trajanje razgovora",
+ "conversationInactivityTimeout": "Vrijeme do prekida zbog neaktivnosti",
+ "conversationSpeechPause": "Stanka prije završetka reda",
+ "conversationSpeechPauseHint": "Koliko dugo možeš zastati usred rečenice prije slanja svojeg reda. Automatski način prilagođava se tvojoj razini.",
+ "speechPauseAuto": "Automatski",
+ "speechPauseSec1": "1 sekunda",
+ "speechPauseSec2": "2 sekunde",
+ "speechPauseSec3": "3 sekunde",
+ "min15": "15 minuta",
+ "min30": "30 minuta",
+ "min1": "1 minuta",
+ "min3": "3 minute",
+ "min5": "5 minuta",
+ "saveConversation": "Spremi",
+ "conversationSaved": "Postavke razgovora spremljene su.",
+ "sectionLegal": "Pravne informacije",
+ "termsOfService": "Uvjeti korištenja",
+ "privacyPolicy": "Pravila privatnosti",
+ "contactLink": "Kontakt",
+ "deleteAccount": "Izbriši račun",
+ "deleteAccountTitle": "Izbriši račun",
+ "deleteAccountMessage": "Tvoj račun i svi podaci bit će trajno izbrisani. Ovu radnju nije moguće poništiti.",
+ "deleteAccountConfirm": "Izbriši račun",
+ "sectionUsageLimits": "Ograničenja upotrebe",
+ "sectionVoice": "Glas",
+ "voiceHint": "Odaberi željeni AI glas s popisa pa klikni na reprodukciju da ga poslušaš.",
+ "voicePlay": "Reproduciraj",
+ "voiceStop": "Zaustavi",
+ "quotaSessions": "Razgovori / tjedan",
+ "quotaMinutesDay": "Minute / dan",
+ "quotaMinutesWeek": "Minute / tjedan",
+ "quotaTokens": "Tokeni / mjesec",
+ "quotaUnlimited": "Neograničeno",
+ "quotaLoading": "Učitavanje...",
+ "quotaHint": "Upotreba se poništava svaki tjedan (razgovori i minute) i svaki mjesec (tokeni).",
+ "sectionMemory": "Uspomene",
+ "memoryEmpty": "Još nema spremljenih uspomena. AI će tijekom razgovora pamtiti važne pojedinosti o tebi i spremiti ih ovdje.",
+ "memoryClearAll": "Izbriši sve uspomene",
+ "memoryClearAllTitle": "Izbriši sve uspomene",
+ "memoryClearAllMessage": "Sve spremljene uspomene bit će trajno izbrisane. Ovu radnju nije moguće poništiti.",
+ "memoryClearAllConfirm": "Izbriši sve",
+ "memoryManage": "Upravljaj uspomenama",
+ "memoryBreadcrumb": "Postavke uspomena",
+ "memoryDescription": "Uspomene su zajedničke za sve jezike koje učiš. Lingu može spremiti korisne pojedinosti iz razgovora, a ti ih ovdje možeš dodati ili ukloniti.",
+ "memoryInputLabel": "Dodaj uspomenu",
+ "memoryInputPlaceholder": "Na primjer: Volim kratka objašnjenja.",
+ "memoryInputHint": "Najviše {max} znakova",
+ "memoryAdd": "Dodaj",
+ "memoryAdding": "Dodavanje...",
+ "memoryAddSuccess": "Uspomena je dodana.",
+ "memoryAddError": "Uspomenu nije bilo moguće dodati. Pokušaj ponovno.",
+ "memoryDuplicateError": "Ta uspomena već postoji.",
+ "memoryLoadError": "Uspomene nije bilo moguće učitati.",
+ "memoryDeleteLabel": "Izbriši uspomenu: {content}",
+ "memoryDeleteSuccess": "Uspomena je izbrisana.",
+ "memoryDeleteError": "Uspomenu nije bilo moguće izbrisati.",
+ "memoryClearing": "Brisanje...",
+ "memoryClearSuccess": "Sve uspomene su izbrisane.",
+ "memoryClearError": "Uspomene nije bilo moguće izbrisati.",
+ "memorySourceChat": "Chat",
+ "memorySourceVoice": "Glas",
+ "memorySourceManual": "Ručno",
+ "memorySourceUnknown": "Uspomena",
+ "sectionLanguages": "Jezici",
+ "languagesManage": "Moji jezici",
+ "cardProfileAccess": "Profil i pristup",
+ "cardSessionSecurity": "Sesija i sigurnost",
+ "cardTheme": "Tema",
+ "cardConversationTiming": "Trajanje razgovora",
+ "cardTutorVoice": "Glas učitelja",
+ "cardCurrentUsage": "Trenutačna upotreba",
+ "cardProductReview": "Recenzija proizvoda",
+ "cardLegalDocuments": "Pravni dokumenti",
+ "sectionReview": "Recenzija",
+ "reviewDescription": "Podijeli svoje iskustvo s FreeLingom. Ako urediš već odobrenu recenziju, morat će proći ponovni pregled prije javnog prikazivanja.",
+ "reviewPending": "Čeka odobrenje",
+ "reviewUpdate": "Ažuriraj",
+ "reviewSaved": "Recenzija je spremljena.",
+ "reviewDelete": "Izbriši recenziju",
+ "reviewDeleteTitle": "Izbriši recenziju",
+ "reviewDeleteMessage": "Želiš li izbrisati recenziju? Ovu radnju nije moguće poništiti.",
+ "reviewDeleteConfirm": "Izbriši",
+ "reviewDeleted": "Recenzija je izbrisana.",
+ "reviewDeleteError": "Nismo mogli izbrisati tvoju recenziju. Pokušaj ponovno."
+ },
+ "legal": {
+ "terms": {
+ "pageTitle": "Uvjeti korištenja",
+ "updated": "Posljednje ažuriranje: kolovoz 2026.",
+ "s1Title": "1. O FreeLingu",
+ "s1Body": "FreeLingo je platforma otvorenog koda za učenje jezika uz AI, dostupna na dva načina: (1) samostalnim hostanjem, kada je besplatno pokrećeš na vlastitoj infrastrukturi, i (2) kao hostirana usluga kojom upravlja tim FreeLingo, s besplatnim i plaćenim planovima. Korištenjem usluge na bilo koji od tih načina prihvaćaš ove Uvjete korištenja.",
+ "s2Title": "2. Dopuštena upotreba",
+ "s2Intro": "FreeLingo smiješ koristiti isključivo za osobno učenje jezika. Obvezuješ se da nećeš:",
+ "s2i1": "Pokušati neovlašteno pristupiti bilo kojem dijelu sustava.",
+ "s2i2": "Učitati štetan sadržaj ili pokušati ometati uslugu.",
+ "s2i3": "Dijeliti podatke za prijavu s trećim stranama.",
+ "s2i4": "Koristiti automatizirane skripte ili botove za zloupotrebu funkcija AI učitelja.",
+ "s3Title": "3. Korisnički računi",
+ "s3Body": "Moraš imati najmanje 16 godina da bi stvorio račun i koristio FreeLingo. Registracijom potvrđuješ da ispunjavaš taj dobni uvjet. Ako saznamo da račun pripada osobi mlađoj od 16 godina, izbrisat ćemo ga i sve povezane podatke bez prethodne obavijesti. Odgovoran si za čuvanje svojih pristupnih podataka i sve aktivnosti na računu. Upravitelj instance – ti pri samostalnom hostanju ili tim FreeLingo za hostiranu uslugu – može privremeno ili trajno zatvoriti račune koji krše ove uvjete.",
+ "s4Title": "4. Sadržaj i interakcije s AI-jem",
+ "s4Body": "Razgovori s AI učiteljem pohranjuju se u bazu podataka radi kontinuiteta učenja. U hostiranoj usluzi AI chat i glasovne funkcije obrađuju se putem OpenAI API-ja, a tvoj sadržaj podliježe pravilima korištenja OpenAI-ja. AI može dati netočne odgovore – uvijek prosuđuj sam. FreeLingo ne odgovara za sadržaj koji stvaraju temeljni jezični modeli. Sadržaj koji stvara AI učitelj (vježbe, ispravci, objašnjenja i drugi materijali) pripada tebi za osobnu upotrebu. FreeLingo ne polaže pravo vlasništva nad tvojim razgovorima ni sadržajem nastalim iz njih.",
+ "s5Title": "5. Načini pružanja usluge",
+ "s5Body": "Samostalno hostane instalacije besplatne su i upravitelj ima potpunu kontrolu nad svojim podacima i infrastrukturom. Hostirana usluga tima FreeLingo nudi besplatan plan s ograničenjima upotrebe i plaćene mjesečne ili godišnje pretplate za neograničen pristup. Besplatan plan uključuje sedmodnevnu probnu upotrebu s punim pristupom (bez potrebe za načinom plaćanja), a nakon toga dnevna ograničenja za lekcije i AI chat te tjedna za vježbe slušanja, čitanja i glasovne razgovore. Plaćene pretplate uklanjaju sva ograničenja tih funkcija. U hostiranoj usluzi tvoji se podaci čuvaju na poslužiteljima kojima upravlja tim FreeLingo. Trenutačna ograničenja svojeg računa možeš vidjeti na nadzornoj ploči.",
+ "s6Title": "6. Pretplata i naplata",
+ "s6Body": "Besplatan plan pruža ograničen pristup AI funkcijama, uz sedmodnevnu probnu upotrebu s punim pristupom za nove korisnike (bez potrebe za načinom plaćanja). Plaćene pretplate dostupne su mjesečno ili godišnje i automatski se obnavljaju ako ih prethodno ne otkažeš. Pretplatu možeš otkazati u bilo kojem trenutku u Postavkama; otkazivanje stupa na snagu krajem tekućeg razdoblja. Upravitelj također može otkazati obnove ili pretplate ako se usluga promijeni ili zatvori. Ako usluga prestane prije isteka plaćenog razdoblja, primjenjuje se povrat koji zahtijeva zakon o zaštiti potrošača. Pravo na odustajanje i važeće cijene prikazuju se i obrađuju u skladu s važećim zakonom.",
+ "s7Title": "7. Dostupnost",
+ "s7Body": "Hostirana usluga pruža se bez jamstva neprekidnog rada ili dostupnosti. Upravitelj je može promijeniti, privremeno obustaviti ili zatvoriti u bilo kojem trenutku zbog tehničkih, financijskih ili drugih razloga. Planirana zatvaranja objavit će se u aplikaciji ili e-poštom kada je to razumno moguće. Nakon objavljenog datuma računi i operativni podaci mogu se nepovratno izbrisati. FreeLingo ne pruža uslugu sigurnosnog kopiranja ni automatizirani alat za izvoz.",
+ "s8Title": "8. Ograničenje odgovornosti",
+ "s8Body": "FreeLingo i njegovi suradnici ne odgovaraju za izravnu, neizravnu, slučajnu ili posljedičnu štetu nastalu korištenjem ove usluge ili nemogućnošću njezina korištenja. U najvećoj mjeri dopuštenoj mjerodavnim pravom, ukupna odgovornost ne može biti veća od iznosa koji si platio tijekom tri mjeseca prije podnošenja zahtjeva. Obvezuješ se obeštetiti FreeLingo i njegove suradnike te ih zaštititi od zahtjeva trećih osoba, štete ili troškova proizašlih iz tvojeg korištenja usluge protivno ovim uvjetima.",
+ "s9Title": "9. Mjerodavno pravo",
+ "s9Body": "Na ove uvjete primjenjuju se zakoni Španjolske. Za sporove proizašle iz ovih uvjeta isključivo su nadležni sudovi u Španjolskoj, osim ako obvezni propisi o zaštiti potrošača u tvojoj državi prebivališta određuju drukčije.",
+ "s10Title": "10. Izmjene uvjeta",
+ "s10Body": "Ovi se uvjeti mogu ažurirati u bilo kojem trenutku. O važnim izmjenama po potrebi ćemo obavijestiti putem sučelja usluge ili e-poštom. Nastavkom korištenja nakon izmjene prihvaćaš izmijenjene uvjete.",
+ "s11Title": "11. Djelomična ništetnost",
+ "s11Body": "Ako nadležni sud utvrdi da je neka odredba ovih uvjeta nevažeća ili neprovediva, ograničit će se ili ukloniti u najmanjoj potrebnoj mjeri, a ostale odredbe ostat će u punoj snazi.",
+ "linkPrivacy": "Pravila privatnosti",
+ "linkBack": "Natrag na registraciju",
+ "linkBackSettings": "Natrag na Postavke",
+ "linkContact": "Za pitanja o ovim uvjetima upotrijebi kontaktni obrazac na freelingo.app dok je dostupan ili kontaktni kanal naveden u FreeLingovu repozitoriju na GitHubu."
+ },
+ "privacy": {
+ "pageTitle": "Pravila privatnosti",
+ "updated": "Posljednje ažuriranje: kolovoz 2026.",
+ "s1Title": "1. Pregled",
+ "s1Body": "FreeLingo je dostupan kao aplikacija za samostalno hostanje i kao hostirana usluga. Pri samostalnom hostanju svi se podaci pohranjuju isključivo na poslužitelju upravitelja i autori projekta FreeLingo nemaju pristup njima. Za hostiranu uslugu tim FreeLingo voditelj je obrade podataka; ova pravila objašnjavaju kako se tvoji osobni podaci prikupljaju, koriste i štite.",
+ "s2Title": "2. Podaci koje prikupljamo",
+ "s2Intro": "FreeLingo nije namijenjen djeci mlađoj od 16 godina. Ne prikupljamo svjesno osobne podatke osoba mlađih od 16 godina. Ako si mlađi od 16 godina, nemoj stvarati račun ni davati osobne podatke. Kada stvoriš račun i koristiš FreeLingo, pohranjuju se sljedeći podaci:",
+ "s2i1": "Korisničko ime, ime za prikaz i adresa e-pošte.",
+ "s2i2": "Lozinka pohranjena kao sažetak (bcrypt – tvoja lozinka u običnom tekstu nikada se ne pohranjuje).",
+ "s2i3": "Odabrani materinski jezik i postavke učenja.",
+ "s2i4": "Neobavezna profilna slika.",
+ "s2i5": "Povijest chata i razgovora s AI učiteljem.",
+ "s2i6": "Kartice, napredak u učenju i podaci za ponavljanje u razmacima.",
+ "s2i7": "Rezultati procjene razine i napredak prema nastavnom planu.",
+ "s2i8": "Status pretplate, vrsta plana i Stripeov korisnički identifikator (za pretplatnike plaćene usluge).",
+ "s2i9": "Opće uspomene i postavke računa koje AI učitelj nauči tijekom razgovora ili ih samostalno dodaš kako bi prilagodio buduće interakcije na svim jezicima koje učiš.",
+ "s2i10": "Prijedlozi značajki, prijave pogrešaka, komentari i glasovi koje šalješ putem ploče za povratne informacije. Javno se prikazuje samo tvoje korisničko ime; svi korisnici mogu vidjeti objavljene prijedloge i prijave.",
+ "s3Title": "3. Kako koristimo tvoje podatke",
+ "s3Intro": "Tvoji se podaci koriste isključivo za pružanje FreeLingova iskustva učenja:",
+ "s3i1": "Za provjeru identiteta i održavanje sesije.",
+ "s3i2": "Za prilagodbu plana učenja i razgovora s učiteljem.",
+ "s3i3": "Za praćenje napretka u učenju tijekom vremena.",
+ "s3i4": "Za generiranje odgovora AI učitelja prilagođenih kontekstu.",
+ "s3i5": "Za pamćenje tvojih postavki i okolnosti kako bi učitelj mogao bolje prilagoditi buduće razgovore.",
+ "s3i6": "Za prikaz i upravljanje prijedlozima značajki i prijavama pogrešaka korisnika.",
+ "s3Footer": "Tvoji se podaci nikada ne prodaju, ne iznajmljuju niti dijele s uslugama oglašavanja trećih strana.",
+ "s4Title": "4. Vanjske AI usluge",
+ "s4Body": "U hostiranoj usluzi AI chat, sinteza govora (TTS) i transkripcija govora (STT) pružaju se putem OpenAI API-ja. Sadržaj tvojih razgovora i zvuk šalju se OpenAI-ju na obradu u skladu s njegovim Pravilima privatnosti (openai.com/privacy). Plaćanja obrađuje Stripe, Inc. (SAD); Stripe izravno obrađuje podatke o plaćanju, a mi ne pohranjujemo podatke o karticama na svojim poslužiteljima – na podatke o transakcijama primjenjuju se Stripeova Pravila privatnosti (stripe.com/privacy). Pri samostalnom hostanju upravitelj bira AI pružatelja; zadano se koristi lokalna instanca Ollame i podaci ne napuštaju poslužitelj.",
+ "s5Title": "5. Kolačići i tokeni sesije",
+ "s5Body": "FreeLingo koristi dva tehnička kolačića nužna za rad usluge: refresh_token (httpOnly, istječe nakon 30 dana) za autentikaciju sesije i NEXT_LOCALE (istječe nakon jedne godine) za pamćenje odabranog jezika. Ne koriste se kolačići za praćenje ni oglašavanje. Za anonimno mjerenje prometa koristimo Umami – samostalno hostiran alat za analitiku otvorenog koda koji ne koristi kolačiće, ne prikuplja osobne podatke i čuva sve podatke na našim poslužiteljima.",
+ "s6Title": "6. Tvoja prava (GDPR)",
+ "s6Body": "Ako si u Europskom gospodarskom prostoru, prema GDPR-u imaš pravo na pristup, ispravak, brisanje, prigovor i prenosivost podataka. Račun možeš izbrisati u Postavkama. Ostali zahtjevi obrađuju se ručno putem kontakta iz odjeljka 8; FreeLingo nema automatizirani alat za izvoz. Podatke obrađujemo radi pružanja usluge (čl. 6. st. 1. t. b GDPR-a). OpenAI i Stripe mogu obrađivati podatke u SAD-u uz zaštitne mjere primjenjive na međunarodne prijenose. Možeš podnijeti pritužbu španjolskom tijelu za zaštitu podataka (aepd.es) ili lokalnom nadzornom tijelu.",
+ "s7Title": "7. Čuvanje i brisanje podataka",
+ "s7Body": "Operativni podaci čuvaju se dok tvoj račun postoji i hostirana usluga radi. U Postavkama možeš izbrisati uspomene, jezike ili cijeli račun. Upravitelj također može izbrisati sve račune i podatke pri zatvaranju usluge, uz razumnu najavu kada je moguća. Brisanje se ne može poništiti. Nakon brisanja sustava i kopija pod upraviteljevom kontrolom FreeLingo više ne čuva osobne podatke. Stripe može zadržati podatke o plaćanjima i transakcijama u skladu sa svojim pravilima i zakonskim obvezama.",
+ "s8Title": "8. Kontakt",
+ "s8Body": "Za pitanja o privatnosti hostirane usluge upotrijebi kontaktni obrazac na freelingo.app dok je dostupan. Nakon zatvaranja koristi kontaktni kanal objavljen u FreeLingovu repozitoriju na GitHubu i ne objavljuj osobne podatke u javnim prijavama. Za samostalno hostane instalacije obrati se upravitelju te instance.",
+ "s9Title": "9. Djelomična ništetnost",
+ "s9Body": "Ako se utvrdi da je neka odredba ovih pravila nevažeća ili neprovediva, ograničit će se ili ukloniti u najmanjoj potrebnoj mjeri, a ostale odredbe ostat će na snazi.",
+ "linkTerms": "Uvjeti korištenja",
+ "linkBack": "Natrag na registraciju",
+ "linkBackSettings": "Natrag na Postavke"
+ }
+ },
+ "dashboard": {
+ "welcomeBack": "Dobrodošli natrag",
+ "streak": "Niz",
+ "xp": "XP",
+ "level": "Razina",
+ "skills": "Vještine",
+ "today": "Danas",
+ "noSkills": "Dovrši procjenu kako bi pratio svoje vještine.",
+ "noLessons": "Za danas nema planiranih lekcija.",
+ "lessonDone": "gotovo",
+ "startLesson": "Započni",
+ "loadingProgress": "Učitavanje napretka...",
+ "noLevel": "Razina još nije određena.",
+ "completeAssessment": "Riješi procjenu",
+ "allCaughtUp": "Tvoj plan, tvojim tempom",
+ "levelTestReady": "Tvoj test razine je spreman.",
+ "levelTestReadyDesc": "Stigao si do kraja svojeg plana. Riješi test razine kako bi potvrdio napredak.",
+ "levelTestCompleted": "Test razine je dovršen.",
+ "levelTestScoreLine": "Konačni rezultat: {score}. Tvoj rezultat spremljen je u Moj plan.",
+ "startWithAssessment": "Započni procjenom razine.",
+ "takeAssessmentArrow": "Riješi procjenu →",
+ "goToMyPlan": "Moj plan →",
+ "quickActions": "Brze radnje",
+ "dayProgress": "Dan {current} od {total}",
+ "skipDay": "Preskoči danas",
+ "pendingLessons": "lekcije na čekanju",
+ "nextStep": "Sljedeći korak",
+ "assessmentCreatesPlan": "Procjena utvrđuje tvoju razinu i stvara prilagođen plan za aktivni jezik.",
+ "lessonReady": "Sljedeći korak, tvojim tempom",
+ "pendingStillAvailable": "U tvojem planu još ima lekcija na čekanju: {count}.",
+ "noPendingToday": "U svojem planu možeš vidjeti kako nastaviti.",
+ "planDayGoal": "Cilj za ovaj dan tvojeg plana",
+ "lessonsCompleted": "Lekcije",
+ "accuracy": "Točnost",
+ "exerciseStats": "{correct}/{total} zadataka",
+ "noExercisesYet": "Još nema zadataka",
+ "planProgress": "Napredak plana",
+ "currentDay": "Trenutačni dan",
+ "daysRemaining": "Preostalo dana",
+ "completedToday": "{completed}/{total} dovršeno",
+ "vocabularyProgress": "Rječnik {level}",
+ "vocabularyWords": "{mastered}/{total} riječi",
+ "recentPerformance": "Nedavni rezultati",
+ "recentPerformanceDescription": "Na temelju tvojih posljednjih odgovora i ponavljanja.",
+ "performanceNeedsPractice": "Potrebno vježbati",
+ "performanceInProgress": "U tijeku",
+ "performanceStrong": "Dobro",
+ "premiumBannerTitle": "Otključaj razgovor, slušanje i čitanje",
+ "premiumBannerDesc": "Počni s besplatnim probnim razdobljem i vježbaj s Linguom tekstom, glasom i zadacima prilagođenima tvojoj razini.",
+ "premiumBannerDescTrialUsed": "Aktiviraj Premium za tekstualni chat s Linguom, glasovne razgovore, čitanje i slušanje.",
+ "premiumBannerCta": "Besplatno probno razdoblje",
+ "premiumBannerCtaTrialUsed": "Pogledaj planove",
+ "premiumBannerPastDueTitle": "Plaćanje na čekanju",
+ "premiumBannerPastDueDesc": "Ažuriraj način plaćanja kako bi ponovno dobio pristup Premium chatu, glasovnim razgovorima, čitanju i slušanju.",
+ "premiumBannerPastDueCta": "Vrati Premium",
+ "freemiumTrialTitle": "{days, plural, one {# dan besplatnog probnog razdoblja} few {# dana besplatnog probnog razdoblja} other {# dana besplatnog probnog razdoblja}}",
+ "freemiumTrialDesc": "Uživaj u neograničenom pristupu chatu, glasu, slušanju i čitanju. Isprobaj sve Premium značajke besplatno.",
+ "announcementDismiss": "Zatvori obavijest",
+ "announcementDismissError": "Obavijest nije bilo moguće zatvoriti. Pokušaj ponovno."
+ },
+ "assessment": {
+ "title": "Procjena razine",
+ "subtitle": "Određivanje razine prema CEFR-u",
+ "intro": "Postavit ćemo nekoliko pitanja kako bismo odredili tvoju razinu engleskog.",
+ "startWriting": "Započni procjenu pisanja",
+ "startQuiz": "Započni kviz",
+ "writing": {
+ "title": "Uzorak pisanja",
+ "submit": "Pošalji tekst",
+ "placeholder": "Ovdje napiši odgovor...",
+ "evaluating": "Procjena u tijeku..."
+ },
+ "quiz": {
+ "question": "Pitanje",
+ "of": "od",
+ "next": "Dalje",
+ "submit": "Pošalji kviz",
+ "submitting": "Slanje..."
+ },
+ "result": {
+ "title": "Tvoja razina",
+ "xpEarned": "Osvojeni XP",
+ "skillsTitle": "Vještine",
+ "goToPlan": "Pogledaj Moj plan",
+ "generatePlan": "Stvori plan učenja"
+ },
+ "currentLevel": "Trenutačna razina",
+ "assessedOn": "Procijenjeno",
+ "alreadyHasPlan": "Već imaš aktivan plan učenja. Nova procjena stvorit će novi plan.",
+ "retake": "Ponovi procjenu",
+ "evaluating": "Procjena u tijeku...",
+ "resultStep": "Rezultat",
+ "cefrLevel": "CEFR razina",
+ "overrideLevel": "Promijeni početnu razinu",
+ "strengths": "Prednosti",
+ "needsWork": "Potrebno poboljšati",
+ "createPlan": "Stvori plan →",
+ "creating": "Stvaranje...",
+ "loading": "Učitavanje...",
+ "levelTest": {
+ "title": "Završni test razine",
+ "loading": "Učitavanje pitanja...",
+ "loadingTest": "Učitavanje testa...",
+ "submittingTest": "Slanje odgovora...",
+ "quizTitle": "Test razine {level}",
+ "resultsTitle": "Test razine {level} – rezultati",
+ "finalScore": "Konačni rezultat",
+ "correctCount": "{correct} / {total} točnih",
+ "correctAnswer": "✓ Točno",
+ "incorrectAnswer": "✗ Netočno – točan odgovor: {answer}",
+ "nextLevelFallback": "SLJEDEĆA RAZINA",
+ "advanceLabel": "SPREMNO ZA RAZINU {level}",
+ "advanceMessage": "Pokazao si dobro znanje na razini {level}. Ponovi procjenu kako bi stvorio plan za razinu {next}.",
+ "extendLabel": "PRODULJENJE ZA ČETIRI TJEDNA",
+ "extendMessage": "Otkrivena su područja za poboljšanje: {areas}. Preporučujemo još četiri tjedna usmjerenog vježbanja.",
+ "repeatLabel": "PONOVI RAZINU {level}",
+ "repeatMessage": "Rezultat ispod 55 %. Potrebno je učvrstiti nekoliko ključnih znanja razine {level}.",
+ "reviewPlan": "Pregledaj Moj plan →",
+ "goToPlan": "Idi na plan",
+ "invalidPlan": "Neispravan ID plana. Otvori test razine sa stranice svojeg plana.",
+ "noQuestions": "Poslužitelj nije poslao pitanja.",
+ "loadFailed": "Test razine nije bilo moguće učitati.",
+ "submitFailed": "Slanje odgovora nije uspjelo.",
+ "question": "Pitanje",
+ "of": "od",
+ "confirm": "Potvrdi odgovor",
+ "nextQuestion": "Sljedeće pitanje",
+ "submit": "Pošalji test",
+ "result": {
+ "title": "Test je dovršen",
+ "recommendation": "Preporuka",
+ "advance": "Spreman za sljedeću razinu",
+ "extend": "Nastavi na trenutačnoj razini",
+ "repeat": "Preporučujemo ponavljanje trenutačne razine",
+ "back": "Natrag na plan"
+ }
+ },
+ "suggestedLevel": "Predložena razina {aiLevel}; plan će koristiti {selectedLevel}",
+ "step1": "Korak 1 / 3: Tvoja razina",
+ "step2": "Korak 2 / 3: Pitanje {questionNumber} / {totalQuestions}",
+ "dontKnow": "Ne znam",
+ "step3": "Korak 3 / 3: Tvoj program",
+ "studiedBefore": "Jesi li već učio ovaj jezik ({language})?",
+ "studiedBeforeHint": "To nam pomaže da kviz počne na odgovarajućoj razini.",
+ "beginnerOption": "Potpuni sam početnik",
+ "beginnerOptionHint": "Počet ću od A1, kviz nije potreban.",
+ "hasExperienceOption": "Da, imam nešto iskustva",
+ "hasExperienceOptionHint": "Riješit ću kratak prilagodljiv kviz (oko 12 pitanja).",
+ "skills": {
+ "grammar": "Gramatika",
+ "vocabulary": "Rječnik",
+ "reading": "Čitanje"
+ },
+ "howManyWeeks": "Koliko tjedana za razinu {cefr_level}?",
+ "nWeeks": "{count, plural, one {# tjedan} few {# tjedna} other {# tjedana}}",
+ "intensity": {
+ "intensive": "Intenzivno",
+ "standard": "Standardno",
+ "relaxed": "Opušteno",
+ "veryRelaxed": "Vrlo opušteno"
+ },
+ "approxLessons": "{count, plural, one {Oko # lekcija} few {Oko # lekcije} other {Oko # lekcija}}",
+ "daysPerWeek": "{count, plural, one {# dan/tjedan} few {# dana/tjedan} other {# dana/tjedan}}",
+ "mainGoals": "Glavni ciljevi (odaberi sve što vrijedi)",
+ "goals": {
+ "grammar": "Gramatika",
+ "vocabulary": "Rječnik",
+ "reading": "Čitanje",
+ "writing": "Pisanje",
+ "conversation": "Razgovor",
+ "listening": "Slušanje"
+ },
+ "summaryLevel": "Razina",
+ "summaryDuration": "Trajanje",
+ "summaryGoals": "Ciljevi",
+ "noneSelected": "nije odabrano",
+ "buildingPlan": "Izrada plana...",
+ "startWarningTitle": "Prije početka",
+ "startWarningMessage": "Ako napustiš procjenu prije završetka, izgubit ćeš napredak i morat ćeš početi ispočetka.",
+ "startWarningMessageLevelTest": "Ako napustiš test razine prije završetka, izgubit ćeš napredak i morat ćeš ga ponoviti.",
+ "startWarningConfirm": "Započni",
+ "startMyPlan": "Započni Moj plan",
+ "voiceTrialLabel": "Probaj glasovni razgovor",
+ "voiceTrialTitle": "Isprobaj pravi razgovor sada",
+ "voiceTrialDesc": "Razgovaraj s Linguom {minutes} minuta besplatno. Tvoj učitelj prilagodit će se razini koju si upravo otključao.",
+ "voiceTrialStart": "Započni besplatan razgovor",
+ "voiceTrialSkip": "Preskoči zasad"
+ },
+ "plan": {
+ "unitLocked": "Jedinica je zaključana",
+ "unitAvailable": "Jedinica je dostupna",
+ "lessonPending": "Lekcija na čekanju",
+ "title": "Moj plan",
+ "subtitle": "Plan učenja",
+ "noPlan": "Nije pronađen aktivan plan učenja.",
+ "startAssessment": "Najprije riješi procjenu",
+ "week": "Tjedan",
+ "currentLesson": "Trenutačna lekcija",
+ "currentUnit": "Trenutačna jedinica",
+ "startLesson": "Započni lekciju",
+ "completed": "Dovršeno",
+ "generating": "Stvaranje lekcije...",
+ "generatingError": "Lekciju nije bilo moguće stvoriti",
+ "loading": "Učitavanje plana...",
+ "learningRoadmap": "Put učenja",
+ "level": "Razina",
+ "duration": "Trajanje",
+ "durationDetail": "{weeks, plural, one {# tjedan} few {# tjedna} other {# tjedana}} · {days, plural, one {# dan/tjedan} few {# dana/tjedan} other {# dana/tjedan}}",
+ "unitsLabel": "Jedinice",
+ "noUnitsForLevel": "Za razinu {level} nema dostupnih jedinica",
+ "takeAssessmentArrow": "Riješi procjenu →",
+ "unitLabel": "Jedinica",
+ "grammarCovered": "Obrađena gramatika",
+ "noLessons": "Za ovu jedinicu još nema planiranih lekcija.",
+ "weekDay": "Tjedan {week}, dan {day}",
+ "lessonTypes": {
+ "grammar": "Gramatika",
+ "vocabulary": "Rječnik",
+ "reading": "Čitanje",
+ "writing": "Pisanje",
+ "listening": "Slušanje",
+ "conversation": "Razgovor",
+ "review": "Ponavljanje",
+ "level_test": "Test razine"
+ },
+ "levelComplete": "Razina {level} dovršena. Riješi test razine",
+ "levelCompleteDesc": "Stigao si do kraja plana za razinu {level}. Riješi završni test kako bi potvrdio napredak i otključao sljedeću razinu.",
+ "levelCompleteHint": "Test traje oko 20 minuta i obuhvaća gramatiku, rječnik i čitanje.",
+ "beginLevelTest": "Započni test razine",
+ "nLessons": "{count, plural, one {# lekcija} few {# lekcije} other {# lekcija}}",
+ "nGrammar": "{count, plural, one {# lekcija gramatike} few {# lekcije gramatike} other {# lekcija gramatike}}",
+ "levelTestLabel": "Test razine",
+ "unitAriaLabel": "Jedinica {index}: {title}",
+ "noUnitsDesc": "Sadržaj za ovu razinu priprema se. Vrati se uskoro.",
+ "completionTestTitle": "Završni test razine {level}",
+ "levelTestResult": "Rezultat testa razine",
+ "testScore": "Rezultat:",
+ "pendingLessons": "Lekcije na čekanju",
+ "pendingReassurance": "Možeš im se vratiti kad god ti odgovara.",
+ "resume": "Nastavi",
+ "lessonsHeader": "{count, plural, one {Lekcija · #} few {Lekcije · #} other {Lekcija · #}}",
+ "reviewLesson": "Ponavljanje",
+ "noPlanLabel": "ZAPOČNI",
+ "noPlanTitle": "Stvori svoj plan učenja",
+ "noPlanDesc": "Riješi kratku procjenu kako bi saznao svoju razinu i dobio osobni put učenja.",
+ "noPlanHint": "Traje oko 10 minuta"
+ },
+ "lesson": {
+ "loading": "Učitavanje lekcije...",
+ "notFound": "Lekcija nije pronađena",
+ "backToPlan": "Natrag na plan",
+ "progress": "Napredak",
+ "exercise": "Zadatak",
+ "of": "od",
+ "checkAnswer": "Provjeri odgovor",
+ "nextExercise": "Sljedeći zadatak",
+ "submitWriting": "Pošalji tekst",
+ "yourAnswer": "Tvoj odgovor...",
+ "correct": "Točno",
+ "incorrect": "Netočno",
+ "feedback": "Povratne informacije",
+ "corrections": "Ispravci",
+ "completeLesson": "Dovrši lekciju",
+ "completing": "Dovršavanje...",
+ "relatedGrammar": "Povezana gramatika",
+ "explanation": "Objašnjenje",
+ "exercises": "Zadaci",
+ "vocabulary": "Rječnik",
+ "label": "Lekcija",
+ "submitAnswer": "Pošalji odgovor",
+ "lessonDone": "Odlično. Lekcija je dovršena.",
+ "exerciseSummary": "Ocijenjeni zadaci: {completed} od {total}",
+ "score": "Rezultat",
+ "showNativeExplanation": "Prikaži na jeziku",
+ "hint": "Savjet",
+ "showNativeHint": "Prikaži savjet na jeziku",
+ "regenerateExercise": "Ponovno stvori zadatak",
+ "regeneratingExercise": "Ponovno stvaranje...",
+ "regenerateError": "Zadatak nije bilo moguće ponovno stvoriti. Pokušaj ponovno.",
+ "regenerateNotNeeded": "Ovaj zadatak ne treba ponovno stvarati.",
+ "examples": "Primjeri",
+ "commonTraps": "Česte zamke",
+ "miniGlossary": "Kratki pojmovnik",
+ "pronunciationExercise": "Izgovor",
+ "exerciseTypeMultipleChoice": "Višestruki odabir",
+ "exerciseTypeFillBlank": "Popuni prazninu",
+ "exerciseTypeFreeWrite": "Slobodno pisanje",
+ "exerciseTypePronunciation": "Izgovor",
+ "tapToListen": "Dodirni za slušanje",
+ "recordNow": "Snimi odgovor",
+ "exit": "Izađi",
+ "exitConfirmTitle": "Izaći iz lekcije?",
+ "exitConfirmMessage": "Izgubit ćeš napredak u ovoj lekciji.",
+ "dayComplete": "Dan je dovršen!",
+ "dayCompleteMsg": "Dovršio si ovaj dan svojeg plana. Nastavi kad god ti odgovara."
+ },
+ "flashcards": {
+ "title": "Kartice",
+ "due": "Za ponavljanje",
+ "total": "Ukupno",
+ "noDue": "Trenutačno nema kartica za ponavljanje",
+ "allCaughtUp": "Sve je riješeno!",
+ "generate": "Stvori kartice",
+ "generating": "Stvaranje...",
+ "topic": "Tema",
+ "topicPlaceholder": "npr. rječnik za putovanja",
+ "count": "Broj",
+ "level": "Razina",
+ "submit": "Stvori",
+ "again": "Ponovno",
+ "hard": "Teško",
+ "good": "Dobro",
+ "easy": "Lako",
+ "front": "Riječ",
+ "back": "Definicija",
+ "loading": "Učitavanje kartica...",
+ "sectionGenerate": "Stvori nove kartice",
+ "speakingMode": "Govor",
+ "standardMode": "Standardno",
+ "sayWord": "Izgovori riječ naglas",
+ "refresh": "Osvježi",
+ "generateBtn": "Stvori",
+ "cards": "kartica",
+ "myVocabularyBtn": "Moj rječnik",
+ "myVocabulary": "Moj rječnik",
+ "myVocabularyEmpty": "Još nema spremljenih riječi. Označi riječ tijekom čitanja da je spremiš ovdje.",
+ "myVocabularyNoResults": "Nema spremljenih riječi koje odgovaraju pretraživanju.",
+ "vocabularySearch": "Pretraži riječi...",
+ "vocabularyPageInfo": "Stranica {page} od {pages}",
+ "vocabularyPrev": "Prethodna",
+ "vocabularyNext": "Sljedeća",
+ "backToFlashcards": "Kartice",
+ "noCardsHint": "Pritisni + Stvori kako bi napravio prve kartice uz AI",
+ "tapToHide": "dodirni za skrivanje",
+ "tapToReveal": "dodirni za otkrivanje",
+ "interval": "Razmak",
+ "repetitions": "Ponavljanja"
+ },
+ "chat": {
+ "title": "AI učitelj",
+ "subtitle": "{language} – pitaj što god želiš",
+ "placeholder": "Napiši poruku...",
+ "send": "Pošalji",
+ "sending": "Slanje...",
+ "noConversation": "Odaberi ili započni razgovor",
+ "newConversation": "Novi",
+ "toggleSidebarShow": "Prikaži chatove",
+ "toggleSidebarHide": "Sakrij chatove",
+ "conversations": "Razgovori",
+ "voiceSession": "Glasovni razgovor",
+ "loading": "Učitavanje...",
+ "thinking": "Razmišljam...",
+ "takingLonger": "Traje dulje nego što smo očekivali...",
+ "errorMessage": "Nešto je pošlo po zlu. Pokušaj ponovno.",
+ "deleteTitle": "Izbriši chat",
+ "deleteMessage": "Ovaj će razgovor biti trajno izbrisan.",
+ "deleteConfirm": "Izbriši",
+ "enterToSend": "Enter za slanje",
+ "continueInVoice": "Nastavi glasom",
+ "memoryUpdated": "Uspomena je ažurirana"
+ },
+ "progress": {
+ "title": "Napredak",
+ "subtitle": "Praćenje vještina",
+ "xp": "XP",
+ "streak": "Niz",
+ "lessons": "Lekcije",
+ "accuracy": "Točnost",
+ "noPlan": "Nema aktivnog plana učenja.",
+ "startPlan": "Započni procjenu",
+ "competencies": "Znanja po jedinicama",
+ "vocabularySection": "Rječnik",
+ "skills": "Točnost po vještini",
+ "mastered": "Usvojeno",
+ "inProgress": "U tijeku",
+ "notStarted": "Nije započeto",
+ "loading": "Učitavanje napretka...",
+ "days": "dana",
+ "noData": "Dovrši nekoliko lekcija kako bi ovdje vidio svoj napredak.",
+ "exploreVocab": "Istraži zbirke riječi",
+ "competenciesSection": "Gramatička znanja za razinu {level}",
+ "noCompetencies": "Dovrši lekcije kako bi počeo pratiti svoja znanja.",
+ "goToMyPlan": "→ Idi na Moj plan",
+ "addToFlashcards": "Dodaj riječi iz zbirke u kartice kako bi pratio usvajanje pojedinih riječi.",
+ "noActivePlanDesc": "Dovrši procjenu kako bi počeo pratiti svoje vještine.",
+ "vocabularyHeader": "Rječnik razine {level}",
+ "currentLevelOnly": "Samo trenutačna razina",
+ "allLevels": "Sve razine"
+ },
+ "grammar": {
+ "title": "Gramatički priručnik",
+ "subtitle": "Priručnik",
+ "searchPlaceholder": "Pretraži gramatičke teme...",
+ "allLevels": "Sve razine",
+ "allCategories": "Sve",
+ "noResults": "Nema pronađenih tema.",
+ "structure": "Struktura",
+ "keyRules": "Glavna pravila",
+ "examples": "Primjeri",
+ "commonMistakes": "Česte pogreške",
+ "relatedTopics": "Povezane teme",
+ "backToGrammar": "Gramatički priručnik",
+ "explanation": "Objašnjenje",
+ "backLink": "Natrag na gramatiku",
+ "notFound": "Tema nije pronađena.",
+ "topicsFound": "{count, plural, one {Pronađena # tema} few {Pronađene # teme} other {Pronađeno # tema}}"
+ },
+ "vocabulary": {
+ "title": "Rječnik",
+ "subtitle": "Rječnik",
+ "all": "Sve",
+ "searchPlaceholder": "Pretraži zbirke riječi...",
+ "sets": "{count, plural, one {# zbirka} few {# zbirke} other {# zbirki}}",
+ "words": "riječi",
+ "addAll": "Dodaj svih {count} u kartice",
+ "adding": "Dodavanje...",
+ "added": "Nove kartice: {count}",
+ "addError": "Kartice nije bilo moguće dodati",
+ "frequency": "učestalost",
+ "backToVocabulary": "Rječnik",
+ "notFound": "Zbirka nije pronađena.",
+ "vocabularySet": "Zbirka riječi",
+ "cardsAdded": "Broj kartica dodanih u tvoj špil: {count}.",
+ "goToFlashcards": "Idi na kartice →",
+ "addFailed": "Kartice nije bilo moguće dodati.",
+ "noResults": "Nema zbirki riječi koje odgovaraju pretraživanju"
+ },
+ "phrasebook": {
+ "title": "Zbirka izraza",
+ "subtitle": "Izrazi",
+ "all": "Sve",
+ "allRegisters": "Svi registri",
+ "formal": "Formalno",
+ "neutral": "Neutralno",
+ "informal": "Neformalno",
+ "copied": "Kopirano!",
+ "copy": "Kopiraj",
+ "copyPhrase": "Kopiraj izraz",
+ "noResults": "Nema izraza koji odgovaraju tvojim filtrima",
+ "level": "Razina",
+ "register": "Registar",
+ "statsLine": "Situacije: {situationCount} · Izrazi: {phraseCount} · {range}",
+ "situationsShown": "{count, plural, one {Prikazana # situacija} few {Prikazane # situacije} other {Prikazano # situacija}}",
+ "searchPlaceholder": "Pretraži izraze..."
+ },
+ "admin": {
+ "title": "Administracija",
+ "overview": "Pregled",
+ "users": "Korisnici",
+ "feedback": "Povratne informacije",
+ "reviews": "Recenzije",
+ "system": "Sustav",
+ "invites": "Pozivnice",
+ "createUser": "Stvori korisnika",
+ "createInvite": "Stvori pozivnicu",
+ "deactivate": "Deaktiviraj",
+ "activate": "Aktiviraj",
+ "loading": "Učitavanje...",
+ "loadError": "Korisničke podatke nije bilo moguće učitati",
+ "userNotFound": "Korisnik nije pronađen",
+ "role": "Uloga",
+ "status": "Status",
+ "active": "Aktivan",
+ "inactive": "Neaktivan",
+ "inviteCreated": "Pozivnica je stvorena",
+ "copyLink": "Kopiraj poveznicu",
+ "inviteCopied": "Pozivnica je kopirana",
+ "deleteUser": "Izbriši korisnika",
+ "deleteUserMessage": "Izbrisati ovog korisnika? Ovu radnju nije moguće poništiti.",
+ "deleteConfirm": "Izbriši",
+ "noUsers": "Nema pronađenih korisnika",
+ "inviteLink": "Pozivnica (vrijedi 48 sati)",
+ "total": "ukupno",
+ "delete": "Izbriši",
+ "inviteBtn": "Pozivnica",
+ "createUserBtn": "Stvori korisnika",
+ "prevPage": "← Prethodna",
+ "nextPage": "Sljedeća →",
+ "roleUser": "Korisnik",
+ "roleAdmin": "Administrator",
+ "fieldUsername": "Korisničko ime",
+ "invalidUsernameChars": "Korisničko ime smije sadržavati samo slova, brojeve, crtice, podvlake i točke",
+ "fieldEmail": "Adresa e-pošte",
+ "fieldPassword": "Lozinka",
+ "fieldDisplayName": "Ime za prikaz",
+ "fieldNativeLanguage": "Materinski jezik",
+ "fieldTargetLanguage": "Jezik koji se uči",
+ "fieldRole": "Vrsta računa",
+ "submitCreate": "Stvori",
+ "searchPlaceholder": "Pretraži po korisničkom imenu ili e-pošti...",
+ "statsTitle": "Statistika upotrebe",
+ "statsCefr": "Trenutačna razina",
+ "statsUnit": "Trenutačna jedinica",
+ "statsPlanWeeks": "Trajanje plana",
+ "sectionXpProgress": "XP i napredak",
+ "statsXp": "Ukupni XP",
+ "statsStreak": "Niz",
+ "statsActiveDays": "Aktivni dani",
+ "statsLessons": "Dovršene lekcije",
+ "statsExercises": "Točno riješeni zadaci",
+ "statsMessages": "Poslane poruke",
+ "statsTokensTotal": "Potrošeni tokeni (ukupno)",
+ "statsTokensChat": "Tokeni učitelja",
+ "statsTokensConversation": "Glasovni tokeni",
+ "statsTokensNote": "Dostupno samo ako AI pružatelj pošalje te podatke",
+ "statsNoData": "Nema podataka o napretku",
+ "viewStats": "Pogledaj statistiku",
+ "weeks": "tjedana",
+ "statsTestScore": "Konačni rezultat testa",
+ "statsDays": "dana",
+ "sectionQuota": "Ograničenja razgovora",
+ "quotaWeeklySessions": "Tjedno ograničenje razgovora",
+ "quotaDailyMinutes": "Dnevno ograničenje minuta",
+ "quotaWeeklyMinutes": "Tjedno ograničenje minuta",
+ "quotaMonthlyTokens": "Mjesečno ograničenje tokena",
+ "quotaUnlimitedLabel": "Neograničeno (0)",
+ "quotaUsedSessions": "Iskorišteno ovaj tjedan",
+ "quotaUsedMinutes": "Iskorišteno danas",
+ "quotaUsedWeeklyMinutes": "Iskorišteno ovaj tjedan (min)",
+ "quotaSave": "Spremi ograničenja",
+ "quotaSaved": "Ograničenja su spremljena.",
+ "emailVerified": "E-pošta je potvrđena",
+ "emailNotVerified": "E-pošta nije potvrđena",
+ "verifyEmail": "Potvrdi e-poštu",
+ "unverifyEmail": "Označi kao nepotvrđenu",
+ "emailVerifiedSuccess": "Adresa e-pošte označena je kao potvrđena.",
+ "emailUnverifiedSuccess": "Adresa e-pošte označena je kao nepotvrđena.",
+ "fieldSubscription": "Pretplata",
+ "fieldEndsRenews": "Završava / Obnavlja se",
+ "fieldStripeCustomer": "Stripe korisnik",
+ "maintenanceTitle": "Način održavanja",
+ "maintenanceOn": "UKLJUČENO",
+ "maintenanceEnable": "Uključi održavanje",
+ "maintenanceDisable": "Isključi održavanje",
+ "maintenanceDesc": "Kada je uključen, nijedna funkcija koja zahtijeva pretplatu (chat s učiteljem, glasovni razgovori te vježbe slušanja i čitanja) nije dostupna korisnicima.",
+ "cannotDeactivateSelf": "Ne možeš deaktivirati vlastiti račun",
+ "cannotDeleteSelf": "Ne možeš izbrisati vlastiti račun",
+ "sectionLanguages": "Jezici",
+ "sectionActivity": "Aktivnost",
+ "statsActive": "Aktivan",
+ "manageUsers": "Upravljaj korisnicima",
+ "manageUsersDesc": "Pregledaj račune, pretplate, uloge i status pristupa.",
+ "createUserDesc": "Stvori upravljani korisnički račun s početnim jezikom za učenje.",
+ "reviewFeedback": "Pregledaj povratne informacije",
+ "reviewFeedbackDesc": "Obradi prijedloge značajki i prijave pogrešaka korisnika.",
+ "reviewReviews": "Pregledaj recenzije",
+ "reviewReviewsDesc": "Odobri provjerene recenzije korisnika za prikaz na početnoj stranici.",
+ "maintenanceOnDesc": "Način održavanja uključen je za alate za učenje koji zahtijevaju pretplatu.",
+ "maintenanceOffDesc": "Način održavanja je isključen. Alati za učenje koji zahtijevaju pretplatu dostupni su.",
+ "openSystemControls": "Postavke sustava",
+ "adminRequired": "Potreban je administratorski pristup",
+ "usersLoadError": "Korisnike nije bilo moguće učitati",
+ "createUserError": "Korisnika nije bilo moguće stvoriti",
+ "updateUserError": "Korisnika nije bilo moguće ažurirati",
+ "deleteUserError": "Korisnika nije bilo moguće izbrisati",
+ "inviteError": "Pozivnicu nije bilo moguće stvoriti",
+ "copyInviteError": "Pozivnicu nije bilo moguće kopirati",
+ "maintenanceError": "Način održavanja nije bilo moguće promijeniti",
+ "maintenanceOff": "ISKLJUČENO",
+ "inviteExpiry": "Jednokratna poveznica. Istječe za 48 sati.",
+ "searchAction": "Pretraži korisnike",
+ "roleFilter": "Filtriraj po ulozi",
+ "statusFilter": "Filtriraj po statusu",
+ "subscriptionFilter": "Filtriraj po pretplati",
+ "allRoles": "Sve uloge",
+ "allStatuses": "Svi statusi",
+ "allSubscriptions": "Sve pretplate",
+ "clearFilters": "Očisti",
+ "userColumn": "Korisnik",
+ "actions": "Radnje",
+ "createUserSheetDesc": "Račun se stvara odmah. Poruka za potvrdu e-pošte šalje se ako je e-pošta omogućena.",
+ "activateUser": "Aktiviraj korisnika",
+ "deactivateUser": "Deaktiviraj korisnika",
+ "activateUserMessage": "Aktivirati ovaj korisnički račun?",
+ "deactivateUserMessage": "Deaktivirati ovaj korisnički račun? Korisnik neće moći koristiti aplikaciju dok se račun ponovno ne aktivira.",
+ "tabProfile": "Profil",
+ "tabLanguages": "Jezici",
+ "tabActivity": "Aktivnost",
+ "tabQuota": "Ograničenja",
+ "tabSubscription": "Pretplata",
+ "sectionCommunication": "Komunikacija",
+ "quotaCurrentUsage": "Trenutačna upotreba",
+ "quotaConfiguredLimits": "Postavljena ograničenja",
+ "unitMinutes": "min",
+ "unitSessions": "razgovora",
+ "unitTokens": "tokena",
+ "quotaZeroMeansUnlimited": "Postavi ograničenje na 0 za neograničen pristup.",
+ "quotaValidationError": "Sva ograničenja moraju biti cijeli brojevi od 0 naviše.",
+ "quotaSaveError": "Ograničenja nije bilo moguće spremiti.",
+ "subscriptionOverride": "Ručno nadjačavanje pretplate",
+ "subscriptionOverrideDesc": "Time se izravno mijenja razdoblje pristupa korisnika i može se nadjačati stanje kojim upravlja Stripe.",
+ "subscriptionUpdateError": "Pretplatu nije bilo moguće ažurirati.",
+ "saving": "Spremanje...",
+ "verifyEmailConfirmMessage": "Označiti ovu adresu e-pošte kao potvrđenu?",
+ "unverifyEmailConfirmMessage": "Označiti ovu adresu e-pošte kao nepotvrđenu?",
+ "subscriptionConfirmTitle": "Promijeni pretplatu",
+ "subscriptionConfirmMessage": "Primijeniti plan pretplate: {plan}?",
+ "subscriptionConfirm": "Primijeni",
+ "feedbackStatusError": "Status povratnih informacija nije bilo moguće ažurirati.",
+ "feedbackDeleteError": "Zapis povratnih informacija nije bilo moguće izbrisati.",
+ "allFeedback": "Sve povratne informacije",
+ "feedbackTotal": "Ukupno povratnih informacija",
+ "feedbackQueue": "Red za povratne informacije",
+ "feedbackSearchPlaceholder": "Pretraži po naslovu, opisu ili autoru...",
+ "feedbackSearchAction": "Pretraži povratne informacije",
+ "feedbackTypeFilter": "Filtriraj po vrsti povratnih informacija",
+ "noFilteredFeedback": "Nema povratnih informacija koje odgovaraju trenutačnim filtrima.",
+ "feedbackItem": "Zapis",
+ "type": "Vrsta",
+ "signals": "Signali",
+ "feedbackStatusAction": "Promijeni status povratnih informacija",
+ "adminStatsError": "Administratorsku statistiku nije bilo moguće učitati.",
+ "activeUsers": "Aktivni korisnici",
+ "paidAccess": "Aktivno / probno",
+ "pendingFeedback": "Povratne informacije na čekanju",
+ "pendingReviews": "Recenzije koje čekaju odobrenje",
+ "operationalAlerts": "Operativna upozorenja",
+ "pendingBugs": "Prijave pogrešaka na čekanju",
+ "pendingBugsDesc": "Prijave pogrešaka koje čekaju pregled administratora.",
+ "pastDueSubscriptions": "Pretplate s dospjelim plaćanjem",
+ "pastDueSubscriptionsDesc": "Računi s problemima plaćanja koje možda treba pregledati.",
+ "dashboardBanner": {
+ "title": "Obavijest na nadzornoj ploči",
+ "description": "Stvori, prevedi, pregledaj i objavi obavijest na nadzornoj ploči svakog korisnika.",
+ "loading": "Učitavanje obavijesti...",
+ "loadError": "Obavijest na nadzornoj ploči nije bilo moguće učitati.",
+ "sourceLocale": "Izvorni jezik",
+ "activeLabel": "Objavi obavijest",
+ "activeHint": "Neaktivne se obavijesti spremaju, ali se ne prikazuju korisnicima.",
+ "fieldTitle": "Naslov",
+ "fieldSubtitle": "Podnaslov",
+ "fieldDescription": "Opis",
+ "translate": "Prevedi",
+ "translateSuccess": "Prijevodi su stvoreni. Pregledaj ih prije spremanja.",
+ "translateError": "Prevođenje nije uspjelo. Trenutačni sadržaj je sačuvan.",
+ "editTranslation": "Prijevod za uređivanje",
+ "pending": "na čekanju",
+ "completion": "{complete}/{total} jezika dovršeno",
+ "save": "Spremi obavijest",
+ "saveSuccess": "Obavijest na nadzornoj ploči je spremljena.",
+ "saveError": "Obavijest na nadzornoj ploči nije bilo moguće spremiti.",
+ "revision": "Inačica {revision}",
+ "updated": "Ažurirano {date}",
+ "locales": {
+ "en": "Engleski",
+ "es": "Španjolski",
+ "fr": "Francuski",
+ "pt": "Portugalski",
+ "de": "Njemački",
+ "it": "Talijanski",
+ "ru": "Ruski",
+ "nl": "Nizozemski",
+ "pl": "Poljski",
+ "ro": "Rumunjski",
+ "tr": "Turski",
+ "sv": "Švedski",
+ "da": "Danski",
+ "fi": "Finski",
+ "hr": "Hrvatski"
+ }
+ }
+ },
+ "faq": {
+ "title": "Česta pitanja",
+ "subtitle": "Često postavljana pitanja i odgovori",
+ "q_start": "Odakle početi?",
+ "a_start": "Počni s procjenom razine . Ona utvrđuje tvoju razinu engleskog (A1–C2) te tvoje prednosti i slabosti. Na temelju rezultata možeš stvoriti osobni plan učenja koji ispunjava nadzornu ploču dnevnim lekcijama. Bez procjene nadzorna ploča ostaje prazna.",
+ "q_workflow": "Kako izgleda cijeli postupak?",
+ "workflowStep1": "Registriraj se i odaberi materinski jezik",
+ "workflowStep2": "Riješi procjenu → utvrdit će se tvoja CEFR razina",
+ "workflowStep3": "Stvori plan učenja s nadzorne ploče",
+ "workflowStep4": "Dovrši dnevne lekcije (gumb ZAPOČNI na nadzornoj ploči)",
+ "workflowStep5": "Istodobno vježbaj rječnik karticama",
+ "workflowStep6": "Vježbaj slobodan razgovor s AI učiteljem",
+ "q_language": "Koji ću jezik učiti?",
+ "a_language": "FreeLingo trenutačno podržava više jezika (američki engleski, britanski engleski, francuski, njemački, talijanski, japanski, korejski, kineski koji se govori u kontinentalnoj Kini, portugalski i španjolski), a s vremenom ćemo dodati još jezika. Kad novi jezik postane dostupan, pronaći ćeš ga pod Postavke → Moji jezici – dodaj ga i odmah počni učiti.",
+ "q_assessment": "Što je procjena razine?",
+ "a_assessment": "Test od 15 pitanja koji stvara AI, a obuhvaća gramatiku, rječnik, razumijevanje pročitanog i preoblikovanje rečenica, od razine A1 do C1. Kad ga dovršiš, dobit ćeš svoju CEFR razinu, rezultat i analizu prednosti i slabosti. Rezultat se koristi u planu učenja i stvorenim karticama.",
+ "q_studyPlan": "Što je plan učenja i kako ga stvaram?",
+ "a_studyPlan": "Nakon procjene idi na nadzornu ploču i klikni Stvori plan učenja . Automatski nastaje tjedni plan s dnevnim lekcijama prilagođenima tvojoj CEFR razini i ciljevima. Svaka lekcija sadrži pitanja s višestrukim odabirom i zadatke slobodnog pisanja koje procjenjuje AI.",
+ "q_resources": "Što su Materijali?",
+ "a_resources": "Odjeljak Materijali okuplja sadržaj koji ne ovisi o tvojem planu učenja: gramatiku po razinama, zbirke riječi i korisne izraze. Koristi ga kad god želiš ponavljati, potražiti objašnjenje ili proširiti rječnik bez čekanja na dnevnu lekciju.",
+ "q_flashcards": "Čemu služe kartice?",
+ "a_flashcards": "Sustav za učenje riječi uz algoritam ponavljanja u razmacima SM-2 . Stvori kartice za bilo koju temu (npr. \"posao\", \"frazni glagoli\" ili \"hrana\") s definicijom, primjerom rečenice i prijevodom na tvoj materinski jezik. Sustav računa kada ponovno prikazati karticu, neposredno prije nego što je zaboraviš. Potpuno je neovisan o planu učenja.",
+ "q_vocabulary": "Kako dodajem riječi u svoj rječnik?",
+ "a_vocabulary": "Riječi možeš spremiti na dva načina: (1) Dok čitaš lekciju, rješavaš vježbu slušanja ili razgovaraš s učiteljem, označi riječ mišem i klikni Spremi u izborniku koji se prikaže. AI će stvoriti definiciju, primjer rečenice i prijevod na tvoj materinski jezik. (2) Idi u Rječnik , otvori zbirku riječi za svoju razinu i klikni Dodaj sve za uvoz cijele zbirke. Spremljene riječi prikazuju se u Mojem rječniku među karticama.",
+ "q_tutor": "Kako radi AI učitelj?",
+ "a_tutor": "Slobodan chat u kojem se odgovori prikazuju tijekom stvaranja. Učitelj zna tvoju CEFR razinu i slabe točke te prilagođava odgovore. Pitaj o gramatici, zatraži objašnjenje, vježbaj pisanje na engleskom ili jednostavno razgovaraj. Razgovori se spremaju i dostupni su u bočnoj traci između sesija.",
+ "q_voice": "Kako funkcionira glasovni razgovor?",
+ "a_voice": "U načinu Glasovni razgovor možeš razgovarati s AI učiteljem u stvarnom vremenu. Pritisni Započni – aplikacija automatski sluša tvoj glas prepoznavanjem govorne aktivnosti (VAD), a učitelj odgovara sintetiziranim govorom. Učitelja možeš prekinuti u bilo kojem trenutku. Sesija završava kada istekne vrijeme ili pritisneš Zaustavi. Dopusti pregledniku pristup mikrofonu.",
+ "q_listening": "Kako funkcioniraju vježbe slušanja?",
+ "a_listening": "Odjeljak Slušanje stvara kratke zvučne ulomke na tvojoj CEFR razini, pročitane glasom AI-ja. Poslušaj i odgovori na pet pitanja za provjeru razumijevanja. Nakon slanja dobit ćeš rezultat (0–5), XP, točne odgovore i cijeli prijepis. Nove vježbe možeš stvarati kad poželiš.",
+ "q_reading": "Kako funkcioniraju vježbe čitanja?",
+ "a_reading": "Odjeljak Čitanje stvara tekstove na tvojoj CEFR razini – obavijesti, e-poruke, članke, objave na blogu i drugo. Pročitaj tekst i odgovori na pet pitanja s višestrukim odabirom. Nakon slanja dobit ćeš rezultat (0–5) i XP. Nove vježbe možeš stvarati kad poželiš.",
+ "q_providers": "Koje AI pružatelje FreeLingo podržava?",
+ "a_providers_intro": "Postavlja se u datoteci .env putem LLM_PROVIDER:",
+ "provider_ollama": "Lokalno i besplatno. Ollama mora biti pokrenuta na poslužitelju. Preporučeni model: gemma4:e4b. Bez GPU-a može biti sporo.",
+ "provider_openai": "OpenAI API (GPT-4o itd.). Brz i pouzdan. Potreban je OPENAI_API_KEY.",
+ "provider_anthropic": "Anthropic Claude. Potreban je ANTHROPIC_API_KEY.",
+ "provider_deepseek": "DeepSeek API. Potreban je DEEPSEEK_API_KEY.",
+ "q_invite": "Mogu li pozvati druge?",
+ "a_invite": "Da. U odjeljku Administracija → Korisnici administrator može izravno stvarati korisnike ili stvarati jednokratne pozivnice koje istječu nakon 48 sati. Javna registracija zadano je isključena (ALLOW_REGISTRATION=false).",
+ "q_password": "Kako mijenjam lozinku ili materinski jezik?",
+ "a_password": "Idi u Postavke . Ondje možeš promijeniti ime za prikaz, materinski jezik i lozinku.",
+ "q_uiLanguage": "Kako mijenjam jezik platforme?",
+ "a_uiLanguage": "Idi u Postavke i u odjeljku Profil pronađi izbornik Jezik sučelja . Odaberi jezik i cijelo sučelje odmah će se promijeniti.",
+ "q_feedback": "Kako predložiti novu značajku ili prijaviti pogrešku?",
+ "a_feedback": "Upotrijebi odjeljak Povratne informacije u bočnoj traci. Možeš predložiti novu značajku ili prijaviti pogrešku. Glasaj za ideje drugih korisnika, ostavljaj komentare i prati status svakog zahtjeva. Svi su podnesci javni, a administrator ih pregledava."
+ },
+ "cookieBanner": {
+ "message": "Koristimo samo nužne kolačiće kako bi ostao prijavljen. Ne koristimo kolačiće za praćenje ni oglašavanje.",
+ "learnMore": "Pravila privatnosti",
+ "accept": "Razumijem"
+ },
+ "error": {
+ "label": "Neočekivana pogreška",
+ "title": "Nešto je pošlo po zlu",
+ "body": "Dogodila se neočekivana pogreška. Pokušaj ponovno ili se vrati na nadzornu ploču.",
+ "retry": "Pokušaj ponovno",
+ "dashboard": "Idi na nadzornu ploču",
+ "digest": "Oznaka pogreške"
+ },
+ "notFound": {
+ "title": "Stranica nije pronađena",
+ "body": "Stranica koju tražiš ne postoji ili je premještena.",
+ "dashboard": "Idi na nadzornu ploču",
+ "home": "Početna"
+ },
+ "errors": {
+ "notFound": "Stranica nije pronađena",
+ "serverError": "Nešto je pošlo po zlu",
+ "unauthorized": "Prijavi se",
+ "backHome": "Natrag na početnu"
+ },
+ "conversation": {
+ "title": "Razgovor",
+ "subtitle": "Govorna vježba",
+ "start": "Započni razgovor",
+ "stop": "Zaustavi",
+ "startNew": "Započni novi razgovor",
+ "statusLoading": "Učitavanje glasovnog modela...",
+ "statusReady": "Spremno",
+ "statusWarming": "Učitavanje modela...",
+ "statusConnecting": "Povezivanje...",
+ "statusListening": "Slušanje...",
+ "statusDetecting": "Prepoznavanje govora...",
+ "statusSpeaking": "Govori...",
+ "statusError": "Pogreška",
+ "errorServicesDisabled": "Glasovne usluge nisu omogućene na ovom poslužitelju.",
+ "errorConnection": "Pogreška veze. Pokušaj ponovno.",
+ "errorTranscription": "Nije moguće pretvoriti tvoj govor u tekst. Pokušaj ponovno govoriti.",
+ "errorResponse": "Lingu nije mogao odgovoriti. Pokušaj ponovno.",
+ "errorSpeech": "Nije moguće pretvoriti Linguov odgovor u govor. Pokušaj ponovno.",
+ "errorUnauthorized": "Sesija je istekla. Osvježi stranicu.",
+ "errorMic": "Pristup mikrofonu je odbijen.",
+ "errorVadInit": "Prepoznavanje govora nije bilo moguće pokrenuti. Provjeri podržava li tvoj preglednik WebAssembly.",
+ "warningTimeout": "Razgovor završava za {seconds} s",
+ "sessionEnded": "Razgovor je završio",
+ "tapToStart": "Govori kad budeš spreman",
+ "you": "Ti",
+ "assistant": "Lingu",
+ "quotaSessions": "Razgovori ovaj tjedan",
+ "quotaMinutes": "Minute danas",
+ "quotaUnlimited": "Neograničeno",
+ "quotaExceededSessions": "Iskoristio si sve razgovore za ovaj tjedan.",
+ "quotaExceededTime": "Iskoristio si sve dnevno vrijeme za vježbu.",
+ "quotaExceededTokens": "Iskoristio si sve mjesečne AI tokene.",
+ "freemiumVoiceRemaining": "Besplatni glasovni razgovori: {remaining}/{limit} min ovaj tjedan",
+ "quotaTokens": "Tokeni ovaj mjesec",
+ "memoryUpdated": "Uspomena je ažurirana",
+ "trialBanner": "Besplatan razgovor za procjenu · {minutes} min",
+ "trialCtaLabel": "Nastavi razgovarati",
+ "trialCtaTitle": "Nastavi vježbati s Linguom",
+ "trialCtaDesc": "Otključaj pune glasovne razgovore, chat s učiteljem te vježbe slušanja i čitanja uz Premium.",
+ "startersHint": "Ili započni temom",
+ "starters": [
+ "Umjetnost i kreativnost",
+ "Knjige i čitanje",
+ "Sjećanja iz djetinjstva",
+ "Grad ili selo",
+ "Kuhanje i recepti",
+ "Snovi i ambicije",
+ "Obitelj i odnosi",
+ "Moda i stil",
+ "Omiljeni filmovi",
+ "Festivali i tradicije",
+ "Hrana iz cijeloga svijeta",
+ "Zdravlje i dobrobit",
+ "Hobiji i rukotvorine",
+ "Učenje jezika",
+ "Učenje novih vještina",
+ "Novac i proračun",
+ "Glazba i koncerti",
+ "Kućni ljubimci i životinje",
+ "Javni prijevoz",
+ "Znanost i svemir",
+ "Kupovne navike",
+ "Društvene mreže",
+ "Sport i tjelovježba",
+ "Tehnologija i uređaji",
+ "Okoliš",
+ "Putna odredišta",
+ "TV emisije i serije",
+ "Planovi za vikend",
+ "Posao i ciljevi u karijeri",
+ "Rad od kuće"
+ ]
+ },
+ "voiceRecorder": {
+ "record": "Snimi",
+ "stop": "Zaustavi",
+ "processing": "Obrada",
+ "error": "Pogreška",
+ "ariaRecord": "Započni snimanje",
+ "ariaStop": "Zaustavi snimanje"
+ },
+ "audioPlayer": {
+ "listen": "Poslušaj",
+ "stop": "Zaustavi",
+ "ariaListen": "Poslušaj izgovor",
+ "ariaStop": "Zaustavi zvuk"
+ },
+ "languages": {
+ "en": "Engleski",
+ "es": "Španjolski",
+ "fr": "Francuski",
+ "pt": "Portugalski",
+ "de": "Njemački",
+ "it": "Talijanski",
+ "pl": "Poljski",
+ "nl": "Nizozemski",
+ "ro": "Rumunjski",
+ "ru": "Ruski",
+ "tr": "Turski",
+ "sv": "Švedski",
+ "da": "Danski",
+ "fi": "Finski",
+ "hr": "Hrvatski",
+ "myLanguages": "Moji jezici",
+ "addLanguage": "Dodaj novi jezik",
+ "selectLanguage": "Odaberi jezik koji želiš učiti",
+ "activeLanguage": "Aktivan",
+ "switchTo": "Prebaci se na ovaj",
+ "switching": "Promjena jezika: {language}...",
+ "switched": "Jezik je promijenjen: {language} ({level})",
+ "removeLanguage": "Ukloni jezik",
+ "removeConfirmTitle": "Ukloniti jezik {language}?",
+ "removeConfirmMessage": "Sav napredak, lekcije, kartice i razgovori povezani s ovim jezikom bit će trajno izbrisani. Uspomene računa ostat će sačuvane. Ovu radnju nije moguće poništiti.",
+ "removeConfirmButton": "Ukloni",
+ "deleteError": "Jezik {language} nije bilo moguće ukloniti. Pokušaj ponovno.",
+ "noLanguages": "Nemaš dodanih jezika.",
+ "progressLabel": "Napredak",
+ "levelLabel": "Razina",
+ "xpLabel": "Ukupni XP",
+ "streakLabel": "Niz",
+ "lessonsLabel": "Lekcije",
+ "flashcardsLabel": "Kartice",
+ "viewDetails": "Pogledaj detalje",
+ "supportedLanguages": "Dostupni jezici"
+ },
+ "targetLanguages": {
+ "en-US": "Američki engleski",
+ "en-GB": "Britanski engleski",
+ "es-ES": "Španjolski",
+ "it-IT": "Talijanski",
+ "pt-PT": "Portugalski",
+ "fr-FR": "Francuski",
+ "de-DE": "Njemački",
+ "ja-JP": "Japanski",
+ "ko-KR": "Korejski",
+ "zh-CN": "Kineski (kontinentalna Kina)",
+ "en": "Engleski",
+ "es": "Španjolski",
+ "it": "Talijanski",
+ "pt": "Portugalski",
+ "fr": "Francuski",
+ "de": "Njemački",
+ "ja": "Japanski",
+ "ko": "Korejski",
+ "zh": "Kineski",
+ "es-ES-description": "Španjolski kakav se govori u Španjolskoj.",
+ "it-IT-description": "Talijanski kakav se govori u Italiji.",
+ "pt-PT-description": "Portugalski kakav se govori u Portugalu.",
+ "en-US-description": "Engleski kakav se govori u SAD-u.",
+ "en-GB-description": "Engleski kakav se govori u Ujedinjenoj Kraljevini.",
+ "fr-FR-description": "Francuski kakav se govori u Francuskoj.",
+ "de-DE-description": "Njemački kakav se govori u Njemačkoj.",
+ "ja-JP-description": "Japanski kakav se govori u Japanu.",
+ "ko-KR-description": "Korejski kakav se govori u Južnoj Koreji.",
+ "zh-CN-description": "Mandarinski kineski kakav se govori u kontinentalnoj Kini, s pojednostavljenim znakovima."
+ },
+ "onboarding": {
+ "title": "Odaberi svoj jezik",
+ "subtitle": "Koji jezik želiš učiti?",
+ "chooseVariant": "Odaberi inačicu",
+ "continue": "Nastavi",
+ "saveFailed": "Postavke nije bilo moguće spremiti",
+ "goals": {
+ "title": "Tvoji ciljevi učenja",
+ "subtitle": "Jezik: {language}. Za što ga želiš koristiti? Odaberi sve što vrijedi.",
+ "skip": "Preskoči",
+ "travel": "Putovanja i turizam",
+ "work": "Posao i karijera",
+ "academic": "Studij i obrazovanje",
+ "daily": "Svakodnevni razgovor",
+ "media": "Filmovi, glazba i serije",
+ "emigration": "Život u inozemstvu",
+ "exams": "Ispiti i certifikati",
+ "social": "Druženje"
+ },
+ "trialHeadline": "Besplatno probno razdoblje",
+ "trialHeadlineTrialUsed": "Premium",
+ "trialTitle": "Počni s {days} besplatnih dana",
+ "trialTitleTrialUsed": "Aktiviraj FreeLingo Premium",
+ "trialDesc": "Dobij puni pristup Lingu, govornim vježbama, slušanju i čitanju prije prve uplate.",
+ "trialDescTrialUsed": "Odaberi plan kako bi otključao Lingu, govorne vježbe, slušanje i čitanje.",
+ "trialCtaMonthly": "Isprobaj besplatno, zatim €{price}/mjesečno",
+ "trialCtaMonthlyTrialUsed": "Fleksibilni mjesečni plan · €{price}/mjesečno",
+ "trialCtaYearly": "Isprobaj besplatno, zatim €{price}/godišnje · 2 mjeseca gratis",
+ "trialCtaYearlyTrialUsed": "Najbolja vrijednost · €{price}/godišnje · 2 mjeseca gratis",
+ "trialSkip": "Nastavi s besplatnim značajkama",
+ "trialNoCharge": "Danas nema naplate. Kartica je potrebna, otkaži kad god želiš.",
+ "trialNoChargeTrialUsed": "Provjeri iznos prije pretplate. Otkaži kad god želiš.",
+ "trialError": "Plaćanje nije bilo moguće pokrenuti. Pokušaj ponovno.",
+ "freemiumTrialTitle": "Tvoje besplatno probno razdoblje je počelo",
+ "freemiumTrialDesc": "Koristi sve značajke bez ograničenja sedam dana. Kartica nije potrebna. Na Premium možeš prijeći kad god želiš na nadzornoj ploči.",
+ "freemiumChatLabel": "Chat s AI učiteljem",
+ "freemiumVoiceLabel": "Glasovni razgovori",
+ "freemiumListeningLabel": "Vježbe slušanja",
+ "freemiumReadingLabel": "Vježbe čitanja",
+ "goToDashboard": "Idi na nadzornu ploču",
+ "newLanguageHeadline": "Koji novi jezik želiš učiti?",
+ "newLanguageSubtitle": "Za taj jezik stvorit će se novi plan učenja."
+ },
+ "tour": {
+ "skip": "Preskoči",
+ "prev": "Natrag",
+ "next": "Dalje",
+ "done": "Započni",
+ "step1": {
+ "label": "Dobrodošli",
+ "title": "Dobrodošli u FreeLingo",
+ "desc": "FreeLingo je tvoj AI učitelj jezika. Vježbaj svojim tempom, kad god želiš, uz razgovore, lekcije i pametne kartice."
+ },
+ "step2": {
+ "label": "Tekstualni chat",
+ "title": "Razgovaraj sa svojim učiteljem",
+ "desc": "Razgovaraj pisanjem s AI učiteljem. Postavljaj pitanja, dobivaj ispravke i vježbaj gramatiku i rječnik na prirodan način."
+ },
+ "step3": {
+ "label": "Glas",
+ "title": "Govori u stvarnom vremenu",
+ "desc": "U glasovnim razgovorima vježbaj govor uz prijepis u stvarnom vremenu i trenutne povratne informacije o izgovoru."
+ },
+ "step4": {
+ "label": "Kartice",
+ "title": "Ne zaboravi nijednu riječ",
+ "desc": "Kartice koriste SM-2 algoritam ponavljanja u razmacima kako bi ponovio riječi upravo kad ih počinješ zaboravljati – za dugotrajnije pamćenje."
+ },
+ "step5": {
+ "label": "Plan učenja",
+ "title": "Tvoj osobni plan",
+ "desc": "Na temelju procjene razine FreeLingo stvara strukturirani plan s dnevnim lekcijama prilagođenima tvojim ciljevima i napretku."
+ },
+ "step6": {
+ "label": "Vježba",
+ "title": "Slušanje i čitanje",
+ "desc": "Provjeri razumijevanje uz vježbe koje stvara AI. Poslušaj ulomak i odgovori na pet pitanja ili pročitaj tekst na svojoj CEFR razini – osvoji XP za svaki točan odgovor."
+ },
+ "step7": {
+ "label": "Spremno",
+ "title": "Sve je spremno!",
+ "desc": "Započni današnjom lekcijom, otvori chat ili vježbaj slušanje i čitanje – FreeLingo se prilagođava tvojem načinu učenja."
+ }
+ },
+ "billing": {
+ "paywallLabel": "Potreban je Premium",
+ "paywallTitle": "Otključaj FreeLingo Premium",
+ "paywallDesc": "Započni besplatno probno razdoblje od {days} dana i otključaj sve Premium vježbe prije prve uplate.",
+ "paywallDescTrialUsed": "Otključaj sve Premium vježbe i provjeri iznos prije potvrde.",
+ "paywallChatTitle": "Vježbaj s Linguom bez ograničenja",
+ "paywallChatDesc": "Postavljaj pitanja, dobivaj ispravke i prirodno razgovaraj pisanjem sa svojim AI učiteljem.",
+ "paywallLessonsTitle": "Dovrši neograničeno mnogo lekcija",
+ "paywallLessonsDesc": "Gramatika, rječnik, čitanje i pisanje – svaki dan dovrši onoliko lekcija koliko želiš.",
+ "paywallConversationTitle": "Razgovaraj s Linguom u stvarnom vremenu",
+ "paywallConversationDesc": "Vježbaj govor uz prijepis, brze odgovore i povratne informacije za veću tečnost.",
+ "paywallListeningTitle": "Vježbaj sluh na svojoj razini",
+ "paywallListeningDesc": "Slušaj AI vježbe prilagođene svojem napretku i odmah provjeri razumijevanje.",
+ "paywallReadingTitle": "Čitaj tekstove prilagođene svojem napretku",
+ "paywallReadingDesc": "Poboljšaj razumijevanje pročitanog uz tekstove primjerene razini, pitanja i trenutačne povratne informacije.",
+ "paywallNoCharge": "Danas nema naplate · Kartica je potrebna · Otkaži kad god želiš",
+ "paywallNoChargeTrialUsed": "Sigurno plaćanje · Otkaži kad god želiš",
+ "paywallSkip": "Ne sada",
+ "trialDays": "{days, plural, one {# dan probnog razdoblja} few {# dana probnog razdoblja} other {# dana probnog razdoblja}}",
+ "planMonthly": "Fleksibilni mjesečni plan · €{price}/mjesečno →",
+ "planYearly": "Najbolja vrijednost · €{price}/godišnje · 2 mjeseca gratis →",
+ "checkoutError": "Plaćanje nije bilo moguće pokrenuti. Pokušaj ponovno.",
+ "pricingTitle": "Jednostavne, jasne cijene",
+ "pricingDesc": "{days} besplatnih dana · Danas nema naplate · Otkaži kad god želiš",
+ "pricingDescTrialUsed": "Odaberi mjesečni ili godišnji plan · Otkaži kad god želiš",
+ "planMonthlyName": "Mjesečno",
+ "planYearlyName": "Godišnje",
+ "bestValue": "Najbolja vrijednost",
+ "month": "mjesec",
+ "year": "godina",
+ "planFeature": {
+ "feature1": "Neograničen pristup – bez dnevnih ili tjednih ograničenja",
+ "feature2": "2 mjeseca gratis"
+ },
+ "planFreeName": "Besplatno",
+ "trialBadge": "Besplatno probno razdoblje",
+ "trialBadgeTrialUsed": "Premium",
+ "ctaRegister": "Započni besplatno probno razdoblje",
+ "ctaRegisterTrialUsed": "Odaberi plan",
+ "freeFeature": {
+ "f1": "Test i procjena razine",
+ "f2": "Lekcije i plan učenja",
+ "f3": "Materijali za gramatiku i rječnik",
+ "f4": "Nadzorna ploča i praćenje napretka",
+ "f5": "Kartice i ponavljanje u razmacima",
+ "l1": "Chat s AI učiteljem",
+ "l2": "Glasovni razgovori",
+ "l3": "Vježbe slušanja",
+ "l4": "Vježbe čitanja",
+ "l5": "Sedmodnevno probno razdoblje s punim pristupom"
+ },
+ "successLabel": "Plaćanje je potvrđeno",
+ "successTitle": "Dobrodošli u FreeLingo Premium!",
+ "successDesc": "Tvoja pretplata sada je aktivna. Uživaj u neograničenom pristupu svim značajkama.",
+ "successCheckingLabel": "Potvrda pretplate",
+ "successCheckingTitle": "Provjeravamo tvoju pretplatu",
+ "successCheckingDesc": "To obično traje nekoliko sekundi.",
+ "successPendingLabel": "Skoro gotovo",
+ "successPendingTitle": "Tvoja se pretplata potvrđuje",
+ "successPendingDesc": "Uskoro bi trebao dobiti Premium pristup. Ako ga ne dobiješ, provjeri postavke plaćanja.",
+ "successErrorLabel": "Potvrda na čekanju",
+ "successErrorTitle": "Još nismo mogli potvrditi tvoju pretplatu",
+ "successErrorDesc": "Idi na nadzornu ploču i osvježi je za trenutak ili provjeri postavke plaćanja.",
+ "successRedirect": "Preusmjeravanje na nadzornu ploču za {seconds} s...",
+ "successCta": "Idi na nadzornu ploču",
+ "canceledLabel": "Plaćanje je otkazano",
+ "canceledTitle": "Ništa nije naplaćeno",
+ "canceledDesc": "Ništa nije naplaćeno. Tvoj je napredak sačuvan, a plan možeš odabrati kasnije u Postavkama.",
+ "canceledCtaDashboard": "Idi na nadzornu ploču",
+ "canceledCtaSettings": "Pogledaj planove",
+ "canceledNoCharge": "Ništa nije naplaćeno · Odaberi plan kad god želiš",
+ "section": "Pretplata",
+ "status": "Status",
+ "statusActive": "Aktivna",
+ "statusCanceling": "U postupku otkazivanja",
+ "statusCanceled": "Otkazana",
+ "statusTrialing": "Probno razdoblje",
+ "statusPastDue": "Dospjelo plaćanje",
+ "statusUnpaid": "Neplaćeno",
+ "statusPaused": "Pauzirano",
+ "statusIncomplete": "Plaćanje na čekanju",
+ "statusIncompleteExpired": "Plaćanje je isteklo",
+ "statusNone": "Nema pretplate",
+ "nextBilling": "Sljedeća naplata",
+ "accessUntil": "Pristup do",
+ "manage": "Upravljaj pretplatom",
+ "updatePayment": "Ažuriraj način plaćanja",
+ "pastDueTitle": "Plaćanje na čekanju",
+ "pastDueDesc": "Ažuriraj način plaćanja kako bi vratio Premium pristup.",
+ "subscribe": "Pretplati se",
+ "portalError": "Portal za plaćanje nije bilo moguće otvoriti. Pokušaj ponovno.",
+ "openSourceTitle": "Otvoreni kod",
+ "openSourceDesc": "Besplatno hostaj na svojem poslužitelju",
+ "openSourceCta": "Pogledaj na GitHubu",
+ "planFreeBadge": "Zauvijek besplatno",
+ "priceAmount": "€{amount}",
+ "pricePerPeriod": "/ {period}",
+ "priceOriginal": "€{price} / {period}",
+ "planFreeDesc": "Sedmodnevno probno razdoblje s punim pristupom, zatim besplatno uz dnevna i tjedna ograničenja",
+ "planFreeCta": "Započni",
+ "limitedLabel": "Ograničeno"
+ },
+ "freemium": {
+ "chatLabel": "Chat",
+ "lessonsLabel": "Lekcije",
+ "listeningLabel": "Slušanje",
+ "readingLabel": "Čitanje",
+ "voiceLabel": "Glas (min)",
+ "freeLimit": "Uzmi Premium",
+ "trialDaysLeft": "{days, plural, one {Preostao je # dan besplatnog probnog razdoblja} few {Preostala su # dana besplatnog probnog razdoblja} other {Preostalo je # dana besplatnog probnog razdoblja}}",
+ "trialUnlimited": "Neograničen pristup",
+ "requiresSubscription": "Samo za Premium"
+ },
+ "whatsNew": {
+ "title": "Novosti",
+ "version": "v1.9.25",
+ "cta": "Razumijem",
+ "entry1": {
+ "label": "Pet novih jezika sučelja",
+ "desc": "FreeLingo je dostupan na danskom, finskom, hrvatskom, švedskom i turskom. Odaberi jezik sučelja u Postavkama."
+ },
+ "entry2": {
+ "label": "Ugodnije iskustvo",
+ "desc": "Ispravili smo i manje pogreške te doradili pojedinosti kako bi svakodnevno učenje uz FreeLingo bilo ugodnije."
+ }
+ },
+ "contact": {
+ "title": "Kontakt",
+ "labelEmail": "Adresa e-pošte",
+ "placeholderEmail": "tvoja@e-posta.hr",
+ "labelSubject": "Predmet",
+ "placeholderSubject": "Kratak opis tvojeg pitanja",
+ "labelDescription": "Poruka",
+ "placeholderDescription": "Opiši svoje pitanje ili povratnu informaciju...",
+ "send": "Pošalji",
+ "sending": "Slanje...",
+ "sent": "Poruka je poslana",
+ "errorGeneric": "Slanje nije uspjelo. Pokušaj ponovno."
+ },
+ "listening": {
+ "title": "Slušanje",
+ "topic": "Tema",
+ "listenLabel": "Poslušaj",
+ "questionsLabel": "Pitanja",
+ "submit": "Pošalji odgovore",
+ "generating": "Stvaranje vježbe...",
+ "generatingDesc": "AI stvara novu vježbu. To obično traje oko 30 sekundi.",
+ "generatingLong": "Traje dulje nego što smo očekivali...",
+ "noExercises": "Nema vježbi dostupnih za tvoju razinu.",
+ "generate": "Stvori vježbu",
+ "audioError": "Zvuk nije bilo moguće učitati",
+ "alreadyAttempted": "Već si dovršio ovu vježbu.",
+ "errorLoading": "Vježbu nije bilo moguće učitati. Pokušaj ponovno.",
+ "errorSubmit": "Odgovore nije bilo moguće poslati. Pokušaj ponovno.",
+ "resultsLabel": "Rezultati",
+ "transcript": "Prijepis",
+ "review": "Pregled odgovora",
+ "nextExercise": "Sljedeća vježba",
+ "viewHistory": "Povijest",
+ "history": "Povijest",
+ "historyTitle": "Povijest vježbi slušanja",
+ "historyEmpty": "Još nema dovršenih vježbi.",
+ "practiceMore": "Vježbaj",
+ "moreInHistory": "+{count} više",
+ "practiceAgain": "Vježbaj ponovno",
+ "replayNoXp": "Ponovi (bez XP-a)"
+ },
+ "reading": {
+ "title": "Čitanje",
+ "topic": "Tema",
+ "textLabel": "Tekst",
+ "questionsLabel": "Pitanja",
+ "submit": "Pošalji odgovore",
+ "generating": "Stvaranje vježbe...",
+ "generatingDesc": "AI stvara novu vježbu. To obično traje oko 30 sekundi.",
+ "generatingLong": "Traje dulje nego što smo očekivali...",
+ "noExercises": "Nema vježbi dostupnih za tvoju razinu.",
+ "generate": "Stvori vježbu",
+ "alreadyAttempted": "Već si dovršio ovu vježbu.",
+ "errorLoading": "Vježbu nije bilo moguće učitati. Pokušaj ponovno.",
+ "errorSubmit": "Odgovore nije bilo moguće poslati. Pokušaj ponovno.",
+ "resultsLabel": "Rezultati",
+ "review": "Pregled odgovora",
+ "nextExercise": "Sljedeća vježba",
+ "viewHistory": "Povijest",
+ "history": "Povijest",
+ "historyTitle": "Povijest vježbi čitanja",
+ "historyEmpty": "Još nema dovršenih vježbi.",
+ "practiceMore": "Vježbaj",
+ "moreInHistory": "+{count} više",
+ "practiceAgain": "Vježbaj ponovno",
+ "replayNoXp": "Ponovi (bez XP-a)",
+ "selectWordHint": "Označi riječ kako bi je spremio u svoj rječnik"
+ },
+ "feedback": {
+ "title": "Povratne informacije",
+ "subtitle": "Prijedlozi značajki i prijave pogrešaka",
+ "tabFeatures": "Prijedlozi",
+ "tabBugs": "Prijave pogrešaka",
+ "newFeature": "+ Novi prijedlog",
+ "newBug": "+ Prijavi pogrešku",
+ "sortBy": "Poredaj po",
+ "sortVotes": "Najviše glasova",
+ "sortDate": "Najnovije",
+ "filterStatus": "Status",
+ "filterAll": "Sve",
+ "noEntries": "Još nema objava. Budi prvi koji će nešto poslati.",
+ "votes": "{count} glasova",
+ "vote": "1 glas",
+ "voteAction": "Glasaj",
+ "removeVoteAction": "Poništi glas",
+ "comments": "{count, plural, one {# komentar} few {# komentara} other {# komentara}}",
+ "comment": "1 komentar",
+ "by": "autor",
+ "statusPending": "Na čekanju",
+ "statusPlanned": "Planirano",
+ "statusInProgress": "U tijeku",
+ "statusDone": "Dovršeno",
+ "statusDeclined": "Odbijeno",
+ "unread": "Nepročitano",
+ "modalCreateTitleFeature": "Novi prijedlog",
+ "modalCreateTitleBug": "Prijavi pogrešku",
+ "labelTitle": "Naslov",
+ "placeholderTitleFeature": "Opiši svoj prijedlog u jednoj rečenici",
+ "placeholderTitleBug": "Kratak opis pogreške",
+ "labelDescription": "Pojedinosti",
+ "placeholderDescriptionFeature": "Objasni značajku i zašto bi bila korisna",
+ "placeholderDescriptionBug": "Koraci za ponavljanje, očekivani rezultat i što se dogodilo",
+ "submit": "Pošalji",
+ "submitting": "Slanje...",
+ "cancel": "Odustani",
+ "deleteEntry": "Izbriši",
+ "deleteEntryConfirmTitle": "Izbriši objavu",
+ "deleteEntryConfirmMessage": "Želiš li zaista izbrisati ovu objavu? Ovu radnju nije moguće poništiti.",
+ "deleteEntryConfirm": "Izbriši",
+ "addComment": "Dodaj komentar",
+ "commentPlaceholder": "Napiši komentar...",
+ "postComment": "Objavi",
+ "postingComment": "Objavljivanje...",
+ "deleteComment": "Izbriši",
+ "deleteCommentConfirmTitle": "Izbriši komentar",
+ "deleteCommentConfirmMessage": "Izbrisati ovaj komentar?",
+ "deleteCommentConfirm": "Izbriši",
+ "loading": "Učitavanje...",
+ "backToList": "← Natrag",
+ "errorLoad": "Učitavanje nije uspjelo. Pokušaj ponovno.",
+ "errorSubmit": "Slanje nije uspjelo. Pokušaj ponovno.",
+ "prevPage": "← Prethodna",
+ "nextPage": "Sljedeća →"
+ },
+ "reviewPrompt": {
+ "statusError": "Nismo mogli provjeriti status tvoje recenzije. Pokušaj poslije.",
+ "ratingRequiredError": "Odaberi ocjenu prije slanja.",
+ "saveError": "Nismo mogli spremiti tvoju recenziju. Pokušaj ponovno.",
+ "eyebrow": "Ocijeni FreeLingo",
+ "title": "Kakvo je bilo tvoje iskustvo?",
+ "closeLabel": "Zatvori obrazac za recenziju",
+ "checking": "Provjera statusa recenzije...",
+ "ratingLabel": "Ocjena je obvezna",
+ "ratingGroupLabel": "Ocjena",
+ "star": "zvjezdica",
+ "stars": "od 5 zvjezdica",
+ "commentLabel": "Komentar nije obvezan",
+ "commentPlaceholder": "Reci nam što ti je pomoglo u učenju.",
+ "cancel": "Odustani",
+ "submit": "Pošalji"
+ },
+ "landingReviews": {
+ "eyebrow": "Iskustva korisnika",
+ "title": "Učenje postaje stvarnije kada podijeliš svoje iskustvo",
+ "subtitle": "Recenzije korisnika koji već vježbaju s Linguom na FreeLingu.",
+ "averageLabel": "{average}/5 · {count, plural, one {ukupno # recenzija} few {ukupno # recenzije} other {ukupno # recenzija}}",
+ "learningLanguage": "Uči jezik: {language}",
+ "starsLabel": "Ocjena: {rating} od 5",
+ "ratingOnly": "Potvrđena ocjena za FreeLingo."
+ },
+ "adminReviews": {
+ "title": "Recenzije",
+ "eyebrow": "Administracija",
+ "totalFiltered": "Ukupno filtriranih",
+ "approvedOnPage": "Odobrenih na stranici",
+ "pendingOnPage": "Na čekanju na stranici",
+ "filters": "Filtri",
+ "approvalFilter": "Filtriraj prema odobrenju",
+ "allReviews": "Sve recenzije",
+ "pending": "Na čekanju",
+ "approved": "Odobreno",
+ "ratingFilter": "Filtriraj prema ocjeni",
+ "allRatings": "Sve ocjene",
+ "starsOption": "Ocjena {rating}",
+ "clear": "Očisti",
+ "loadError": "Recenzije nije bilo moguće učitati.",
+ "updateError": "Recenziju nije bilo moguće ažurirati.",
+ "deleteError": "Recenziju nije bilo moguće izbrisati.",
+ "loading": "Učitavanje recenzija...",
+ "queue": "Red za recenzije",
+ "total": "Ukupno: {total}",
+ "empty": "Nema pronađenih recenzija.",
+ "starsLabel": "Ocjena: {rating} od 5",
+ "learningLanguage": "Uči jezik: {language}",
+ "ratingOnly": "Recenzija samo s ocjenom.",
+ "unapprove": "Poništi odobrenje",
+ "approve": "Odobri",
+ "delete": "Izbriši",
+ "prev": "Prethodna",
+ "next": "Sljedeća",
+ "deleteTitle": "Izbriši recenziju",
+ "deleteMessage": "Izbrisati ovu recenziju? Ovu radnju nije moguće poništiti."
+ },
+ "maintenance": {
+ "label": "Održavanje",
+ "title": "Održavanje u tijeku",
+ "description": "Ovaj je odjeljak privremeno nedostupan zbog održavanja. Vrati se poslije."
+ }
+}
diff --git a/messages/it.json b/messages/it.json
index 572c1a756..e790f2fe8 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Caricamento...",
"error": "Errore",
+ "errorMessage": "Qualcosa è andato storto. Per favore riprova.",
"save": "Salva modifiche",
"saving": "Salvataggio...",
"cancel": "Annulla",
@@ -78,6 +79,9 @@
"backToDashboard": "Torna alla dashboard",
"tagline": "apprendimento linguistico con IA",
"close": "Chiudi",
+ "delete": "Elimina",
+ "openMenu": "Apri menu",
+ "closeMenu": "Chiudi menu",
"verifyEmailBanner": "Verifica il tuo indirizzo e-mail per completare la configurazione dell'account.",
"verifyEmailSent": "E-mail inviata. Controlla la tua casella di posta.",
"resendVerification": "Reinvia e-mail di verifica",
@@ -205,7 +209,7 @@
"saving": "Salvataggio...",
"success": "Password aggiornata. Reindirizzamento...",
"mismatch": "Le password non corrispondono",
- "tooShort": "La password deve contenere almeno 8 caratteri",
+ "tooShort": "La password deve contenere almeno 10 caratteri",
"missingToken": "Link non valido. Richiedi un nuovo reset.",
"error": "Link non valido o scaduto",
"backToLogin": "Torna al login"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Se abbandoni il test di livello prima di completarlo, perderai i progressi e dovrai rifarlo.",
"startWarningConfirm": "Inizia",
"startMyPlan": "Inizia il mio piano",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Prova di conversazione vocale",
+ "voiceTrialTitle": "Prova subito una vera conversazione",
+ "voiceTrialDesc": "Parla gratis con Lingu per {minutes} minuti. Il tuo tutor si adatterà al livello che hai appena sbloccato.",
+ "voiceTrialStart": "Inizia la conversazione gratuita",
+ "voiceTrialSkip": "Salta per ora"
},
"plan": {
"unitLocked": "Unità bloccata",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Mostra chat",
"toggleSidebarHide": "Nascondi chat",
"conversations": "Conversazioni",
+ "voiceSession": "Sessione vocale",
"loading": "Caricamento...",
"thinking": "Sto pensando...",
"takingLonger": "Ci sta mettendo più del previsto...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabolario",
"all": "Tutti",
"searchPlaceholder": "Cerca set...",
- "sets": "set",
+ "sets": "{count, plural, one {# set} other {# set}}",
"words": "parole",
"addAll": "Aggiungi tutti i {count} alle schede",
"adding": "Aggiunta...",
@@ -848,6 +853,7 @@
"informal": "Informale",
"copied": "Copiato!",
"copy": "Copia",
+ "copyPhrase": "Copia frase",
"noResults": "Nessuna frase corrisponde ai filtri",
"level": "Livello",
"register": "Registro",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Traduzioni generate. Controllale prima di salvare.",
"translateError": "Traduzione non riuscita. Il contenuto attuale è stato mantenuto.",
"editTranslation": "Traduzione da modificare",
+ "pending": "in attesa",
"completion": "{complete}/{total} lingue complete",
"save": "Salva annuncio",
"saveSuccess": "Annuncio della dashboard salvato.",
@@ -1062,7 +1069,12 @@
"ru": "Russo",
"nl": "Olandese",
"pl": "Polacco",
- "ro": "Rumeno"
+ "ro": "Rumeno",
+ "tr": "Turco",
+ "sv": "Svedese",
+ "da": "Danese",
+ "fi": "Finlandese",
+ "hr": "Croato"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Errore",
"errorServicesDisabled": "I servizi vocali non sono abilitati su questo server.",
"errorConnection": "Errore di connessione. Riprova.",
+ "errorTranscription": "Non è stato possibile trascrivere ciò che hai detto. Prova a parlare di nuovo.",
+ "errorResponse": "Lingu non è riuscito a rispondere. Riprova.",
+ "errorSpeech": "Non è stato possibile generare l'audio della risposta di Lingu. Riprova.",
"errorUnauthorized": "Sessione scaduta. Aggiorna la pagina.",
"errorMic": "Accesso al microfono negato.",
"errorVadInit": "Errore durante l'inizializzazione del riconoscimento vocale. Verifica che il browser supporti WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Voce gratis: {remaining}/{limit} min questa settimana",
"quotaTokens": "Token questo mese",
"memoryUpdated": "Memoria aggiornata",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Conversazione gratuita dopo la valutazione · {minutes} min",
+ "trialCtaLabel": "Continua a parlare",
+ "trialCtaTitle": "Continua a esercitarti con Lingu",
+ "trialCtaDesc": "Sblocca conversazioni vocali complete, chat con il tutor ed esercizi di ascolto e lettura con Premium.",
"startersHint": "O inizia con un argomento",
"starters": [
"Arte e creatività",
@@ -1234,6 +1249,11 @@
"nl": "Olandese",
"ro": "Rumeno",
"ru": "Russo",
+ "tr": "Turco",
+ "sv": "Svedese",
+ "da": "Danese",
+ "fi": "Finlandese",
+ "hr": "Croato",
"myLanguages": "Le mie lingue",
"addLanguage": "Aggiungi lingua",
"selectLanguage": "Seleziona la lingua che vuoi imparare",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Novità",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Capito",
"entry1": {
- "label": "Piani più equilibrati",
- "desc": "I nuovi piani distribuiscono meglio le lezioni tra tutte le unità e danno più spazio a diversi tipi di attività, anche nei percorsi più brevi."
+ "label": "Cinque nuove lingue dell'interfaccia",
+ "desc": "FreeLingo è disponibile in croato, danese, finlandese, svedese e turco. Scegli la lingua dell'interfaccia nelle Impostazioni."
},
"entry2": {
- "label": "Un test per concludere il piano",
- "desc": "Quando arrivi all'ultimo giorno e completi le lezioni in sospeso, puoi fare il test di fine livello dalla dashboard o dal tuo piano e scoprire quanto hai imparato."
- },
- "entry3": {
- "label": "Più chiarezza sui prossimi passi",
- "desc": "I risultati del test e i consigli sono ora nella tua lingua, con indicazioni più chiare per fare il passo successivo o ripassare ciò che hai imparato."
- },
- "entry4": {
"label": "Un'esperienza più fluida",
"desc": "Abbiamo anche corretto piccoli errori e curato alcuni dettagli per rendere ancora più piacevole imparare ogni giorno con FreeLingo."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Niente ancora. Sii il primo a inviare qualcosa!",
"votes": "{count} voti",
"vote": "1 voto",
+ "voteAction": "Vota",
+ "removeVoteAction": "Rimuovi voto",
"comments": "{count} commenti",
"comment": "1 commento",
"by": "di",
@@ -1691,4 +1705,4 @@
"title": "In manutenzione",
"description": "Questa sezione è temporaneamente non disponibile per manutenzione. Riprova più tardi."
}
-}
\ No newline at end of file
+}
diff --git a/messages/nl.json b/messages/nl.json
index 84196e65a..7f8276a06 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Laden...",
"error": "Fout",
+ "errorMessage": "Er ging iets mis. Probeer opnieuw.",
"save": "Wijzigingen opslaan",
"saving": "Opslaan...",
"cancel": "Annuleren",
@@ -78,6 +79,9 @@
"backToDashboard": "Terug naar dashboard",
"tagline": "AI-taalonderwijs",
"close": "Sluiten",
+ "delete": "Verwijderen",
+ "openMenu": "Menu openen",
+ "closeMenu": "Menu sluiten",
"verifyEmailBanner": "Verifieer je e-mailadres om je accountinstellingen te voltooien.",
"verifyEmailSent": "E-mail verzonden. Controleer je inbox.",
"resendVerification": "Verificatie-e-mail opnieuw verzenden",
@@ -205,7 +209,7 @@
"saving": "Opslaan...",
"success": "Wachtwoord bijgewerkt. Doorsturen...",
"mismatch": "Wachtwoorden komen niet overeen",
- "tooShort": "Het wachtwoord moet minimaal 8 tekens bevatten",
+ "tooShort": "Het wachtwoord moet minimaal 10 tekens bevatten",
"missingToken": "Ongeldige link. Vraag een nieuwe reset aan.",
"error": "Ongeldige of verlopen link",
"backToLogin": "Terug naar inloggen"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Als u de niveautest verlaat voordat u klaar bent, gaat uw voortgang verloren en moet u hem opnieuw afleggen.",
"startWarningConfirm": "Beginnen",
"startMyPlan": "Mijn plan starten",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Proefgesprek",
+ "voiceTrialTitle": "Probeer nu een echt gesprek",
+ "voiceTrialDesc": "Praat {minutes} minuten gratis met Lingu. Je tutor past zich aan het niveau aan dat je zojuist hebt ontgrendeld.",
+ "voiceTrialStart": "Start een gratis gesprek",
+ "voiceTrialSkip": "Voorlopig overslaan"
},
"plan": {
"unitLocked": "Eenheid vergrendeld",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Chats tonen",
"toggleSidebarHide": "Chats verbergen",
"conversations": "Gesprekken",
+ "voiceSession": "Spraaksessie",
"loading": "Laden...",
"thinking": "Nadenken...",
"takingLonger": "Duurt langer dan verwacht...",
@@ -823,7 +828,7 @@
"subtitle": "Woordenschat",
"all": "Alle",
"searchPlaceholder": "Zoek sets...",
- "sets": "sets",
+ "sets": "{count, plural, one {# set} other {# sets}}",
"words": "woorden",
"addAll": "Alle {count} toevoegen aan flashcards",
"adding": "Toevoegen...",
@@ -848,6 +853,7 @@
"informal": "Informeel",
"copied": "Gekopieerd!",
"copy": "Kopiëren",
+ "copyPhrase": "Zin kopiëren",
"noResults": "Geen zinnen komen overeen met je filters",
"level": "Niveau",
"register": "Register",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Vertalingen gegenereerd. Controleer ze voor het opslaan.",
"translateError": "Vertalen mislukt. De huidige inhoud is behouden.",
"editTranslation": "Vertaling bewerken",
+ "pending": "in behandeling",
"completion": "{complete}/{total} talen compleet",
"save": "Aankondiging opslaan",
"saveSuccess": "Dashboardaankondiging opgeslagen.",
@@ -1062,7 +1069,12 @@
"ru": "Russisch",
"nl": "Nederlands",
"pl": "Pools",
- "ro": "Roemeens"
+ "ro": "Roemeens",
+ "tr": "Turks",
+ "sv": "Zweeds",
+ "da": "Deens",
+ "fi": "Fins",
+ "hr": "Kroatisch"
}
}
},
@@ -1105,7 +1117,7 @@
"provider_anthropic": "Anthropic Claude. Vereist ANTHROPIC_API_KEY.",
"provider_deepseek": "DeepSeek API. Vereist DEEPSEEK_API_KEY.",
"q_invite": "Kan ik anderen uitnodigen?",
- "a_invite": "Ja. Via Admin → Gebruikers kan de beheerder gebruikers direct aanmaken of eenmalige uitnodigingslinks genereren die na 48 uur verlopen.",
+ "a_invite": "Ja. Via Admin → Gebruikers kan de beheerder gebruikers direct aanmaken of eenmalige uitnodigingslinks genereren die na 48 uur verlopen. Openbare registratie is standaard uitgeschakeld (ALLOW_REGISTRATION=false).",
"q_password": "Hoe verander ik mijn wachtwoord of moedertaal?",
"a_password": "Ga naar Instellingen . Daar kun je je weergavenaam, moedertaal en wachtwoord bijwerken.",
"q_uiLanguage": "Hoe verander ik de taal van het platform?",
@@ -1154,6 +1166,9 @@
"statusError": "Fout",
"errorServicesDisabled": "Spraakdiensten zijn niet ingeschakeld op deze server.",
"errorConnection": "Verbindingsfout. Probeer opnieuw.",
+ "errorTranscription": "Je spraak kon niet worden omgezet in tekst. Probeer opnieuw te spreken.",
+ "errorResponse": "Lingu kon niet antwoorden. Probeer het opnieuw.",
+ "errorSpeech": "Er kon geen audio worden gemaakt voor Lingu's antwoord. Probeer het opnieuw.",
"errorUnauthorized": "Sessie verlopen. Vernieuw de pagina.",
"errorMic": "Microfoon toegang geweigerd.",
"errorVadInit": "Initialiseren van spraakherkenning mislukt. Controleer of je browser WebAssembly ondersteunt.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Gratis stem: {remaining}/{limit} min deze week",
"quotaTokens": "Tokens deze maand",
"memoryUpdated": "Geheugen bijgewerkt",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Gratis gesprek na de niveautest · {minutes} min",
+ "trialCtaLabel": "Blijf praten",
+ "trialCtaTitle": "Blijf oefenen met Lingu",
+ "trialCtaDesc": "Ontgrendel volledige spraakgesprekken, tutorchat en luister- en leesoefeningen met Premium.",
"startersHint": "Of begin met een onderwerp",
"starters": [
"Kunst en creativiteit",
@@ -1234,6 +1249,11 @@
"nl": "Nederlands",
"ro": "Roemeens",
"ru": "Russisch",
+ "tr": "Turks",
+ "sv": "Zweeds",
+ "da": "Deens",
+ "fi": "Fins",
+ "hr": "Kroatisch",
"myLanguages": "Mijn talen",
"addLanguage": "Taal toevoegen",
"selectLanguage": "Kies de taal die je wilt leren",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Nieuw",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Begrepen",
"entry1": {
- "label": "Studieplannen met meer balans",
- "desc": "Nieuwe plannen verdelen de lessen beter over alle onderdelen en bieden meer afwisseling in de activiteiten, ook bij kortere plannen."
+ "label": "Vijf nieuwe interfacetalen",
+ "desc": "FreeLingo is beschikbaar in het Deens, Fins, Kroatisch, Turks en Zweeds. Kies je interfacetaal in Instellingen."
},
"entry2": {
- "label": "Een toets om je plan af te ronden",
- "desc": "Zodra je de laatste dag bereikt en alle openstaande lessen afrondt, kun je via het dashboard of je studieplan de eindtoets maken en zien hoeveel je hebt geleerd."
- },
- "entry3": {
- "label": "Duidelijker weten hoe je verdergaat",
- "desc": "Toetsresultaten en aanbevelingen zijn nu in jouw taal beschikbaar, met duidelijkere uitleg om de volgende stap te zetten of het geleerde te herhalen."
- },
- "entry4": {
"label": "Een soepelere ervaring",
"desc": "We hebben ook kleine fouten opgelost en een paar details verbeterd, zodat je elke dag met nog meer plezier leert met FreeLingo."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nog niets hier. Wees de eerste!",
"votes": "{count} stemmen",
"vote": "1 stem",
+ "voteAction": "Stemmen",
+ "removeVoteAction": "Stem intrekken",
"comments": "{count} reacties",
"comment": "1 reactie",
"by": "door",
@@ -1691,4 +1705,4 @@
"title": "In onderhoud",
"description": "Dit gedeelte is tijdelijk niet beschikbaar vanwege onderhoud. Probeer het later opnieuw."
}
-}
\ No newline at end of file
+}
diff --git a/messages/pl.json b/messages/pl.json
index 11e10f47d..fc9b617ae 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Ładowanie...",
"error": "Błąd",
+ "errorMessage": "Coś poszło nie tak. Spróbuj ponownie.",
"save": "Zapisz zmiany",
"saving": "Zapisywanie...",
"cancel": "Anuluj",
@@ -78,6 +79,9 @@
"backToDashboard": "Wróć do panelu",
"tagline": "Nauka języków z AI",
"close": "Zamknij",
+ "delete": "Usuń",
+ "openMenu": "Otwórz menu",
+ "closeMenu": "Zamknij menu",
"verifyEmailBanner": "Prosimy o potwierdzenie adresu e-mail w celu dokończenia konfiguracji konta.",
"verifyEmailSent": "Wiadomość wysłana. Sprawdź swoją skrzynkę odbiorczą.",
"resendVerification": "Wyślij ponownie e-mail weryfikacyjny",
@@ -205,7 +209,7 @@
"saving": "Zapisywanie...",
"success": "Hasło zaktualizowane. Przekierowanie...",
"mismatch": "Hasła nie są zgodne",
- "tooShort": "Hasło musi mieć co najmniej 8 znaków",
+ "tooShort": "Hasło musi mieć co najmniej 10 znaków",
"missingToken": "Nieprawidłowy link. Poproś o nowe resetowanie.",
"error": "Nieprawidłowy lub wygasły link",
"backToLogin": "Powrót do logowania"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Jeśli opuścisz test poziomujący przed jego ukończeniem, utracisz postęp i będziesz musiał go powtórzyć.",
"startWarningConfirm": "Rozpocznij",
"startMyPlan": "Rozpocznij mój plan",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Próbna rozmowa głosowa",
+ "voiceTrialTitle": "Wypróbuj prawdziwą rozmowę już teraz",
+ "voiceTrialDesc": "Porozmawiaj z Lingu przez {minutes} min za darmo. Twój nauczyciel dostosuje się do właśnie odblokowanego poziomu.",
+ "voiceTrialStart": "Rozpocznij bezpłatną rozmowę",
+ "voiceTrialSkip": "Na razie pomiń"
},
"plan": {
"unitLocked": "Jednostka zablokowana",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Pokaż czaty",
"toggleSidebarHide": "Ukryj czaty",
"conversations": "Rozmowy",
+ "voiceSession": "Sesja głosowa",
"loading": "Ładowanie...",
"thinking": "Myślenie...",
"takingLonger": "Trwa to dłużej niż oczekiwano...",
@@ -823,7 +828,7 @@
"subtitle": "Słownictwo",
"all": "Wszystkie",
"searchPlaceholder": "Szukaj zestawów...",
- "sets": "zestawy",
+ "sets": "{count, plural, one {# zestaw} few {# zestawy} other {# zestawów}}",
"words": "słowa",
"addAll": "Dodaj wszystkie {count} do fiszek",
"adding": "Dodawanie...",
@@ -848,6 +853,7 @@
"informal": "Nieformalny",
"copied": "Skopiowano!",
"copy": "Kopiuj",
+ "copyPhrase": "Kopiuj frazę",
"noResults": "Brak zwrotów pasujących do filtrów",
"level": "Poziom",
"register": "Rejestr",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Tłumaczenia wygenerowane. Sprawdź je przed zapisaniem.",
"translateError": "Tłumaczenie nie powiodło się. Obecna treść została zachowana.",
"editTranslation": "Edytuj tłumaczenie",
+ "pending": "oczekuje",
"completion": "Ukończone języki: {complete}/{total}",
"save": "Zapisz ogłoszenie",
"saveSuccess": "Ogłoszenie zapisane.",
@@ -1062,7 +1069,12 @@
"ru": "Rosyjski",
"nl": "Niderlandzki",
"pl": "Polski",
- "ro": "Rumuński"
+ "ro": "Rumuński",
+ "tr": "Turecki",
+ "sv": "Szwedzki",
+ "da": "Duński",
+ "fi": "Fiński",
+ "hr": "Chorwacki"
}
}
},
@@ -1105,7 +1117,7 @@
"provider_anthropic": "Anthropic Claude. Wymaga ANTHROPIC_API_KEY.",
"provider_deepseek": "DeepSeek API. Wymaga DEEPSEEK_API_KEY.",
"q_invite": "Czy mogę zapraszać innych?",
- "a_invite": "Tak. Z Admin → Użytkownicy administrator może tworzyć użytkowników bezpośrednio lub generować jednorazowe linki zaproszenia wygasające po 48 godzinach.",
+ "a_invite": "Tak. Z Admin → Użytkownicy administrator może tworzyć użytkowników bezpośrednio lub generować jednorazowe linki zaproszenia wygasające po 48 godzinach. Rejestracja publiczna jest domyślnie wyłączona (ALLOW_REGISTRATION=false).",
"q_password": "Jak zmienić hasło lub język ojczysty?",
"a_password": "Przejdź do Ustawień . Możesz tam zaktualizować wyświetlaną nazwę, język ojczysty i hasło.",
"q_uiLanguage": "Jak zmienić język platformy?",
@@ -1154,6 +1166,9 @@
"statusError": "Błąd",
"errorServicesDisabled": "Usługi głosowe nie są włączone na tym serwerze.",
"errorConnection": "Błąd połączenia. Spróbuj ponownie.",
+ "errorTranscription": "Nie udało się przekształcić Twojej wypowiedzi w tekst. Spróbuj powiedzieć to jeszcze raz.",
+ "errorResponse": "Lingu nie mógł odpowiedzieć. Spróbuj ponownie.",
+ "errorSpeech": "Nie udało się wygenerować dźwięku odpowiedzi Lingu. Spróbuj ponownie.",
"errorUnauthorized": "Sesja wygasła. Odśwież stronę.",
"errorMic": "Odmówiono dostępu do mikrofonu.",
"errorVadInit": "Nie udało się zainicjować rozpoznawania mowy. Upewnij się, że Twoja przeglądarka obsługuje WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Darmowy głos: {remaining}/{limit} min w tym tygodniu",
"quotaTokens": "Tokeny w tym miesiącu",
"memoryUpdated": "Pamięć zaktualizowana",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Bezpłatna rozmowa po ocenie poziomu · {minutes} min",
+ "trialCtaLabel": "Mów dalej",
+ "trialCtaTitle": "Ćwicz dalej z Lingu",
+ "trialCtaDesc": "Odblokuj pełne rozmowy głosowe, czat z nauczycielem oraz ćwiczenia słuchania i czytania dzięki Premium.",
"startersHint": "Lub zacznij od tematu",
"starters": [
"Sztuka i kreatywność",
@@ -1234,6 +1249,11 @@
"nl": "Niderlandzki",
"ro": "Rumuński",
"ru": "Rosyjski",
+ "tr": "Turecki",
+ "sv": "Szwedzki",
+ "da": "Duński",
+ "fi": "Fiński",
+ "hr": "Chorwacki",
"myLanguages": "Moje języki",
"addLanguage": "Dodaj język",
"selectLanguage": "Wybierz język, którego chcesz się uczyć",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Co nowego",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Rozumiem",
"entry1": {
- "label": "Lepiej zrównoważone plany nauki",
- "desc": "Nowe plany równomierniej rozkładają lekcje między wszystkie działy i zapewniają więcej różnorodnych ćwiczeń, także w krótszych planach."
+ "label": "Pięć nowych języków interfejsu",
+ "desc": "FreeLingo jest dostępne po chorwacku, duńsku, fińsku, szwedzku i turecku. Wybierz język interfejsu w Ustawieniach."
},
"entry2": {
- "label": "Test na zakończenie planu",
- "desc": "Gdy dotrzesz do ostatniego dnia i ukończysz zaległe lekcje, możesz przejść do testu kończącego poziom z panelu lub swojego planu i sprawdzić swoje postępy."
- },
- "entry3": {
- "label": "Jaśniejsze wskazówki na dalszą naukę",
- "desc": "Wyniki testu i zalecenia są teraz w Twoim języku, z jaśniejszymi wskazówkami, jak zrobić kolejny krok lub powtórzyć materiał."
- },
- "entry4": {
"label": "Płynniejsze korzystanie",
"desc": "Poprawiliśmy też drobne błędy i dopracowaliśmy kilka szczegółów, aby codzienna nauka z FreeLingo była jeszcze przyjemniejsza."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nic tu jeszcze nie ma. Bądź pierwszy!",
"votes": "{count} głosów",
"vote": "1 głos",
+ "voteAction": "Zagłosuj",
+ "removeVoteAction": "Cofnij głos",
"comments": "{count} komentarzy",
"comment": "1 komentarz",
"by": "przez",
@@ -1691,4 +1705,4 @@
"title": "W trakcie konserwacji",
"description": "Ta sekcja jest tymczasowo niedostępna z powodu prac konserwacyjnych. Spróbuj ponownie później."
}
-}
\ No newline at end of file
+}
diff --git a/messages/pt.json b/messages/pt.json
index b93d321c8..721cc28b5 100644
--- a/messages/pt.json
+++ b/messages/pt.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Carregando...",
"error": "Erro",
+ "errorMessage": "Algo deu errado. Por favor, tente novamente.",
"save": "Salvar alterações",
"saving": "Salvando...",
"cancel": "Cancelar",
@@ -78,6 +79,9 @@
"backToDashboard": "Voltar ao painel",
"tagline": "aprendizado de idiomas com IA",
"close": "Fechar",
+ "delete": "Eliminar",
+ "openMenu": "Abrir menu",
+ "closeMenu": "Fechar menu",
"verifyEmailBanner": "Por favor, verifique o seu endereço de e-mail para concluir a configuração da conta.",
"verifyEmailSent": "E-mail enviado. Verifique a sua caixa de entrada.",
"resendVerification": "Reenviar e-mail de verificação",
@@ -205,7 +209,7 @@
"saving": "Salvando...",
"success": "Senha atualizada. Redirecionando...",
"mismatch": "As senhas não coincidem",
- "tooShort": "A senha deve ter pelo menos 8 caracteres",
+ "tooShort": "A senha deve ter pelo menos 10 caracteres",
"missingToken": "Link inválido. Solicite uma nova redefinição.",
"error": "Link inválido ou expirado",
"backToLogin": "Voltar ao login"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Se você sair do teste de nível antes de concluí-lo, seu progresso será perdido e você precisará refazê-lo.",
"startWarningConfirm": "Começar",
"startMyPlan": "Iniciar meu plano",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Teste de conversa por voz",
+ "voiceTrialTitle": "Experimente uma conversa real agora",
+ "voiceTrialDesc": "Fale com o Lingu durante {minutes} minutos, gratuitamente. O seu tutor adaptar-se-á ao nível que acabou de desbloquear.",
+ "voiceTrialStart": "Iniciar conversa gratuita",
+ "voiceTrialSkip": "Saltar por agora"
},
"plan": {
"unitLocked": "Unidade bloqueada",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Mostrar chats",
"toggleSidebarHide": "Ocultar chats",
"conversations": "Conversas",
+ "voiceSession": "Sessão de voz",
"loading": "Carregando...",
"thinking": "Pensando...",
"takingLonger": "Está a demorar mais do que o esperado...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabulário",
"all": "Todos",
"searchPlaceholder": "Pesquisar conjuntos...",
- "sets": "conjuntos",
+ "sets": "{count, plural, one {# conjunto} other {# conjuntos}}",
"words": "palavras",
"addAll": "Adicionar {count} aos cartões",
"adding": "Adicionando...",
@@ -848,6 +853,7 @@
"informal": "Informal",
"copied": "Copiado!",
"copy": "Copiar",
+ "copyPhrase": "Copiar frase",
"noResults": "Nenhuma frase corresponde aos filtros",
"level": "Nível",
"register": "Registo",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Traduções geradas. Revê-as antes de guardar.",
"translateError": "A tradução falhou. O conteúdo atual foi mantido.",
"editTranslation": "Tradução a editar",
+ "pending": "pendente",
"completion": "{complete}/{total} idiomas completos",
"save": "Guardar anúncio",
"saveSuccess": "Anúncio do painel guardado.",
@@ -1062,7 +1069,12 @@
"ru": "Russo",
"nl": "Neerlandês",
"pl": "Polaco",
- "ro": "Romeno"
+ "ro": "Romeno",
+ "tr": "Turco",
+ "sv": "Sueco",
+ "da": "Dinamarquês",
+ "fi": "Finlandês",
+ "hr": "Croata"
}
}
},
@@ -1154,6 +1166,9 @@
"statusError": "Erro",
"errorServicesDisabled": "Os serviços de voz não estão ativados neste servidor.",
"errorConnection": "Erro de ligação. Por favor, tente novamente.",
+ "errorTranscription": "Não foi possível transcrever o que disse. Tente falar novamente.",
+ "errorResponse": "O Lingu não conseguiu responder. Tente novamente.",
+ "errorSpeech": "Não foi possível gerar áudio para a resposta do Lingu. Tente novamente.",
"errorUnauthorized": "Sessão expirada. Por favor, atualize.",
"errorMic": "Acesso ao microfone negado.",
"errorVadInit": "Falha ao inicializar o reconhecimento de voz. Verifique se o seu navegador suporta WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Voz grátis: {remaining}/{limit} min esta semana",
"quotaTokens": "Tokens este mês",
"memoryUpdated": "Memória atualizada",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Conversa gratuita após a avaliação · {minutes} min",
+ "trialCtaLabel": "Continue a falar",
+ "trialCtaTitle": "Continue a praticar com o Lingu",
+ "trialCtaDesc": "Desbloqueie conversas por voz completas, chat com o tutor e exercícios de compreensão oral e leitura com o Premium.",
"startersHint": "Ou comece com um tema",
"starters": [
"Arte e criatividade",
@@ -1234,6 +1249,11 @@
"nl": "Neerlandês",
"ro": "Romeno",
"ru": "Russo",
+ "tr": "Turco",
+ "sv": "Sueco",
+ "da": "Dinamarquês",
+ "fi": "Finlandês",
+ "hr": "Croata",
"myLanguages": "Meus idiomas",
"addLanguage": "Adicionar idioma",
"selectLanguage": "Selecione o idioma que deseja aprender",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Novidades",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Entendido",
"entry1": {
- "label": "Planos mais equilibrados",
- "desc": "Os novos planos distribuem melhor as lições por todas as unidades e dão mais espaço a diferentes atividades, mesmo nos planos mais curtos."
+ "label": "Cinco novos idiomas de interface",
+ "desc": "O FreeLingo está disponível em croata, dinamarquês, finlandês, sueco e turco. Escolhe o idioma da interface nas Definições."
},
"entry2": {
- "label": "Um teste para concluir o teu plano",
- "desc": "Quando chegares ao último dia e terminares as lições pendentes, podes fazer o teste de fim de nível a partir do painel ou do teu plano e ver quanto aprendeste."
- },
- "entry3": {
- "label": "Mais clareza nos próximos passos",
- "desc": "Os resultados do teste e as recomendações estão agora no teu idioma, com indicações mais claras para dares o próximo passo ou reveres o que aprendeste."
- },
- "entry4": {
"label": "Uma experiência mais fluida",
"desc": "Também corrigimos pequenos erros e afinámos alguns detalhes para tornar a tua aprendizagem diária com o FreeLingo ainda mais agradável."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nada aqui ainda. Seja o primeiro a enviar!",
"votes": "{count} votos",
"vote": "1 voto",
+ "voteAction": "Votar",
+ "removeVoteAction": "Retirar voto",
"comments": "{count} comentários",
"comment": "1 comentário",
"by": "por",
@@ -1691,4 +1705,4 @@
"title": "Em manutenção",
"description": "Esta seção está temporariamente indisponível para manutenção. Volte mais tarde."
}
-}
\ No newline at end of file
+}
diff --git a/messages/ro.json b/messages/ro.json
index 3b66a1f66..381101c28 100644
--- a/messages/ro.json
+++ b/messages/ro.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Se încarcă...",
"error": "Eroare",
+ "errorMessage": "Ceva a mers greșit. Încearcă din nou.",
"save": "Salvează modificările",
"saving": "Se salvează...",
"cancel": "Anulează",
@@ -78,6 +79,9 @@
"backToDashboard": "Înapoi la panou",
"tagline": "Educație lingvistică AI",
"close": "Închide",
+ "delete": "Șterge",
+ "openMenu": "Deschide meniul",
+ "closeMenu": "Închide meniul",
"verifyEmailBanner": "Vă rugăm să vă verificați adresa de e-mail pentru a finaliza configurarea contului.",
"verifyEmailSent": "E-mail trimis. Verificați inbox-ul.",
"resendVerification": "Retrimite e-mail de verificare",
@@ -205,7 +209,7 @@
"saving": "Se salvează...",
"success": "Parolă actualizată. Redirecționare...",
"mismatch": "Parolele nu se potrivesc",
- "tooShort": "Parola trebuie să aibă cel puțin 8 caractere",
+ "tooShort": "Parola trebuie să aibă cel puțin 10 caractere",
"missingToken": "Link invalid. Solicitați o nouă resetare.",
"error": "Link invalid sau expirat",
"backToLogin": "Înapoi la autentificare"
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Dacă părăsești testul de nivel înainte de a-l finaliza, progresul tău va fi pierdut și va trebui să îl refaci.",
"startWarningConfirm": "Începe",
"startMyPlan": "Pornește planul meu",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Conversație vocală de probă",
+ "voiceTrialTitle": "Încearcă o conversație reală acum",
+ "voiceTrialDesc": "Vorbește gratuit cu Lingu timp de {minutes} min. Profesorul tău se va adapta la nivelul pe care tocmai l-ai deblocat.",
+ "voiceTrialStart": "Începe conversația gratuită",
+ "voiceTrialSkip": "Omite deocamdată"
},
"plan": {
"unitLocked": "Unitate blocată",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Arată chat-urile",
"toggleSidebarHide": "Ascunde chat-urile",
"conversations": "Conversații",
+ "voiceSession": "Sesiune vocală",
"loading": "Se încarcă...",
"thinking": "Se gândește...",
"takingLonger": "Durează mai mult decât de obicei...",
@@ -823,7 +828,7 @@
"subtitle": "Vocabular",
"all": "Toate",
"searchPlaceholder": "Caută seturi...",
- "sets": "seturi",
+ "sets": "{count, plural, one {# set} few {# seturi} other {# seturi}}",
"words": "cuvinte",
"addAll": "Adaugă toate cele {count} la carduri flash",
"adding": "Se adaugă...",
@@ -848,6 +853,7 @@
"informal": "Informal",
"copied": "Copiat!",
"copy": "Copiază",
+ "copyPhrase": "Copiază fraza",
"noResults": "Nicio frază nu corespunde filtrelor tale",
"level": "Nivel",
"register": "Registru",
@@ -1046,6 +1052,7 @@
"translateSuccess": "Traduceri generate. Verifică-le înainte de salvare.",
"translateError": "Traducerea a eșuat. Conținutul actual a fost păstrat.",
"editTranslation": "Traducere de editat",
+ "pending": "în așteptare",
"completion": "{complete}/{total} limbi complete",
"save": "Salvează anunțul",
"saveSuccess": "Anunțul a fost salvat.",
@@ -1062,7 +1069,12 @@
"ru": "Rusă",
"nl": "Neerlandeză",
"pl": "Poloneză",
- "ro": "Română"
+ "ro": "Română",
+ "tr": "Turcă",
+ "sv": "Suedeză",
+ "da": "Daneză",
+ "fi": "Finlandeză",
+ "hr": "Croată"
}
}
},
@@ -1105,7 +1117,7 @@
"provider_anthropic": "Anthropic Claude. Necesită ANTHROPIC_API_KEY.",
"provider_deepseek": "DeepSeek API. Necesită DEEPSEEK_API_KEY.",
"q_invite": "Pot invita pe alții?",
- "a_invite": "Da. Prin Admin → Utilizatori , administratorul poate crea utilizatori direct sau poate genera linkuri de invitație de unică folosință care expiră după 48 de ore.",
+ "a_invite": "Da. Prin Admin → Utilizatori , administratorul poate crea utilizatori direct sau poate genera linkuri de invitație de unică folosință care expiră după 48 de ore. Înregistrarea publică este dezactivată implicit (ALLOW_REGISTRATION=false).",
"q_password": "Cum îmi schimb parola sau limba maternă?",
"a_password": "Mergi la Setări . Acolo poți actualiza numele afișat, limba maternă și parola.",
"q_uiLanguage": "Cum schimb limba platformei?",
@@ -1154,6 +1166,9 @@
"statusError": "Eroare",
"errorServicesDisabled": "Serviciile vocale nu sunt activate pe acest server.",
"errorConnection": "Eroare de conexiune. Încearcă din nou.",
+ "errorTranscription": "Nu s-a putut transcrie ce ai spus. Încearcă să vorbești din nou.",
+ "errorResponse": "Lingu nu a putut răspunde. Încearcă din nou.",
+ "errorSpeech": "Nu s-a putut genera sunetul pentru răspunsul lui Lingu. Încearcă din nou.",
"errorUnauthorized": "Sesiune expirată. Reîmprospătează pagina.",
"errorMic": "Accesul la microfon a fost refuzat.",
"errorVadInit": "Inițializarea detectării vocii a eșuat. Verifică dacă browserul tău suportă WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Voce gratuită: {remaining}/{limit} min săptămâna aceasta",
"quotaTokens": "Tokeni luna aceasta",
"memoryUpdated": "Memorie actualizată",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Conversație gratuită după evaluare · {minutes} min",
+ "trialCtaLabel": "Continuă să vorbești",
+ "trialCtaTitle": "Continuă să exersezi cu Lingu",
+ "trialCtaDesc": "Deblochează conversații vocale complete, chat cu profesorul și exerciții de ascultare și citire cu Premium.",
"startersHint": "Sau începe cu un subiect",
"starters": [
"Artă și creativitate",
@@ -1234,6 +1249,11 @@
"nl": "Olandeză",
"ro": "Română",
"ru": "Rusă",
+ "tr": "Turcă",
+ "sv": "Suedeză",
+ "da": "Daneză",
+ "fi": "Finlandeză",
+ "hr": "Croată",
"myLanguages": "Limbile mele",
"addLanguage": "Adaugă limbă",
"selectLanguage": "Alege limba pe care vrei să o înveți",
@@ -1486,21 +1506,13 @@
},
"whatsNew": {
"title": "Noutăți",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Am înțeles",
"entry1": {
- "label": "Planuri mai echilibrate",
- "desc": "Noile planuri distribuie lecțiile mai uniform între toate unitățile și oferă mai multă varietate de activități, chiar și în planurile scurte."
+ "label": "Cinci limbi noi pentru interfață",
+ "desc": "FreeLingo este disponibil în croată, daneză, finlandeză, suedeză și turcă. Alege limba interfeței în Setări."
},
"entry2": {
- "label": "Un test la finalul planului",
- "desc": "Când ajungi la ultima zi și termini lecțiile restante, poți susține testul de final de nivel din panoul principal sau din planul tău și poți vedea cât ai învățat."
- },
- "entry3": {
- "label": "Pași mai clari pentru a continua",
- "desc": "Rezultatele testului și recomandările sunt acum în limba ta, cu îndrumări mai clare pentru a face următorul pas sau a recapitula ce ai învățat."
- },
- "entry4": {
"label": "O experiență mai fluidă",
"desc": "Am corectat și mici erori și am finisat câteva detalii, ca învățarea de zi cu zi cu FreeLingo să fie și mai plăcută."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Nimic aici încă. Fii primul care trimite ceva!",
"votes": "{count} voturi",
"vote": "1 vot",
+ "voteAction": "Votează",
+ "removeVoteAction": "Retrage votul",
"comments": "{count} comentarii",
"comment": "1 comentariu",
"by": "de",
@@ -1691,4 +1705,4 @@
"title": "În mentenanță",
"description": "Această secțiune este temporar indisponibilă pentru lucrări de întreținere. Încearcă din nou mai târziu."
}
-}
\ No newline at end of file
+}
diff --git a/messages/ru.json b/messages/ru.json
index 07ca35a14..86851aacc 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -54,6 +54,7 @@
"common": {
"loading": "Загрузка...",
"error": "Ошибка",
+ "errorMessage": "Что-то пошло не так. Попробуйте снова.",
"save": "Сохранить изменения",
"saving": "Сохранение...",
"cancel": "Отмена",
@@ -78,6 +79,9 @@
"backToDashboard": "На главную",
"tagline": "ИИ-обучение языкам",
"close": "Закрыть",
+ "delete": "Удалить",
+ "openMenu": "Открыть меню",
+ "closeMenu": "Закрыть меню",
"verifyEmailBanner": "Пожалуйста, подтвердите свой адрес электронной почты для завершения настройки аккаунта.",
"verifyEmailSent": "Письмо отправлено. Проверьте входящие.",
"resendVerification": "Повторно отправить письмо для подтверждения",
@@ -205,7 +209,7 @@
"saving": "Сохранение...",
"success": "Пароль обновлён. Перенаправление...",
"mismatch": "Пароли не совпадают",
- "tooShort": "Пароль должен содержать не менее 8 символов",
+ "tooShort": "Пароль должен содержать не менее 10 символов",
"missingToken": "Недействительная ссылка. Запросите новый сброс.",
"error": "Недействительная или просроченная ссылка",
"backToLogin": "Вернуться ко входу"
@@ -467,7 +471,7 @@
"premiumBannerPastDueTitle": "Платёж ожидает",
"premiumBannerPastDueDesc": "Обновите способ оплаты, чтобы восстановить Premium-доступ к чату, голосовым разговорам, reading и listening.",
"premiumBannerPastDueCta": "Восстановить Premium",
- "freemiumTrialTitle": "{days, plural, one {# день пробного периода} other {# дней пробного периода}}",
+ "freemiumTrialTitle": "{days, plural, one {# день пробного периода} few {# дня пробного периода} many {# дней пробного периода} other {# дня пробного периода}}",
"freemiumTrialDesc": "Наслаждайтесь неограниченным доступом к чату, голосу, listening и reading. Попробуйте всё Premium бесплатно.",
"announcementDismiss": "Закрыть объявление",
"announcementDismissError": "Не удалось закрыть объявление. Попробуйте снова."
@@ -594,11 +598,11 @@
"startWarningMessageLevelTest": "Если вы покинете тест уровня до его завершения, ваш прогресс будет утерян и вам придётся пройти его повторно.",
"startWarningConfirm": "Начать",
"startMyPlan": "Начать мой план",
- "voiceTrialLabel": "Voice trial",
- "voiceTrialTitle": "Try a real conversation now",
- "voiceTrialDesc": "Talk with Lingu for {minutes} minutes, free. Your tutor will adapt to the level you just unlocked.",
- "voiceTrialStart": "Start free conversation",
- "voiceTrialSkip": "Skip for now"
+ "voiceTrialLabel": "Пробный голосовой разговор",
+ "voiceTrialTitle": "Попробуйте настоящий разговор прямо сейчас",
+ "voiceTrialDesc": "Поговорите с Lingu бесплатно в течение {minutes} мин. Ваш преподаватель подстроится под только что открытый уровень.",
+ "voiceTrialStart": "Начать бесплатный разговор",
+ "voiceTrialSkip": "Пока пропустить"
},
"plan": {
"unitLocked": "Модуль заблокирован",
@@ -761,6 +765,7 @@
"toggleSidebarShow": "Показать чаты",
"toggleSidebarHide": "Скрыть чаты",
"conversations": "Разговоры",
+ "voiceSession": "Голосовой сеанс",
"loading": "Загрузка...",
"thinking": "Думает...",
"takingLonger": "Это занимает больше времени...",
@@ -816,14 +821,14 @@
"explanation": "Объяснение",
"backLink": "Назад к грамматике",
"notFound": "Тема не найдена.",
- "topicsFound": "{count, plural, one {# тема найдена} few {# темы найдено} other {# тем найдено}}"
+ "topicsFound": "{count, plural, one {Найдена # тема} few {Найдены # темы} many {Найдено # тем} other {Найдено # темы}}"
},
"vocabulary": {
"title": "Центр словарного запаса",
"subtitle": "Словарный запас",
"all": "Все",
"searchPlaceholder": "Поиск наборов...",
- "sets": "наборов",
+ "sets": "{count, plural, one {# набор} few {# набора} many {# наборов} other {# набора}}",
"words": "слов",
"addAll": "Добавить все {count} в карточки",
"adding": "Добавление...",
@@ -848,11 +853,12 @@
"informal": "Неформальный",
"copied": "Скопировано!",
"copy": "Копировать",
+ "copyPhrase": "Скопировать фразу",
"noResults": "Нет фраз, соответствующих фильтрам",
"level": "Уровень",
"register": "Регистр",
"statsLine": "{situationCount} ситуаций · {phraseCount} фраз · {range}",
- "situationsShown": "{count, plural, one {# ситуация показана} few {# ситуации показано} other {# ситуаций показано}}",
+ "situationsShown": "{count, plural, one {Показана # ситуация} few {Показаны # ситуации} many {Показано # ситуаций} other {Показано # ситуации}}",
"searchPlaceholder": "Поиск фраз..."
},
"admin": {
@@ -1046,6 +1052,7 @@
"translateSuccess": "Переводы созданы. Проверьте их перед сохранением.",
"translateError": "Перевод не выполнен. Текущий текст сохранён.",
"editTranslation": "Редактировать перевод",
+ "pending": "ожидает",
"completion": "Готово языков: {complete}/{total}",
"save": "Сохранить объявление",
"saveSuccess": "Объявление сохранено.",
@@ -1062,7 +1069,12 @@
"ru": "Русский",
"nl": "Нидерландский",
"pl": "Польский",
- "ro": "Румынский"
+ "ro": "Румынский",
+ "tr": "Турецкий",
+ "sv": "Шведский",
+ "da": "Датский",
+ "fi": "Финский",
+ "hr": "Хорватский"
}
}
},
@@ -1105,7 +1117,7 @@
"provider_anthropic": "Anthropic Claude. Требует ANTHROPIC_API_KEY.",
"provider_deepseek": "DeepSeek API. Требует DEEPSEEK_API_KEY.",
"q_invite": "Могу ли я пригласить других?",
- "a_invite": "Да. Через Администрирование → Пользователи администратор может создавать пользователей напрямую или генерировать одноразовые ссылки-приглашения, которые истекают через 48 часов.",
+ "a_invite": "Да. Через Администрирование → Пользователи администратор может создавать пользователей напрямую или генерировать одноразовые ссылки-приглашения, которые истекают через 48 часов. Публичная регистрация по умолчанию отключена (ALLOW_REGISTRATION=false).",
"q_password": "Как изменить пароль или родной язык?",
"a_password": "Перейдите в Настройки . Там вы можете обновить отображаемое имя, родной язык и пароль.",
"q_uiLanguage": "Как изменить язык платформы?",
@@ -1154,6 +1166,9 @@
"statusError": "Ошибка",
"errorServicesDisabled": "Голосовые сервисы не активированы на этом сервере.",
"errorConnection": "Ошибка соединения. Попробуйте снова.",
+ "errorTranscription": "Не удалось преобразовать вашу речь в текст. Попробуйте произнести фразу ещё раз.",
+ "errorResponse": "Lingu не удалось ответить. Попробуйте снова.",
+ "errorSpeech": "Не удалось озвучить ответ Lingu. Попробуйте снова.",
"errorUnauthorized": "Сессия истекла. Обновите страницу.",
"errorMic": "Доступ к микрофону запрещён.",
"errorVadInit": "Не удалось инициализировать обнаружение речи. Проверьте, поддерживает ли ваш браузер WebAssembly.",
@@ -1171,10 +1186,10 @@
"freemiumVoiceRemaining": "Бесплатный голос: {remaining}/{limit} мин на этой неделе",
"quotaTokens": "Токены в этом месяце",
"memoryUpdated": "Память обновлена",
- "trialBanner": "Free assessment conversation · {minutes} min",
- "trialCtaLabel": "Keep speaking",
- "trialCtaTitle": "Continue practising with Lingu",
- "trialCtaDesc": "Unlock full voice conversations, tutor chat, listening and reading practice with Premium.",
+ "trialBanner": "Бесплатный разговор после оценки уровня · {minutes} мин",
+ "trialCtaLabel": "Продолжайте говорить",
+ "trialCtaTitle": "Продолжайте практиковаться с Lingu",
+ "trialCtaDesc": "Откройте полный доступ к голосовым разговорам, чату с преподавателем, упражнениям на аудирование и чтение с Premium.",
"startersHint": "Или начните с темы",
"starters": [
"Искусство и творчество",
@@ -1234,6 +1249,11 @@
"nl": "Нидерландский",
"ro": "Румынский",
"ru": "Русский",
+ "tr": "Турецкий",
+ "sv": "Шведский",
+ "da": "Датский",
+ "fi": "Финский",
+ "hr": "Хорватский",
"myLanguages": "Мои языки",
"addLanguage": "Добавить язык",
"selectLanguage": "Выберите язык, который хотите изучать",
@@ -1480,27 +1500,19 @@
"readingLabel": "Чтение",
"voiceLabel": "Голос (мин)",
"freeLimit": "Получить Premium",
- "trialDaysLeft": "{days, plural, one {# день бесплатного пробного периода} other {# дней бесплатного пробного периода}}",
+ "trialDaysLeft": "{days, plural, one {# день бесплатного пробного периода} few {# дня бесплатного пробного периода} many {# дней бесплатного пробного периода} other {# дня бесплатного пробного периода}}",
"trialUnlimited": "Неограниченный доступ",
"requiresSubscription": "Только Premium"
},
"whatsNew": {
"title": "Что нового",
- "version": "v1.9.20",
+ "version": "v1.9.25",
"cta": "Понятно",
"entry1": {
- "label": "Более сбалансированные планы",
- "desc": "В новых планах уроки распределены равномернее между всеми разделами, а задания стали разнообразнее — даже в коротких планах."
+ "label": "Пять новых языков интерфейса",
+ "desc": "FreeLingo доступен на датском, турецком, финском, хорватском и шведском языках. Выбери язык интерфейса в настройках."
},
"entry2": {
- "label": "Тест в конце твоего плана",
- "desc": "Когда дойдёшь до последнего дня и завершишь оставшиеся уроки, сможешь открыть итоговый тест уровня с главной страницы или из своего плана и оценить свои успехи."
- },
- "entry3": {
- "label": "Понятнее, как двигаться дальше",
- "desc": "Результаты теста и рекомендации теперь доступны на твоём языке. Подсказки стали понятнее: будет проще сделать следующий шаг или повторить пройденное."
- },
- "entry4": {
"label": "Более плавная работа",
"desc": "Мы также исправили небольшие ошибки и доработали несколько деталей, чтобы учиться с FreeLingo каждый день было ещё приятнее."
}
@@ -1588,6 +1600,8 @@
"noEntries": "Пока ничего нет. Будьте первым!",
"votes": "{count} голосов",
"vote": "1 голос",
+ "voteAction": "Голосовать",
+ "removeVoteAction": "Отменить голос",
"comments": "{count} комментариев",
"comment": "1 комментарий",
"by": "от",
@@ -1691,4 +1705,4 @@
"title": "Техническое обслуживание",
"description": "Этот раздел временно недоступен в связи с техническим обслуживанием. Пожалуйста, зайдите позже."
}
-}
\ No newline at end of file
+}
diff --git a/messages/sv.json b/messages/sv.json
new file mode 100644
index 000000000..1b058a16f
--- /dev/null
+++ b/messages/sv.json
@@ -0,0 +1,1708 @@
+{
+ "landing": {
+ "microDemo": {
+ "title": "Så hjälper Lingu dig",
+ "exampleLabel": "Exempel på engelsk övning",
+ "questionLabel": "Lingu · Fråga",
+ "answerLabel": "Du · Svar",
+ "correctionLabel": "Lingu · Rättelse",
+ "explanation": "När du pratar om gårdagen använder du 'went', dåtidsformen av 'go'."
+ },
+ "hero": "Lär dig språk med en samtalspartner som du kan prata med",
+ "heroSub": "Prata. Lyssna. Utvecklas.",
+ "signIn": "Logga in",
+ "dashboard": "Fortsätt",
+ "feature1Title": "Studieplan",
+ "feature1Desc": "Nivåbedömning och veckoplan från A1 till C2. Lektioner låses upp i tur och ordning, i din egen takt.",
+ "feature2Title": "AI-språkhandledare i chatten",
+ "feature2Desc": "Öva med lektioner, uppgifter och gloskort med stöd av din handledare. Få återkoppling vid varje steg.",
+ "feature3Title": "Röstsamtal",
+ "feature3Desc": "Prata med din handledare i realtid. Röststyrt, med omedelbara svar och utan att vänta på din tur.",
+ "feature4Title": "Hörförståelse och läsning",
+ "feature4Desc": "AI-genererade övningar på din GERS-nivå. Lyssna på ett avsnitt, svara på fem förståelsefrågor och lås upp transkriptionen – eller läs en text och svara direkt. Nya övningar när du vill.",
+ "feature5Title": "Gloskort",
+ "feature5Desc": "Repetera ord med utspridd repetition. SM-2-algoritmen schemalägger varje kort vid rätt tillfälle för att stärka långtidsminnet.",
+ "feature6Title": "Framsteg och sviter",
+ "feature6Desc": "XP, dagliga sviter, färdighetsresultat och enhetskompetenser. Följ din utveckling vecka för vecka och testa din nivå efter varje GERS-steg.",
+ "footer": "Öppen källkod · Drift på egen server kostnadsfritt eller prenumerera på den hostade tjänsten",
+ "navFeatures": "Funktioner",
+ "navReviews": "Omdömen",
+ "navPricing": "Priser",
+ "navFAQ": "Vanliga frågor",
+ "howItWorks": "Så fungerar det",
+ "faqTitle": "Vanliga frågor",
+ "terms": "Villkor",
+ "privacy": "Integritet",
+ "contact": "Kontakt",
+ "aboutMe": "Om mig",
+ "github": "GitHub",
+ "footerProduct": "Produkt",
+ "footerLegal": "Juridiskt",
+ "languageGreetings": {
+ "en-US": "Hello!",
+ "en-GB": "Hello!",
+ "de-DE": "Hallo!",
+ "es-ES": "¡Hola!",
+ "fr-FR": "Bonjour !",
+ "it-IT": "Ciao!",
+ "pt-PT": "Olá!",
+ "ja-JP": "こんにちは!",
+ "ko-KR": "안녕하세요!",
+ "zh-CN": "你好!"
+ }
+ },
+ "common": {
+ "loading": "Laddar...",
+ "error": "Fel",
+ "errorMessage": "Något gick fel. Försök igen.",
+ "save": "Spara ändringar",
+ "saving": "Sparar...",
+ "cancel": "Avbryt",
+ "back": "Tillbaka",
+ "done": "Klart",
+ "next": "Nästa",
+ "submit": "Skicka",
+ "start": "Starta",
+ "retry": "Försök igen",
+ "or": "eller",
+ "appName": "FreeLingo",
+ "days": "dagar",
+ "logout": "Logga ut",
+ "logoutConfirmTitle": "Logga ut",
+ "logoutConfirmMessage": "Är du säker på att du vill logga ut?",
+ "initializing": "Startar...",
+ "clearFilters": "Rensa filter",
+ "all": "Alla",
+ "score": "Poäng",
+ "complete": "Slutför",
+ "checking": "Kontrollerar...",
+ "backToDashboard": "Tillbaka till översikten",
+ "tagline": "Språkinlärning med AI",
+ "close": "Stäng",
+ "delete": "Ta bort",
+ "openMenu": "Öppna menyn",
+ "closeMenu": "Stäng menyn",
+ "verifyEmailBanner": "Bekräfta din e-postadress för att slutföra registreringen.",
+ "verifyEmailSent": "E-postmeddelandet har skickats. Kolla din inkorg.",
+ "resendVerification": "Skicka bekräftelsemeddelandet igen",
+ "saveWord": "Spara",
+ "wordSaved": "Sparat",
+ "wordAlreadySaved": "Redan sparat",
+ "wordSaveError": "Något gick fel",
+ "selectWordHint": "Markera ett ord för att spara det i ditt ordförråd",
+ "noActivePlan": "Ingen aktiv studieplan hittades",
+ "memorySavedToast": "Lingu sparade ett nytt minne. Du kan granska det i Inställningar.",
+ "removeLanguageConfirmTitle": "Ta bort språk",
+ "removeLanguageConfirmMessage": "Alla språkspecifika data (studieplan, framsteg, gloskort och samtal) raderas permanent. Dina kontominnen behålls. Åtgärden kan inte ångras.",
+ "removeLanguageConfirmLabel": "Ta bort",
+ "nativeHelpTitle": "Hjälp på {language}",
+ "nativeHelpLoading": "Förbereder hjälp på {language}...",
+ "nativeHelpShow": "Visa hjälp på {language}",
+ "nativeHelpKeyPoints": "Viktiga punkter",
+ "nativeHelpCommonTraps": "Vanliga fallgropar",
+ "nativeHelpMiniGlossary": "Kort ordlista",
+ "nativeHelpUsageTips": "Användningstips",
+ "nativeHelpRegisterNotes": "Språkregister",
+ "nativeHelpPhraseNotes": "Anteckningar om fraser",
+ "nativeHelpStudyTips": "Studietips",
+ "nativeHelpWordNotes": "Anteckningar om ord",
+ "nativeHelpPractice": "Övning"
+ },
+ "nav": {
+ "home": "Hem",
+ "myPlan": "Min plan",
+ "progress": "Framsteg",
+ "flashcards": "Gloskort",
+ "tutor": "Chatta med handledaren",
+ "assessment": "Nivåbedömning",
+ "resources": "Resurser",
+ "grammar": "Grammatik",
+ "vocabulary": "Ordförråd",
+ "phrasebook": "Frasbok",
+ "settings": "Inställningar",
+ "faq": "Vanliga frågor",
+ "admin": "Administration",
+ "conversation": "Samtal",
+ "listening": "Hörförståelse",
+ "reading": "Läsning",
+ "contact": "Kontakt",
+ "feedback": "Feedback",
+ "reviews": "Omdömen",
+ "switchLanguage": "Byt språk"
+ },
+ "auth": {
+ "login": {
+ "title": "Logga in",
+ "subtitle": "Välkommen tillbaka",
+ "email": "E-postadress",
+ "password": "Lösenord",
+ "submit": "Logga in",
+ "noAccount": "Har du inget konto?",
+ "register": "Registrera dig",
+ "error": "Fel e-postadress eller lösenord",
+ "emailRequired": "Ange din e-postadress",
+ "passwordRequired": "Ange ditt lösenord",
+ "loginFailed": "Inloggningen misslyckades",
+ "signingIn": "Loggar in...",
+ "showPassword": "Visa lösenord",
+ "hidePassword": "Dölj lösenord",
+ "accountCreated": "Kontot har skapats. Du kan nu logga in",
+ "invalidEmail": "Ange en giltig e-postadress",
+ "forgotPassword": "Har du glömt lösenordet?"
+ },
+ "register": {
+ "title": "Skapa konto",
+ "subtitle": "Börja lära dig",
+ "username": "Användarnamn",
+ "email": "E-postadress",
+ "password": "Lösenord",
+ "displayName": "Visningsnamn",
+ "nativeLanguage": "Modersmål",
+ "inviteToken": "Inbjudningskod",
+ "inviteTokenPlaceholder": "Ange inbjudningskod",
+ "submit": "Skapa konto",
+ "hasAccount": "Har du redan ett konto?",
+ "login": "Logga in",
+ "registrationClosed": "Registreringen är stängd. Du behöver en inbjudningskod.",
+ "error": "Registreringen misslyckades",
+ "passwordMismatch": "Lösenorden stämmer inte överens",
+ "confirmPassword": "Bekräfta lösenord",
+ "showPassword": "Visa lösenord",
+ "hidePassword": "Dölj lösenord",
+ "displayNamePlaceholder": "Samma som användarnamnet om fältet lämnas tomt",
+ "inviteActive": "Inbjudan aktiv",
+ "creatingAccount": "Skapar konto...",
+ "invalidEmail": "Ange en giltig e-postadress",
+ "usernameTaken": "Användarnamnet är redan upptaget",
+ "emailTaken": "E-postadressen är redan registrerad",
+ "invalidInvite": "Ogiltig eller utgången inbjudningskod",
+ "termsAccept": "Jag har läst och godkänner",
+ "termsLink": "Användarvillkoren",
+ "andWord": "och",
+ "privacyLink": "Integritetspolicyn",
+ "termsRequired": "Du måste godkänna villkoren för att fortsätta",
+ "invalidPassword": "Lösenordet måste vara 10–25 tecken långt och innehålla minst en stor bokstav, en siffra och en symbol",
+ "invalidUsernameChars": "Användarnamnet får bara innehålla bokstäver, siffror, bindestreck, understreck och punkter"
+ },
+ "verifyEmail": {
+ "title": "Bekräfta e-postadress",
+ "loading": "Bekräftar...",
+ "success": "Din e-postadress har bekräftats. Du kan nu logga in.",
+ "error": "Ogiltig eller utgången bekräftelselänk. Begär en ny från ditt konto.",
+ "goToLogin": "Gå till inloggningen"
+ },
+ "forgotPassword": {
+ "title": "Glömt lösenord",
+ "description": "Ange e-postadressen som är kopplad till ditt konto så skickar vi en återställningslänk.",
+ "emailPlaceholder": "din@e-post.se",
+ "submit": "Skicka återställningslänk",
+ "sending": "Skickar...",
+ "sent": "Om e-postadressen är registrerad har en återställningslänk skickats. Kolla din inkorg.",
+ "backToLogin": "Tillbaka till inloggningen",
+ "error": "Något gick fel. Försök igen."
+ },
+ "resetPassword": {
+ "title": "Återställ lösenord",
+ "newPassword": "Nytt lösenord",
+ "confirmPassword": "Bekräfta nytt lösenord",
+ "submit": "Ange nytt lösenord",
+ "saving": "Sparar...",
+ "success": "Lösenordet har uppdaterats. Du skickas vidare till inloggningen...",
+ "mismatch": "Lösenorden stämmer inte överens",
+ "tooShort": "Lösenordet måste vara minst 10 tecken långt",
+ "missingToken": "Ogiltig återställningslänk. Begär en ny lösenordsåterställning.",
+ "error": "Ogiltig eller utgången återställningslänk",
+ "backToLogin": "Tillbaka till inloggningen"
+ }
+ },
+ "settings": {
+ "title": "Inställningar",
+ "sectionAccount": "Konto",
+ "sectionProfile": "Profil",
+ "avatarChange": "Byt bild",
+ "avatarRemove": "Ta bort",
+ "avatarUploading": "Laddar upp...",
+ "avatarTypeError": "Endast JPEG- och PNG-bilder är tillåtna",
+ "avatarSizeError": "Bilden är för stor (högst 2 MB)",
+ "displayName": "Visningsnamn",
+ "email": "E-postadress",
+ "nativeLanguage": "Modersmål",
+ "uiLocale": "Gränssnittsspråk",
+ "uiLocaleHint": "Ändrar språket i menyer och på knappar. Sidan behöver laddas om.",
+ "newPassword": "Nytt lösenord",
+ "newPasswordPlaceholder": "Lämna tomt för att behålla det nuvarande",
+ "confirmPassword": "Bekräfta lösenord",
+ "confirmPasswordPlaceholder": "Upprepa det nya lösenordet",
+ "bio": "Om dig (AI-sammanhang)",
+ "bioPlaceholder": "Till exempel: Jag är programutvecklare och reser ofta i jobbet...",
+ "bioHint": "AI-handledaren använder detta för att anpassa samtalen.",
+ "saveChanges": "Spara ändringar",
+ "saving": "Sparar...",
+ "saved": "Profilen har uppdaterats.",
+ "passwordMismatch": "Lösenorden stämmer inte överens",
+ "saveFailed": "Det gick inte att spara",
+ "logout": "Logga ut",
+ "sectionAppearance": "Utseende",
+ "theme": "Tema",
+ "darkActive": "Mörkt läge aktivt",
+ "lightActive": "Ljust läge aktivt",
+ "systemActive": "Automatiskt (system)",
+ "themeSystem": "Auto",
+ "themeDark": "Mörkt",
+ "themeLight": "Ljust",
+ "switchToLight": "Byt till ljust läge",
+ "switchToDark": "Byt till mörkt läge",
+ "sectionConversation": "Samtal",
+ "conversationMaxDuration": "Maximal samtalslängd",
+ "conversationInactivityTimeout": "Tidsgräns vid inaktivitet",
+ "conversationSpeechPause": "Paus innan turen avslutas",
+ "conversationSpeechPauseHint": "Hur länge du kan pausa mitt i en mening innan din tur skickas. Automatiskt läge anpassas efter din nivå.",
+ "speechPauseAuto": "Auto",
+ "speechPauseSec1": "1 sekund",
+ "speechPauseSec2": "2 sekunder",
+ "speechPauseSec3": "3 sekunder",
+ "min15": "15 minuter",
+ "min30": "30 minuter",
+ "min1": "1 minut",
+ "min3": "3 minuter",
+ "min5": "5 minuter",
+ "saveConversation": "Spara",
+ "conversationSaved": "Samtalsinställningarna har sparats.",
+ "sectionLegal": "Juridiskt",
+ "termsOfService": "Användarvillkor",
+ "privacyPolicy": "Integritetspolicy",
+ "contactLink": "Kontakt",
+ "deleteAccount": "Radera konto",
+ "deleteAccountTitle": "Radera konto",
+ "deleteAccountMessage": "Detta raderar ditt konto och alla dina uppgifter permanent. Åtgärden kan inte ångras.",
+ "deleteAccountConfirm": "Radera konto",
+ "sectionUsageLimits": "Användningsgränser",
+ "sectionVoice": "Röst",
+ "voiceHint": "Välj den AI-röst du föredrar i listan och klicka sedan på spela upp för att höra den.",
+ "voicePlay": "Spela upp",
+ "voiceStop": "Stoppa",
+ "quotaSessions": "Samtal per vecka",
+ "quotaMinutesDay": "Minuter per dag",
+ "quotaMinutesWeek": "Minuter per vecka",
+ "quotaTokens": "Token per månad",
+ "quotaUnlimited": "Obegränsat",
+ "quotaLoading": "Laddar...",
+ "quotaHint": "Användningen nollställs varje vecka (samtal och minuter) och varje månad (token).",
+ "sectionMemory": "Minnen",
+ "memoryEmpty": "Inga minnen sparade än. AI:n kommer att minnas viktiga detaljer om dig från era samtal och spara dem här.",
+ "memoryClearAll": "Rensa alla minnen",
+ "memoryClearAllTitle": "Rensa alla minnen",
+ "memoryClearAllMessage": "Alla sparade minnen raderas permanent. Åtgärden kan inte ångras.",
+ "memoryClearAllConfirm": "Rensa alla",
+ "memoryManage": "Hantera minnen",
+ "memoryBreadcrumb": "Minnesinställningar",
+ "memoryDescription": "Minnen delas mellan alla språk du lär dig. Lingu kan spara användbara detaljer under samtal, och du kan lägga till eller ta bort dem här.",
+ "memoryInputLabel": "Lägg till ett minne",
+ "memoryInputPlaceholder": "Till exempel: Jag föredrar kortfattade förklaringar.",
+ "memoryInputHint": "Högst {max} tecken",
+ "memoryAdd": "Lägg till",
+ "memoryAdding": "Lägger till...",
+ "memoryAddSuccess": "Minnet har lagts till.",
+ "memoryAddError": "Det gick inte att lägga till minnet. Försök igen.",
+ "memoryDuplicateError": "Det minnet finns redan.",
+ "memoryLoadError": "Det gick inte att ladda minnena.",
+ "memoryDeleteLabel": "Radera minne: {content}",
+ "memoryDeleteSuccess": "Minnet har raderats.",
+ "memoryDeleteError": "Det gick inte att radera minnet.",
+ "memoryClearing": "Rensar...",
+ "memoryClearSuccess": "Alla minnen har raderats.",
+ "memoryClearError": "Det gick inte att rensa minnena.",
+ "memorySourceChat": "Chatt",
+ "memorySourceVoice": "Röst",
+ "memorySourceManual": "Manuellt",
+ "memorySourceUnknown": "Minne",
+ "sectionLanguages": "Språk",
+ "languagesManage": "Mina språk",
+ "cardProfileAccess": "Profil och åtkomst",
+ "cardSessionSecurity": "Session och säkerhet",
+ "cardTheme": "Tema",
+ "cardConversationTiming": "Samtalstid",
+ "cardTutorVoice": "Handledarens röst",
+ "cardCurrentUsage": "Aktuell användning",
+ "cardProductReview": "Produktomdöme",
+ "cardLegalDocuments": "Juridiska dokument",
+ "sectionReview": "Omdöme",
+ "reviewDescription": "Berätta om din upplevelse av FreeLingo. Om du redigerar ett redan godkänt omdöme behöver det granskas igen innan det visas offentligt.",
+ "reviewPending": "Väntar på godkännande",
+ "reviewUpdate": "Uppdatera",
+ "reviewSaved": "Omdömet har sparats.",
+ "reviewDelete": "Radera omdöme",
+ "reviewDeleteTitle": "Radera omdöme",
+ "reviewDeleteMessage": "Vill du radera ditt omdöme? Åtgärden kan inte ångras.",
+ "reviewDeleteConfirm": "Radera",
+ "reviewDeleted": "Omdömet har raderats.",
+ "reviewDeleteError": "Det gick inte att radera omdömet. Försök igen."
+ },
+ "legal": {
+ "terms": {
+ "pageTitle": "Användarvillkor",
+ "updated": "Senast uppdaterad: augusti 2026",
+ "s1Title": "1. Om FreeLingo",
+ "s1Body": "FreeLingo är en plattform med öppen källkod för språkinlärning med AI, tillgänglig på två sätt: (1) med egen drift, där du kör tjänsten på din egen infrastruktur utan kostnad, och (2) som en hostad tjänst som drivs av FreeLingo-teamet med en gratisplan och betalda prenumerationer. Genom att använda tjänsten på något av dessa sätt godkänner du dessa användarvillkor.",
+ "s2Title": "2. Tillåten användning",
+ "s2Intro": "Du får endast använda FreeLingo för personlig språkinlärning. Du förbinder dig att inte:",
+ "s2i1": "Försöka få obehörig åtkomst till någon del av systemet.",
+ "s2i2": "Ladda upp skadligt innehåll eller försöka störa tjänsten.",
+ "s2i3": "Dela dina kontouppgifter med tredje part.",
+ "s2i4": "Använda automatiserade skript eller bottar för att missbruka AI-handledarens funktioner.",
+ "s3Title": "3. Användarkonton",
+ "s3Body": "Du måste vara minst 16 år för att skapa ett konto och använda FreeLingo. Genom att registrera dig bekräftar du att du uppfyller ålderskravet. Om vi får veta att ett konto tillhör någon under 16 år raderar vi kontot och alla tillhörande uppgifter utan föregående meddelande. Du ansvarar för att skydda dina inloggningsuppgifter och för all aktivitet på ditt konto. Den som driver instansen – du själv vid egen drift eller FreeLingo-teamet för den hostade tjänsten – kan stänga av eller avsluta konton som bryter mot dessa villkor.",
+ "s4Title": "4. Innehåll och AI-interaktioner",
+ "s4Body": "Samtal med AI-handledaren lagras i databasen för att din inlärning ska kunna fortsätta över tid. I den hostade tjänsten behandlas AI-chatt och röstfunktioner via OpenAI:s API, och ditt innehåll omfattas av OpenAI:s användningspolicyer. AI kan ge felaktiga svar – använd alltid ditt eget omdöme. FreeLingo ansvarar inte för innehåll som genereras av underliggande språkmodeller. Det innehåll som AI-handledaren genererar (övningar, rättelser, förklaringar och annat material) tillhör dig för ditt personliga bruk. FreeLingo gör inga anspråk på äganderätten till dina samtal eller det innehåll som genereras från dem.",
+ "s5Title": "5. Tjänstens olika former",
+ "s5Body": "Installationer med egen drift är kostnadsfria och den som driver dem har full kontroll över sina data och sin infrastruktur. Den hostade tjänst som drivs av FreeLingo-teamet erbjuder en gratisplan med användningsgränser och betalda prenumerationer för obegränsad åtkomst, per månad eller år. Gratisplanen omfattar en sjudagars provperiod med full åtkomst (inget betalningsmedel krävs) och därefter begränsade dagliga kvoter för lektioner och AI-chatt samt veckovisa kvoter för hörförståelse, läsning och röstsamtal. Betalda prenumerationer tar bort alla användningsgränser för dessa funktioner. I den hostade tjänsten lagras dina data på servrar som drivs av FreeLingo-teamet. Aktuella användningsgränser för ditt konto visas på din översikt.",
+ "s6Title": "6. Prenumeration och betalning",
+ "s6Body": "Gratisplanen ger begränsad åtkomst till AI-funktioner med en sjudagars provperiod med full åtkomst för nya användare (inget betalningsmedel krävs). Betalda prenumerationer erbjuds per månad eller år och förnyas automatiskt om de inte sägs upp i förväg. Du kan när som helst säga upp prenumerationen under Inställningar; uppsägningen träder i kraft när den aktuella perioden löper ut. Operatören kan också avsluta förnyelser eller prenumerationer om tjänsten ändras eller stängs. Om tjänsten upphör innan en betald period är slut gäller eventuell återbetalning som krävs enligt konsumentlagstiftning. Ångerrätt och gällande priser visas och hanteras i enlighet med tillämplig lag.",
+ "s7Title": "7. Tillgänglighet",
+ "s7Body": "Den hostade tjänsten tillhandahålls utan garantier för kontinuitet eller tillgänglighet. Operatören kan när som helst ändra, pausa eller stänga den av tekniska, ekonomiska eller andra skäl. Planerade nedstängningar meddelas genom appen eller via e-post när det är rimligen möjligt. Efter det angivna datumet kan konton och driftsdata raderas utan möjlighet till återställning. FreeLingo erbjuder ingen säkerhetskopieringstjänst eller något automatiserat exportverktyg.",
+ "s8Title": "8. Ansvarsbegränsning",
+ "s8Body": "FreeLingo och dess bidragsgivare ansvarar inte för direkta, indirekta, tillfälliga eller efterföljande skador som uppstår genom din användning av, eller oförmåga att använda, tjänsten. I den utsträckning tillämplig lag tillåter får det sammanlagda ansvaret inte överstiga det belopp du betalat under de tre månader som föregick anspråket. Du förbinder dig att hålla FreeLingo och dess bidragsgivare skadeslösa från tredje parts anspråk, skador eller kostnader som uppstår genom att du använder tjänsten i strid med dessa villkor.",
+ "s9Title": "9. Tillämplig lag",
+ "s9Body": "Dessa villkor regleras av spansk lag. Tvister som uppstår till följd av villkoren ska avgöras exklusivt av domstolar i Spanien, om inte tvingande konsumentskyddslagstiftning i ditt bosättningsland föreskriver annat.",
+ "s10Title": "10. Ändringar av villkoren",
+ "s10Body": "Villkoren kan uppdateras när som helst. Väsentliga ändringar meddelas via tjänstens gränssnitt eller via e-post när det är lämpligt. Fortsatt användning efter en ändring innebär att du godkänner de reviderade villkoren.",
+ "s11Title": "11. Ogiltiga bestämmelser",
+ "s11Body": "Om en domstol finner att en bestämmelse i dessa villkor är ogiltig eller inte kan verkställas, ska den begränsas eller tas bort i minsta möjliga utsträckning och övriga bestämmelser fortsätta att gälla fullt ut.",
+ "linkPrivacy": "Integritetspolicy",
+ "linkBack": "Tillbaka till registreringen",
+ "linkBackSettings": "Tillbaka till Inställningar",
+ "linkContact": "Om du har frågor om villkoren kan du använda kontaktformuläret på freelingo.app så länge det finns tillgängligt eller kontaktvägen som anges i FreeLingos GitHub-arkiv."
+ },
+ "privacy": {
+ "pageTitle": "Integritetspolicy",
+ "updated": "Senast uppdaterad: augusti 2026",
+ "s1Title": "1. Översikt",
+ "s1Body": "FreeLingo finns som en app för egen drift och som en hostad tjänst. Vid egen drift lagras alla data uteslutande på operatörens server och FreeLingo-projektets upphovspersoner har inte tillgång till dem. För den hostade tjänsten är FreeLingo-teamet personuppgiftsansvarigt, och denna policy beskriver hur dina personuppgifter samlas in, används och skyddas.",
+ "s2Title": "2. Uppgifter vi samlar in",
+ "s2Intro": "FreeLingo riktar sig inte till barn under 16 år. Vi samlar inte medvetet in personuppgifter från personer under 16 år. Om du är under 16 år ska du inte skapa ett konto eller lämna personuppgifter. När du skapar ett konto och använder FreeLingo lagras följande uppgifter:",
+ "s2i1": "Användarnamn, visningsnamn och e-postadress.",
+ "s2i2": "Hashat lösenord (bcrypt – ditt lösenord i klartext lagras aldrig).",
+ "s2i3": "Val av modersmål och inlärningsinställningar.",
+ "s2i4": "Valfri profilbild.",
+ "s2i5": "Historik över chattar och samtal med AI-handledaren.",
+ "s2i6": "Gloskort, studieframsteg och data för utspridd repetition.",
+ "s2i7": "Resultat från nivåbedömningar och framsteg i kursplanen.",
+ "s2i8": "Prenumerationsstatus, plantyp och Stripe-kund-ID (för prenumeranter på den betalda tjänsten).",
+ "s2i9": "Globala kontominnen och preferenser som AI-handledaren lär sig under samtal eller som du själv lägger till för att anpassa framtida interaktioner på alla inlärningsspråk.",
+ "s2i10": "Funktionsförslag, felrapporter, kommentarer och röster som du skickar in via feedbacktavlan. Endast ditt användarnamn visas offentligt; alla användare kan se publicerade förslag och rapporter.",
+ "s3Title": "3. Hur vi använder dina uppgifter",
+ "s3Intro": "Dina uppgifter används uteslutande för att ge dig FreeLingos inlärningsupplevelse:",
+ "s3i1": "För att autentisera dig och upprätthålla din session.",
+ "s3i2": "För att anpassa din studieplan och dina interaktioner med handledaren.",
+ "s3i3": "För att följa dina inlärningsframsteg över tid.",
+ "s3i4": "För att generera sammanhangsanpassade svar från AI-handledaren.",
+ "s3i5": "För att komma ihåg dina preferenser och din situation så att handledaren kan anpassa framtida interaktioner.",
+ "s3i6": "För att visa och hantera förslag och felrapporter från användarna.",
+ "s3Footer": "Dina uppgifter säljs, hyrs ut eller delas aldrig med marknadsföringstjänster från tredje part.",
+ "s4Title": "4. Externa AI-tjänster",
+ "s4Body": "I den hostade tjänsten tillhandahålls AI-chatt, röstsyntes (TTS) och taltranskribering (STT) via OpenAI:s API. Ditt samtalsinnehåll och ljud skickas till OpenAI för behandling enligt OpenAI:s integritetspolicy (openai.com/privacy). Betalningar hanteras av Stripe, Inc. (USA); Stripe behandlar betalningsuppgifter direkt och vi lagrar inga betalkortsuppgifter på våra servrar – dina transaktionsuppgifter omfattas av Stripes integritetspolicy (stripe.com/privacy). Vid egen drift väljer operatören AI-leverantör; som standard används en lokal Ollama-instans och inga uppgifter lämnar servern.",
+ "s5Title": "5. Kakor och sessionstoken",
+ "s5Body": "FreeLingo använder två tekniska kakor som är nödvändiga för att tjänsten ska fungera: refresh_token (httpOnly, 30 dagars giltighet) för sessionens autentisering och NEXT_LOCALE (ett års giltighet) för att spara ditt språkval. Inga kakor för spårning eller reklam används. För anonym mätning av trafik använder vi Umami – ett självhostat verktyg med öppen källkod som inte använder kakor, inte samlar in personuppgifter och lagrar all data på våra egna servrar.",
+ "s6Title": "6. Dina rättigheter (GDPR)",
+ "s6Body": "Om du befinner dig inom Europeiska ekonomiska samarbetsområdet har du enligt GDPR rätt till tillgång, rättelse, radering, invändning och dataportabilitet. Du kan själv radera ditt konto i Inställningar. Andra förfrågningar hanteras manuellt via kontakten i avsnitt 8; FreeLingo erbjuder inget automatiserat exportverktyg. Vi behandlar personuppgifter för att tillhandahålla tjänsten (artikel 6.1 b i GDPR). OpenAI och Stripe kan behandla uppgifter i USA enligt de skyddsåtgärder som gäller för internationella överföringar. Du kan lämna klagomål till den spanska dataskyddsmyndigheten (aepd.es) eller tillsynsmyndigheten i ditt land.",
+ "s7Title": "7. Lagring och radering av uppgifter",
+ "s7Body": "Driftsdata sparas så länge ditt konto finns och den hostade tjänsten är aktiv. Du kan radera minnen, språk eller hela kontot i Inställningar. Operatören kan också radera alla konton och uppgifter om tjänsten stängs, efter ett skäligt meddelande när det är möjligt. Radering kan inte ångras. Efter att system och kopior under operatörens kontroll har raderats behåller FreeLingo inga personuppgifter. Stripe kan behålla betalnings- och transaktionsuppgifter enligt egna regler och rättsliga skyldigheter.",
+ "s8Title": "8. Kontakt",
+ "s8Body": "För integritetsfrågor om den hostade tjänsten kan du använda kontaktformuläret på freelingo.app så länge det finns tillgängligt. Efter en nedstängning använder du kontaktvägen som anges i FreeLingos GitHub-arkiv och lägger inte upp personuppgifter i offentliga ärenden. Vid egen drift kontaktar du instansens administratör.",
+ "s9Title": "9. Ogiltiga bestämmelser",
+ "s9Body": "Om en bestämmelse i denna policy bedöms vara ogiltig eller inte kan verkställas, ska den begränsas eller tas bort i minsta möjliga utsträckning och övriga bestämmelser fortsätta att gälla fullt ut.",
+ "linkTerms": "Användarvillkor",
+ "linkBack": "Tillbaka till registreringen",
+ "linkBackSettings": "Tillbaka till Inställningar"
+ }
+ },
+ "dashboard": {
+ "welcomeBack": "Välkommen tillbaka",
+ "streak": "Svit",
+ "xp": "XP",
+ "level": "Nivå",
+ "skills": "Färdigheter",
+ "today": "I dag",
+ "noSkills": "Slutför nivåbedömningen för att följa dina färdigheter.",
+ "noLessons": "Inga lektioner planerade i dag.",
+ "lessonDone": "klar",
+ "startLesson": "Starta",
+ "loadingProgress": "Laddar framsteg...",
+ "noLevel": "Ingen nivå har angetts än.",
+ "completeAssessment": "Gör nivåbedömningen",
+ "allCaughtUp": "Din plan, i din takt",
+ "levelTestReady": "Ditt nivåtest är redo.",
+ "levelTestReadyDesc": "Du har nått slutet av din plan. Gör nivåtestet för att bekräfta dina framsteg.",
+ "levelTestCompleted": "Nivåtestet är slutfört.",
+ "levelTestScoreLine": "Slutresultat: {score}. Ditt resultat har sparats i Min plan.",
+ "startWithAssessment": "Börja med en nivåbedömning.",
+ "takeAssessmentArrow": "Gör nivåbedömningen →",
+ "goToMyPlan": "Min plan →",
+ "quickActions": "Snabbåtgärder",
+ "dayProgress": "Dag {current} av {total}",
+ "skipDay": "Hoppa över i dag",
+ "pendingLessons": "väntande lektioner",
+ "nextStep": "Nästa steg",
+ "assessmentCreatesPlan": "Nivåbedömningen hittar din nivå och skapar en personlig plan för ditt aktiva språk.",
+ "lessonReady": "Ditt nästa steg, i din takt",
+ "pendingStillAvailable": "Du har {count} tidigare lektioner som väntar i din plan.",
+ "noPendingToday": "Se i din plan hur du kan fortsätta.",
+ "planDayGoal": "Dagens mål i din plan",
+ "lessonsCompleted": "Lektioner",
+ "accuracy": "Träffsäkerhet",
+ "exerciseStats": "{correct}/{total} övningar",
+ "noExercisesYet": "Inga övningar än",
+ "planProgress": "Planens framsteg",
+ "currentDay": "Aktuell dag",
+ "daysRemaining": "Dagar kvar",
+ "completedToday": "{completed}/{total} klara",
+ "vocabularyProgress": "Ordförråd {level}",
+ "vocabularyWords": "{mastered}/{total} ord",
+ "recentPerformance": "Senaste resultat",
+ "recentPerformanceDescription": "Baserat på dina senaste svar och repetitioner.",
+ "performanceNeedsPractice": "Behöver övas",
+ "performanceInProgress": "Pågår",
+ "performanceStrong": "Stark",
+ "premiumBannerTitle": "Lås upp samtal, hörförståelse och läsning",
+ "premiumBannerDesc": "Börja med en gratis provperiod och öva med Lingu genom text, röst och övningar anpassade till din nivå.",
+ "premiumBannerDescTrialUsed": "Aktivera Premium för att låsa upp textchatt med Lingu, röstsamtal, läsning och hörförståelse.",
+ "premiumBannerCta": "Gratis provperiod",
+ "premiumBannerCtaTrialUsed": "Se planer",
+ "premiumBannerPastDueTitle": "Betalning väntar",
+ "premiumBannerPastDueDesc": "Uppdatera din betalningsmetod för att återfå Premium-åtkomst till chatt, röstsamtal, läsning och hörförståelse.",
+ "premiumBannerPastDueCta": "Återställ Premium",
+ "freemiumTrialTitle": "{days, plural, one {# dag gratis provperiod} other {# dagar gratis provperiod}}",
+ "freemiumTrialDesc": "Få obegränsad åtkomst till chatt, röst, hörförståelse och läsning. Prova alla Premium-funktioner gratis.",
+ "announcementDismiss": "Stäng meddelandet",
+ "announcementDismissError": "Det gick inte att stänga meddelandet. Försök igen."
+ },
+ "assessment": {
+ "title": "Nivåbedömning",
+ "subtitle": "Placering enligt GERS",
+ "intro": "Vi ställer några frågor för att avgöra din nivå i engelska.",
+ "startWriting": "Starta skrivbedömning",
+ "startQuiz": "Starta quiz",
+ "writing": {
+ "title": "Skrivprov",
+ "submit": "Skicka in text",
+ "placeholder": "Skriv ditt svar här...",
+ "evaluating": "Bedömer..."
+ },
+ "quiz": {
+ "question": "Fråga",
+ "of": "av",
+ "next": "Nästa",
+ "submit": "Skicka in quiz",
+ "submitting": "Skickar..."
+ },
+ "result": {
+ "title": "Din nivå",
+ "xpEarned": "Intjänade XP",
+ "skillsTitle": "Färdigheter",
+ "goToPlan": "Visa min plan",
+ "generatePlan": "Skapa studieplan"
+ },
+ "currentLevel": "Aktuell nivå",
+ "assessedOn": "Bedömd den",
+ "alreadyHasPlan": "Du har redan en aktiv studieplan. Om du gör om bedömningen skapas en ny.",
+ "retake": "Gör om nivåbedömningen",
+ "evaluating": "Bedömer...",
+ "resultStep": "Resultat",
+ "cefrLevel": "GERS-nivå",
+ "overrideLevel": "Ändra startnivå",
+ "strengths": "Styrkor",
+ "needsWork": "Behöver övas",
+ "createPlan": "Skapa plan →",
+ "creating": "Skapar...",
+ "loading": "Laddar...",
+ "levelTest": {
+ "title": "Nivåtest",
+ "loading": "Laddar frågor...",
+ "loadingTest": "Laddar test...",
+ "submittingTest": "Skickar svar...",
+ "quizTitle": "Nivåtest för {level}",
+ "resultsTitle": "Nivåtest för {level} – Resultat",
+ "finalScore": "Slutresultat",
+ "correctCount": "{correct} / {total} rätt",
+ "correctAnswer": "✓ Rätt",
+ "incorrectAnswer": "✗ Fel – rätt svar: {answer}",
+ "nextLevelFallback": "NÄSTA NIVÅ",
+ "advanceLabel": "REDO FÖR {level}",
+ "advanceMessage": "Du har visat goda kunskaper på nivå {level}. Gör om nivåbedömningen för att skapa din plan för {next}.",
+ "extendLabel": "FYRA VECKORS FÖRLÄNGNING",
+ "extendMessage": "Vi har hittat områden att utveckla: {areas}. Vi rekommenderar fyra extra veckor med riktad övning.",
+ "repeatLabel": "REPETERA {level}",
+ "repeatMessage": "Resultat under 55 %. Flera viktiga färdigheter på nivå {level} behöver stärkas.",
+ "reviewPlan": "Granska min plan →",
+ "goToPlan": "Gå till planen",
+ "invalidPlan": "Ogiltigt plan-ID. Öppna nivåtestet från din plansida.",
+ "noQuestions": "Inga frågor togs emot från servern.",
+ "loadFailed": "Det gick inte att ladda nivåtestet.",
+ "submitFailed": "Det gick inte att skicka in svaren.",
+ "question": "Fråga",
+ "of": "av",
+ "confirm": "Bekräfta svar",
+ "nextQuestion": "Nästa fråga",
+ "submit": "Skicka in test",
+ "result": {
+ "title": "Testet är klart",
+ "recommendation": "Rekommendation",
+ "advance": "Redo att gå vidare till nästa nivå",
+ "extend": "Fortsätt på din nuvarande nivå",
+ "repeat": "Vi rekommenderar att du repeterar din nuvarande nivå",
+ "back": "Tillbaka till planen"
+ }
+ },
+ "suggestedLevel": "Föreslagen nivå {aiLevel}; planen använder {selectedLevel}",
+ "step1": "Steg 1 / 3: Din nivå",
+ "step2": "Steg 2 / 3: Fråga {questionNumber} / {totalQuestions}",
+ "dontKnow": "Jag vet inte",
+ "step3": "Steg 3 / 3: Ditt program",
+ "studiedBefore": "Har du studerat {language} tidigare?",
+ "studiedBeforeHint": "Det hjälper oss att starta quizet på rätt nivå.",
+ "beginnerOption": "Jag är helt nybörjare",
+ "beginnerOptionHint": "Jag börjar på A1 och behöver inget quiz.",
+ "hasExperienceOption": "Ja, jag har viss erfarenhet",
+ "hasExperienceOptionHint": "Jag gör ett kort adaptivt quiz (cirka 12 frågor).",
+ "skills": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordförråd",
+ "reading": "Läsning"
+ },
+ "howManyWeeks": "Hur många veckor för nivå {cefr_level}?",
+ "nWeeks": "{count} veckor",
+ "intensity": {
+ "intensive": "Intensiv",
+ "standard": "Standard",
+ "relaxed": "Lugn",
+ "veryRelaxed": "Mycket lugn"
+ },
+ "approxLessons": "Cirka {count} lektioner",
+ "daysPerWeek": "{count} dagar/vecka",
+ "mainGoals": "Huvudmål (välj alla som passar)",
+ "goals": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordförråd",
+ "reading": "Läsning",
+ "writing": "Skrivning",
+ "conversation": "Samtal",
+ "listening": "Hörförståelse"
+ },
+ "summaryLevel": "Nivå",
+ "summaryDuration": "Längd",
+ "summaryGoals": "Mål",
+ "noneSelected": "inga valda",
+ "buildingPlan": "Bygger plan...",
+ "startWarningTitle": "Innan du börjar",
+ "startWarningMessage": "Om du lämnar nivåbedömningen innan den är klar förlorar du dina framsteg och måste börja om.",
+ "startWarningMessageLevelTest": "Om du lämnar nivåtestet innan det är klart förlorar du dina framsteg och måste göra om det.",
+ "startWarningConfirm": "Starta",
+ "startMyPlan": "Starta min plan",
+ "voiceTrialLabel": "Prova röst",
+ "voiceTrialTitle": "Prova ett riktigt samtal nu",
+ "voiceTrialDesc": "Prata med Lingu i {minutes} minuter, gratis. Din handledare anpassar sig till den nivå du just uppnått.",
+ "voiceTrialStart": "Starta gratis samtal",
+ "voiceTrialSkip": "Hoppa över just nu"
+ },
+ "plan": {
+ "unitLocked": "Enheten är låst",
+ "unitAvailable": "Enheten är tillgänglig",
+ "lessonPending": "Väntande lektion",
+ "title": "Min plan",
+ "subtitle": "Studieplan",
+ "noPlan": "Ingen aktiv studieplan hittades.",
+ "startAssessment": "Gör nivåbedömningen först",
+ "week": "Vecka",
+ "currentLesson": "Aktuell lektion",
+ "currentUnit": "Aktuell enhet",
+ "startLesson": "Starta lektion",
+ "completed": "Slutförd",
+ "generating": "Skapar lektion...",
+ "generatingError": "Det gick inte att skapa lektionen",
+ "loading": "Laddar plan...",
+ "learningRoadmap": "Lärandeplan",
+ "level": "Nivå",
+ "duration": "Längd",
+ "durationDetail": "{weeks} veckor · {days} dagar/vecka",
+ "unitsLabel": "Enheter",
+ "noUnitsForLevel": "Inga enheter tillgängliga för nivå {level}",
+ "takeAssessmentArrow": "Gör nivåbedömningen →",
+ "unitLabel": "Enhet",
+ "grammarCovered": "Genomgången grammatik",
+ "noLessons": "Inga lektioner planerade för den här enheten än.",
+ "weekDay": "Vecka {week}, dag {day}",
+ "lessonTypes": {
+ "grammar": "Grammatik",
+ "vocabulary": "Ordförråd",
+ "reading": "Läsning",
+ "writing": "Skrivning",
+ "listening": "Hörförståelse",
+ "conversation": "Samtal",
+ "review": "Repetition",
+ "level_test": "Nivåtest"
+ },
+ "levelComplete": "Nivå {level} klar. Gör nivåtestet",
+ "levelCompleteDesc": "Du har nått slutet av din plan för nivå {level}. Gör nivåtestet för att bekräfta dina framsteg och låsa upp nästa nivå.",
+ "levelCompleteHint": "Testet tar cirka 20 minuter och omfattar grammatik, ordförråd och läsning.",
+ "beginLevelTest": "Starta nivåtestet",
+ "nLessons": "{count, plural, one {# lektion} other {# lektioner}}",
+ "nGrammar": "{count} grammatiklektioner",
+ "levelTestLabel": "Nivåtest",
+ "unitAriaLabel": "Enhet {index}: {title}",
+ "noUnitsDesc": "Innehållet för denna nivå håller på att förberedas. Kom tillbaka snart.",
+ "completionTestTitle": "Avslutande test för nivå {level}",
+ "levelTestResult": "Resultat från nivåtest",
+ "testScore": "Poäng:",
+ "pendingLessons": "Väntande lektioner",
+ "pendingReassurance": "Du kan fortsätta med dem när det passar dig.",
+ "resume": "Fortsätt",
+ "lessonsHeader": "Lektioner · {count}",
+ "reviewLesson": "Repetition",
+ "noPlanLabel": "KOM IGÅNG",
+ "noPlanTitle": "Skapa din studieplan",
+ "noPlanDesc": "Gör en kort nivåbedömning för att upptäcka din nivå och få en personlig lärandeplan.",
+ "noPlanHint": "Tar cirka 10 minuter"
+ },
+ "lesson": {
+ "loading": "Laddar lektion...",
+ "notFound": "Lektionen hittades inte",
+ "backToPlan": "Tillbaka till planen",
+ "progress": "Framsteg",
+ "exercise": "Övning",
+ "of": "av",
+ "checkAnswer": "Kontrollera svar",
+ "nextExercise": "Nästa övning",
+ "submitWriting": "Skicka in text",
+ "yourAnswer": "Ditt svar...",
+ "correct": "Rätt",
+ "incorrect": "Fel",
+ "feedback": "Återkoppling",
+ "corrections": "Rättelser",
+ "completeLesson": "Slutför lektion",
+ "completing": "Slutför...",
+ "relatedGrammar": "Relaterad grammatik",
+ "explanation": "Förklaring",
+ "exercises": "Övningar",
+ "vocabulary": "Ordförråd",
+ "label": "Lektion",
+ "submitAnswer": "Skicka svar",
+ "lessonDone": "Bra jobbat. Lektionen är klar.",
+ "exerciseSummary": "Bedömda övningar: {completed} av {total}",
+ "score": "Poäng",
+ "showNativeExplanation": "Visa på",
+ "hint": "Ledtråd",
+ "showNativeHint": "Visa ledtråd på",
+ "regenerateExercise": "Skapa om övningen",
+ "regeneratingExercise": "Skapar om...",
+ "regenerateError": "Det gick inte att skapa om övningen. Försök igen.",
+ "regenerateNotNeeded": "Den här övningen behöver inte skapas om.",
+ "examples": "Exempel",
+ "commonTraps": "Vanliga fallgropar",
+ "miniGlossary": "Kort ordlista",
+ "pronunciationExercise": "Uttal",
+ "exerciseTypeMultipleChoice": "Flervalsfråga",
+ "exerciseTypeFillBlank": "Fyll i luckan",
+ "exerciseTypeFreeWrite": "Fritt skrivande",
+ "exerciseTypePronunciation": "Uttal",
+ "tapToListen": "Tryck för att lyssna",
+ "recordNow": "Spela in ditt svar",
+ "exit": "Avsluta",
+ "exitConfirmTitle": "Lämna lektionen?",
+ "exitConfirmMessage": "Dina framsteg i den här lektionen går förlorade.",
+ "dayComplete": "Dagen är klar!",
+ "dayCompleteMsg": "Du har slutfört den här dagen i din plan. Fortsätt när det passar dig."
+ },
+ "flashcards": {
+ "title": "Gloskort",
+ "due": "Dags att repetera",
+ "total": "Totalt",
+ "noDue": "Inga kort att repetera just nu",
+ "allCaughtUp": "Du är ikapp!",
+ "generate": "Skapa gloskort",
+ "generating": "Skapar...",
+ "topic": "Ämne",
+ "topicPlaceholder": "t.ex. reseordförråd",
+ "count": "Antal",
+ "level": "Nivå",
+ "submit": "Skapa",
+ "again": "Igen",
+ "hard": "Svårt",
+ "good": "Bra",
+ "easy": "Lätt",
+ "front": "Ord",
+ "back": "Definition",
+ "loading": "Laddar gloskort...",
+ "sectionGenerate": "Skapa nya kort",
+ "speakingMode": "Tala",
+ "standardMode": "Standard",
+ "sayWord": "Säg ordet högt",
+ "refresh": "Uppdatera",
+ "generateBtn": "Skapa",
+ "cards": "kort",
+ "myVocabularyBtn": "Mitt ordförråd",
+ "myVocabulary": "Mitt ordförråd",
+ "myVocabularyEmpty": "Inga sparade ord än. Markera ett ord när du läser för att spara det här.",
+ "myVocabularyNoResults": "Inga sparade ord matchar din sökning.",
+ "vocabularySearch": "Sök ord...",
+ "vocabularyPageInfo": "Sida {page} av {pages}",
+ "vocabularyPrev": "Föregående",
+ "vocabularyNext": "Nästa",
+ "backToFlashcards": "Gloskort",
+ "noCardsHint": "Använd + Skapa för att göra dina första AI-genererade kort",
+ "tapToHide": "tryck för att dölja",
+ "tapToReveal": "tryck för att visa",
+ "interval": "Intervall",
+ "repetitions": "Repetitioner"
+ },
+ "chat": {
+ "title": "AI-handledare",
+ "subtitle": "{language} – Fråga vad du vill",
+ "placeholder": "Skriv ett meddelande...",
+ "send": "Skicka",
+ "sending": "Skickar...",
+ "noConversation": "Välj eller starta ett samtal",
+ "newConversation": "Nytt",
+ "toggleSidebarShow": "Visa chattar",
+ "toggleSidebarHide": "Dölj chattar",
+ "conversations": "Samtal",
+ "voiceSession": "Röstsamtal",
+ "loading": "Laddar...",
+ "thinking": "Tänker...",
+ "takingLonger": "Det tar längre tid än väntat...",
+ "errorMessage": "Något gick fel. Försök igen.",
+ "deleteTitle": "Radera chatt",
+ "deleteMessage": "Det här samtalet raderas permanent.",
+ "deleteConfirm": "Radera",
+ "enterToSend": "Tryck på Enter för att skicka",
+ "continueInVoice": "Fortsätt med röst",
+ "memoryUpdated": "Minnet har uppdaterats"
+ },
+ "progress": {
+ "title": "Framsteg",
+ "subtitle": "Följ dina färdigheter",
+ "xp": "XP",
+ "streak": "Svit",
+ "lessons": "Lektioner",
+ "accuracy": "Träffsäkerhet",
+ "noPlan": "Ingen aktiv studieplan.",
+ "startPlan": "Starta nivåbedömningen",
+ "competencies": "Enhetskompetenser",
+ "vocabularySection": "Ordförråd",
+ "skills": "Träffsäkerhet per färdighet",
+ "mastered": "Behärskad",
+ "inProgress": "Pågår",
+ "notStarted": "Inte påbörjad",
+ "loading": "Laddar framsteg...",
+ "days": "dagar",
+ "noData": "Slutför några lektioner för att se dina framsteg här.",
+ "exploreVocab": "Utforska ordförråd",
+ "competenciesSection": "Grammatikkompetenser för {level}",
+ "noCompetencies": "Slutför lektioner för att börja följa dina kompetenser.",
+ "goToMyPlan": "→ Gå till Min plan",
+ "addToFlashcards": "Lägg till ord från en samling i dina gloskort för att följa hur väl du kan varje ord.",
+ "noActivePlanDesc": "Gör nivåbedömningen för att börja följa dina färdigheter.",
+ "vocabularyHeader": "Ordförråd för {level}",
+ "currentLevelOnly": "Endast aktuell nivå",
+ "allLevels": "Alla nivåer"
+ },
+ "grammar": {
+ "title": "Grammatikreferens",
+ "subtitle": "Referens",
+ "searchPlaceholder": "Sök grammatikämnen...",
+ "allLevels": "Alla nivåer",
+ "allCategories": "Alla",
+ "noResults": "Inga ämnen hittades.",
+ "structure": "Struktur",
+ "keyRules": "Viktiga regler",
+ "examples": "Exempel",
+ "commonMistakes": "Vanliga misstag",
+ "relatedTopics": "Relaterade ämnen",
+ "backToGrammar": "Grammatikreferens",
+ "explanation": "Förklaring",
+ "backLink": "Tillbaka till grammatiken",
+ "notFound": "Ämnet hittades inte.",
+ "topicsFound": "{count, plural, one {# ämne hittades} other {# ämnen hittades}}"
+ },
+ "vocabulary": {
+ "title": "Ordförråd",
+ "subtitle": "Ordförråd",
+ "all": "Alla",
+ "searchPlaceholder": "Sök ordsamlingar...",
+ "sets": "{count, plural, one {# samling} other {# samlingar}}",
+ "words": "ord",
+ "addAll": "Lägg till alla {count} som gloskort",
+ "adding": "Lägger till...",
+ "added": "{count} gloskort har lagts till",
+ "addError": "Det gick inte att lägga till gloskorten",
+ "frequency": "frekvens",
+ "backToVocabulary": "Ordförråd",
+ "notFound": "Samlingen hittades inte.",
+ "vocabularySet": "Ordsamling",
+ "cardsAdded": "{count} kort har lagts till bland dina gloskort.",
+ "goToFlashcards": "Gå till gloskort →",
+ "addFailed": "Det gick inte att lägga till gloskorten.",
+ "noResults": "Inga ordsamlingar matchar din sökning"
+ },
+ "phrasebook": {
+ "title": "Frasbok",
+ "subtitle": "Fraser",
+ "all": "Alla",
+ "allRegisters": "Alla stilnivåer",
+ "formal": "Formell",
+ "neutral": "Neutral",
+ "informal": "Informell",
+ "copied": "Kopierat!",
+ "copy": "Kopiera",
+ "copyPhrase": "Kopiera fras",
+ "noResults": "Inga fraser matchar dina filter",
+ "level": "Nivå",
+ "register": "Stilnivå",
+ "statsLine": "{situationCount} situationer · {phraseCount} fraser · {range}",
+ "situationsShown": "{count, plural, one {# situation visas} other {# situationer visas}}",
+ "searchPlaceholder": "Sök fraser..."
+ },
+ "admin": {
+ "title": "Administration",
+ "overview": "Översikt",
+ "users": "Användare",
+ "feedback": "Feedback",
+ "reviews": "Omdömen",
+ "system": "System",
+ "invites": "Inbjudningslänkar",
+ "createUser": "Skapa användare",
+ "createInvite": "Skapa inbjudan",
+ "deactivate": "Inaktivera",
+ "activate": "Aktivera",
+ "loading": "Laddar...",
+ "loadError": "Det gick inte att ladda användardata",
+ "userNotFound": "Användaren hittades inte",
+ "role": "Roll",
+ "status": "Status",
+ "active": "Aktiv",
+ "inactive": "Inaktiv",
+ "inviteCreated": "Inbjudningslänken har skapats",
+ "copyLink": "Kopiera länk",
+ "inviteCopied": "Inbjudningslänken har kopierats",
+ "deleteUser": "Radera användare",
+ "deleteUserMessage": "Vill du radera den här användaren? Åtgärden kan inte ångras.",
+ "deleteConfirm": "Radera",
+ "noUsers": "Inga användare hittades",
+ "inviteLink": "Inbjudningslänk (giltig i 48 timmar)",
+ "total": "totalt",
+ "delete": "Radera",
+ "inviteBtn": "Inbjudningslänk",
+ "createUserBtn": "Skapa användare",
+ "prevPage": "← Föregående",
+ "nextPage": "Nästa →",
+ "roleUser": "Användare",
+ "roleAdmin": "Administratör",
+ "fieldUsername": "Användarnamn",
+ "invalidUsernameChars": "Användarnamnet får bara innehålla bokstäver, siffror, bindestreck, understreck och punkter",
+ "fieldEmail": "E-postadress",
+ "fieldPassword": "Lösenord",
+ "fieldDisplayName": "Visningsnamn",
+ "fieldNativeLanguage": "Modersmål",
+ "fieldTargetLanguage": "Språk att lära sig",
+ "fieldRole": "Kontotyp",
+ "submitCreate": "Skapa",
+ "searchPlaceholder": "Sök efter användarnamn eller e-postadress...",
+ "statsTitle": "Användningsstatistik",
+ "statsCefr": "Aktuell nivå",
+ "statsUnit": "Aktuell enhet",
+ "statsPlanWeeks": "Planens längd",
+ "sectionXpProgress": "XP och framsteg",
+ "statsXp": "Totalt antal XP",
+ "statsStreak": "Svit",
+ "statsActiveDays": "Aktiva dagar",
+ "statsLessons": "Slutförda lektioner",
+ "statsExercises": "Rätt besvarade övningar",
+ "statsMessages": "Skickade meddelanden",
+ "statsTokensTotal": "Förbrukade token (totalt)",
+ "statsTokensChat": "Token för handledaren",
+ "statsTokensConversation": "Token för röst",
+ "statsTokensNote": "Tillgängligt endast när AI-leverantören skickar denna information",
+ "statsNoData": "Inga framstegsdata",
+ "viewStats": "Visa statistik",
+ "weeks": "veckor",
+ "statsTestScore": "Slutresultat på testet",
+ "statsDays": "dagar",
+ "sectionQuota": "Samtalskvoter",
+ "quotaWeeklySessions": "Gräns för samtal per vecka",
+ "quotaDailyMinutes": "Gräns för minuter per dag",
+ "quotaWeeklyMinutes": "Gräns för minuter per vecka",
+ "quotaMonthlyTokens": "Gräns för token per månad",
+ "quotaUnlimitedLabel": "Obegränsat (0)",
+ "quotaUsedSessions": "Använt den här veckan",
+ "quotaUsedMinutes": "Använt i dag",
+ "quotaUsedWeeklyMinutes": "Använt den här veckan (min)",
+ "quotaSave": "Spara kvoter",
+ "quotaSaved": "Kvoterna har sparats.",
+ "emailVerified": "E-postadress bekräftad",
+ "emailNotVerified": "E-postadress inte bekräftad",
+ "verifyEmail": "Bekräfta e-postadress",
+ "unverifyEmail": "Markera som obekräftad",
+ "emailVerifiedSuccess": "E-postadressen har markerats som bekräftad.",
+ "emailUnverifiedSuccess": "E-postadressen har markerats som obekräftad.",
+ "fieldSubscription": "Prenumeration",
+ "fieldEndsRenews": "Slutar / Förnyas",
+ "fieldStripeCustomer": "Stripe-kund",
+ "maintenanceTitle": "Underhållsläge",
+ "maintenanceOn": "PÅ",
+ "maintenanceEnable": "Aktivera underhållsläge",
+ "maintenanceDisable": "Inaktivera underhållsläge",
+ "maintenanceDesc": "När läget är aktivt är alla funktioner som kräver prenumeration (chatt med handledaren, röstsamtal samt hörförståelse- och läsövningar) otillgängliga för alla användare.",
+ "cannotDeactivateSelf": "Du kan inte inaktivera ditt eget konto",
+ "cannotDeleteSelf": "Du kan inte radera ditt eget konto",
+ "sectionLanguages": "Språk",
+ "sectionActivity": "Aktivitet",
+ "statsActive": "Aktiv",
+ "manageUsers": "Hantera användare",
+ "manageUsersDesc": "Granska konton, prenumerationer, roller och åtkomststatus.",
+ "createUserDesc": "Skapa ett administrerat användarkonto med ett första studiespråk.",
+ "reviewFeedback": "Granska feedback",
+ "reviewFeedbackDesc": "Hantera elevers funktionsförslag och felrapporter.",
+ "reviewReviews": "Granska omdömen",
+ "reviewReviewsDesc": "Godkänn verifierade omdömen för visning på startsidan.",
+ "maintenanceOnDesc": "Underhållsläget är aktivt för prenumerationsstyrda inlärningsverktyg.",
+ "maintenanceOffDesc": "Underhållsläget är avstängt. Prenumerationsstyrda inlärningsverktyg är tillgängliga.",
+ "openSystemControls": "Systeminställningar",
+ "adminRequired": "Administratörsåtkomst krävs",
+ "usersLoadError": "Det gick inte att ladda användare",
+ "createUserError": "Det gick inte att skapa användaren",
+ "updateUserError": "Det gick inte att uppdatera användaren",
+ "deleteUserError": "Det gick inte att radera användaren",
+ "inviteError": "Det gick inte att skapa inbjudningslänken",
+ "copyInviteError": "Det gick inte att kopiera inbjudningslänken",
+ "maintenanceError": "Det gick inte att ändra underhållsläget",
+ "maintenanceOff": "AV",
+ "inviteExpiry": "Engångslänk. Går ut efter 48 timmar.",
+ "searchAction": "Sök användare",
+ "roleFilter": "Filtrera efter roll",
+ "statusFilter": "Filtrera efter status",
+ "subscriptionFilter": "Filtrera efter prenumeration",
+ "allRoles": "Alla roller",
+ "allStatuses": "Alla statusar",
+ "allSubscriptions": "Alla prenumerationer",
+ "clearFilters": "Rensa",
+ "userColumn": "Användare",
+ "actions": "Åtgärder",
+ "createUserSheetDesc": "Kontot skapas direkt. E-postbekräftelse skickas om e-post är aktiverad.",
+ "activateUser": "Aktivera användare",
+ "deactivateUser": "Inaktivera användare",
+ "activateUserMessage": "Vill du aktivera det här användarkontot?",
+ "deactivateUserMessage": "Vill du inaktivera det här användarkontot? Användaren kan inte använda appen förrän kontot återaktiveras.",
+ "tabProfile": "Profil",
+ "tabLanguages": "Språk",
+ "tabActivity": "Aktivitet",
+ "tabQuota": "Kvoter",
+ "tabSubscription": "Prenumeration",
+ "sectionCommunication": "Kommunikation",
+ "quotaCurrentUsage": "Aktuell användning",
+ "quotaConfiguredLimits": "Inställda gränser",
+ "unitMinutes": "min",
+ "unitSessions": "samtal",
+ "unitTokens": "token",
+ "quotaZeroMeansUnlimited": "Ange gränsen 0 för obegränsad åtkomst.",
+ "quotaValidationError": "Alla kvotvärden måste vara heltal som är 0 eller högre.",
+ "quotaSaveError": "Det gick inte att spara kvotgränserna.",
+ "subscriptionOverride": "Manuell åsidosättning av prenumeration",
+ "subscriptionOverrideDesc": "Detta ändrar användarens åtkomstperiod direkt och kan åsidosätta status som hanteras av Stripe.",
+ "subscriptionUpdateError": "Det gick inte att uppdatera prenumerationen.",
+ "saving": "Sparar...",
+ "verifyEmailConfirmMessage": "Vill du markera den här e-postadressen som bekräftad?",
+ "unverifyEmailConfirmMessage": "Vill du markera den här e-postadressen som obekräftad?",
+ "subscriptionConfirmTitle": "Ändra prenumeration",
+ "subscriptionConfirmMessage": "Vill du använda prenumerationsplanen {plan}?",
+ "subscriptionConfirm": "Använd",
+ "feedbackStatusError": "Det gick inte att ändra status för feedback.",
+ "feedbackDeleteError": "Det gick inte att radera feedbackinlägget.",
+ "allFeedback": "All feedback",
+ "feedbackTotal": "Feedback totalt",
+ "feedbackQueue": "Feedbackkö",
+ "feedbackSearchPlaceholder": "Sök efter titel, beskrivning eller författare...",
+ "feedbackSearchAction": "Sök feedback",
+ "feedbackTypeFilter": "Filtrera efter feedbacktyp",
+ "noFilteredFeedback": "Ingen feedback matchar de aktuella filtren.",
+ "feedbackItem": "Inlägg",
+ "type": "Typ",
+ "signals": "Signaler",
+ "feedbackStatusAction": "Ändra feedbackstatus",
+ "adminStatsError": "Det gick inte att ladda administrationsstatistiken.",
+ "activeUsers": "Aktiva användare",
+ "paidAccess": "Aktiv / provperiod",
+ "pendingFeedback": "Väntande feedback",
+ "pendingReviews": "Omdömen som väntar på godkännande",
+ "operationalAlerts": "Driftvarningar",
+ "pendingBugs": "Väntande felrapporter",
+ "pendingBugsDesc": "Felrapporter som väntar på granskning av administratör.",
+ "pastDueSubscriptions": "Förfallna prenumerationer",
+ "pastDueSubscriptionsDesc": "Konton med betalningsproblem som kan behöva granskas.",
+ "dashboardBanner": {
+ "title": "Meddelande på översikten",
+ "description": "Skapa, översätt, granska och publicera ett meddelande på varje elevs översikt.",
+ "loading": "Laddar meddelande...",
+ "loadError": "Det gick inte att ladda meddelandet på översikten.",
+ "sourceLocale": "Källspråk",
+ "activeLabel": "Publicera meddelande",
+ "activeHint": "Inaktiva meddelanden sparas men visas inte för eleverna.",
+ "fieldTitle": "Titel",
+ "fieldSubtitle": "Underrubrik",
+ "fieldDescription": "Beskrivning",
+ "translate": "Översätt",
+ "translateSuccess": "Översättningarna har skapats. Granska dem innan du sparar.",
+ "translateError": "Översättningen misslyckades. Ditt nuvarande innehåll behölls.",
+ "editTranslation": "Översättning att redigera",
+ "pending": "väntar",
+ "completion": "{complete}/{total} språk klara",
+ "save": "Spara meddelande",
+ "saveSuccess": "Meddelandet på översikten har sparats.",
+ "saveError": "Det gick inte att spara meddelandet på översikten.",
+ "revision": "Version {revision}",
+ "updated": "Uppdaterad {date}",
+ "locales": {
+ "en": "Engelska",
+ "es": "Spanska",
+ "fr": "Franska",
+ "pt": "Portugisiska",
+ "de": "Tyska",
+ "it": "Italienska",
+ "ru": "Ryska",
+ "nl": "Nederländska",
+ "pl": "Polska",
+ "ro": "Rumänska",
+ "tr": "Turkiska",
+ "sv": "Svenska",
+ "da": "Danska",
+ "fi": "Finska",
+ "hr": "Kroatiska"
+ }
+ }
+ },
+ "faq": {
+ "title": "Vanliga frågor",
+ "subtitle": "Vanliga frågor och svar",
+ "q_start": "Var börjar jag?",
+ "a_start": "Börja med nivåbedömningen . Den identifierar din nivå i engelska (A1–C2) samt dina styrkor och svagheter. Med resultatet kan du skapa en personlig studieplan som fyller din översikt med dagliga lektioner. Utan nivåbedömningen är översikten tom.",
+ "q_workflow": "Hur går det till från början till slut?",
+ "workflowStep1": "Registrera dig och välj ditt modersmål",
+ "workflowStep2": "Gör nivåbedömningen → din GERS-nivå identifieras",
+ "workflowStep3": "Skapa din studieplan från översikten",
+ "workflowStep4": "Slutför dina dagliga lektioner (STARTA-knappen på översikten)",
+ "workflowStep5": "Använd gloskort för att öva ordförråd parallellt",
+ "workflowStep6": "Öva fria samtal med AI-handledaren",
+ "q_language": "Vilket språk kommer jag att lära mig?",
+ "a_language": "FreeLingo stöder för närvarande flera språk (amerikansk engelska, brittisk engelska, franska, tyska, italienska, japanska, koreanska, kinesiska från Fastlandskina, portugisiska och spanska), och vi kommer att lägga till fler med tiden. När ett nytt språk blir tillgängligt hittar du det under Inställningar → Mina språk – lägg till det och börja lära dig direkt.",
+ "q_assessment": "Vad är nivåbedömningen?",
+ "a_assessment": "Ett AI-genererat test med 15 frågor om grammatik, ordförråd, läsförståelse och meningsomvandling, med svårighetsgrader från A1 till C1. När du är klar får du din GERS-nivå, ett resultat och en analys av dina styrkor och svagheter. Resultatet används i studieplanen och de genererade gloskorten.",
+ "q_studyPlan": "Vad är studieplanen och hur skapar jag en?",
+ "a_studyPlan": "Efter nivåbedömningen går du till översikten och klickar på Skapa studieplan . En plan vecka för vecka med dagliga lektioner anpassade till din GERS-nivå och dina mål skapas automatiskt. Varje lektion innehåller flervalsfrågor och skrivövningar som bedöms av AI.",
+ "q_resources": "Vad är Resurser?",
+ "a_resources": "Under Resurser hittar du referensmaterial som är fristående från din studieplan: grammatik per nivå, ordsamlingar och användbara fraser. Du kan använda det när som helst för att repetera, slå upp förklaringar eller utöka ditt ordförråd utan att vänta på en daglig lektion.",
+ "q_flashcards": "Vad används gloskort till?",
+ "a_flashcards": "Ett system för att öva ord med algoritmen SM-2 för utspridd repetition. Skapa kort om valfritt ämne (t.ex. \"arbete\", \"frasverb\" eller \"mat\") med definition, exempelmening och översättning till ditt modersmål. Systemet räknar ut när varje kort ska visas igen, precis innan du riskerar att glömma det. Helt fristående från studieplanen.",
+ "q_vocabulary": "Hur lägger jag till ord i mitt ordförråd?",
+ "a_vocabulary": "Du kan spara ord på två sätt: (1) Markera ett ord med musen när du läser en lektion, gör en hörövning eller chattar med handledaren och klicka på Spara i menyn som visas. AI genererar då en definition, en exempelmening och en översättning till ditt modersmål. (2) Gå till Ordförråd , öppna en ordsamling för din nivå och klicka på Lägg till alla för att importera hela samlingen. Dina sparade ord visas under Mitt ordförråd bland gloskorten.",
+ "q_tutor": "Hur fungerar AI-handledaren?",
+ "a_tutor": "En öppen chatt där svaren visas allt eftersom. Handledaren känner till din GERS-nivå och vad du behöver öva mer på och anpassar sina svar därefter. Ställ frågor om grammatik, be om förklaringar, öva på att skriva engelska eller bara samtala. Samtal sparas och finns i sidofältet även nästa gång du loggar in.",
+ "q_voice": "Hur fungerar röstsamtal?",
+ "a_voice": "I läget Röstsamtal kan du prata med AI-handledaren i realtid. Tryck på Starta så lyssnar appen automatiskt med röstaktivitetsdetektering (VAD), och handledaren svarar med syntetiskt tal. Du kan avbryta handledaren när som helst. Samtalet avslutas när tidsgränsen nås eller när du trycker på Stoppa. Se till att webbläsaren har åtkomst till mikrofonen.",
+ "q_listening": "Hur fungerar hörövningar?",
+ "a_listening": "Under Hörförståelse skapas korta ljudavsnitt på din GERS-nivå med AI-röster. Lyssna och svara sedan på fem förståelsefrågor. När du skickat in får du ditt resultat (0–5), XP, rätt svar och hela transkriptionen. Du kan skapa nya övningar när du vill.",
+ "q_reading": "Hur fungerar läsövningar?",
+ "a_reading": "Under Läsning skapas texter på din GERS-nivå – meddelanden, e-post, artiklar, blogginlägg med mera. Läs texten och svara på fem flervalsfrågor. När du skickat in får du ditt resultat (0–5) och XP. Du kan skapa nya övningar när du vill.",
+ "q_providers": "Vilka AI-leverantörer stöder FreeLingo?",
+ "a_providers_intro": "Konfigureras i .env med LLM_PROVIDER:",
+ "provider_ollama": "Lokalt och kostnadsfritt. Kräver att Ollama körs på värddatorn. Rekommenderad modell: gemma4:e4b. Kan vara långsam utan GPU.",
+ "provider_openai": "OpenAI API (GPT-4o med flera). Snabbt och tillförlitligt. Kräver OPENAI_API_KEY.",
+ "provider_anthropic": "Anthropic Claude. Kräver ANTHROPIC_API_KEY.",
+ "provider_deepseek": "DeepSeek API. Kräver DEEPSEEK_API_KEY.",
+ "q_invite": "Kan jag bjuda in andra?",
+ "a_invite": "Ja. Under Administration → Användare kan administratören skapa användare direkt eller generera engångslänkar som går ut efter 48 timmar. Offentlig registrering är avstängd som standard (ALLOW_REGISTRATION=false).",
+ "q_password": "Hur ändrar jag lösenord eller modersmål?",
+ "a_password": "Gå till Inställningar . Där kan du uppdatera visningsnamn, modersmål och lösenord.",
+ "q_uiLanguage": "Hur ändrar jag plattformens språk?",
+ "a_uiLanguage": "Gå till Inställningar och hitta menyn Gränssnittsspråk under Profil. Välj önskat språk så byts hela gränssnittet direkt.",
+ "q_feedback": "Hur föreslår jag en ny funktion eller rapporterar ett fel?",
+ "a_feedback": "Använd Feedback i sidofältet. Du kan föreslå nya funktioner eller rapportera fel. Rösta på andras idéer, lämna kommentarer och följ statusen för varje ärende. Alla inlägg är offentliga och granskas av administratören."
+ },
+ "cookieBanner": {
+ "message": "Vi använder endast nödvändiga kakor för att hålla dig inloggad. Inga kakor för spårning eller reklam används.",
+ "learnMore": "Integritetspolicy",
+ "accept": "Jag förstår"
+ },
+ "error": {
+ "label": "Oväntat fel",
+ "title": "Något gick fel",
+ "body": "Ett oväntat fel inträffade. Du kan försöka igen eller gå tillbaka till översikten.",
+ "retry": "Försök igen",
+ "dashboard": "Gå till översikten",
+ "digest": "Felreferens"
+ },
+ "notFound": {
+ "title": "Sidan hittades inte",
+ "body": "Sidan du letar efter finns inte eller har flyttats.",
+ "dashboard": "Gå till översikten",
+ "home": "Hem"
+ },
+ "errors": {
+ "notFound": "Sidan hittades inte",
+ "serverError": "Något gick fel",
+ "unauthorized": "Logga in",
+ "backHome": "Tillbaka till startsidan"
+ },
+ "conversation": {
+ "title": "Samtal",
+ "subtitle": "Röstövning",
+ "start": "Starta samtal",
+ "stop": "Stoppa",
+ "startNew": "Starta nytt samtal",
+ "statusLoading": "Laddar röstmodell...",
+ "statusReady": "Redo",
+ "statusWarming": "Laddar modeller...",
+ "statusConnecting": "Ansluter...",
+ "statusListening": "Lyssnar...",
+ "statusDetecting": "Identifierar tal...",
+ "statusSpeaking": "Talar...",
+ "statusError": "Fel",
+ "errorServicesDisabled": "Rösttjänster är inte aktiverade på den här servern.",
+ "errorConnection": "Anslutningsfel. Försök igen.",
+ "errorTranscription": "Det gick inte att omvandla ditt tal till text. Försök säga det igen.",
+ "errorResponse": "Lingu kunde inte svara. Försök igen.",
+ "errorSpeech": "Det gick inte att läsa upp Lingus svar. Försök igen.",
+ "errorUnauthorized": "Sessionen har gått ut. Uppdatera sidan.",
+ "errorMic": "Åtkomst till mikrofonen nekades.",
+ "errorVadInit": "Det gick inte att starta röstigenkänningen. Kontrollera att din webbläsare stöder WebAssembly.",
+ "warningTimeout": "Samtalet avslutas om {seconds} sekunder",
+ "sessionEnded": "Samtalet avslutades",
+ "tapToStart": "Prata när du är redo",
+ "you": "Du",
+ "assistant": "Lingu",
+ "quotaSessions": "Samtal den här veckan",
+ "quotaMinutes": "Minuter i dag",
+ "quotaUnlimited": "Obegränsat",
+ "quotaExceededSessions": "Du har använt alla dina samtal för den här veckan.",
+ "quotaExceededTime": "Du har använt all din dagliga övningstid.",
+ "quotaExceededTokens": "Du har använt alla dina AI-token för den här månaden.",
+ "freemiumVoiceRemaining": "Gratis röst: {remaining}/{limit} min den här veckan",
+ "quotaTokens": "Token den här månaden",
+ "memoryUpdated": "Minnet har uppdaterats",
+ "trialBanner": "Gratis samtal för nivåbedömning · {minutes} min",
+ "trialCtaLabel": "Fortsätt prata",
+ "trialCtaTitle": "Fortsätt öva med Lingu",
+ "trialCtaDesc": "Lås upp fullständiga röstsamtal, handledarchatt, hörförståelse och läsning med Premium.",
+ "startersHint": "Eller börja med ett ämne",
+ "starters": [
+ "Konst och kreativitet",
+ "Böcker och läsning",
+ "Barndomsminnen",
+ "Stad eller landsbygd",
+ "Matlagning och recept",
+ "Drömmar och ambitioner",
+ "Familj och relationer",
+ "Mode och stil",
+ "Favoritfilmer",
+ "Festivaler och traditioner",
+ "Mat från hela världen",
+ "Hälsa och välbefinnande",
+ "Hobbyer och hantverk",
+ "Språkinlärning",
+ "Att lära sig nya färdigheter",
+ "Pengar och budget",
+ "Musik och konserter",
+ "Husdjur och djur",
+ "Kollektivtrafik",
+ "Vetenskap och rymden",
+ "Shoppingvanor",
+ "Sociala medier",
+ "Sport och träning",
+ "Teknik och prylar",
+ "Miljön",
+ "Resmål",
+ "TV-program och serier",
+ "Helgplaner",
+ "Arbete och karriärmål",
+ "Att arbeta hemifrån"
+ ]
+ },
+ "voiceRecorder": {
+ "record": "Spela in",
+ "stop": "Stoppa",
+ "processing": "Behandlar",
+ "error": "Fel",
+ "ariaRecord": "Starta inspelning",
+ "ariaStop": "Stoppa inspelning"
+ },
+ "audioPlayer": {
+ "listen": "Lyssna",
+ "stop": "Stoppa",
+ "ariaListen": "Lyssna på uttal",
+ "ariaStop": "Stoppa ljudet"
+ },
+ "languages": {
+ "en": "Engelska",
+ "es": "Spanska",
+ "fr": "Franska",
+ "pt": "Portugisiska",
+ "de": "Tyska",
+ "it": "Italienska",
+ "pl": "Polska",
+ "nl": "Nederländska",
+ "ro": "Rumänska",
+ "ru": "Ryska",
+ "tr": "Turkiska",
+ "sv": "Svenska",
+ "da": "Danska",
+ "fi": "Finska",
+ "hr": "Kroatiska",
+ "myLanguages": "Mina språk",
+ "addLanguage": "Lägg till nytt språk",
+ "selectLanguage": "Välj språket du vill lära dig",
+ "activeLanguage": "Aktivt",
+ "switchTo": "Byt till detta",
+ "switching": "Byter till {language}...",
+ "switched": "Bytte till {language} ({level})",
+ "removeLanguage": "Ta bort språk",
+ "removeConfirmTitle": "Ta bort {language}?",
+ "removeConfirmMessage": "Alla framsteg, lektioner, gloskort och samtal som hör till det här språket raderas permanent. Dina kontominnen behålls. Åtgärden kan inte ångras.",
+ "removeConfirmButton": "Ta bort",
+ "deleteError": "Det gick inte att ta bort {language}. Försök igen.",
+ "noLanguages": "Du har inte lagt till några språk.",
+ "progressLabel": "Framsteg",
+ "levelLabel": "Nivå",
+ "xpLabel": "Totalt antal XP",
+ "streakLabel": "Svit",
+ "lessonsLabel": "Lektioner",
+ "flashcardsLabel": "Gloskort",
+ "viewDetails": "Visa detaljer",
+ "supportedLanguages": "Tillgängliga språk"
+ },
+ "targetLanguages": {
+ "en-US": "Amerikansk engelska",
+ "en-GB": "Brittisk engelska",
+ "es-ES": "Spanska",
+ "it-IT": "Italienska",
+ "pt-PT": "Portugisiska",
+ "fr-FR": "Franska",
+ "de-DE": "Tyska",
+ "ja-JP": "Japanska",
+ "ko-KR": "Koreanska",
+ "zh-CN": "Kinesiska (Fastlandskina)",
+ "en": "Engelska",
+ "es": "Spanska",
+ "it": "Italienska",
+ "pt": "Portugisiska",
+ "fr": "Franska",
+ "de": "Tyska",
+ "ja": "Japanska",
+ "ko": "Koreanska",
+ "zh": "Kinesiska",
+ "es-ES-description": "Spanska som talas i Spanien.",
+ "it-IT-description": "Italienska som talas i Italien.",
+ "pt-PT-description": "Portugisiska som talas i Portugal.",
+ "en-US-description": "Engelska som talas i USA.",
+ "en-GB-description": "Engelska som talas i Storbritannien.",
+ "fr-FR-description": "Franska som talas i Frankrike.",
+ "de-DE-description": "Tyska som talas i Tyskland.",
+ "ja-JP-description": "Japanska som talas i Japan.",
+ "ko-KR-description": "Koreanska som talas i Sydkorea.",
+ "zh-CN-description": "Mandarinkinesiska som talas i Fastlandskina med förenklade tecken."
+ },
+ "onboarding": {
+ "title": "Välj ditt språk",
+ "subtitle": "Vilket språk vill du lära dig?",
+ "chooseVariant": "Välj variant",
+ "continue": "Fortsätt",
+ "saveFailed": "Det gick inte att spara inställningarna",
+ "goals": {
+ "title": "Dina inlärningsmål",
+ "subtitle": "Språk: {language}. Vad vill du använda det till? Välj alla som passar.",
+ "skip": "Hoppa över",
+ "travel": "Resor och turism",
+ "work": "Arbete och yrkesliv",
+ "academic": "Studier och akademi",
+ "daily": "Vardagliga samtal",
+ "media": "Film, musik och serier",
+ "emigration": "Att bo utomlands",
+ "exams": "Prov och certifikat",
+ "social": "Socialt umgänge"
+ },
+ "trialHeadline": "Gratis provperiod",
+ "trialHeadlineTrialUsed": "Premium",
+ "trialTitle": "Börja med {days} dagar gratis",
+ "trialTitleTrialUsed": "Aktivera FreeLingo Premium",
+ "trialDesc": "Få full åtkomst till Lingu, röstövningar, hörförståelse och läsning före din första betalning.",
+ "trialDescTrialUsed": "Välj en plan för att låsa upp Lingu, röstövningar, hörförståelse och läsning.",
+ "trialCtaMonthly": "Prova gratis, därefter €{price}/månad",
+ "trialCtaMonthlyTrialUsed": "Flexibel månadsplan · €{price}/månad",
+ "trialCtaYearly": "Prova gratis, därefter €{price}/år · 2 månader gratis",
+ "trialCtaYearlyTrialUsed": "Bästa värdet · €{price}/år · 2 månader gratis",
+ "trialSkip": "Fortsätt med gratis funktioner",
+ "trialNoCharge": "Ingen debitering i dag. Kort krävs, avsluta när du vill.",
+ "trialNoChargeTrialUsed": "Granska beloppet innan du prenumererar. Avsluta när du vill.",
+ "trialError": "Det gick inte att starta betalningen. Försök igen.",
+ "freemiumTrialTitle": "Din gratis provperiod har börjat",
+ "freemiumTrialDesc": "Få obegränsad åtkomst till alla funktioner i sju dagar. Inget kort krävs. Uppgradera till Premium när du vill från översikten.",
+ "freemiumChatLabel": "Chatt med AI-handledaren",
+ "freemiumVoiceLabel": "Röstsamtal",
+ "freemiumListeningLabel": "Hörövningar",
+ "freemiumReadingLabel": "Läsövningar",
+ "goToDashboard": "Gå till översikten",
+ "newLanguageHeadline": "Vilket nytt språk vill du lära dig?",
+ "newLanguageSubtitle": "En ny studieplan skapas för det här språket."
+ },
+ "tour": {
+ "skip": "Hoppa över",
+ "prev": "Tillbaka",
+ "next": "Nästa",
+ "done": "Kom igång",
+ "step1": {
+ "label": "Välkommen",
+ "title": "Välkommen till FreeLingo",
+ "desc": "FreeLingo är din AI-drivna språkhandledare. Öva i din egen takt, när som helst, genom samtal, lektioner och smarta gloskort."
+ },
+ "step2": {
+ "label": "Textchatt",
+ "title": "Chatta med din handledare",
+ "desc": "Ha skriftliga samtal med din AI-handledare. Ställ frågor, få rättelser och öva grammatik och ordförråd på ett naturligt sätt."
+ },
+ "step3": {
+ "label": "Röst",
+ "title": "Prata i realtid",
+ "desc": "Använd röstsamtal för att öva talspråk med transkription i realtid och omedelbar återkoppling på ditt uttal."
+ },
+ "step4": {
+ "label": "Gloskort",
+ "title": "Glöm aldrig ett ord",
+ "desc": "Gloskorten använder SM-2-algoritmen för utspridd repetition så att du repeterar orden precis innan du håller på att glömma dem – för bättre minne på lång sikt."
+ },
+ "step5": {
+ "label": "Studieplan",
+ "title": "Din personliga plan",
+ "desc": "Utifrån din nivåbedömning skapar FreeLingo en strukturerad studieplan med dagliga lektioner som anpassas till dina mål och framsteg."
+ },
+ "step6": {
+ "label": "Öva",
+ "title": "Hörförståelse och läsning",
+ "desc": "Testa din förståelse med AI-genererade övningar. Lyssna på ett avsnitt och svara på fem frågor eller läs en text på din GERS-nivå – få XP för varje rätt svar."
+ },
+ "step7": {
+ "label": "Redo",
+ "title": "Nu är du redo!",
+ "desc": "Börja med dagens lektion, öppna en chatt eller öva hörförståelse och läsning – FreeLingo anpassar sig till hur du vill lära dig."
+ }
+ },
+ "billing": {
+ "paywallLabel": "Premium krävs",
+ "paywallTitle": "Lås upp FreeLingo Premium",
+ "paywallDesc": "Börja din gratis provperiod på {days} dagar och lås upp alla Premium-övningar före din första betalning.",
+ "paywallDescTrialUsed": "Lås upp alla Premium-övningar och granska beloppet innan du bekräftar.",
+ "paywallChatTitle": "Öva med Lingu utan begränsningar",
+ "paywallChatDesc": "Ställ frågor, få rättelser och ha naturliga textsamtal med din AI-handledare.",
+ "paywallLessonsTitle": "Slutför obegränsat antal lektioner",
+ "paywallLessonsDesc": "Grammatik, ordförråd, läsning och skrivning – slutför så många lektioner du vill varje dag.",
+ "paywallConversationTitle": "Prata med Lingu i realtid",
+ "paywallConversationDesc": "Öva muntliga samtal med transkription, snabba svar och återkoppling för att bli mer flytande.",
+ "paywallListeningTitle": "Träna din hörförståelse på din nivå",
+ "paywallListeningDesc": "Lyssna på AI-genererade övningar anpassade till dina framsteg och kontrollera direkt hur mycket du förstod.",
+ "paywallReadingTitle": "Läs texter anpassade till dina framsteg",
+ "paywallReadingDesc": "Förbättra läsförståelsen med texter på rätt nivå, frågor och omedelbar återkoppling.",
+ "paywallNoCharge": "Ingen debitering i dag · Kort krävs · Avsluta när du vill",
+ "paywallNoChargeTrialUsed": "Säker betalning · Avsluta när du vill",
+ "paywallSkip": "Inte nu",
+ "trialDays": "{days, plural, one {# provdag} other {# provdagar}}",
+ "planMonthly": "Flexibel månadsplan · €{price}/månad →",
+ "planYearly": "Bästa värdet · €{price}/år · 2 månader gratis →",
+ "checkoutError": "Det gick inte att starta betalningen. Försök igen.",
+ "pricingTitle": "Enkla, tydliga priser",
+ "pricingDesc": "{days} dagar gratis · Ingen debitering i dag · Avsluta när du vill",
+ "pricingDescTrialUsed": "Välj månads- eller årsplan · Avsluta när du vill",
+ "planMonthlyName": "Månadsvis",
+ "planYearlyName": "Årsvis",
+ "bestValue": "Bästa värdet",
+ "month": "månad",
+ "year": "år",
+ "planFeature": {
+ "feature1": "Obegränsad åtkomst – inga dagliga eller veckovisa gränser",
+ "feature2": "2 månader gratis"
+ },
+ "planFreeName": "Gratis",
+ "trialBadge": "Gratis provperiod",
+ "trialBadgeTrialUsed": "Premium",
+ "ctaRegister": "Starta gratis provperiod",
+ "ctaRegisterTrialUsed": "Välj plan",
+ "freeFeature": {
+ "f1": "Nivåtest och nivåbedömning",
+ "f2": "Lektioner och studieplan",
+ "f3": "Referensmaterial för grammatik och ordförråd",
+ "f4": "Översikt och uppföljning av framsteg",
+ "f5": "Gloskort och utspridd repetition",
+ "l1": "Chatt med AI-handledaren",
+ "l2": "Röstsamtal",
+ "l3": "Hörövningar",
+ "l4": "Läsövningar",
+ "l5": "7 dagars provperiod med full åtkomst"
+ },
+ "successLabel": "Betalningen bekräftad",
+ "successTitle": "Välkommen till FreeLingo Premium!",
+ "successDesc": "Din prenumeration är nu aktiv. Njut av obegränsad åtkomst till alla funktioner.",
+ "successCheckingLabel": "Bekräftar prenumeration",
+ "successCheckingTitle": "Kontrollerar din prenumeration",
+ "successCheckingDesc": "Det tar vanligtvis några sekunder.",
+ "successPendingLabel": "Nästan klart",
+ "successPendingTitle": "Din prenumeration bekräftas",
+ "successPendingDesc": "Din Premium-åtkomst bör vara tillgänglig snart. Om den inte är det, kontrollera betalningsinställningarna.",
+ "successErrorLabel": "Bekräftelse väntar",
+ "successErrorTitle": "Vi kunde ännu inte verifiera din prenumeration",
+ "successErrorDesc": "Gå till översikten och uppdatera om en stund, eller kontrollera betalningsinställningarna.",
+ "successRedirect": "Skickar dig till översikten om {seconds} sekunder...",
+ "successCta": "Gå till översikten",
+ "canceledLabel": "Betalningen avbröts",
+ "canceledTitle": "Du har inte debiterats",
+ "canceledDesc": "Du har inte debiterats. Dina framsteg finns kvar, och du kan välja en plan senare i Inställningar.",
+ "canceledCtaDashboard": "Gå till översikten",
+ "canceledCtaSettings": "Visa planer",
+ "canceledNoCharge": "Ingen debitering gjordes denna gång · Välj en plan när du vill",
+ "section": "Prenumeration",
+ "status": "Status",
+ "statusActive": "Aktiv",
+ "statusCanceling": "Sägs upp",
+ "statusCanceled": "Avslutad",
+ "statusTrialing": "Provperiod",
+ "statusPastDue": "Förfallen betalning",
+ "statusUnpaid": "Obetald",
+ "statusPaused": "Pausad",
+ "statusIncomplete": "Betalning väntar",
+ "statusIncompleteExpired": "Betalningen har gått ut",
+ "statusNone": "Ingen prenumeration",
+ "nextBilling": "Nästa betalning",
+ "accessUntil": "Åtkomst till och med",
+ "manage": "Hantera prenumeration",
+ "updatePayment": "Uppdatera betalning",
+ "pastDueTitle": "Betalning väntar",
+ "pastDueDesc": "Uppdatera din betalningsmetod för att återfå Premium-åtkomst.",
+ "subscribe": "Prenumerera",
+ "portalError": "Det gick inte att öppna betalningsportalen. Försök igen.",
+ "openSourceTitle": "Öppen källkod",
+ "openSourceDesc": "Gratis att köra på din egen server",
+ "openSourceCta": "Visa på GitHub",
+ "planFreeBadge": "Gratis för alltid",
+ "priceAmount": "€{amount}",
+ "pricePerPeriod": "/ {period}",
+ "priceOriginal": "€{price} / {period}",
+ "planFreeDesc": "7 dagars provperiod med full åtkomst, därefter gratis med dagliga och veckovisa gränser",
+ "planFreeCta": "Starta",
+ "limitedLabel": "Begränsat"
+ },
+ "freemium": {
+ "chatLabel": "Chatt",
+ "lessonsLabel": "Lektioner",
+ "listeningLabel": "Hörförståelse",
+ "readingLabel": "Läsning",
+ "voiceLabel": "Röst (min)",
+ "freeLimit": "Skaffa Premium",
+ "trialDaysLeft": "{days, plural, one {# dag kvar av gratis provperiod} other {# dagar kvar av gratis provperiod}}",
+ "trialUnlimited": "Obegränsad åtkomst",
+ "requiresSubscription": "Endast Premium"
+ },
+ "whatsNew": {
+ "title": "Nyheter",
+ "version": "v1.9.25",
+ "cta": "Jag förstår",
+ "entry1": {
+ "label": "Fem nya gränssnittsspråk",
+ "desc": "FreeLingo finns på danska, finska, kroatiska, svenska och turkiska. Välj gränssnittsspråk i Inställningar."
+ },
+ "entry2": {
+ "label": "En smidigare upplevelse",
+ "desc": "Vi har också rättat mindre fel och förbättrat några detaljer så att det blir ännu roligare att lära sig med FreeLingo varje dag."
+ }
+ },
+ "contact": {
+ "title": "Kontakt",
+ "labelEmail": "E-postadress",
+ "placeholderEmail": "din@e-post.se",
+ "labelSubject": "Ämne",
+ "placeholderSubject": "Kort beskrivning av din fråga",
+ "labelDescription": "Meddelande",
+ "placeholderDescription": "Beskriv din fråga eller återkoppling...",
+ "send": "Skicka",
+ "sending": "Skickar...",
+ "sent": "Meddelandet har skickats",
+ "errorGeneric": "Det gick inte att skicka. Försök igen."
+ },
+ "listening": {
+ "title": "Hörförståelse",
+ "topic": "Ämne",
+ "listenLabel": "Lyssna",
+ "questionsLabel": "Frågor",
+ "submit": "Skicka svar",
+ "generating": "Skapar övning...",
+ "generatingDesc": "AI skapar en ny övning. Det tar vanligtvis cirka 30 sekunder.",
+ "generatingLong": "Det tar längre tid än väntat...",
+ "noExercises": "Inga övningar tillgängliga för din nivå.",
+ "generate": "Skapa övning",
+ "audioError": "Det gick inte att ladda ljudet",
+ "alreadyAttempted": "Du har redan slutfört den här övningen.",
+ "errorLoading": "Det gick inte att ladda övningen. Försök igen.",
+ "errorSubmit": "Det gick inte att skicka svaren. Försök igen.",
+ "resultsLabel": "Resultat",
+ "transcript": "Transkription",
+ "review": "Gå igenom svaren",
+ "nextExercise": "Nästa övning",
+ "viewHistory": "Historik",
+ "history": "Historik",
+ "historyTitle": "Historik för hörförståelse",
+ "historyEmpty": "Inga slutförda övningar än.",
+ "practiceMore": "Öva",
+ "moreInHistory": "+{count} till",
+ "practiceAgain": "Öva igen",
+ "replayNoXp": "Gör om (inga XP)"
+ },
+ "reading": {
+ "title": "Läsning",
+ "topic": "Ämne",
+ "textLabel": "Text",
+ "questionsLabel": "Frågor",
+ "submit": "Skicka svar",
+ "generating": "Skapar övning...",
+ "generatingDesc": "AI skapar en ny övning. Det tar vanligtvis cirka 30 sekunder.",
+ "generatingLong": "Det tar längre tid än väntat...",
+ "noExercises": "Inga övningar tillgängliga för din nivå.",
+ "generate": "Skapa övning",
+ "alreadyAttempted": "Du har redan slutfört den här övningen.",
+ "errorLoading": "Det gick inte att ladda övningen. Försök igen.",
+ "errorSubmit": "Det gick inte att skicka svaren. Försök igen.",
+ "resultsLabel": "Resultat",
+ "review": "Gå igenom svaren",
+ "nextExercise": "Nästa övning",
+ "viewHistory": "Historik",
+ "history": "Historik",
+ "historyTitle": "Läshistorik",
+ "historyEmpty": "Inga slutförda övningar än.",
+ "practiceMore": "Öva",
+ "moreInHistory": "+{count} till",
+ "practiceAgain": "Öva igen",
+ "replayNoXp": "Gör om (inga XP)",
+ "selectWordHint": "Markera ett ord för att spara det i ditt ordförråd"
+ },
+ "feedback": {
+ "title": "Feedback",
+ "subtitle": "Funktionsförslag och felrapporter",
+ "tabFeatures": "Förslag",
+ "tabBugs": "Felrapporter",
+ "newFeature": "+ Nytt förslag",
+ "newBug": "+ Rapportera ett fel",
+ "sortBy": "Sortera efter",
+ "sortVotes": "Flest röster",
+ "sortDate": "Senaste",
+ "filterStatus": "Status",
+ "filterAll": "Alla",
+ "noEntries": "Inget här än. Bli först med att skicka in något.",
+ "votes": "{count} röster",
+ "vote": "1 röst",
+ "voteAction": "Rösta",
+ "removeVoteAction": "Ta tillbaka rösten",
+ "comments": "{count} kommentarer",
+ "comment": "1 kommentar",
+ "by": "av",
+ "statusPending": "Väntar",
+ "statusPlanned": "Planerad",
+ "statusInProgress": "Pågår",
+ "statusDone": "Klar",
+ "statusDeclined": "Avböjd",
+ "unread": "Oläst",
+ "modalCreateTitleFeature": "Nytt förslag",
+ "modalCreateTitleBug": "Rapportera ett fel",
+ "labelTitle": "Titel",
+ "placeholderTitleFeature": "Beskriv ditt förslag i en mening",
+ "placeholderTitleBug": "Kort beskrivning av felet",
+ "labelDescription": "Detaljer",
+ "placeholderDescriptionFeature": "Förklara funktionen och varför den vore användbar",
+ "placeholderDescriptionBug": "Steg för att återskapa felet, förväntat resultat och vad som hände i stället",
+ "submit": "Skicka",
+ "submitting": "Skickar...",
+ "cancel": "Avbryt",
+ "deleteEntry": "Radera",
+ "deleteEntryConfirmTitle": "Radera inlägg",
+ "deleteEntryConfirmMessage": "Är du säker på att du vill radera det här inlägget? Åtgärden kan inte ångras.",
+ "deleteEntryConfirm": "Radera",
+ "addComment": "Lägg till en kommentar",
+ "commentPlaceholder": "Skriv en kommentar...",
+ "postComment": "Publicera",
+ "postingComment": "Publicerar...",
+ "deleteComment": "Radera",
+ "deleteCommentConfirmTitle": "Radera kommentar",
+ "deleteCommentConfirmMessage": "Vill du radera den här kommentaren?",
+ "deleteCommentConfirm": "Radera",
+ "loading": "Laddar...",
+ "backToList": "← Tillbaka",
+ "errorLoad": "Det gick inte att ladda. Försök igen.",
+ "errorSubmit": "Det gick inte att skicka. Försök igen.",
+ "prevPage": "← Föregående",
+ "nextPage": "Nästa →"
+ },
+ "reviewPrompt": {
+ "statusError": "Vi kunde inte kontrollera statusen för ditt omdöme. Försök igen senare.",
+ "ratingRequiredError": "Välj ett betyg innan du skickar in.",
+ "saveError": "Det gick inte att spara ditt omdöme. Försök igen.",
+ "eyebrow": "Recensera FreeLingo",
+ "title": "Hur var din upplevelse?",
+ "closeLabel": "Stäng omdömesrutan",
+ "checking": "Kontrollerar omdömesstatus...",
+ "ratingLabel": "Betyg krävs",
+ "ratingGroupLabel": "Betyg",
+ "star": "stjärna",
+ "stars": "stjärnor",
+ "commentLabel": "Kommentar valfri",
+ "commentPlaceholder": "Berätta vad som hjälpte dig att lära dig.",
+ "cancel": "Avbryt",
+ "submit": "Skicka"
+ },
+ "landingReviews": {
+ "eyebrow": "Elevernas berättelser",
+ "title": "Lärandet blir verkligare när du själv berättar om det",
+ "subtitle": "Omdömen från elever som redan övar med Lingu på FreeLingo.",
+ "averageLabel": "{average}/5 · {count, plural, one {# omdöme totalt} other {# omdömen totalt}}",
+ "learningLanguage": "Lär sig {language}",
+ "starsLabel": "{rating} av 5 stjärnor",
+ "ratingOnly": "Verifierat FreeLingo-betyg."
+ },
+ "adminReviews": {
+ "title": "Omdömen",
+ "eyebrow": "Administration",
+ "totalFiltered": "Totalt filtrerade",
+ "approvedOnPage": "Godkända på sidan",
+ "pendingOnPage": "Väntande på sidan",
+ "filters": "Filter",
+ "approvalFilter": "Filtrera efter godkännande",
+ "allReviews": "Alla omdömen",
+ "pending": "Väntar",
+ "approved": "Godkända",
+ "ratingFilter": "Filtrera efter betyg",
+ "allRatings": "Alla betyg",
+ "starsOption": "{rating} stjärnor",
+ "clear": "Rensa",
+ "loadError": "Det gick inte att ladda omdömen.",
+ "updateError": "Det gick inte att uppdatera omdömet.",
+ "deleteError": "Det gick inte att radera omdömet.",
+ "loading": "Laddar omdömen...",
+ "queue": "Omdömeskö",
+ "total": "{total} totalt",
+ "empty": "Inga omdömen hittades.",
+ "starsLabel": "{rating} stjärnor",
+ "learningLanguage": "Lär sig {language}",
+ "ratingOnly": "Omdöme med endast betyg.",
+ "unapprove": "Ta bort godkännande",
+ "approve": "Godkänn",
+ "delete": "Radera",
+ "prev": "Föregående",
+ "next": "Nästa",
+ "deleteTitle": "Radera omdöme",
+ "deleteMessage": "Vill du radera det här omdömet? Åtgärden kan inte ångras."
+ },
+ "maintenance": {
+ "label": "Underhåll",
+ "title": "Underhåll pågår",
+ "description": "Det här avsnittet är tillfälligt otillgängligt medan vi utför underhåll. Kom tillbaka senare."
+ }
+}
diff --git a/messages/tr.json b/messages/tr.json
new file mode 100644
index 000000000..7fd3948bf
--- /dev/null
+++ b/messages/tr.json
@@ -0,0 +1,1708 @@
+{
+ "landing": {
+ "microDemo": {
+ "title": "Lingu size şöyle yardımcı olur",
+ "exampleLabel": "İngilizce pratik örneği",
+ "questionLabel": "Lingu · Soru",
+ "answerLabel": "Siz · Cevap",
+ "correctionLabel": "Lingu · Düzeltme",
+ "explanation": "Dün hakkında konuşmak için 'go' fiilinin geçmiş zaman hâli olan 'went' kullanılır."
+ },
+ "hero": "Gerçek bir sesli konuşma partneriyle dil öğrenin",
+ "heroSub": "Konuşun. Dinleyin. Gelişin.",
+ "signIn": "Giriş yap",
+ "dashboard": "Devam et",
+ "feature1Title": "Öğrenme Planı",
+ "feature1Desc": "Seviye tespiti + A1'den C2'ye haftalık plan. Dersler sırayla, kendi hızınızda açılır.",
+ "feature2Title": "Yapay Zekâ Sohbet Eğitmeni",
+ "feature2Desc": "Eğitmeninizin rehberliğinde derslerle, alıştırmalarla ve bilgi kartlarıyla pratik yapın. Her adımda geri bildirim alın.",
+ "feature3Title": "Sesli Konuşma",
+ "feature3Desc": "Eğitmeninizle gerçek zamanlı konuşun. Sesle etkinleşir, anında yanıt verir, sıra beklemek yok.",
+ "feature4Title": "Dinleme ve Okuma",
+ "feature4Desc": "CEFR seviyenize uygun, yapay zekâ ile üretilmiş alıştırmalar. Bir metni dinleyin, 5 anlama sorusunu cevaplayın ve deşifreyi açın — ya da bir metni okuyup soruları doğrudan cevaplayın. İstediğiniz zaman yeni alıştırmalar.",
+ "feature5Title": "Bilgi Kartları",
+ "feature5Desc": "Aralıklı tekrarla kelime tekrarı. SM-2 algoritması, uzun süreli akılda kalıcılığı en üst düzeye çıkarmak için her kartı en uygun zamana planlar.",
+ "feature6Title": "İlerleme ve Seriler",
+ "feature6Desc": "XP, günlük seriler, beceri puanları ve ünite yeterlilikleri. Gelişiminizi hafta hafta takip edin ve her CEFR aşamasının sonunda seviyenizi test edin.",
+ "footer": "Açık kaynak · Ücretsiz olarak kendiniz barındırın veya barındırılan hizmete abone olun",
+ "navFeatures": "Özellikler",
+ "navReviews": "Yorumlar",
+ "navPricing": "Fiyatlandırma",
+ "navFAQ": "SSS",
+ "howItWorks": "Nasıl çalışır",
+ "faqTitle": "Sıkça Sorulan Sorular",
+ "terms": "Koşullar",
+ "privacy": "Gizlilik",
+ "contact": "İletişim",
+ "aboutMe": "Hakkımda",
+ "github": "GitHub",
+ "footerProduct": "Ürün",
+ "footerLegal": "Yasal",
+ "languageGreetings": {
+ "en-US": "Hello!",
+ "en-GB": "Hello!",
+ "de-DE": "Hallo!",
+ "es-ES": "¡Hola!",
+ "fr-FR": "Bonjour !",
+ "it-IT": "Ciao!",
+ "pt-PT": "Olá!",
+ "ja-JP": "こんにちは!",
+ "ko-KR": "안녕하세요!",
+ "zh-CN": "你好!"
+ }
+ },
+ "common": {
+ "loading": "Yükleniyor...",
+ "error": "Hata",
+ "errorMessage": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
+ "save": "Değişiklikleri kaydet",
+ "saving": "Kaydediliyor...",
+ "cancel": "İptal",
+ "back": "Geri",
+ "done": "Tamam",
+ "next": "İleri",
+ "submit": "Gönder",
+ "start": "Başla",
+ "retry": "Tekrar dene",
+ "or": "veya",
+ "appName": "FreeLingo",
+ "days": "gün",
+ "logout": "Çıkış yap",
+ "logoutConfirmTitle": "Çıkış Yap",
+ "logoutConfirmMessage": "Çıkış yapmak istediğinizden emin misiniz?",
+ "initializing": "Başlatılıyor...",
+ "clearFilters": "Filtreleri temizle",
+ "all": "Tümü",
+ "score": "Puan",
+ "complete": "Tamamla",
+ "checking": "Kontrol ediliyor...",
+ "backToDashboard": "Panoya dön",
+ "tagline": "Yapay zekâ ile dil öğrenimi",
+ "close": "Kapat",
+ "delete": "Sil",
+ "openMenu": "Menüyü aç",
+ "closeMenu": "Menüyü kapat",
+ "verifyEmailBanner": "Hesap kurulumunuzu tamamlamak için lütfen e-posta adresinizi doğrulayın.",
+ "verifyEmailSent": "E-posta gönderildi. Gelen kutunuzu kontrol edin.",
+ "resendVerification": "Doğrulama e-postasını tekrar gönder",
+ "saveWord": "Kaydet",
+ "wordSaved": "Kaydedildi",
+ "wordAlreadySaved": "Zaten kaydedilmiş",
+ "wordSaveError": "Bir şeyler ters gitti",
+ "selectWordHint": "Kelime dağarcığınıza kaydetmek için bir kelime seçin",
+ "noActivePlan": "Etkin bir çalışma planı bulunamadı",
+ "memorySavedToast": "Lingu yeni bir anı kaydetti. Ayarlar'dan inceleyebilirsiniz.",
+ "removeLanguageConfirmTitle": "Dili kaldır",
+ "removeLanguageConfirmMessage": "Bu dile ait tüm veriler (çalışma planı, ilerleme, bilgi kartları ve konuşmalar) kalıcı olarak silinecek. Hesabınıza ait anılar korunacak. Bu işlem geri alınamaz.",
+ "removeLanguageConfirmLabel": "Kaldır",
+ "nativeHelpTitle": "{language} yardım",
+ "nativeHelpLoading": "{language} yardım hazırlanıyor...",
+ "nativeHelpShow": "{language} yardımı göster",
+ "nativeHelpKeyPoints": "Önemli noktalar",
+ "nativeHelpCommonTraps": "Sık yapılan hatalar",
+ "nativeHelpMiniGlossary": "Mini sözlük",
+ "nativeHelpUsageTips": "Kullanım ipuçları",
+ "nativeHelpRegisterNotes": "Üslup",
+ "nativeHelpPhraseNotes": "İfade notları",
+ "nativeHelpStudyTips": "Çalışma ipuçları",
+ "nativeHelpWordNotes": "Kelime notları",
+ "nativeHelpPractice": "Pratik"
+ },
+ "nav": {
+ "home": "Ana sayfa",
+ "myPlan": "Planım",
+ "progress": "İlerleme",
+ "flashcards": "Bilgi Kartları",
+ "tutor": "Eğitmenle sohbet",
+ "assessment": "Seviye Tespiti",
+ "resources": "Kaynaklar",
+ "grammar": "Dil Bilgisi",
+ "vocabulary": "Kelime Bilgisi",
+ "phrasebook": "Konuşma Kılavuzu",
+ "settings": "Ayarlar",
+ "faq": "SSS",
+ "admin": "Yönetim",
+ "conversation": "Konuşma",
+ "listening": "Dinleme",
+ "reading": "Okuma",
+ "contact": "İletişim",
+ "feedback": "Geri Bildirim",
+ "reviews": "Yorumlar",
+ "switchLanguage": "Dil değiştir"
+ },
+ "auth": {
+ "login": {
+ "title": "Giriş yap",
+ "subtitle": "Tekrar hoş geldiniz",
+ "email": "E-posta",
+ "password": "Parola",
+ "submit": "Giriş yap",
+ "noAccount": "Hesabınız yok mu?",
+ "register": "Kaydol",
+ "error": "Geçersiz e-posta veya parola",
+ "emailRequired": "Lütfen e-posta adresinizi girin",
+ "passwordRequired": "Lütfen parolanızı girin",
+ "loginFailed": "Giriş başarısız",
+ "signingIn": "Giriş yapılıyor...",
+ "showPassword": "Parolayı göster",
+ "hidePassword": "Parolayı gizle",
+ "accountCreated": "Hesap oluşturuldu. Artık giriş yapabilirsiniz",
+ "invalidEmail": "Lütfen geçerli bir e-posta adresi girin",
+ "forgotPassword": "Parolanızı mı unuttunuz?"
+ },
+ "register": {
+ "title": "Hesap oluştur",
+ "subtitle": "Öğrenmeye başlayın",
+ "username": "Kullanıcı adı",
+ "email": "E-posta",
+ "password": "Parola",
+ "displayName": "Görünen ad",
+ "nativeLanguage": "Ana dil",
+ "inviteToken": "Davet kodu",
+ "inviteTokenPlaceholder": "Davet kodunu girin",
+ "submit": "Hesap oluştur",
+ "hasAccount": "Zaten hesabınız var mı?",
+ "login": "Giriş yap",
+ "registrationClosed": "Kayıtlar kapalı. Bir davet koduna ihtiyacınız var.",
+ "error": "Kayıt başarısız",
+ "passwordMismatch": "Parolalar eşleşmiyor",
+ "confirmPassword": "Parolayı Onayla",
+ "showPassword": "Parolayı göster",
+ "hidePassword": "Parolayı gizle",
+ "displayNamePlaceholder": "Boş bırakılırsa kullanıcı adıyla aynı olur",
+ "inviteActive": "Davet etkin",
+ "creatingAccount": "Oluşturuluyor...",
+ "invalidEmail": "Lütfen geçerli bir e-posta adresi girin",
+ "usernameTaken": "Bu kullanıcı adı zaten alınmış",
+ "emailTaken": "Bu e-posta adresi zaten kayıtlı",
+ "invalidInvite": "Geçersiz veya süresi dolmuş davet kodu",
+ "termsAccept": "Okudum ve kabul ediyorum:",
+ "termsLink": "Hizmet Koşulları",
+ "andWord": "ve",
+ "privacyLink": "Gizlilik Politikası",
+ "termsRequired": "Devam etmek için koşulları kabul etmelisiniz",
+ "invalidPassword": "Parola 10-25 karakter olmalı ve en az bir büyük harf, bir rakam ve bir sembol içermelidir",
+ "invalidUsernameChars": "Kullanıcı adı yalnızca harf, rakam, kısa çizgi, alt çizgi ve nokta içerebilir"
+ },
+ "verifyEmail": {
+ "title": "E-posta doğrulama",
+ "loading": "Doğrulanıyor...",
+ "success": "E-posta adresiniz doğrulandı. Artık giriş yapabilirsiniz.",
+ "error": "Geçersiz veya süresi dolmuş doğrulama bağlantısı. Lütfen hesabınızdan yeni bir bağlantı isteyin.",
+ "goToLogin": "Girişe git"
+ },
+ "forgotPassword": {
+ "title": "Parolamı unuttum",
+ "description": "Hesabınızla ilişkili e-posta adresini girin, size bir sıfırlama bağlantısı gönderelim.",
+ "emailPlaceholder": "adiniz@eposta.com",
+ "submit": "Sıfırlama bağlantısı gönder",
+ "sending": "Gönderiliyor...",
+ "sent": "Bu e-posta adresi kayıtlıysa bir sıfırlama bağlantısı gönderildi. Gelen kutunuzu kontrol edin.",
+ "backToLogin": "Girişe dön",
+ "error": "Bir şeyler ters gitti. Lütfen tekrar deneyin."
+ },
+ "resetPassword": {
+ "title": "Parolayı sıfırla",
+ "newPassword": "Yeni parola",
+ "confirmPassword": "Yeni parolayı onayla",
+ "submit": "Yeni parolayı belirle",
+ "saving": "Kaydediliyor...",
+ "success": "Parola güncellendi. Giriş sayfasına yönlendiriliyorsunuz...",
+ "mismatch": "Parolalar eşleşmiyor",
+ "tooShort": "Parola en az 10 karakter olmalıdır",
+ "missingToken": "Geçersiz sıfırlama bağlantısı. Lütfen yeni bir parola sıfırlama isteyin.",
+ "error": "Geçersiz veya süresi dolmuş sıfırlama bağlantısı",
+ "backToLogin": "Girişe dön"
+ }
+ },
+ "settings": {
+ "title": "Ayarlar",
+ "sectionAccount": "Hesap",
+ "sectionProfile": "Profil",
+ "avatarChange": "Fotoğrafı değiştir",
+ "avatarRemove": "Kaldır",
+ "avatarUploading": "Yükleniyor...",
+ "avatarTypeError": "Yalnızca JPEG ve PNG görsellere izin verilir",
+ "avatarSizeError": "Görsel çok büyük (en fazla 2 MB)",
+ "displayName": "Görünen Ad",
+ "email": "E-posta",
+ "nativeLanguage": "Ana Dil",
+ "uiLocale": "Arayüz Dili",
+ "uiLocaleHint": "Menülerin ve düğmelerin dilini değiştirir. Sayfanın yeniden yüklenmesi gerekir.",
+ "newPassword": "Yeni Parola",
+ "newPasswordPlaceholder": "Değiştirmemek için boş bırakın",
+ "confirmPassword": "Parolayı Onayla",
+ "confirmPasswordPlaceholder": "Yeni parolayı tekrar girin",
+ "bio": "Hakkınızda (Yapay Zekâ Bağlamı)",
+ "bioPlaceholder": "ör. İş nedeniyle sık sık seyahat eden bir yazılım geliştiricisiyim...",
+ "bioHint": "Yapay zekâ eğitmeni, konuşmaları kişiselleştirmek için bu bilgiyi kullanır.",
+ "saveChanges": "Değişiklikleri Kaydet",
+ "saving": "Kaydediliyor...",
+ "saved": "Profil güncellendi.",
+ "passwordMismatch": "Parolalar eşleşmiyor",
+ "saveFailed": "Kaydedilemedi",
+ "logout": "Çıkış yap",
+ "sectionAppearance": "Görünüm",
+ "theme": "Tema",
+ "darkActive": "Koyu mod etkin",
+ "lightActive": "Açık mod etkin",
+ "systemActive": "Otomatik (sistem)",
+ "themeSystem": "Otomatik",
+ "themeDark": "Koyu",
+ "themeLight": "Açık",
+ "switchToLight": "Açık temaya geç",
+ "switchToDark": "Koyu temaya geç",
+ "sectionConversation": "Konuşma",
+ "conversationMaxDuration": "En Uzun Oturum Süresi",
+ "conversationInactivityTimeout": "Hareketsizlik Zaman Aşımı",
+ "conversationSpeechPause": "Sıra Sonu Duraklaması",
+ "conversationSpeechPauseHint": "Sıranız gönderilmeden önce cümle ortasında ne kadar duraklayabileceğiniz. Otomatik seçenek seviyenize göre ayarlanır.",
+ "speechPauseAuto": "Otomatik",
+ "speechPauseSec1": "1 saniye",
+ "speechPauseSec2": "2 saniye",
+ "speechPauseSec3": "3 saniye",
+ "min15": "15 dakika",
+ "min30": "30 dakika",
+ "min1": "1 dakika",
+ "min3": "3 dakika",
+ "min5": "5 dakika",
+ "saveConversation": "Kaydet",
+ "conversationSaved": "Konuşma ayarları kaydedildi.",
+ "sectionLegal": "Yasal",
+ "termsOfService": "Hizmet Koşulları",
+ "privacyPolicy": "Gizlilik Politikası",
+ "contactLink": "İletişim",
+ "deleteAccount": "Hesabı Sil",
+ "deleteAccountTitle": "Hesabı Sil",
+ "deleteAccountMessage": "Bu işlem hesabınızı ve tüm verilerinizi kalıcı olarak silecek. Bu işlem geri alınamaz.",
+ "deleteAccountConfirm": "Hesabı Sil",
+ "sectionUsageLimits": "Kullanım Sınırları",
+ "sectionVoice": "Ses",
+ "voiceHint": "Açılır listeden tercih ettiğiniz yapay zekâ sesini seçin, ardından dinlemek için oynat'a tıklayın.",
+ "voicePlay": "Oynat",
+ "voiceStop": "Durdur",
+ "quotaSessions": "Oturum / hafta",
+ "quotaMinutesDay": "Dakika / gün",
+ "quotaMinutesWeek": "Dakika / hafta",
+ "quotaTokens": "Token / ay",
+ "quotaUnlimited": "Sınırsız",
+ "quotaLoading": "Yükleniyor...",
+ "quotaHint": "Kullanım haftalık (oturumlar ve dakikalar) ve aylık (token'lar) olarak sıfırlanır.",
+ "sectionMemory": "Anılar",
+ "memoryEmpty": "Henüz kaydedilmiş anı yok. Yapay zekâ, konuşmalar sırasında sizinle ilgili önemli ayrıntıları hatırlayacak ve buraya kaydedecek.",
+ "memoryClearAll": "Tüm Anıları Temizle",
+ "memoryClearAllTitle": "Tüm Anıları Temizle",
+ "memoryClearAllMessage": "Bu işlem kaydedilmiş tüm anıları kalıcı olarak silecek. Bu işlem geri alınamaz.",
+ "memoryClearAllConfirm": "Tümünü Temizle",
+ "memoryManage": "Anıları yönet",
+ "memoryBreadcrumb": "Anı ayarları",
+ "memoryDescription": "Anılar, öğrendiğiniz tüm diller arasında paylaşılır. Lingu konuşmalar sırasında faydalı ayrıntıları kaydedebilir; siz de buradan anı ekleyip kaldırabilirsiniz.",
+ "memoryInputLabel": "Anı ekle",
+ "memoryInputPlaceholder": "Örneğin: Kısa ve öz açıklamaları tercih ederim.",
+ "memoryInputHint": "En fazla {max} karakter",
+ "memoryAdd": "Ekle",
+ "memoryAdding": "Ekleniyor...",
+ "memoryAddSuccess": "Anı eklendi.",
+ "memoryAddError": "Anı eklenemedi. Lütfen tekrar deneyin.",
+ "memoryDuplicateError": "Bu anı zaten mevcut.",
+ "memoryLoadError": "Anılar yüklenemedi.",
+ "memoryDeleteLabel": "Anıyı sil: {content}",
+ "memoryDeleteSuccess": "Anı silindi.",
+ "memoryDeleteError": "Anı silinemedi.",
+ "memoryClearing": "Temizleniyor...",
+ "memoryClearSuccess": "Tüm anılar silindi.",
+ "memoryClearError": "Anılar temizlenemedi.",
+ "memorySourceChat": "Sohbet",
+ "memorySourceVoice": "Sesli",
+ "memorySourceManual": "Elle",
+ "memorySourceUnknown": "Anı",
+ "sectionLanguages": "Diller",
+ "languagesManage": "Dillerim",
+ "cardProfileAccess": "Profil ve erişim",
+ "cardSessionSecurity": "Oturum ve güvenlik",
+ "cardTheme": "Tema",
+ "cardConversationTiming": "Konuşma zamanlaması",
+ "cardTutorVoice": "Eğitmen sesi",
+ "cardCurrentUsage": "Mevcut kullanım",
+ "cardProductReview": "Ürün yorumu",
+ "cardLegalDocuments": "Yasal belgeler",
+ "sectionReview": "Yorum",
+ "reviewDescription": "FreeLingo deneyiminizi paylaşın. Onaylanmış bir yorumu düzenlerseniz, herkese açık olarak görünmeden önce tekrar incelemeye gönderilir.",
+ "reviewPending": "Onay bekliyor",
+ "reviewUpdate": "Güncelle",
+ "reviewSaved": "Yorum kaydedildi.",
+ "reviewDelete": "Yorumu sil",
+ "reviewDeleteTitle": "Yorumu sil",
+ "reviewDeleteMessage": "Yorumunuzu silmek istiyor musunuz? Bu işlem geri alınamaz.",
+ "reviewDeleteConfirm": "Sil",
+ "reviewDeleted": "Yorum silindi.",
+ "reviewDeleteError": "Yorumunuz silinemedi. Lütfen tekrar deneyin."
+ },
+ "legal": {
+ "terms": {
+ "pageTitle": "Hizmet Koşulları",
+ "updated": "Son güncelleme: Ağustos 2026",
+ "s1Title": "1. FreeLingo Hakkında",
+ "s1Body": "FreeLingo, iki şekilde kullanılabilen, yapay zekâ destekli açık kaynaklı bir dil öğrenme platformudur: (1) kendi altyapınızda ücretsiz olarak çalıştırdığınız, kendi kendine barındırılan kurulum ve (2) FreeLingo ekibi tarafından işletilen, ücretsiz plan ve ücretli abonelikler sunan barındırılan hizmet. Bu hizmeti her iki şekilde de kullanarak bu Hizmet Koşulları'nı kabul etmiş olursunuz.",
+ "s2Title": "2. Kabul Edilebilir Kullanım",
+ "s2Intro": "FreeLingo'yu yalnızca kişisel dil öğrenimi amacıyla kullanabilirsiniz. Aşağıdakileri yapmamayı kabul edersiniz:",
+ "s2i1": "Sistemin herhangi bir bölümüne yetkisiz erişim sağlamaya çalışmak.",
+ "s2i2": "Kötü amaçlı içerik yüklemek veya hizmeti aksatmaya çalışmak.",
+ "s2i3": "Hesap bilgilerinizi üçüncü taraflarla paylaşmak.",
+ "s2i4": "Yapay zekâ eğitmenlik özelliklerini kötüye kullanmak için otomatik betikler veya botlar kullanmak.",
+ "s3Title": "3. Kullanıcı Hesapları",
+ "s3Body": "Hesap oluşturmak ve FreeLingo'yu kullanmak için en az 16 yaşında olmalısınız. Kaydolarak bu yaş şartını karşıladığınızı onaylarsınız. Bir hesabın 16 yaşından küçük birine ait olduğunu fark edersek, hesabı ve ilişkili tüm verileri bildirimde bulunmadan sileriz. Hesap bilgilerinizin gizliliğini korumaktan ve hesabınız altında gerçekleşen tüm etkinliklerden siz sorumlusunuz. Kurulumun işletmecisi — kendi kendine barındırılan kurulumlarda siz, barındırılan hizmette FreeLingo ekibi — bu koşulları ihlal eden hesapları askıya alabilir veya kapatabilir.",
+ "s4Title": "4. İçerik ve Yapay Zekâ Etkileşimleri",
+ "s4Body": "Yapay zekâ eğitmeniyle yaptığınız konuşmalar, öğrenmenin sürekliliği için veritabanında saklanır. Barındırılan hizmette yapay zekâ sohbet ve ses özellikleri OpenAI API'si üzerinden işlenir ve içeriğiniz OpenAI'nin kullanım politikalarına tabidir. Yapay zekâ hatalı yanıtlar üretebilir — her zaman kendi muhakemenizi kullanın. FreeLingo, altta yatan dil modellerinin ürettiği içerikten sorumlu değildir. Yapay zekâ eğitmeninin ürettiği içerik (alıştırmalar, düzeltmeler, açıklamalar ve diğer materyaller) kişisel kullanımınız için size aittir. FreeLingo, konuşmalarınız veya bunlardan üretilen içerik üzerinde mülkiyet iddia etmez.",
+ "s5Title": "5. Hizmet Şekilleri",
+ "s5Body": "Kendi kendine barındırılan kurulumlar ücretsizdir ve işletmeci, verileri ile altyapısı üzerinde tam denetime sahiptir. FreeLingo ekibinin işlettiği barındırılan hizmet, kullanım sınırları olan ücretsiz bir plan ile sınırsız erişim sağlayan, aylık veya yıllık olarak alınabilen ücretli abonelikler sunar. Ücretsiz plan, 7 günlük tam erişimli bir deneme süresi (ödeme yöntemi gerekmez) ve ardından dersler ile yapay zekâ sohbeti için sınırlı günlük kotaların yanı sıra dinleme alıştırmaları, okuma alıştırmaları ve sesli konuşmalar için haftalık kotalar içerir. Ücretli abonelikler bu özelliklerdeki tüm kullanım sınırlarını kaldırır. Barındırılan hizmette verileriniz FreeLingo ekibinin işlettiği sunucularda saklanır. Hesabınız için geçerli kullanım sınırlarını panonuzda görebilirsiniz.",
+ "s6Title": "6. Abonelik ve Faturalandırma",
+ "s6Body": "Ücretsiz plan, yeni kullanıcılar için 7 günlük tam erişimli bir deneme süresiyle (ödeme yöntemi gerekmez) yapay zekâ özelliklerine sınırlı erişim sağlar. Ücretli abonelikler aylık veya yıllık olarak sunulur ve önceden iptal edilmedikçe otomatik olarak yenilenir. Aboneliğinizi istediğiniz zaman Ayarlar'dan iptal edebilirsiniz; iptal, mevcut dönemin sonunda geçerli olur. İşletmeci de hizmetin değiştirilmesi veya kapatılması durumunda yenilemeleri veya abonelikleri iptal edebilir. Hizmet, ücreti ödenmiş bir dönem bitmeden sona ererse, tüketici mevzuatının gerektirdiği her türlü iade uygulanır. Cayma hakları ve geçerli fiyatlar, yürürlükteki mevzuata uygun olarak gösterilir ve işlenir.",
+ "s7Title": "7. Erişilebilirlik",
+ "s7Body": "Barındırılan hizmet, süreklilik veya erişilebilirlik garantisi olmaksızın sunulur. İşletmeci, teknik, mali veya başka herhangi bir nedenle hizmeti istediği zaman değiştirebilir, askıya alabilir veya kapatabilir. Makul ölçüde mümkün olduğunda, planlanan kapanışlar uygulama üzerinden veya e-postayla duyurulur. Duyurulan tarihten sonra hesaplar ve operasyonel veriler geri getirilemeyecek şekilde silinebilir. FreeLingo bir yedekleme hizmeti veya otomatik dışa aktarma aracı sunmaz.",
+ "s8Title": "8. Sorumluluğun Sınırlandırılması",
+ "s8Body": "FreeLingo ve katkıda bulunanları, bu hizmeti kullanmanızdan veya kullanamamanızdan doğan hiçbir doğrudan, dolaylı, arızi veya sonuç olarak ortaya çıkan zarardan sorumlu değildir. Yürürlükteki mevzuatın izin verdiği en geniş ölçüde, toplam sorumluluk, talepten önceki üç ay içinde ödediğiniz tutarı aşamaz. Hizmeti bu koşulları ihlal ederek kullanmanızdan doğan üçüncü taraf taleplerine, zararlara veya masraflara karşı FreeLingo'yu ve katkıda bulunanlarını tazmin etmeyi ve zarardan ari tutmayı kabul edersiniz.",
+ "s9Title": "9. Uygulanacak Hukuk",
+ "s9Body": "Bu koşullar İspanya kanunlarına tabidir. İkamet ettiğiniz ülkedeki emredici tüketici koruma mevzuatı aksini öngörmedikçe, bu koşullardan doğan uyuşmazlıklarda münhasıran İspanya mahkemeleri yetkilidir.",
+ "s10Title": "10. Bu Koşullardaki Değişiklikler",
+ "s10Body": "Bu koşullar herhangi bir zamanda güncellenebilir. Önemli değişiklikler, uygun olduğunda hizmet arayüzü üzerinden veya e-postayla bildirilir. Bir değişiklikten sonra hizmeti kullanmaya devam etmeniz, güncellenmiş koşulları kabul ettiğiniz anlamına gelir.",
+ "s11Title": "11. Bölünebilirlik",
+ "s11Body": "Bu koşulların herhangi bir hükmü yetkili bir mahkeme tarafından geçersiz veya uygulanamaz bulunursa, söz konusu hüküm gereken en az ölçüde sınırlandırılır veya kaldırılır ve diğer hükümler tüm geçerliliğiyle yürürlükte kalır.",
+ "linkPrivacy": "Gizlilik Politikası",
+ "linkBack": "Kayda Dön",
+ "linkBackSettings": "Ayarlara Dön",
+ "linkContact": "Bu koşullarla ilgili sorularınız için, erişilebilir olduğu sürece freelingo.app adresindeki iletişim formunu veya FreeLingo GitHub deposunda yayımlanan iletişim kanalını kullanın."
+ },
+ "privacy": {
+ "pageTitle": "Gizlilik Politikası",
+ "updated": "Son güncelleme: Ağustos 2026",
+ "s1Title": "1. Genel Bakış",
+ "s1Body": "FreeLingo, kendi kendine barındırılan bir uygulama ve barındırılan bir hizmet olarak kullanılabilir. Kendi kendine barındırılan kurulumlarda tüm veriler yalnızca işletmecinin sunucusunda saklanır ve FreeLingo proje yazarlarının bu verilere erişimi yoktur. Barındırılan hizmette veri sorumlusu FreeLingo ekibidir ve bu politika, kişisel verilerinizin nasıl toplandığını, kullanıldığını ve korunduğunu açıklar.",
+ "s2Title": "2. Topladığımız Veriler",
+ "s2Intro": "FreeLingo 16 yaşından küçük çocuklara yönelik değildir. 16 yaşından küçük kimseden bilerek kişisel veri toplamayız. 16 yaşından küçükseniz hesap oluşturmayın ve kişisel veri paylaşmayın. Bir hesap oluşturup FreeLingo'yu kullandığınızda aşağıdaki veriler saklanır:",
+ "s2i1": "Kullanıcı adı, görünen ad ve e-posta adresi.",
+ "s2i2": "Özetlenmiş (hash) parola (bcrypt — parolanız hiçbir zaman düz metin olarak saklanmaz).",
+ "s2i3": "Ana dil tercihi ve öğrenme ayarları.",
+ "s2i4": "İsteğe bağlı profil fotoğrafı.",
+ "s2i5": "Yapay zekâ eğitmeniyle yapılan sohbet ve konuşma geçmişi.",
+ "s2i6": "Bilgi kartı desteleri, çalışma ilerlemesi ve aralıklı tekrar verileri.",
+ "s2i7": "Seviye tespiti sonuçları ve müfredat ilerlemesi.",
+ "s2i8": "Abonelik durumu, plan türü ve Stripe müşteri kimliği (ücretli hizmet aboneleri için).",
+ "s2i9": "Yapay zekâ eğitmeninin konuşmalar sırasında öğrendiği veya öğrendiğiniz diller genelinde gelecekteki etkileşimleri kişiselleştirmek için sizin elle eklediğiniz genel hesap anıları ve tercihleri.",
+ "s2i10": "Geri bildirim panosu üzerinden gönderdiğiniz özellik istekleri, hata bildirimleri, yorumlar ve oylar. Herkese açık olarak yalnızca kullanıcı adınız gösterilir; yayımlanan öneri ve bildirimleri tüm kullanıcılar görebilir.",
+ "s3Title": "3. Verilerinizi Nasıl Kullanıyoruz",
+ "s3Intro": "Verileriniz yalnızca FreeLingo öğrenme deneyimini sunmak için kullanılır:",
+ "s3i1": "Kimliğinizi doğrulamak ve oturumunuzu sürdürmek.",
+ "s3i2": "Çalışma planınızı ve eğitmen etkileşimlerinizi kişiselleştirmek.",
+ "s3i3": "Öğrenme ilerlemenizi zaman içinde takip etmek.",
+ "s3i4": "Bağlama uygun yapay zekâ eğitmeni yanıtları üretmek.",
+ "s3i5": "Yapay zekâ eğitmeninin size daha kişiselleştirilmiş etkileşimler sunabilmesi için tercihlerinizi ve bağlamınızı hatırlamak.",
+ "s3i6": "Topluluğun özellik isteklerini ve hata bildirimlerini göstermek ve yönetmek.",
+ "s3Footer": "Verileriniz hiçbir zaman satılmaz, kiralanmaz veya üçüncü taraf pazarlama hizmetleriyle paylaşılmaz.",
+ "s4Title": "4. Harici Yapay Zekâ Hizmetleri",
+ "s4Body": "Barındırılan hizmette yapay zekâ sohbeti, ses sentezi (TTS) ve ses deşifresi (STT) OpenAI API'si üzerinden sağlanır. Konuşma içeriğiniz ve ses kayıtlarınız işlenmek üzere OpenAI'ye gönderilir ve OpenAI Gizlilik Politikası'na (openai.com/privacy) tabidir. Ödeme işlemleri Stripe, Inc. (ABD) tarafından yürütülür; Stripe ödeme verilerini doğrudan işler ve biz kart bilgilerini sunucularımızda saklamayız — işlem verileriniz Stripe Gizlilik Politikası'na (stripe.com/privacy) tabidir. Kendi kendine barındırılan kurulumlarda yapay zekâ sağlayıcısını işletmeci seçer; varsayılan olarak yerel bir Ollama kurulumu kullanılır ve hiçbir veri sunucudan çıkmaz.",
+ "s5Title": "5. Çerezler ve Oturum Anahtarları",
+ "s5Body": "FreeLingo, hizmetin çalışması için kesinlikle gerekli olan iki teknik çerez kullanır: oturum kimlik doğrulaması için kullanılan refresh_token (httpOnly, 30 gün geçerli) ve dil tercihinizi hatırlamak için NEXT_LOCALE (1 yıl geçerli). İzleme veya reklam çerezi kullanılmaz. Anonim trafik ölçümü için, çerez kullanmayan, kişisel veri toplamayan ve tüm verileri kendi sunucularımızda tutan, kendi kendine barındırılan açık kaynaklı bir analiz aracı olan Umami'yi kullanırız.",
+ "s6Title": "6. Haklarınız (GDPR)",
+ "s6Body": "Avrupa Ekonomik Alanı'ndaysanız GDPR kapsamında erişim, düzeltme, silme, itiraz ve veri taşınabilirliği haklarına sahipsiniz. Hesabınızı doğrudan Ayarlar'dan silebilirsiniz. Diğer talepler 8. bölümdeki iletişim kanalı üzerinden elle işlenir; FreeLingo otomatik bir dışa aktarma aracı sunmaz. Verileri hizmeti sunmak için işleriz (GDPR md. 6(1)(b)). OpenAI ve Stripe, uluslararası aktarımlar için geçerli güvenceler kapsamında verileri ABD'de işleyebilir. İspanya Veri Koruma Kurumu'na (aepd.es) veya bulunduğunuz yerdeki denetim makamına şikâyette bulunabilirsiniz.",
+ "s7Title": "7. Verilerin Saklanması ve Silinmesi",
+ "s7Body": "Operasyonel veriler, hesabınız var olduğu ve barındırılan hizmet faaliyette olduğu sürece saklanır. Anılarınızı, dillerinizi veya hesabınızın tamamını Ayarlar'dan silebilirsiniz. İşletmeci, hizmeti kapatırken mümkün olan makul bildirimi yaptıktan sonra tüm hesapları ve verilerini de silebilir. Silme işlemi geri alınamaz. İşletmecinin denetimindeki sistemler ve kopyalar silindikten sonra FreeLingo kişisel veri saklamaz. Stripe, ödeme ve işlem bilgilerini kendi politikası ve yasal yükümlülükleri çerçevesinde saklayabilir.",
+ "s8Title": "8. İletişim",
+ "s8Body": "Barındırılan hizmetle ilgili gizlilik sorularınız için, erişilebilir olduğu sürece freelingo.app adresindeki iletişim formunu kullanın. Hizmet kapandıktan sonra FreeLingo GitHub deposunda yayımlanan iletişim kanalını kullanın ve herkese açık issue'larda kişisel veri paylaşmayın. Kendi kendine barındırılan kurulumlar için ilgili kurulumun yöneticisiyle iletişime geçin.",
+ "s9Title": "9. Bölünebilirlik",
+ "s9Body": "Bu politikanın herhangi bir hükmü geçersiz veya uygulanamaz bulunursa, söz konusu hüküm gereken en az ölçüde sınırlandırılır ve diğer hükümler tüm geçerliliğiyle yürürlükte kalır.",
+ "linkTerms": "Hizmet Koşulları",
+ "linkBack": "Kayda Dön",
+ "linkBackSettings": "Ayarlara Dön"
+ }
+ },
+ "dashboard": {
+ "welcomeBack": "Tekrar hoş geldiniz",
+ "streak": "Seri",
+ "xp": "XP",
+ "level": "Seviye",
+ "skills": "Beceriler",
+ "today": "Bugün",
+ "noSkills": "Becerilerinizi takip etmek için seviye tespitini tamamlayın.",
+ "noLessons": "Bugün için planlanmış ders yok.",
+ "lessonDone": "tamamlandı",
+ "startLesson": "Başla",
+ "loadingProgress": "İlerleme yükleniyor...",
+ "noLevel": "Henüz bir seviye belirlenmedi.",
+ "completeAssessment": "Seviye tespitine gir",
+ "allCaughtUp": "Planınız, kendi hızınızda",
+ "levelTestReady": "Seviye testiniz hazır.",
+ "levelTestReadyDesc": "Planınızın sonuna ulaştınız. İlerlemenizi doğrulamak için seviye testine girin.",
+ "levelTestCompleted": "Seviye testi tamamlandı.",
+ "levelTestScoreLine": "Final puanı: {score}. Sonucunuz Planım bölümüne kaydedildi.",
+ "startWithAssessment": "Seviye tespitiyle başlayın.",
+ "takeAssessmentArrow": "Seviye Tespitine Gir →",
+ "goToMyPlan": "Planım →",
+ "quickActions": "Hızlı İşlemler",
+ "dayProgress": "Gün {current} / {total}",
+ "skipDay": "Bugünü atla",
+ "pendingLessons": "bekleyen ders",
+ "nextStep": "Sonraki adım",
+ "assessmentCreatesPlan": "Seviye tespiti, seviyenizi belirler ve etkin diliniz için size özel bir plan oluşturur.",
+ "lessonReady": "Sonraki adımınız, kendi hızınızda",
+ "pendingStillAvailable": "Planınızda bekleyen {count} önceki ders var.",
+ "noPendingToday": "Nasıl devam edeceğinizi görmek için planınıza göz atın.",
+ "planDayGoal": "Planınızın bu günü için hedef",
+ "lessonsCompleted": "Dersler",
+ "accuracy": "Doğruluk",
+ "exerciseStats": "{correct}/{total} alıştırma",
+ "noExercisesYet": "Henüz alıştırma yok",
+ "planProgress": "Plan ilerlemesi",
+ "currentDay": "Mevcut gün",
+ "daysRemaining": "Kalan gün",
+ "completedToday": "{completed}/{total} tamamlandı",
+ "vocabularyProgress": "Kelime bilgisi {level}",
+ "vocabularyWords": "{mastered}/{total} kelime",
+ "recentPerformance": "Son performans",
+ "recentPerformanceDescription": "En son cevaplarınıza ve tekrarlarınıza göre.",
+ "performanceNeedsPractice": "Pratik gerekiyor",
+ "performanceInProgress": "Gelişiyor",
+ "performanceStrong": "Güçlü",
+ "premiumBannerTitle": "Konuşma, dinleme ve okumanın kilidini açın",
+ "premiumBannerDesc": "Ücretsiz denemeyle başlayın ve seviyenize uyarlanmış yazılı sohbet, sesli konuşma ve alıştırmalarla Lingu ile pratik yapın.",
+ "premiumBannerDescTrialUsed": "Lingu ile yazılı sohbetin, sesli konuşmanın, okuma ve dinlemenin kilidini açmak için Premium'u etkinleştirin.",
+ "premiumBannerCta": "Ücretsiz deneme",
+ "premiumBannerCtaTrialUsed": "Planları görüntüle",
+ "premiumBannerPastDueTitle": "Ödeme bekleniyor",
+ "premiumBannerPastDueDesc": "Sohbet, sesli konuşma, okuma ve dinlemeye Premium erişiminizi geri kazanmak için ödeme yönteminizi güncelleyin.",
+ "premiumBannerPastDueCta": "Premium'u geri yükle",
+ "freemiumTrialTitle": "{days, plural, one {# günlük ücretsiz deneme} other {# günlük ücretsiz deneme}}",
+ "freemiumTrialDesc": "Sohbet, ses, dinleme ve okumaya sınırsız erişimin keyfini çıkarın. Premium'un tüm özelliklerini ücretsiz deneyin.",
+ "announcementDismiss": "Duyuruyu kapat",
+ "announcementDismissError": "Duyuru kapatılamadı. Tekrar deneyin."
+ },
+ "assessment": {
+ "title": "Seviye Tespiti",
+ "subtitle": "CEFR Seviye Belirleme",
+ "intro": "İngilizce seviyenizi belirlemek için size birkaç soru soracağız.",
+ "startWriting": "Yazma Değerlendirmesine Başla",
+ "startQuiz": "Teste Başla",
+ "writing": {
+ "title": "Yazı Örneği",
+ "submit": "Yazıyı Gönder",
+ "placeholder": "Cevabınızı buraya yazın...",
+ "evaluating": "Değerlendiriliyor..."
+ },
+ "quiz": {
+ "question": "Soru",
+ "of": "/",
+ "next": "İleri",
+ "submit": "Testi Gönder",
+ "submitting": "Gönderiliyor..."
+ },
+ "result": {
+ "title": "Seviyeniz",
+ "xpEarned": "Kazanılan XP",
+ "skillsTitle": "Beceriler",
+ "goToPlan": "Planımı Görüntüle",
+ "generatePlan": "Çalışma Planı Oluştur"
+ },
+ "currentLevel": "Mevcut Seviye",
+ "assessedOn": "Değerlendirme tarihi",
+ "alreadyHasPlan": "Zaten etkin bir çalışma planınız var. Seviye tespitini tekrarlamak yeni bir plan oluşturur.",
+ "retake": "Seviye Tespitini Tekrarla",
+ "evaluating": "Değerlendiriliyor...",
+ "resultStep": "Sonuç",
+ "cefrLevel": "CEFR Seviyesi",
+ "overrideLevel": "Başlangıç seviyesini değiştir",
+ "strengths": "Güçlü yönler",
+ "needsWork": "Geliştirilmesi gerekenler",
+ "createPlan": "Plan Oluştur →",
+ "creating": "Oluşturuluyor...",
+ "loading": "Yükleniyor...",
+ "levelTest": {
+ "title": "Seviye Tamamlama Testi",
+ "loading": "Sorular yükleniyor...",
+ "loadingTest": "Test yükleniyor...",
+ "submittingTest": "Cevaplar gönderiliyor...",
+ "quizTitle": "{level} Seviye Testi",
+ "resultsTitle": "{level} Seviye Testi — Sonuçlar",
+ "finalScore": "Final Puanı",
+ "correctCount": "{correct} / {total} doğru",
+ "correctAnswer": "✓ Doğru",
+ "incorrectAnswer": "✗ Yanlış — doğru cevap: {answer}",
+ "nextLevelFallback": "SONRAKİ SEVİYE",
+ "advanceLabel": "{level} İÇİN HAZIRSINIZ",
+ "advanceMessage": "{level} seviyesine sağlam bir şekilde hâkim olduğunuzu gösterdiniz. {next} planınızı oluşturmak için seviye tespitini tekrarlayın.",
+ "extendLabel": "4 HAFTALIK UZATMA",
+ "extendMessage": "Zayıf alanlar tespit edildi: {areas}. 4 hafta daha odaklı pratik yapmanızı öneriyoruz.",
+ "repeatLabel": "{level} TEKRARI",
+ "repeatMessage": "Puan %55'in altında. Birkaç temel {level} yeterliliğinin pekiştirilmesi gerekiyor.",
+ "reviewPlan": "Planımı İncele →",
+ "goToPlan": "Plana Git",
+ "invalidPlan": "Geçersiz plan kimliği. Seviye testine plan sayfanızdan erişin.",
+ "noQuestions": "Sunucudan soru alınamadı.",
+ "loadFailed": "Seviye testi yüklenemedi.",
+ "submitFailed": "Gönderim başarısız.",
+ "question": "Soru",
+ "of": "/",
+ "confirm": "Cevabı Onayla",
+ "nextQuestion": "Sonraki Soru",
+ "submit": "Testi Gönder",
+ "result": {
+ "title": "Test Tamamlandı",
+ "recommendation": "Öneri",
+ "advance": "Bir sonraki seviyeye geçmeye hazırsınız",
+ "extend": "Mevcut seviyenizde devam edin",
+ "repeat": "Mevcut seviyenin tekrarı öneriliyor",
+ "back": "Plana Dön"
+ }
+ },
+ "suggestedLevel": "Önerilen: {aiLevel}; plan {selectedLevel} seviyesini kullanacak",
+ "step1": "Adım 1 / 3: Seviyeniz",
+ "step2": "Adım 2 / 3: Soru {questionNumber} / {totalQuestions}",
+ "dontKnow": "Bilmiyorum",
+ "step3": "Adım 3 / 3: Programınız",
+ "studiedBefore": "Daha önce {language} çalıştınız mı?",
+ "studiedBeforeHint": "Bu, teste doğru seviyeden başlamamıza yardımcı olur.",
+ "beginnerOption": "Tamamen yeni başlıyorum",
+ "beginnerOptionHint": "A1'den başlayacağım, teste gerek yok.",
+ "hasExperienceOption": "Evet, biraz deneyimim var",
+ "hasExperienceOptionHint": "Kısa bir uyarlanabilir teste gireceğim (≈12 soru).",
+ "skills": {
+ "grammar": "Dil Bilgisi",
+ "vocabulary": "Kelime Bilgisi",
+ "reading": "Okuma"
+ },
+ "howManyWeeks": "{cefr_level} seviyesi için kaç hafta?",
+ "nWeeks": "{count} hafta",
+ "intensity": {
+ "intensive": "Yoğun",
+ "standard": "Standart",
+ "relaxed": "Rahat",
+ "veryRelaxed": "Çok rahat"
+ },
+ "approxLessons": "~{count} ders",
+ "daysPerWeek": "Haftada {count} gün",
+ "mainGoals": "Ana hedefler (uygun olanların hepsini seçin)",
+ "goals": {
+ "grammar": "Dil Bilgisi",
+ "vocabulary": "Kelime Bilgisi",
+ "reading": "Okuma",
+ "writing": "Yazma",
+ "conversation": "Konuşma",
+ "listening": "Dinleme"
+ },
+ "summaryLevel": "Seviye",
+ "summaryDuration": "Süre",
+ "summaryGoals": "Hedefler",
+ "noneSelected": "hiçbiri seçilmedi",
+ "buildingPlan": "Plan oluşturuluyor...",
+ "startWarningTitle": "Başlamadan önce",
+ "startWarningMessage": "Seviye tespitini bitirmeden ayrılırsanız ilerlemeniz kaybolur ve baştan başlamanız gerekir.",
+ "startWarningMessageLevelTest": "Seviye testini bitirmeden ayrılırsanız ilerlemeniz kaybolur ve testi yeniden yapmanız gerekir.",
+ "startWarningConfirm": "Başla",
+ "startMyPlan": "Planımı başlat",
+ "voiceTrialLabel": "Sesli deneme",
+ "voiceTrialTitle": "Hemen gerçek bir konuşma deneyin",
+ "voiceTrialDesc": "Lingu ile {minutes} dakika ücretsiz konuşun. Eğitmeniniz yeni açtığınız seviyeye uyum sağlayacak.",
+ "voiceTrialStart": "Ücretsiz konuşmayı başlat",
+ "voiceTrialSkip": "Şimdilik atla"
+ },
+ "plan": {
+ "unitLocked": "Ünite kilitli",
+ "unitAvailable": "Ünite kullanılabilir",
+ "lessonPending": "Bekleyen ders",
+ "title": "Planım",
+ "subtitle": "Çalışma Planı",
+ "noPlan": "Etkin bir çalışma planı bulunamadı.",
+ "startAssessment": "Önce seviye tespitine girin",
+ "week": "Hafta",
+ "currentLesson": "Mevcut Ders",
+ "currentUnit": "Mevcut ünite",
+ "startLesson": "Derse Başla",
+ "completed": "Tamamlandı",
+ "generating": "Ders oluşturuluyor...",
+ "generatingError": "Ders oluşturulamadı",
+ "loading": "Plan yükleniyor...",
+ "learningRoadmap": "Öğrenme Yol Haritası",
+ "level": "Seviye",
+ "duration": "Süre",
+ "durationDetail": "{weeks} hafta · haftada {days} gün",
+ "unitsLabel": "Üniteler",
+ "noUnitsForLevel": "{level} seviyesi için ünite yok",
+ "takeAssessmentArrow": "Seviye Tespitine Gir →",
+ "unitLabel": "Ünite",
+ "grammarCovered": "İşlenen dil bilgisi konuları",
+ "noLessons": "Bu ünite için henüz planlanmış ders yok.",
+ "weekDay": "Hafta {week}, Gün {day}",
+ "lessonTypes": {
+ "grammar": "Dil Bilgisi",
+ "vocabulary": "Kelime Bilgisi",
+ "reading": "Okuma",
+ "writing": "Yazma",
+ "listening": "Dinleme",
+ "conversation": "Konuşma",
+ "review": "Tekrar",
+ "level_test": "Seviye Testi"
+ },
+ "levelComplete": "{level} Seviyesi Tamamlandı. Seviye Testine Girin",
+ "levelCompleteDesc": "{level} seviyesi için planınızın sonuna ulaştınız. İlerlemenizi doğrulamak ve bir sonraki seviyenin kilidini açmak için seviye tamamlama testine girin.",
+ "levelCompleteHint": "Test yaklaşık 20 dakika sürer ve dil bilgisi, kelime bilgisi ve okumayı kapsar.",
+ "beginLevelTest": "Seviye Testine Başla",
+ "nLessons": "{count} ders",
+ "nGrammar": "{count} dil bilgisi",
+ "levelTestLabel": "Seviye Testi",
+ "unitAriaLabel": "Ünite {index}: {title}",
+ "noUnitsDesc": "Bu seviyenin içeriği hazırlanıyor. Yakında tekrar göz atın.",
+ "completionTestTitle": "{level} Seviyesi Tamamlama Testi",
+ "levelTestResult": "Seviye Testi Sonucu",
+ "testScore": "Puan:",
+ "pendingLessons": "Bekleyen dersler",
+ "pendingReassurance": "Bunlara size uygun olan herhangi bir zamanda devam edebilirsiniz.",
+ "resume": "Devam et",
+ "lessonsHeader": "Dersler · {count}",
+ "reviewLesson": "Tekrar et",
+ "noPlanLabel": "BAŞLAYIN",
+ "noPlanTitle": "Çalışma planınızı oluşturun",
+ "noPlanDesc": "Seviyenizi keşfetmek ve size özel bir öğrenme yolu edinmek için kısa bir seviye tespitine girin.",
+ "noPlanHint": "Yaklaşık 10 dakika sürer"
+ },
+ "lesson": {
+ "loading": "Ders yükleniyor...",
+ "notFound": "Ders bulunamadı",
+ "backToPlan": "Plana Dön",
+ "progress": "İlerleme",
+ "exercise": "Alıştırma",
+ "of": "/",
+ "checkAnswer": "Cevabı Kontrol Et",
+ "nextExercise": "Sonraki Alıştırma",
+ "submitWriting": "Yazıyı Gönder",
+ "yourAnswer": "Cevabınız...",
+ "correct": "Doğru",
+ "incorrect": "Yanlış",
+ "feedback": "Geri bildirim",
+ "corrections": "Düzeltmeler",
+ "completeLesson": "Dersi Tamamla",
+ "completing": "Tamamlanıyor...",
+ "relatedGrammar": "İlgili Dil Bilgisi",
+ "explanation": "Açıklama",
+ "exercises": "Alıştırmalar",
+ "vocabulary": "Kelime Bilgisi",
+ "label": "Ders",
+ "submitAnswer": "Cevabı Gönder",
+ "lessonDone": "Tebrikler. Ders tamamlandı.",
+ "exerciseSummary": "Değerlendirilen alıştırmalar: {completed} / {total}",
+ "score": "Puan",
+ "showNativeExplanation": "Şu dilde göster:",
+ "hint": "İpucu",
+ "showNativeHint": "İpucunu şu dilde göster:",
+ "regenerateExercise": "Alıştırmayı yeniden oluştur",
+ "regeneratingExercise": "Yeniden oluşturuluyor...",
+ "regenerateError": "Alıştırma yeniden oluşturulamadı. Tekrar deneyin.",
+ "regenerateNotNeeded": "Bu alıştırmanın yeniden oluşturulmasına gerek yok.",
+ "examples": "Örnekler",
+ "commonTraps": "Sık yapılan hatalar",
+ "miniGlossary": "Mini sözlük",
+ "pronunciationExercise": "Telaffuz",
+ "exerciseTypeMultipleChoice": "Çoktan Seçmeli",
+ "exerciseTypeFillBlank": "Boşluk Doldurma",
+ "exerciseTypeFreeWrite": "Serbest Yazma",
+ "exerciseTypePronunciation": "Telaffuz",
+ "tapToListen": "Dinlemek için dokunun",
+ "recordNow": "Cevabınızı kaydedin",
+ "exit": "Çık",
+ "exitConfirmTitle": "Dersten çıkılsın mı?",
+ "exitConfirmMessage": "Bu dersteki ilerlemeniz kaybolacak.",
+ "dayComplete": "Gün tamamlandı!",
+ "dayCompleteMsg": "Planınızın bu gününü tamamladınız. Size uygun olan herhangi bir zamanda devam edin."
+ },
+ "flashcards": {
+ "title": "Bilgi Kartları",
+ "due": "Tekrar zamanı gelen",
+ "total": "Toplam",
+ "noDue": "Tekrar zamanı gelen kart yok",
+ "allCaughtUp": "Hepsi tamam!",
+ "generate": "Bilgi Kartı Oluştur",
+ "generating": "Oluşturuluyor...",
+ "topic": "Konu",
+ "topicPlaceholder": "ör. seyahat kelimeleri",
+ "count": "Adet",
+ "level": "Seviye",
+ "submit": "Oluştur",
+ "again": "Tekrar",
+ "hard": "Zor",
+ "good": "İyi",
+ "easy": "Kolay",
+ "front": "Kelime",
+ "back": "Tanım",
+ "loading": "Bilgi kartları yükleniyor...",
+ "sectionGenerate": "Yeni Kart Oluştur",
+ "speakingMode": "Konuşma",
+ "standardMode": "Standart",
+ "sayWord": "Kelimeyi sesli söyleyin",
+ "refresh": "Yenile",
+ "generateBtn": "Oluştur",
+ "cards": "kart",
+ "myVocabularyBtn": "Kelimelerim",
+ "myVocabulary": "Kelimelerim",
+ "myVocabularyEmpty": "Henüz kaydedilmiş kelime yok. Okurken bir kelime seçerek buraya kaydedebilirsiniz.",
+ "myVocabularyNoResults": "Aramanızla eşleşen kayıtlı kelime yok.",
+ "vocabularySearch": "Kelime ara...",
+ "vocabularyPageInfo": "Sayfa {page} / {pages}",
+ "vocabularyPrev": "Önceki",
+ "vocabularyNext": "Sonraki",
+ "backToFlashcards": "Bilgi Kartları",
+ "noCardsHint": "Yapay zekâ ile ilk kartlarınızı oluşturmak için + Oluştur'u kullanın",
+ "tapToHide": "gizlemek için dokunun",
+ "tapToReveal": "görmek için dokunun",
+ "interval": "Aralık",
+ "repetitions": "Tekrar"
+ },
+ "chat": {
+ "title": "Yapay Zekâ Eğitmeni",
+ "subtitle": "{language} — İstediğinizi sorun",
+ "placeholder": "Bir mesaj yazın...",
+ "send": "Gönder",
+ "sending": "Gönderiliyor...",
+ "noConversation": "Bir konuşma seçin veya başlatın",
+ "newConversation": "Yeni",
+ "toggleSidebarShow": "Sohbetleri göster",
+ "toggleSidebarHide": "Sohbetleri gizle",
+ "conversations": "Konuşmalar",
+ "voiceSession": "Sesli oturum",
+ "loading": "Yükleniyor...",
+ "thinking": "Düşünüyor...",
+ "takingLonger": "Beklenenden uzun sürüyor...",
+ "errorMessage": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
+ "deleteTitle": "Sohbeti Sil",
+ "deleteMessage": "Bu konuşma kalıcı olarak silinecek.",
+ "deleteConfirm": "Sil",
+ "enterToSend": "Göndermek için Enter",
+ "continueInVoice": "Sesli devam et",
+ "memoryUpdated": "Anı güncellendi"
+ },
+ "progress": {
+ "title": "İlerleme",
+ "subtitle": "Beceri Takibi",
+ "xp": "XP",
+ "streak": "Seri",
+ "lessons": "Dersler",
+ "accuracy": "Doğruluk",
+ "noPlan": "Etkin çalışma planı yok.",
+ "startPlan": "Seviye tespitine başla",
+ "competencies": "Ünite Yeterlilikleri",
+ "vocabularySection": "Kelime Bilgisi",
+ "skills": "Beceri Doğruluğu",
+ "mastered": "Öğrenildi",
+ "inProgress": "Devam Ediyor",
+ "notStarted": "Başlanmadı",
+ "loading": "İlerleme yükleniyor...",
+ "days": "gün",
+ "noData": "İlerlemenizi burada görmek için birkaç ders tamamlayın.",
+ "exploreVocab": "Kelime setlerini keşfedin",
+ "competenciesSection": "{level} Dil Bilgisi Yeterlilikleri",
+ "noCompetencies": "Yeterliliklerinizi takip etmeye başlamak için ders tamamlayın.",
+ "goToMyPlan": "→ Planıma Git",
+ "addToFlashcards": "Kelime bazında ilerlemenizi takip etmek için bir setteki kelimeleri bilgi kartı destenize ekleyin.",
+ "noActivePlanDesc": "Becerilerinizi takip etmeye başlamak için seviye tespitini tamamlayın.",
+ "vocabularyHeader": "{level} Kelime Bilgisi",
+ "currentLevelOnly": "Yalnızca mevcut seviye",
+ "allLevels": "Tüm seviyeler"
+ },
+ "grammar": {
+ "title": "Dil Bilgisi Başvuru Kaynağı",
+ "subtitle": "Başvuru",
+ "searchPlaceholder": "Dil bilgisi konularında ara...",
+ "allLevels": "Tüm Seviyeler",
+ "allCategories": "Tümü",
+ "noResults": "Konu bulunamadı.",
+ "structure": "Yapı",
+ "keyRules": "Temel Kurallar",
+ "examples": "Örnekler",
+ "commonMistakes": "Sık Yapılan Hatalar",
+ "relatedTopics": "İlgili Konular",
+ "backToGrammar": "Dil Bilgisi Başvuru Kaynağı",
+ "explanation": "Açıklama",
+ "backLink": "Dil Bilgisine Dön",
+ "notFound": "Konu bulunamadı.",
+ "topicsFound": "{count, plural, one {# konu bulundu} other {# konu bulundu}}"
+ },
+ "vocabulary": {
+ "title": "Kelime Merkezi",
+ "subtitle": "Kelime Bilgisi",
+ "all": "Tümü",
+ "searchPlaceholder": "Setlerde ara...",
+ "sets": "{count, plural, one {# set} other {# set}}",
+ "words": "kelime",
+ "addAll": "{count} Kelimenin Tümünü Bilgi Kartlarına Ekle",
+ "adding": "Ekleniyor...",
+ "added": "{count} bilgi kartı eklendi",
+ "addError": "Bilgi kartları eklenemedi",
+ "frequency": "sıklık",
+ "backToVocabulary": "Kelime Merkezi",
+ "notFound": "Set bulunamadı.",
+ "vocabularySet": "Kelime Seti",
+ "cardsAdded": "Bilgi kartı destenize {count} kart eklendi.",
+ "goToFlashcards": "Bilgi Kartlarına Git →",
+ "addFailed": "Bilgi kartları eklenemedi.",
+ "noResults": "Aramanızla eşleşen kelime seti yok"
+ },
+ "phrasebook": {
+ "title": "Konuşma Kılavuzu",
+ "subtitle": "İfadeler",
+ "all": "Tümü",
+ "allRegisters": "Tüm üsluplar",
+ "formal": "Resmî",
+ "neutral": "Nötr",
+ "informal": "Gündelik",
+ "copied": "Kopyalandı!",
+ "copy": "Kopyala",
+ "copyPhrase": "İfadeyi kopyala",
+ "noResults": "Filtrelerinizle eşleşen ifade yok",
+ "level": "Seviye",
+ "register": "Üslup",
+ "statsLine": "{situationCount} durum · {phraseCount} ifade · {range}",
+ "situationsShown": "{count, plural, one {# durum gösteriliyor} other {# durum gösteriliyor}}",
+ "searchPlaceholder": "İfadelerde ara..."
+ },
+ "admin": {
+ "title": "Yönetim",
+ "overview": "Genel Bakış",
+ "users": "Kullanıcılar",
+ "feedback": "Geri Bildirim",
+ "reviews": "Yorumlar",
+ "system": "Sistem",
+ "invites": "Davet Bağlantıları",
+ "createUser": "Kullanıcı Oluştur",
+ "createInvite": "Davet Oluştur",
+ "deactivate": "Devre dışı bırak",
+ "activate": "Etkinleştir",
+ "loading": "Yükleniyor...",
+ "loadError": "Kullanıcı verileri yüklenemedi",
+ "userNotFound": "Kullanıcı bulunamadı",
+ "role": "Rol",
+ "status": "Durum",
+ "active": "Etkin",
+ "inactive": "Devre dışı",
+ "inviteCreated": "Davet bağlantısı oluşturuldu",
+ "copyLink": "Bağlantıyı kopyala",
+ "inviteCopied": "Davet bağlantısı kopyalandı",
+ "deleteUser": "Kullanıcıyı Sil",
+ "deleteUserMessage": "Bu kullanıcı silinsin mi? Bu işlem geri alınamaz.",
+ "deleteConfirm": "Sil",
+ "noUsers": "Kullanıcı bulunamadı",
+ "inviteLink": "Davet Bağlantısı (48 saat geçerli)",
+ "total": "toplam",
+ "delete": "Sil",
+ "inviteBtn": "Davet Bağlantısı",
+ "createUserBtn": "Kullanıcı Oluştur",
+ "prevPage": "← Önceki",
+ "nextPage": "Sonraki →",
+ "roleUser": "Kullanıcı",
+ "roleAdmin": "Yönetici",
+ "fieldUsername": "Kullanıcı adı",
+ "invalidUsernameChars": "Kullanıcı adı yalnızca harf, rakam, kısa çizgi, alt çizgi ve nokta içerebilir",
+ "fieldEmail": "E-posta",
+ "fieldPassword": "Parola",
+ "fieldDisplayName": "Görünen Ad",
+ "fieldNativeLanguage": "Ana Dil",
+ "fieldTargetLanguage": "Öğrenilecek dil",
+ "fieldRole": "Hesap türü",
+ "submitCreate": "Oluştur",
+ "searchPlaceholder": "Kullanıcı adı veya e-postayla ara...",
+ "statsTitle": "Kullanım istatistikleri",
+ "statsCefr": "Mevcut seviye",
+ "statsUnit": "Mevcut ünite",
+ "statsPlanWeeks": "Plan süresi",
+ "sectionXpProgress": "XP ve İlerleme",
+ "statsXp": "Toplam XP",
+ "statsStreak": "Seri",
+ "statsActiveDays": "Etkin günler",
+ "statsLessons": "Tamamlanan dersler",
+ "statsExercises": "Doğru alıştırmalar",
+ "statsMessages": "Gönderilen mesajlar",
+ "statsTokensTotal": "Harcanan token (toplam)",
+ "statsTokensChat": "Eğitmen token'ları",
+ "statsTokensConversation": "Ses token'ları",
+ "statsTokensNote": "Yalnızca yapay zekâ sağlayıcısı bu bilgiyi döndürdüğünde kullanılabilir",
+ "statsNoData": "İlerleme verisi yok",
+ "viewStats": "İstatistikleri görüntüle",
+ "weeks": "hafta",
+ "statsTestScore": "Final testi puanı",
+ "statsDays": "gün",
+ "sectionQuota": "Konuşma Kotaları",
+ "quotaWeeklySessions": "Haftalık oturum sınırı",
+ "quotaDailyMinutes": "Günlük dakika sınırı",
+ "quotaWeeklyMinutes": "Haftalık dakika sınırı",
+ "quotaMonthlyTokens": "Aylık token sınırı",
+ "quotaUnlimitedLabel": "Sınırsız (0)",
+ "quotaUsedSessions": "Bu hafta kullanılan",
+ "quotaUsedMinutes": "Bugün kullanılan",
+ "quotaUsedWeeklyMinutes": "Bu hafta kullanılan (dk)",
+ "quotaSave": "Kotaları kaydet",
+ "quotaSaved": "Kotalar kaydedildi.",
+ "emailVerified": "E-posta doğrulandı",
+ "emailNotVerified": "E-posta doğrulanmadı",
+ "verifyEmail": "E-postayı doğrula",
+ "unverifyEmail": "Doğrulanmamış olarak işaretle",
+ "emailVerifiedSuccess": "E-posta doğrulanmış olarak işaretlendi.",
+ "emailUnverifiedSuccess": "E-posta doğrulanmamış olarak işaretlendi.",
+ "fieldSubscription": "Abonelik",
+ "fieldEndsRenews": "Bitiş / Yenileme",
+ "fieldStripeCustomer": "Stripe Müşterisi",
+ "maintenanceTitle": "Bakım Modu",
+ "maintenanceOn": "AÇIK",
+ "maintenanceEnable": "Bakımı Etkinleştir",
+ "maintenanceDisable": "Bakımı Devre Dışı Bırak",
+ "maintenanceDesc": "Etkinleştirildiğinde, aboneliğe bağlı tüm özellikler (eğitmen sohbeti, sesli konuşmalar, dinleme ve okuma alıştırmaları) tüm kullanıcılar için kullanılamaz olur.",
+ "cannotDeactivateSelf": "Kendi hesabınızı devre dışı bırakamazsınız",
+ "cannotDeleteSelf": "Kendi hesabınızı silemezsiniz",
+ "sectionLanguages": "Diller",
+ "sectionActivity": "Etkinlik",
+ "statsActive": "Etkin",
+ "manageUsers": "Kullanıcıları yönet",
+ "manageUsersDesc": "Hesapları, abonelikleri, rolleri ve erişim durumunu inceleyin.",
+ "createUserDesc": "Başlangıç çalışma diliyle yönetilen bir kullanıcı hesabı oluşturun.",
+ "reviewFeedback": "Geri bildirimleri incele",
+ "reviewFeedbackDesc": "Öğrencilerden gelen özellik isteklerini ve hata bildirimlerini önceliklendirin.",
+ "reviewReviews": "Yorumları incele",
+ "reviewReviewsDesc": "Doğrulanmış öğrenci yorumlarını açılış sayfasında gösterilmek üzere onaylayın.",
+ "maintenanceOnDesc": "Aboneliğe bağlı öğrenme araçları için bakım modu etkin.",
+ "maintenanceOffDesc": "Bakım modu kapalı. Aboneliğe bağlı öğrenme araçları kullanılabilir.",
+ "openSystemControls": "Sistem denetimleri",
+ "adminRequired": "Yönetici erişimi gerekli",
+ "usersLoadError": "Kullanıcılar yüklenemedi",
+ "createUserError": "Kullanıcı oluşturulamadı",
+ "updateUserError": "Kullanıcı güncellenemedi",
+ "deleteUserError": "Kullanıcı silinemedi",
+ "inviteError": "Davet bağlantısı oluşturulamadı",
+ "copyInviteError": "Davet bağlantısı kopyalanamadı",
+ "maintenanceError": "Bakım modu güncellenemedi",
+ "maintenanceOff": "KAPALI",
+ "inviteExpiry": "Tek kullanımlık bağlantı. 48 saat sonra geçerliliğini yitirir.",
+ "searchAction": "Kullanıcı ara",
+ "roleFilter": "Role göre filtrele",
+ "statusFilter": "Duruma göre filtrele",
+ "subscriptionFilter": "Aboneliğe göre filtrele",
+ "allRoles": "Tüm roller",
+ "allStatuses": "Tüm durumlar",
+ "allSubscriptions": "Tüm abonelikler",
+ "clearFilters": "Temizle",
+ "userColumn": "Kullanıcı",
+ "actions": "İşlemler",
+ "createUserSheetDesc": "Hesap hemen oluşturulur. E-posta etkinse doğrulama e-postası gönderilir.",
+ "activateUser": "Kullanıcıyı etkinleştir",
+ "deactivateUser": "Kullanıcıyı devre dışı bırak",
+ "activateUserMessage": "Bu kullanıcı hesabı etkinleştirilsin mi?",
+ "deactivateUserMessage": "Bu kullanıcı hesabı devre dışı bırakılsın mı? Kullanıcı, hesabı yeniden etkinleştirilene kadar uygulamayı kullanamaz.",
+ "tabProfile": "Profil",
+ "tabLanguages": "Diller",
+ "tabActivity": "Etkinlik",
+ "tabQuota": "Kotalar",
+ "tabSubscription": "Abonelik",
+ "sectionCommunication": "İletişim",
+ "quotaCurrentUsage": "Mevcut kullanım",
+ "quotaConfiguredLimits": "Ayarlanan sınırlar",
+ "unitMinutes": "dk",
+ "unitSessions": "oturum",
+ "unitTokens": "token",
+ "quotaZeroMeansUnlimited": "Sınırsız erişim için sınırı 0 olarak ayarlayın.",
+ "quotaValidationError": "Tüm kota değerleri 0 veya daha büyük tam sayılar olmalıdır.",
+ "quotaSaveError": "Kota sınırları kaydedilemedi.",
+ "subscriptionOverride": "Elle abonelik değişikliği",
+ "subscriptionOverrideDesc": "Bu, kullanıcının erişim süresini doğrudan değiştirir ve Stripe tarafından yönetilen durumu geçersiz kılabilir.",
+ "subscriptionUpdateError": "Abonelik güncellenemedi.",
+ "saving": "Kaydediliyor...",
+ "verifyEmailConfirmMessage": "Bu e-posta adresi doğrulanmış olarak işaretlensin mi?",
+ "unverifyEmailConfirmMessage": "Bu e-posta adresi doğrulanmamış olarak işaretlensin mi?",
+ "subscriptionConfirmTitle": "Aboneliği değiştir",
+ "subscriptionConfirmMessage": "Abonelik planı uygulansın mı: {plan}?",
+ "subscriptionConfirm": "Uygula",
+ "feedbackStatusError": "Geri bildirim durumu güncellenemedi.",
+ "feedbackDeleteError": "Geri bildirim kaydı silinemedi.",
+ "allFeedback": "Tüm geri bildirimler",
+ "feedbackTotal": "Toplam geri bildirim",
+ "feedbackQueue": "Geri bildirim kuyruğu",
+ "feedbackSearchPlaceholder": "Başlık, açıklama veya yazara göre ara...",
+ "feedbackSearchAction": "Geri bildirimlerde ara",
+ "feedbackTypeFilter": "Geri bildirim türüne göre filtrele",
+ "noFilteredFeedback": "Mevcut filtrelerle eşleşen geri bildirim yok.",
+ "feedbackItem": "Kayıt",
+ "type": "Tür",
+ "signals": "Sinyaller",
+ "feedbackStatusAction": "Geri bildirim durumunu değiştir",
+ "adminStatsError": "Yönetim metrikleri yüklenemedi.",
+ "activeUsers": "Etkin kullanıcılar",
+ "paidAccess": "Etkin / deneme",
+ "pendingFeedback": "Bekleyen geri bildirimler",
+ "pendingReviews": "Onay bekleyen yorumlar",
+ "operationalAlerts": "Operasyonel uyarılar",
+ "pendingBugs": "Bekleyen hata bildirimleri",
+ "pendingBugsDesc": "Yönetici tarafından önceliklendirilmeyi bekleyen hata bildirimleri.",
+ "pastDueSubscriptions": "Ödemesi gecikmiş abonelikler",
+ "pastDueSubscriptionsDesc": "İncelenmesi gerekebilecek ödeme sorunu olan hesaplar.",
+ "dashboardBanner": {
+ "title": "Pano duyurusu",
+ "description": "Tüm öğrenci panoları için bir duyuru oluşturun, çevirin, inceleyin ve yayımlayın.",
+ "loading": "Duyuru yükleniyor...",
+ "loadError": "Pano duyurusu yüklenemedi.",
+ "sourceLocale": "Kaynak dil",
+ "activeLabel": "Duyuruyu yayımla",
+ "activeHint": "Etkin olmayan duyurular kaydedilir ancak öğrencilerden gizlenir.",
+ "fieldTitle": "Başlık",
+ "fieldSubtitle": "Alt başlık",
+ "fieldDescription": "Açıklama",
+ "translate": "Çevir",
+ "translateSuccess": "Çeviriler oluşturuldu. Kaydetmeden önce inceleyin.",
+ "translateError": "Çeviri başarısız oldu. Mevcut içeriğiniz korundu.",
+ "editTranslation": "Düzenlenecek çeviri",
+ "pending": "beklemede",
+ "completion": "{complete}/{total} dil tamamlandı",
+ "save": "Duyuruyu kaydet",
+ "saveSuccess": "Pano duyurusu kaydedildi.",
+ "saveError": "Pano duyurusu kaydedilemedi.",
+ "revision": "Revizyon {revision}",
+ "updated": "Güncellendi: {date}",
+ "locales": {
+ "en": "İngilizce",
+ "es": "İspanyolca",
+ "fr": "Fransızca",
+ "pt": "Portekizce",
+ "de": "Almanca",
+ "it": "İtalyanca",
+ "ru": "Rusça",
+ "nl": "Felemenkçe",
+ "pl": "Lehçe",
+ "ro": "Rumence",
+ "tr": "Türkçe",
+ "sv": "İsveççe",
+ "da": "Danca",
+ "fi": "Fince",
+ "hr": "Hırvatça"
+ }
+ }
+ },
+ "faq": {
+ "title": "SSS",
+ "subtitle": "Sıkça Sorulan Sorular",
+ "q_start": "Nereden başlamalıyım?",
+ "a_start": "Seviye Tespiti ile başlayın. İngilizce seviyenizi (A1–C2), güçlü ve zayıf yönlerinizi belirler. Bu sonuçla, Pano'nuzu günlük derslerle dolduran size özel bir Çalışma Planı oluşturabilirsiniz. Seviye Tespiti yapılmadan Pano boş kalır.",
+ "q_workflow": "Adım adım süreç nasıl işliyor?",
+ "workflowStep1": "Kaydolun ve ana dilinizi seçin",
+ "workflowStep2": "Seviye Tespitine girin → CEFR seviyeniz belirlenir",
+ "workflowStep3": "Pano'dan Çalışma Planınızı oluşturun",
+ "workflowStep4": "Günlük derslerinizi tamamlayın (Pano'daki BAŞLA düğmesi)",
+ "workflowStep5": "Kelime bilginizi geliştirmek için paralel olarak Bilgi Kartlarını kullanın",
+ "workflowStep6": "Yapay Zekâ Eğitmeniyle serbest konuşma pratiği yapın",
+ "q_language": "Hangi dili öğreneceğim?",
+ "a_language": "FreeLingo şu anda birçok dili destekliyor (Amerikan İngilizcesi, İngiliz İngilizcesi, Fransızca, Almanca, İtalyanca, Japonca, Korece, Çince (Çin anakarası), Portekizce, İspanyolca) ve zamanla yenilerini eklemeye devam edeceğiz. Yeni bir dil kullanıma sunulduğunda onu Ayarlar → Dillerim bölümünde bulacaksınız — ekleyin ve hemen öğrenmeye başlayın.",
+ "q_assessment": "Seviye Tespiti nedir?",
+ "a_assessment": "Dil bilgisi, kelime bilgisi, okuduğunu anlama ve cümle dönüştürmeyi kapsayan, zorluğu A1'den C1'e kadar değişen, yapay zekâ ile üretilmiş 15 soruluk bir testtir. Bitirdiğinizde CEFR seviyenizi, bir puanı ve güçlü ve zayıf yönlerinizin analizini alırsınız. Bu sonuç, Çalışma Planına ve oluşturulan Bilgi Kartlarına temel oluşturur.",
+ "q_studyPlan": "Çalışma Planı nedir ve nasıl oluşturulur?",
+ "a_studyPlan": "Seviye Tespitinden sonra Pano'ya gidin ve Çalışma Planı Oluştur 'a tıklayın. CEFR seviyenize ve hedeflerinize göre uyarlanmış, günlük derslerden oluşan haftalık bir plan otomatik olarak oluşturulur. Her derste yapay zekâ tarafından değerlendirilen çoktan seçmeli ve serbest yazma alıştırmaları bulunur.",
+ "q_resources": "Kaynaklar nedir?",
+ "a_resources": "Kaynaklar bölümü, Çalışma Planınızdan bağımsız başvuru materyallerini bir araya getirir: seviyeye göre dil bilgisi, kelime setleri ve faydalı ifadeler. Tekrar yapmak, açıklamalara bakmak veya günlük dersi beklemeden kelime bilginizi genişletmek için istediğiniz zaman kullanabilirsiniz.",
+ "q_flashcards": "Bilgi Kartları ne işe yarar?",
+ "a_flashcards": "SM-2 aralıklı tekrar algoritmasını kullanan bir kelime çalışma sistemidir. Herhangi bir konuda (ör. \"iş\", \"phrasal verb'ler\", \"yemek\") tanım, örnek cümle ve ana dilinize çeviri içeren kartlar oluşturun. Sistem, her kartı tam unutmak üzereyken tekrar göstermek için ne zaman göstereceğini hesaplar. Çalışma Planından tamamen bağımsızdır.",
+ "q_vocabulary": "Kelime dağarcığıma nasıl kelime eklerim?",
+ "a_vocabulary": "Kelimeleri iki şekilde kaydedebilirsiniz: (1) Bir ders, dinleme alıştırması okurken veya eğitmenle sohbet ederken herhangi bir kelimeyi farenizle seçin ve beliren ipucundaki Kaydet 'e tıklayın — yapay zekâ bir tanım, örnek cümle ve ana dilinize çeviri oluşturur. (2) Kelime Bilgisi bölümüne gidin, seviyenize uygun bir kelime setini açın ve setin tamamını tek seferde eklemek için Tümünü Ekle 'ye tıklayın. Kaydettiğiniz kelimeler Bilgi Kartları bölümündeki Kelimelerim altında görünür.",
+ "q_tutor": "Yapay Zekâ Eğitmeni nasıl çalışır?",
+ "a_tutor": "Akış hâlinde yanıt veren serbest bir sohbettir. Eğitmen, yanıtlarını size göre uyarlamak için CEFR seviyenizi ve zayıf noktalarınızı bilir. Dil bilgisi soruları sorun, açıklama isteyin, İngilizce yazma pratiği yapın ya da sadece sohbet edin. Konuşmalar kaydedilir ve oturumlar arasında kenar çubuğundan erişilebilir.",
+ "q_voice": "Sesli konuşma nasıl çalışır?",
+ "a_voice": "Sesli Konuşma modu, yapay zekâ eğitmeniyle gerçek zamanlı konuşmanızı sağlar. Başlamak için Başla'ya basın — uygulama ses etkinliği algılama (VAD) ile sesinizi otomatik olarak dinler ve eğitmen sentezlenmiş sesle yanıt verir. Eğitmenin sözünü istediğiniz an kesebilirsiniz. Oturum, süre sınırına ulaştığınızda veya Durdur'a bastığınızda sona erer. Tarayıcınızda mikrofon izninin açık olduğundan emin olun.",
+ "q_listening": "Dinleme alıştırmaları nasıl çalışır?",
+ "a_listening": "Dinleme bölümü, CEFR seviyenize uygun, yapay zekâ tarafından seslendirilen kısa ses parçaları oluşturur. Sesi dinleyin, ardından 5 anlama sorusunu cevaplayın. Gönderdikten sonra puanınızı (0–5), XP'nizi, doğru cevapları ve deşifrenin tamamını görürsünüz. İstediğiniz zaman yeni alıştırmalar oluşturabilirsiniz.",
+ "q_reading": "Okuma alıştırmaları nasıl çalışır?",
+ "a_reading": "Okuma bölümü, CEFR seviyenize uygun metinler oluşturur — duyurular, e-postalar, makaleler, blog yazıları ve daha fazlası. Metni okuyun ve 5 çoktan seçmeli soruyu cevaplayın. Gönderdikten sonra puanınızı (0–5) ve XP'nizi görürsünüz. İstediğiniz zaman yeni alıştırmalar oluşturulabilir.",
+ "q_providers": "FreeLingo hangi yapay zekâ sağlayıcılarını destekliyor?",
+ "a_providers_intro": ".env dosyasında LLM_PROVIDER ile ayarlanabilir:",
+ "provider_ollama": "Yerel, ücretsiz. Sunucuda çalışan bir Ollama gerektirir. Önerilen model: gemma4:e4b. GPU olmadan yavaş olabilir.",
+ "provider_openai": "OpenAI API (GPT-4o vb.). Hızlı ve güvenilir. OPENAI_API_KEY gerektirir.",
+ "provider_anthropic": "Anthropic Claude. ANTHROPIC_API_KEY gerektirir.",
+ "provider_deepseek": "DeepSeek API. DEEPSEEK_API_KEY gerektirir.",
+ "q_invite": "Başka kişileri davet edebilir miyim?",
+ "a_invite": "Evet. Yönetici, Yönetim → Kullanıcılar bölümünden doğrudan kullanıcı oluşturabilir veya 48 saat içinde geçerliliğini yitiren tek kullanımlık davet bağlantıları oluşturabilir. Herkese açık kayıt varsayılan olarak kapalıdır (ALLOW_REGISTRATION=false).",
+ "q_password": "Parolamı veya ana dilimi nasıl değiştiririm?",
+ "a_password": "Ayarlar 'a gidin. Görünen adınızı, ana dilinizi ve parolanızı oradan güncelleyebilirsiniz.",
+ "q_uiLanguage": "Platformun dilini nasıl değiştiririm?",
+ "a_uiLanguage": "Ayarlar 'a gidin ve Profil bölümündeki Arayüz Dili açılır listesini bulun. Tercih ettiğiniz dili seçtiğinizde tüm arayüz hemen değişir.",
+ "q_feedback": "Nasıl yeni bir özellik önerebilir veya hata bildirebilirim?",
+ "a_feedback": "Kenar çubuğundaki Geri Bildirim bölümünü kullanın. Yeni özellikler için öneri oluşturabilir veya hata bildirebilirsiniz. Diğer kullanıcıların fikirlerine oy verin, yorum bırakın ve her isteğin durumunu takip edin. Tüm gönderimler herkese açıktır ve yönetici tarafından incelenir."
+ },
+ "cookieBanner": {
+ "message": "Oturumunuzu açık tutmak için yalnızca kesinlikle gerekli çerezleri kullanıyoruz. İzleme veya reklam çerezi kullanılmaz.",
+ "learnMore": "Gizlilik politikası",
+ "accept": "Anladım"
+ },
+ "error": {
+ "label": "Beklenmeyen hata",
+ "title": "Bir şeyler ters gitti",
+ "body": "Beklenmeyen bir hata oluştu. Tekrar deneyebilir veya panoya dönebilirsiniz.",
+ "retry": "Tekrar dene",
+ "dashboard": "Panoya git",
+ "digest": "Hata referansı"
+ },
+ "notFound": {
+ "title": "Sayfa bulunamadı",
+ "body": "Aradığınız sayfa mevcut değil veya taşınmış.",
+ "dashboard": "Panoya git",
+ "home": "Ana sayfa"
+ },
+ "errors": {
+ "notFound": "Sayfa bulunamadı",
+ "serverError": "Bir şeyler ters gitti",
+ "unauthorized": "Lütfen giriş yapın",
+ "backHome": "Ana sayfaya dön"
+ },
+ "conversation": {
+ "title": "Konuşma",
+ "subtitle": "Sesli Pratik",
+ "start": "Oturumu Başlat",
+ "stop": "Durdur",
+ "startNew": "Yeni Oturum Başlat",
+ "statusLoading": "Ses modeli yükleniyor...",
+ "statusReady": "Hazır",
+ "statusWarming": "Modeller yükleniyor...",
+ "statusConnecting": "Bağlanıyor...",
+ "statusListening": "Dinleniyor...",
+ "statusDetecting": "Konuşma algılanıyor...",
+ "statusSpeaking": "Konuşuyor...",
+ "statusError": "Hata",
+ "errorServicesDisabled": "Bu sunucuda ses hizmetleri etkin değil.",
+ "errorConnection": "Bağlantı hatası. Lütfen tekrar deneyin.",
+ "errorTranscription": "Söyledikleriniz yazıya dökülemedi. Lütfen yeniden konuşmayı deneyin.",
+ "errorResponse": "Lingu yanıt veremedi. Lütfen tekrar deneyin.",
+ "errorSpeech": "Lingu'nun yanıtı seslendirilemedi. Lütfen tekrar deneyin.",
+ "errorUnauthorized": "Oturumun süresi doldu. Lütfen sayfayı yenileyin.",
+ "errorMic": "Mikrofon erişimi reddedildi.",
+ "errorVadInit": "Ses tanıma başlatılamadı. Tarayıcınızın WebAssembly desteklediğinden emin olun.",
+ "warningTimeout": "Oturum {seconds} sn içinde sona eriyor",
+ "sessionEnded": "Oturum sona erdi",
+ "tapToStart": "Hazır olduğunuzda konuşun",
+ "you": "Siz",
+ "assistant": "Lingu",
+ "quotaSessions": "Bu haftaki oturumlar",
+ "quotaMinutes": "Bugünkü dakikalar",
+ "quotaUnlimited": "Sınırsız",
+ "quotaExceededSessions": "Bu haftaki tüm oturumlarınızı kullandınız.",
+ "quotaExceededTime": "Günlük pratik sürenizin tamamını kullandınız.",
+ "quotaExceededTokens": "Aylık yapay zekâ token'larınızın tamamını kullandınız.",
+ "freemiumVoiceRemaining": "Ücretsiz sesli konuşma: bu hafta {remaining}/{limit} dk",
+ "quotaTokens": "Bu ayki token'lar",
+ "memoryUpdated": "Anı güncellendi",
+ "trialBanner": "Ücretsiz seviye tespiti konuşması · {minutes} dk",
+ "trialCtaLabel": "Konuşmaya devam edin",
+ "trialCtaTitle": "Lingu ile pratik yapmaya devam edin",
+ "trialCtaDesc": "Premium ile sesli konuşmaların, eğitmen sohbetinin, dinleme ve okuma pratiğinin tamamının kilidini açın.",
+ "startersHint": "Ya da bir konuyla başlayın",
+ "starters": [
+ "Sanat ve yaratıcılık",
+ "Kitaplar ve okuma",
+ "Çocukluk anıları",
+ "Şehir mi köy mü",
+ "Yemek pişirme ve tarifler",
+ "Hayaller ve hedefler",
+ "Aile ve ilişkiler",
+ "Moda ve stil",
+ "En sevilen filmler",
+ "Festivaller ve gelenekler",
+ "Dünya mutfakları",
+ "Sağlık ve iyi yaşam",
+ "Hobiler ve el işleri",
+ "Dil öğrenimi",
+ "Yeni beceriler öğrenmek",
+ "Para ve bütçe",
+ "Müzik ve konserler",
+ "Evcil hayvanlar ve hayvanlar",
+ "Toplu taşıma",
+ "Bilim ve uzay",
+ "Alışveriş alışkanlıkları",
+ "Sosyal medya",
+ "Spor ve fitness",
+ "Teknoloji ve cihazlar",
+ "Çevre",
+ "Seyahat rotaları",
+ "Diziler ve TV programları",
+ "Hafta sonu planları",
+ "İş ve kariyer hedefleri",
+ "Evden çalışma"
+ ]
+ },
+ "voiceRecorder": {
+ "record": "Kaydet",
+ "stop": "Durdur",
+ "processing": "İşleniyor",
+ "error": "Hata",
+ "ariaRecord": "Kaydı başlat",
+ "ariaStop": "Kaydı durdur"
+ },
+ "audioPlayer": {
+ "listen": "Dinle",
+ "stop": "Durdur",
+ "ariaListen": "Telaffuzu dinle",
+ "ariaStop": "Sesi durdur"
+ },
+ "languages": {
+ "en": "İngilizce",
+ "es": "İspanyolca",
+ "fr": "Fransızca",
+ "pt": "Portekizce",
+ "de": "Almanca",
+ "it": "İtalyanca",
+ "pl": "Lehçe",
+ "nl": "Felemenkçe",
+ "ro": "Rumence",
+ "ru": "Rusça",
+ "tr": "Türkçe",
+ "sv": "İsveççe",
+ "da": "Danca",
+ "fi": "Fince",
+ "hr": "Hırvatça",
+ "myLanguages": "Dillerim",
+ "addLanguage": "Yeni dil ekle",
+ "selectLanguage": "Öğrenmek istediğiniz dili seçin",
+ "activeLanguage": "Etkin",
+ "switchTo": "Bu dile geç",
+ "switching": "Geçiliyor: {language}...",
+ "switched": "Geçildi: {language} ({level})",
+ "removeLanguage": "Dili kaldır",
+ "removeConfirmTitle": "{language} kaldırılsın mı?",
+ "removeConfirmMessage": "Bu dile ait tüm ilerleme, dersler, bilgi kartları ve konuşmalar kalıcı olarak silinecek. Hesabınıza ait anılar korunacak. Bu işlem geri alınamaz.",
+ "removeConfirmButton": "Kaldır",
+ "deleteError": "{language} silinemedi. Lütfen tekrar deneyin.",
+ "noLanguages": "Ayarlanmış bir diliniz yok.",
+ "progressLabel": "İlerleme",
+ "levelLabel": "Seviye",
+ "xpLabel": "Toplam XP",
+ "streakLabel": "Seri",
+ "lessonsLabel": "Dersler",
+ "flashcardsLabel": "Bilgi Kartları",
+ "viewDetails": "Ayrıntıları görüntüle",
+ "supportedLanguages": "Kullanılabilir diller"
+ },
+ "targetLanguages": {
+ "en-US": "Amerikan İngilizcesi",
+ "en-GB": "İngiliz İngilizcesi",
+ "es-ES": "İspanyolca",
+ "it-IT": "İtalyanca",
+ "pt-PT": "Portekizce",
+ "fr-FR": "Fransızca",
+ "de-DE": "Almanca",
+ "ja-JP": "Japonca",
+ "ko-KR": "Korece",
+ "zh-CN": "Çince (Çin anakarası)",
+ "en": "İngilizce",
+ "es": "İspanyolca",
+ "it": "İtalyanca",
+ "pt": "Portekizce",
+ "fr": "Fransızca",
+ "de": "Almanca",
+ "ja": "Japonca",
+ "ko": "Korece",
+ "zh": "Çince",
+ "es-ES-description": "İspanya'da konuşulan İspanyolca.",
+ "it-IT-description": "İtalya'da konuşulan İtalyanca.",
+ "pt-PT-description": "Portekiz'de konuşulan Portekizce.",
+ "en-US-description": "Amerika Birleşik Devletleri'nde konuşulan İngilizce.",
+ "en-GB-description": "Birleşik Krallık'ta konuşulan İngilizce.",
+ "fr-FR-description": "Fransa'da konuşulan Fransızca.",
+ "de-DE-description": "Almanya'da konuşulan Almanca.",
+ "ja-JP-description": "Japonya'da konuşulan Japonca.",
+ "ko-KR-description": "Güney Kore'de konuşulan Korece.",
+ "zh-CN-description": "Çin anakarasında konuşulan, basitleştirilmiş karakterlerle yazılan Mandarin Çincesi."
+ },
+ "onboarding": {
+ "title": "Dilinizi seçin",
+ "subtitle": "Hangi dili öğrenmek istiyorsunuz?",
+ "chooseVariant": "Varyant seçin",
+ "continue": "Devam et",
+ "saveFailed": "Tercihler kaydedilemedi",
+ "goals": {
+ "title": "Öğrenme Hedefleriniz",
+ "subtitle": "Dil: {language}. Bu dili ne için kullanmak istiyorsunuz? Uygun olanların hepsini seçin.",
+ "skip": "Atla",
+ "travel": "Seyahat ve Turizm",
+ "work": "İş ve Meslek",
+ "academic": "Eğitim ve Akademik",
+ "daily": "Günlük Konuşma",
+ "media": "Filmler, Müzik ve Diziler",
+ "emigration": "Yurt Dışında Yaşam",
+ "exams": "Sınavlar ve Sertifikalar",
+ "social": "Sosyalleşme"
+ },
+ "trialHeadline": "Ücretsiz deneme",
+ "trialHeadlineTrialUsed": "Premium",
+ "trialTitle": "{days} gün ücretsiz başlayın",
+ "trialTitleTrialUsed": "FreeLingo Premium'u etkinleştirin",
+ "trialDesc": "İlk ödemenizden önce Lingu'ya, sesli pratiğe, dinleme ve okumaya tam erişim elde edin.",
+ "trialDescTrialUsed": "Lingu'nun, sesli pratiğin, dinleme ve okumanın kilidini açmak için bir plan seçin.",
+ "trialCtaMonthly": "Ücretsiz deneyin, ardından aylık €{price}",
+ "trialCtaMonthlyTrialUsed": "Esnek aylık · aylık €{price}",
+ "trialCtaYearly": "Ücretsiz deneyin, ardından yıllık €{price} · 2 ay ücretsiz",
+ "trialCtaYearlyTrialUsed": "En avantajlı · yıllık €{price} · 2 ay ücretsiz",
+ "trialSkip": "Ücretsiz özelliklerle devam et",
+ "trialNoCharge": "Bugün ücret alınmaz. Kart gereklidir, istediğiniz zaman iptal edebilirsiniz.",
+ "trialNoChargeTrialUsed": "Abone olmadan önce tutarı gözden geçirin. İstediğiniz zaman iptal edebilirsiniz.",
+ "trialError": "Ödeme başlatılamadı. Lütfen tekrar deneyin.",
+ "freemiumTrialTitle": "Ücretsiz denemeniz başladı",
+ "freemiumTrialDesc": "7 gün boyunca tüm özelliklere sınırsız erişimin keyfini çıkarın. Kart gerekmez. İstediğiniz zaman panodan Premium'a geçebilirsiniz.",
+ "freemiumChatLabel": "Yapay zekâ eğitmeni sohbeti",
+ "freemiumVoiceLabel": "Sesli konuşmalar",
+ "freemiumListeningLabel": "Dinleme alıştırmaları",
+ "freemiumReadingLabel": "Okuma alıştırmaları",
+ "goToDashboard": "Panoya git",
+ "newLanguageHeadline": "Hangi yeni dili öğrenmek istiyorsunuz?",
+ "newLanguageSubtitle": "Bu dil için yeni bir çalışma planı oluşturulacak."
+ },
+ "tour": {
+ "skip": "Atla",
+ "prev": "Geri",
+ "next": "İleri",
+ "done": "Başlayalım",
+ "step1": {
+ "label": "Hoş geldiniz",
+ "title": "FreeLingo'ya hoş geldiniz",
+ "desc": "FreeLingo, yapay zekâ destekli dil eğitmeninizdir. Konuşmalar, dersler ve akıllı bilgi kartlarıyla, istediğiniz zaman kendi hızınızda pratik yapın."
+ },
+ "step2": {
+ "label": "Yazılı sohbet",
+ "title": "Eğitmeninizle sohbet edin",
+ "desc": "Yapay zekâ eğitmeninizle yazılı konuşmalar yapın. Soru sorun, düzeltmeler alın ve dil bilgisi ile kelime bilginizi doğal bir şekilde geliştirin."
+ },
+ "step3": {
+ "label": "Ses",
+ "title": "Gerçek zamanlı konuşun",
+ "desc": "Gerçek zamanlı deşifre ve telaffuzunuz hakkında anında geri bildirimle konuşma becerinizi geliştirmek için sesli konuşma modunu kullanın."
+ },
+ "step4": {
+ "label": "Bilgi Kartları",
+ "title": "Hiçbir kelimeyi unutmayın",
+ "desc": "Bilgi kartları SM-2 aralıklı tekrar algoritmasını kullanır; böylece kelimeleri tam unutmak üzereyken tekrar edersiniz ve akılda kalıcılık en üst düzeye çıkar."
+ },
+ "step5": {
+ "label": "Çalışma planı",
+ "title": "Size özel planınız",
+ "desc": "FreeLingo, seviye tespitinize göre hedeflerinize ve ilerlemenize uyarlanmış günlük derslerden oluşan yapılandırılmış bir çalışma planı oluşturur."
+ },
+ "step6": {
+ "label": "Pratik",
+ "title": "Dinleme ve Okuma",
+ "desc": "Yapay zekâ ile üretilmiş alıştırmalarla anlama becerinizi sınayın. Bir metni dinleyip 5 soruyu cevaplayın ya da CEFR seviyenize uygun bir metin okuyun — her doğru cevap için XP kazanın."
+ },
+ "step7": {
+ "label": "Hazır",
+ "title": "Her şey hazır!",
+ "desc": "Bugünün dersiyle başlayın, bir sohbet açın, dinleme veya okuma pratiği yapın — FreeLingo sizin öğrenme tarzınıza uyum sağlar."
+ }
+ },
+ "billing": {
+ "paywallLabel": "Premium gerekli",
+ "paywallTitle": "FreeLingo Premium'un kilidini açın",
+ "paywallDesc": "{days} günlük ücretsiz denemenizi başlatın ve ilk ödemenizden önce tüm Premium pratik modlarının kilidini açın.",
+ "paywallDescTrialUsed": "Tüm Premium pratik modlarının kilidini açın ve onaylamadan önce tutarı gözden geçirin.",
+ "paywallChatTitle": "Lingu ile sınırsız pratik yapın",
+ "paywallChatDesc": "Yapay zekâ eğitmeninize soru sorun, düzeltmeler alın ve doğal yazılı konuşmalar yapın.",
+ "paywallLessonsTitle": "Sınırsız ders tamamlayın",
+ "paywallLessonsDesc": "Dil bilgisi, kelime bilgisi, okuma ve yazma — her gün istediğiniz kadar ders bitirin.",
+ "paywallConversationTitle": "Lingu ile gerçek zamanlı konuşun",
+ "paywallConversationDesc": "Akıcılık kazanmak için deşifre, hızlı yanıtlar ve geri bildirimle sözlü konuşma pratiği yapın.",
+ "paywallListeningTitle": "Seviyenize uygun dinleme çalışması yapın",
+ "paywallListeningDesc": "İlerlemenize uyarlanmış, yapay zekâ ile üretilmiş alıştırmaları dinleyin ve anladıklarınızı hemen kontrol edin.",
+ "paywallReadingTitle": "İlerlemenize uygun metinler okuyun",
+ "paywallReadingDesc": "Seviyenize uygun metinler, sorular ve anında geri bildirimle okuduğunu anlama becerinizi geliştirin.",
+ "paywallNoCharge": "Bugün ücret alınmaz · Kart gerekli · İstediğiniz zaman iptal edin",
+ "paywallNoChargeTrialUsed": "Güvenli ödeme · İstediğiniz zaman iptal edin",
+ "paywallSkip": "Şimdi değil",
+ "trialDays": "{days, plural, one {# günlük deneme} other {# günlük deneme}}",
+ "planMonthly": "Esnek aylık · aylık €{price} →",
+ "planYearly": "En avantajlı · yıllık €{price} · 2 ay ücretsiz →",
+ "checkoutError": "Ödeme başlatılamadı. Lütfen tekrar deneyin.",
+ "pricingTitle": "Basit ve şeffaf fiyatlandırma",
+ "pricingDesc": "{days} gün ücretsiz · Bugün ücret alınmaz · İstediğiniz zaman iptal edin",
+ "pricingDescTrialUsed": "Aylık veya yıllık seçin · İstediğiniz zaman iptal edin",
+ "planMonthlyName": "Aylık",
+ "planYearlyName": "Yıllık",
+ "bestValue": "En avantajlı",
+ "month": "ay",
+ "year": "yıl",
+ "planFeature": {
+ "feature1": "Sınırsız erişim — günlük veya haftalık sınır yok",
+ "feature2": "2 ay ücretsiz"
+ },
+ "planFreeName": "Ücretsiz",
+ "trialBadge": "Ücretsiz deneme",
+ "trialBadgeTrialUsed": "Premium",
+ "ctaRegister": "Ücretsiz denemeyi başlat",
+ "ctaRegisterTrialUsed": "Plan seç",
+ "freeFeature": {
+ "f1": "Seviye testi ve seviye tespiti",
+ "f2": "Dersler ve çalışma planı",
+ "f3": "Dil bilgisi ve kelime başvuru kaynakları",
+ "f4": "Pano ve ilerleme takibi",
+ "f5": "Bilgi kartları ve aralıklı tekrar",
+ "l1": "Yapay zekâ eğitmeni sohbeti",
+ "l2": "Sesli konuşmalar",
+ "l3": "Dinleme alıştırmaları",
+ "l4": "Okuma alıştırmaları",
+ "l5": "7 günlük tam erişimli deneme"
+ },
+ "successLabel": "Ödeme onaylandı",
+ "successTitle": "FreeLingo Premium'a hoş geldiniz!",
+ "successDesc": "Aboneliğiniz artık etkin. Tüm özelliklere sınırsız erişimin keyfini çıkarın.",
+ "successCheckingLabel": "Abonelik onaylanıyor",
+ "successCheckingTitle": "Aboneliğiniz kontrol ediliyor",
+ "successCheckingDesc": "Bu genellikle birkaç saniye sürer.",
+ "successPendingLabel": "Neredeyse hazır",
+ "successPendingTitle": "Aboneliğiniz onaylanıyor",
+ "successPendingDesc": "Premium erişiminiz kısa süre içinde kullanılabilir olacak. Olmazsa faturalandırma ayarlarınızı kontrol edin.",
+ "successErrorLabel": "Onay bekleniyor",
+ "successErrorTitle": "Aboneliğinizi henüz doğrulayamadık",
+ "successErrorDesc": "Panoya gidip birazdan sayfayı yenileyin veya faturalandırma ayarlarınızı kontrol edin.",
+ "successRedirect": "{seconds} sn içinde panoya yönlendiriliyorsunuz...",
+ "successCta": "Panoya git",
+ "canceledLabel": "Ödeme iptal edildi",
+ "canceledTitle": "Herhangi bir ücret alınmadı",
+ "canceledDesc": "Herhangi bir ücret alınmadı. İlerlemeniz güvende; daha sonra ayarlarınızdan bir plan seçebilirsiniz.",
+ "canceledCtaDashboard": "Panoya git",
+ "canceledCtaSettings": "Planları görüntüle",
+ "canceledNoCharge": "Bu oturumda herhangi bir ücret alınmadı · İstediğiniz zaman bir plan seçin",
+ "section": "Abonelik",
+ "status": "Durum",
+ "statusActive": "Etkin",
+ "statusCanceling": "İptal ediliyor",
+ "statusCanceled": "İptal edildi",
+ "statusTrialing": "Deneme",
+ "statusPastDue": "Ödemesi gecikmiş",
+ "statusUnpaid": "Ödenmedi",
+ "statusPaused": "Duraklatıldı",
+ "statusIncomplete": "Ödeme bekleniyor",
+ "statusIncompleteExpired": "Ödemenin süresi doldu",
+ "statusNone": "Abonelik yok",
+ "nextBilling": "Sonraki fatura",
+ "accessUntil": "Erişim bitiş tarihi",
+ "manage": "Aboneliği yönet",
+ "updatePayment": "Ödemeyi güncelle",
+ "pastDueTitle": "Ödeme bekleniyor",
+ "pastDueDesc": "Premium erişiminizi geri kazanmak için ödeme yönteminizi güncelleyin.",
+ "subscribe": "Abone ol",
+ "portalError": "Faturalandırma portalı açılamadı. Lütfen tekrar deneyin.",
+ "openSourceTitle": "Açık kaynak",
+ "openSourceDesc": "Kendi sunucunuzda ücretsiz olarak barındırın",
+ "openSourceCta": "GitHub'da görüntüle",
+ "planFreeBadge": "Sonsuza Dek Ücretsiz",
+ "priceAmount": "€{amount}",
+ "pricePerPeriod": "/ {period}",
+ "priceOriginal": "€{price} / {period}",
+ "planFreeDesc": "7 günlük tam deneme, ardından günlük ve haftalık sınırlarla ücretsiz",
+ "planFreeCta": "Başla",
+ "limitedLabel": "Sınırlı"
+ },
+ "freemium": {
+ "chatLabel": "Sohbet",
+ "lessonsLabel": "Dersler",
+ "listeningLabel": "Dinleme",
+ "readingLabel": "Okuma",
+ "voiceLabel": "Ses (dk)",
+ "freeLimit": "Premium'a geç",
+ "trialDaysLeft": "{days, plural, one {# günlük ücretsiz deneme} other {# günlük ücretsiz deneme}}",
+ "trialUnlimited": "Sınırsız erişim",
+ "requiresSubscription": "Yalnızca Premium"
+ },
+ "whatsNew": {
+ "title": "Yenilikler",
+ "version": "v1.9.25",
+ "cta": "Anladım",
+ "entry1": {
+ "label": "Beş yeni arayüz dili",
+ "desc": "FreeLingo, Danca, Fince, Hırvatça, İsveççe ve Türkçe dillerinde kullanılabilir. Arayüz dilini Ayarlar'dan seçin."
+ },
+ "entry2": {
+ "label": "Daha akıcı bir deneyim",
+ "desc": "FreeLingo ile günlük öğreniminizi daha keyifli hâle getirmek için küçük hataları da düzelttik ve bazı ayrıntıları iyileştirdik."
+ }
+ },
+ "contact": {
+ "title": "İletişim",
+ "labelEmail": "E-posta",
+ "placeholderEmail": "adiniz@eposta.com",
+ "labelSubject": "Konu",
+ "placeholderSubject": "Sorunuzun kısa açıklaması",
+ "labelDescription": "Mesaj",
+ "placeholderDescription": "Sorunuzu veya geri bildiriminizi açıklayın...",
+ "send": "Gönder",
+ "sending": "Gönderiliyor...",
+ "sent": "Mesaj gönderildi",
+ "errorGeneric": "Gönderilemedi. Lütfen tekrar deneyin."
+ },
+ "listening": {
+ "title": "Dinleme",
+ "topic": "Konu",
+ "listenLabel": "Dinle",
+ "questionsLabel": "Sorular",
+ "submit": "Cevapları gönder",
+ "generating": "Alıştırma oluşturuluyor...",
+ "generatingDesc": "Yapay zekâ yeni bir alıştırma oluşturuyor. Bu genellikle 30 saniye kadar sürer.",
+ "generatingLong": "Beklenenden uzun sürüyor...",
+ "noExercises": "Seviyeniz için alıştırma yok.",
+ "generate": "Alıştırma oluştur",
+ "audioError": "Ses yüklenemedi",
+ "alreadyAttempted": "Bu alıştırmayı zaten tamamladınız.",
+ "errorLoading": "Alıştırma yüklenemedi. Lütfen tekrar deneyin.",
+ "errorSubmit": "Cevaplar gönderilemedi. Lütfen tekrar deneyin.",
+ "resultsLabel": "Sonuçlar",
+ "transcript": "Deşifre",
+ "review": "Cevapların incelemesi",
+ "nextExercise": "Sonraki alıştırma",
+ "viewHistory": "Geçmiş",
+ "history": "Geçmiş",
+ "historyTitle": "Dinleme geçmişi",
+ "historyEmpty": "Henüz tamamlanmış alıştırma yok.",
+ "practiceMore": "Pratik",
+ "moreInHistory": "+{count} tane daha",
+ "practiceAgain": "Tekrar pratik yap",
+ "replayNoXp": "Tekrar oyna (XP yok)"
+ },
+ "reading": {
+ "title": "Okuma",
+ "topic": "Konu",
+ "textLabel": "Metin",
+ "questionsLabel": "Sorular",
+ "submit": "Cevapları gönder",
+ "generating": "Alıştırma oluşturuluyor...",
+ "generatingDesc": "Yapay zekâ yeni bir alıştırma oluşturuyor. Bu genellikle 30 saniye kadar sürer.",
+ "generatingLong": "Beklenenden uzun sürüyor...",
+ "noExercises": "Seviyeniz için alıştırma yok.",
+ "generate": "Alıştırma oluştur",
+ "alreadyAttempted": "Bu alıştırmayı zaten tamamladınız.",
+ "errorLoading": "Alıştırma yüklenemedi. Lütfen tekrar deneyin.",
+ "errorSubmit": "Cevaplar gönderilemedi. Lütfen tekrar deneyin.",
+ "resultsLabel": "Sonuçlar",
+ "review": "Cevapların incelemesi",
+ "nextExercise": "Sonraki alıştırma",
+ "viewHistory": "Geçmiş",
+ "history": "Geçmiş",
+ "historyTitle": "Okuma geçmişi",
+ "historyEmpty": "Henüz tamamlanmış alıştırma yok.",
+ "practiceMore": "Pratik",
+ "moreInHistory": "+{count} tane daha",
+ "practiceAgain": "Tekrar pratik yap",
+ "replayNoXp": "Tekrar yap (XP yok)",
+ "selectWordHint": "Kelime dağarcığınıza kaydetmek için bir kelime seçin"
+ },
+ "feedback": {
+ "title": "Geri Bildirim",
+ "subtitle": "Özellik istekleri ve hata bildirimleri",
+ "tabFeatures": "Öneriler",
+ "tabBugs": "Hata bildirimleri",
+ "newFeature": "+ Yeni öneri",
+ "newBug": "+ Hata bildir",
+ "sortBy": "Sırala",
+ "sortVotes": "En çok oy alan",
+ "sortDate": "En yeni",
+ "filterStatus": "Durum",
+ "filterAll": "Tümü",
+ "noEntries": "Burada henüz bir şey yok. İlk gönderen siz olun.",
+ "votes": "{count} oy",
+ "vote": "1 oy",
+ "voteAction": "Oy ver",
+ "removeVoteAction": "Oyu geri çek",
+ "comments": "{count} yorum",
+ "comment": "1 yorum",
+ "by": "Gönderen:",
+ "statusPending": "Beklemede",
+ "statusPlanned": "Planlandı",
+ "statusInProgress": "Devam ediyor",
+ "statusDone": "Tamamlandı",
+ "statusDeclined": "Reddedildi",
+ "unread": "Okunmadı",
+ "modalCreateTitleFeature": "Yeni öneri",
+ "modalCreateTitleBug": "Hata bildir",
+ "labelTitle": "Başlık",
+ "placeholderTitleFeature": "Önerinizi tek cümleyle açıklayın",
+ "placeholderTitleBug": "Hatanın kısa açıklaması",
+ "labelDescription": "Ayrıntılar",
+ "placeholderDescriptionFeature": "Özelliği ve neden faydalı olacağını açıklayın",
+ "placeholderDescriptionBug": "Hatayı yeniden oluşturma adımları, beklenen davranış, bunun yerine ne oldu",
+ "submit": "Gönder",
+ "submitting": "Gönderiliyor...",
+ "cancel": "İptal",
+ "deleteEntry": "Sil",
+ "deleteEntryConfirmTitle": "Kaydı sil",
+ "deleteEntryConfirmMessage": "Bu kaydı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
+ "deleteEntryConfirm": "Sil",
+ "addComment": "Yorum ekle",
+ "commentPlaceholder": "Bir yorum yazın...",
+ "postComment": "Paylaş",
+ "postingComment": "Paylaşılıyor...",
+ "deleteComment": "Sil",
+ "deleteCommentConfirmTitle": "Yorumu sil",
+ "deleteCommentConfirmMessage": "Bu yorum silinsin mi?",
+ "deleteCommentConfirm": "Sil",
+ "loading": "Yükleniyor...",
+ "backToList": "← Geri",
+ "errorLoad": "Yüklenemedi. Lütfen tekrar deneyin.",
+ "errorSubmit": "Gönderilemedi. Lütfen tekrar deneyin.",
+ "prevPage": "← Önceki",
+ "nextPage": "Sonraki →"
+ },
+ "reviewPrompt": {
+ "statusError": "Yorum durumunuz kontrol edilemedi. Lütfen daha sonra tekrar deneyin.",
+ "ratingRequiredError": "Göndermeden önce lütfen bir puan seçin.",
+ "saveError": "Yorumunuz kaydedilemedi. Lütfen tekrar deneyin.",
+ "eyebrow": "FreeLingo'yu değerlendirin",
+ "title": "Deneyiminiz nasıldı?",
+ "closeLabel": "Değerlendirme penceresini kapat",
+ "checking": "Yorum durumu kontrol ediliyor...",
+ "ratingLabel": "Puan zorunlu",
+ "ratingGroupLabel": "Puan",
+ "star": "yıldız",
+ "stars": "yıldız",
+ "commentLabel": "Yorum isteğe bağlı",
+ "commentPlaceholder": "Öğrenmenize neyin yardımcı olduğunu bize anlatın.",
+ "cancel": "İptal",
+ "submit": "Gönder"
+ },
+ "landingReviews": {
+ "eyebrow": "Öğrenci hikâyeleri",
+ "title": "Öğrenmek, kendiniz anlattığınızda daha gerçek hissettirir",
+ "subtitle": "FreeLingo'da Lingu ile pratik yapan öğrencilerden yorumlar.",
+ "averageLabel": "{average}/5 · {count, plural, one {toplam # yorum} other {toplam # yorum}}",
+ "learningLanguage": "Öğrendiği dil: {language}",
+ "starsLabel": "5 üzerinden {rating} yıldız",
+ "ratingOnly": "Doğrulanmış FreeLingo puanı."
+ },
+ "adminReviews": {
+ "title": "Yorumlar",
+ "eyebrow": "Yönetim",
+ "totalFiltered": "Filtrelenen toplam",
+ "approvedOnPage": "Sayfada onaylanan",
+ "pendingOnPage": "Sayfada bekleyen",
+ "filters": "Filtreler",
+ "approvalFilter": "Onay filtresi",
+ "allReviews": "Tüm yorumlar",
+ "pending": "Beklemede",
+ "approved": "Onaylandı",
+ "ratingFilter": "Puan filtresi",
+ "allRatings": "Tüm puanlar",
+ "starsOption": "{rating} yıldız",
+ "clear": "Temizle",
+ "loadError": "Yorumlar yüklenemedi.",
+ "updateError": "Yorum güncellenemedi.",
+ "deleteError": "Yorum silinemedi.",
+ "loading": "Yorumlar yükleniyor...",
+ "queue": "Yorum kuyruğu",
+ "total": "Toplam {total}",
+ "empty": "Yorum bulunamadı.",
+ "starsLabel": "{rating} yıldız",
+ "learningLanguage": "Öğrendiği dil: {language}",
+ "ratingOnly": "Yalnızca puan içeren yorum.",
+ "unapprove": "Onayı kaldır",
+ "approve": "Onayla",
+ "delete": "Sil",
+ "prev": "Önceki",
+ "next": "Sonraki",
+ "deleteTitle": "Yorumu sil",
+ "deleteMessage": "Bu yorum silinsin mi? Bu işlem geri alınamaz."
+ },
+ "maintenance": {
+ "label": "Bakım",
+ "title": "Bakımda",
+ "description": "Bakım çalışması yaptığımız için bu bölüm geçici olarak kullanılamıyor. Lütfen daha sonra tekrar kontrol edin."
+ }
+}
diff --git a/specs/api-endpoints.instructions.md b/specs/api-endpoints.instructions.md
index 66a1b6e7f..9111549ea 100644
--- a/specs/api-endpoints.instructions.md
+++ b/specs/api-endpoints.instructions.md
@@ -34,7 +34,7 @@ Most REST endpoints are prefixed under `/api`. The public health check is at `/h
- **POST `/refresh`** — Rate limit: 60/min. Rotates refresh token, returns new access_token
- **POST `/logout`** — Rate limit: 60/min. Deletes refresh token from Redis, clears cookie
- **GET `/me`** — Rate limit: 60/min. Returns authenticated user profile, including subscription fields (`subscription_status`, `subscription_ends_at`, `cancel_at_period_end`, `trial_used`, `assessment_voice_trial_used`), freemium fields (`freemium_trial_ends_at`, `freemium_trial_used`), and nullable `dismissed_dashboard_banner_revision`.
-- **PATCH `/me`** — Rate limit: 60/min. Updates display name, email, password, native language, target language, UI locale, bio, learning goals, and conversation settings (`conversation_max_duration` ∈ {900, 1800}, `conversation_inactivity_timeout` ∈ {60, 180, 300}, `conversation_speech_pause` ∈ {0, 1000, 2000, 3000} milliseconds, where `0` means automatic). `native_language` is validated against the same supported UI-language codes used at registration (`en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, `ro`); unsupported codes return HTTP 422 even when the API is called outside the selector-based frontend.
+- **PATCH `/me`** — Rate limit: 60/min. Updates display name, email, password, native language, target language, UI locale, bio, learning goals, and conversation settings (`conversation_max_duration` ∈ {900, 1800}, `conversation_inactivity_timeout` ∈ {60, 180, 300}, `conversation_speech_pause` ∈ {0, 1000, 2000, 3000} milliseconds, where `0` means automatic). `native_language` is validated against the same supported UI-language codes used at registration (`en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, `ro`, `tr`, `sv`, `da`, `fi`, `hr`); unsupported codes return HTTP 422 even when the API is called outside the selector-based frontend.
- **POST `/me/avatar`** — Rate limit: 60/min. Uploads the authenticated user's profile avatar (JPEG/PNG, max 2 MB). Validates the declared content type, image signature, and minimal image structure, stores the image on disk under `/app/avatars` using a non-predictable UUID filename, and returns the user profile with `avatar` set to a cache-busted internal reference (`/api/avatars/{uuid}.{ext}?v={ms}`). The file reference is not publicly served.
- **GET `/me/avatar-file`** — Rate limit: 60/min. Authenticated current-user avatar retrieval endpoint. Returns only the authenticated user's own avatar file; this is the supported image retrieval path used by the frontend. Responses are marked `Cache-Control: private, no-store`; client-side avatar reuse is handled by the frontend blob cache keyed by the stored avatar reference.
- **DELETE `/me/avatar`** — Rate limit: 60/min. Removes profile avatar (sets to null)
@@ -72,10 +72,10 @@ Requires `role="admin"`. All endpoints return 403 for non-admin users.
## Admin Dashboard Banner — `/api/admin/dashboard-banner`
-Requires `role="admin"`. Every translation map must contain exactly `en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, and `ro`, each with nonblank plain-text `title` (max 160), `subtitle` (max 240), and `description` (max 2000).
+Requires `role="admin"`. Translation previews and saves must contain exactly `en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, `ro`, `tr`, `sv`, `da`, `fi`, and `hr`, each with nonblank plain-text `title` (max 160), `subtitle` (max 240), and `description` (max 2000). Stored ten- through fourteen-locale banners remain readable but cannot be saved without all fifteen translations.
-- **GET ``** — Rate limit: 60/min. Returns the singleton with `revision`, translations, `source_locale`, `is_active`, and timestamps, or `null` before the first save.
-- **POST `/translate`** — Rate limit: 10/min. Uses the configured LLM to translate `{source_locale, title, subtitle, description}` into all ten UI locales, preserving the validated source text exactly after trimming. Returns `{translations}` as an editable preview without saving; LLM failure returns HTTP 502.
+- **GET ``** — Rate limit: 60/min. Returns the singleton with `revision`, translations, `source_locale`, `is_active`, and timestamps, or `null` before the first save. Incomplete stored maps are returned unchanged for editing.
+- **POST `/translate`** — Rate limit: 10/min. Uses the configured LLM to translate `{source_locale, title, subtitle, description}` into all fifteen UI locales, preserving the validated source text exactly after trimming. Returns `{translations}` as an editable preview without saving; LLM failure returns HTTP 502.
- **PUT ``** — Rate limit: 60/min. Creates or updates the singleton from `{source_locale, is_active, translations}`. The server starts at revision 1 and increments the revision when source locale or translated content changes; changing only `is_active` does not increment it.
---
diff --git a/specs/architecture-frontend.instructions.md b/specs/architecture-frontend.instructions.md
index 33195b804..717c1a339 100644
--- a/specs/architecture-frontend.instructions.md
+++ b/specs/architecture-frontend.instructions.md
@@ -118,6 +118,13 @@ fall back to English according to the platform contract.
Locale selection, profile persistence, and cookies are coordinated by Settings and middleware. A
target-language switch must not mutate UI locale or global account preferences.
+Visible copy, including resource counts, errors, role labels, tooltips, and accessibility text, uses
+the active UI locale's catalog. Dates and numeric prices use that locale rather than the browser's
+default. In the dashboard-banner editor, selecting a source locale loads its current translation
+into the source fields before translation. Both banner language selectors sort translated language
+names alphabetically using the active UI locale's collation. The translation editor marks only
+incomplete translations with a localized pending suffix, without adding completion checkmarks to
+options; the overall completion counter remains visible.
## Streaming and media
diff --git a/specs/database-models.instructions.md b/specs/database-models.instructions.md
index 812b30048..a53444911 100644
--- a/specs/database-models.instructions.md
+++ b/specs/database-models.instructions.md
@@ -55,7 +55,7 @@ Registration, session, billing, and Redis quota behavior is defined in `platform
Singleton global announcement managed by administrators and displayed on authenticated dashboards.
- `id` — integer primary key constrained to `1` by `ck_dashboard_banners_singleton`.
-- `translations` — required PostgreSQL JSONB object containing `title`, `subtitle`, and `description` for exactly the ten UI locales: `en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, and `ro`.
+- `translations` — required PostgreSQL JSONB object containing `title`, `subtitle`, and `description` for the fifteen UI locales: `en`, `es`, `fr`, `pt`, `de`, `it`, `ru`, `nl`, `pl`, `ro`, `tr`, `sv`, `da`, `fi`, and `hr`. Saves require all fifteen; stored objects with ten to fourteen locales remain readable until an administrator completes them.
- `source_locale` — required two-character UI locale used to create the translation set.
- `is_active` — required boolean controlling public visibility; default `false` at the ORM level.
- `revision` — required integer server revision, initially `1`. It increments when the source locale or translated content changes, but not for an active-state-only change.
@@ -63,6 +63,7 @@ Singleton global announcement managed by administrators and displayed on authent
- `updated_at` — datetime set on creation and updated on modification.
Dismissal is valid only for the currently active revision.
+Adding a missing locale to a stored banner changes its content and increments the revision on save.
## UserLanguage (`user_languages`)
diff --git a/specs/feedback.instructions.md b/specs/feedback.instructions.md
index 028ed592e..32ec9e9c4 100644
--- a/specs/feedback.instructions.md
+++ b/specs/feedback.instructions.md
@@ -189,8 +189,10 @@ state. There is no request identity or cancellation between filter loads, so a l
can replace newer list state. Some mutations are optimistically reflected without consistently
checking non-success responses.
-Feedback UI and status/read labels are translated in all supported interface locales. The `ADMIN`
-marker and vote HTML tooltip remain fixed labels.
+Feedback UI, status/read labels, and administrator author badges use the active interface locale.
+Croatian comment counts use ICU plural categories, including the singular form for counts such as
+21 and 31 and the appropriate forms for `few` and `other`.
+The vote HTML tooltip remains a fixed label.
## Related specifications
diff --git a/specs/learning-resources.instructions.md b/specs/learning-resources.instructions.md
index 3071870a6..76177c55b 100644
--- a/specs/learning-resources.instructions.md
+++ b/specs/learning-resources.instructions.md
@@ -23,6 +23,8 @@ active language or owned plans. Frontend resource pages normally send the active
Grammar, Vocabulary, and Phrasebook can generate additional help in the authenticated user's native
language. The client chooses target resource/language but cannot override native language.
+Help labels resolve the profile's native-language code through the UI locale's `languages` catalog;
+`targetLanguages` names only learning languages and cannot label every native language.
Generated help is cached globally in `resource_native_helps` by resource type/key, target language,
native language, and source-content hash. Changing hashed source data invalidates the cache.
diff --git a/specs/platform.instructions.md b/specs/platform.instructions.md
index 2085b0f2a..61942f6dc 100644
--- a/specs/platform.instructions.md
+++ b/specs/platform.instructions.md
@@ -51,6 +51,10 @@ Login uses email and password. Missing-user verification performs a dummy bcrypt
credential-enumeration timing differences. Password validation requires 10-25 characters with an
uppercase letter, number, and symbol.
+The password-reset form displays these requirements in the interface language and checks the 10-25
+character length before submitting, counting Unicode code points consistently with the backend.
+Backend validation enforces password strength; validation errors use localized interface messages.
+
The access token is an HS256 JWT containing user ID, role, and expiration. Its default lifetime is 15
minutes and the frontend stores it in Zustand memory only.
@@ -188,10 +192,14 @@ translation context from the user profile. Reviewing always credits the card's s
The application distinguishes UI locale, native language, learned target language, active
`UserLanguage`, and resource-owning `StudyPlan.target_language`.
-The interface locales are `en`, `es`, `fr`, `pt`, `de`, `it`, `pl`, `nl`, `ro`, and `ru`. Locale
-resolution uses `NEXT_LOCALE`, then `Accept-Language`, then English. Middleware passes the selected
-locale to server rendering; missing catalogs fall back to English. Settings updates profile locale
-and locale cookies before reload.
+The interface locales are `en`, `es`, `fr`, `pt`, `de`, `it`, `pl`, `nl`, `ro`, `ru`, `tr`, `sv`, `da`, `fi`, and `hr`. Locale
+resolution uses `NEXT_LOCALE`, then `Accept-Language`, then English. Only header entries with a parsed
+quality greater than 0 and no greater than 1 are considered. The initial browser script preserves a
+supported `NEXT_LOCALE` value even when `LOCALE_DETECTED` is absent. Middleware passes the selected
+locale to server rendering; missing catalogs fall back to English. After a successful profile save,
+Settings synchronizes `NEXT_LOCALE` and `LOCALE_DETECTED` with the selected interface locale. It reloads
+when that selection differs from the currently rendered locale, even if the profile already stores
+the selected value. A failed save leaves locale cookies and the displayed language unchanged.
Target-language metadata and typography are specified separately from UI localization.
diff --git a/specs/reviews.instructions.md b/specs/reviews.instructions.md
index a9b770183..f54bfd194 100644
--- a/specs/reviews.instructions.md
+++ b/specs/reviews.instructions.md
@@ -111,11 +111,12 @@ all approved reviews.
## Review prompt
`ReviewForm` is shared by automatic prompts and Settings. It provides an accessible 1-5-star radio
-group, optional comment, submit state, and generic localized errors. Rating is required; comment is
-not.
+group with locale-specific "out of five" labels, optional comment, submit state, and generic localized
+errors. Rating is required; comment is not.
When the prompt opens, it requests `/api/reviews/me`:
+- a loading state blocks the form until the lookup completes, including when the prompt reopens;
- an existing review suppresses the prompt;
- a successful no-review response shows the form;
- a failed lookup shows an error-only state and prevents submission.
@@ -164,8 +165,8 @@ The Community section in Settings loads `/api/reviews/me`.
## Administrative frontend
`/admin/reviews` uses ten-item pages and supports approval-state and rating filters. Backend language
-and order filters are not exposed by this page. It shows snapshot name, status, stars, English
-language name, browser-locale date, and comment/fallback.
+and order filters are not exposed by this page. It shows snapshot name, status, localized star labels
+and learning-language name, interface-locale date, and comment/fallback.
Administrators can approve, unapprove, and delete with confirmation. After mutation, the page reloads
the effective page so filters and page bounds remain valid. Backend authorization protects every
diff --git a/specs/services.instructions.md b/specs/services.instructions.md
index 16a4aa514..7525ee8f6 100644
--- a/specs/services.instructions.md
+++ b/specs/services.instructions.md
@@ -160,6 +160,10 @@ notifications. User-facing locale comes from the recipient; administrator notifi
administrator's native language with English fallback. Notification failures after durable feedback or
review creation are logged without rolling back that content.
+All seven email templates declare the rendered content's locale in the HTML `lang` attribute. Omitted
+or unsupported locales use English for both the content and the language attribute. Template values,
+including the locale attribute, retain the renderer's default HTML escaping.
+
## Voice conversation pipeline
`conversation_pipeline.py` orchestrates explicit-language STT, memory-aware LLM streaming, sentence
diff --git a/specs/version.md b/specs/version.md
index 040b7dc23..5938404fb 100644
--- a/specs/version.md
+++ b/specs/version.md
@@ -1,6 +1,6 @@
# Version
-**1.9.20**
+**1.9.25**
This is the canonical current development version. Its publication state and date belong to
`CHANGELOG.md`; the canonical value may therefore correspond to a changelog section marked
@@ -30,4 +30,4 @@ content and storage key.
Before changing them, confirm whether the maintainer wants new entries or intentionally wants to show
the existing entries again under a new key. New content must replace old `entry*` keys consistently in
-all ten locale catalogs. See `whats-new.instructions.md`.
+all fifteen locale catalogs. See `whats-new.instructions.md`.
diff --git a/specs/voice-conversation.instructions.md b/specs/voice-conversation.instructions.md
index 6a439759a..ffdf5497d 100644
--- a/specs/voice-conversation.instructions.md
+++ b/specs/voice-conversation.instructions.md
@@ -143,6 +143,8 @@ fallback element, and prevents stale decoding callbacks from restarting playback
A voice session creates or reuses a `Conversation`. Reuse requires a positive supplied conversation
ID owned by the user and a compatible target language. The current frontend does not send this ID,
so normal voice starts create a new `source="voice"` conversation.
+Voice conversation titles use a native-language label and date. German, Danish, Finnish, and Croatian
+dates place an ordinal dot after the day; Croatian dates also end with a dot after the year.
`ChatHistory` rows store role, content, target language, optional plan provenance, and conversation.
Voice sessions are visible in text-chat history. Continuing from text chat supplies textual context
@@ -220,6 +222,11 @@ in the browser when the session component is created.
## Error recovery and cleanup
+Visible errors are translated by code using the active interface locale. Transcription, tutor-response,
+and speech-generation failures have distinct messages; unknown server errors use a localized generic
+fallback. Raw server messages and socket close reasons remain diagnostic log data. Transport failures
+display the localized connection error without appended backend text or transport diagnostics.
+
`stt_failed`, `llm_failed`, and `tts_failed` are recoverable turn errors. The frontend cancels
playback, clears pending assistant state, releases the turn guard, keeps the session live, and clears
the visible error after the next WAV is sent successfully.
diff --git a/specs/whats-new.instructions.md b/specs/whats-new.instructions.md
index 7abf3bb6e..3ac7c91ee 100644
--- a/specs/whats-new.instructions.md
+++ b/specs/whats-new.instructions.md
@@ -68,7 +68,7 @@ The component reads raw messages with `useMessages()`, selects keys matching `en
numerically. It must not probe unknown translation keys with a `useTranslations` try/catch loop because
missing keys return paths rather than throwing.
-All ten locale catalogs must contain the same entry keys and matching version label. Entry count is
+All fifteen locale catalogs must contain the same entry keys and matching version label. Entry count is
variable. New content replaces old `entry*` keys rather than accumulating release history.
## Version semantics