Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AGENTS.md — FreeLingo

**Current version: 1.9.20**
**Current version: 1.9.25**

## Project

Expand Down
27 changes: 23 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@

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.20] - 2026-09-24
## [1.9.25] - Unreleased

### Added

- Turkish (`tr`) interface language: full interface translation, Turkish as a native-language option at registration and in Settings, and Turkish verification, password reset, welcome, and account deletion emails. The dashboard announcement keeps its existing ten locales and falls back to English for Turkish users.
- 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

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
![Next.js](https://img.shields.io/badge/next.js-16-black?style=flat-square)
![Python](https://img.shields.io/badge/python-3.14-blue?style=flat-square)
![Self-hosted](https://img.shields.io/badge/self--hosted-yes-orange?style=flat-square)
![Version](https://img.shields.io/badge/version-1.9.20-brightgreen?style=flat-square)
![Version](https://img.shields.io/badge/version-1.9.25-brightgreen?style=flat-square)

<p align="left">
<img src="assets/logo_large.png" alt="FreeLingo logo" />
Expand Down Expand Up @@ -71,7 +71,7 @@ freelingo/
├── data/ # Shared data files
├── docs/ # GitHub Pages landing site
├── frontend/ # Next.js (React)
├── messages/ # i18n translation files (de, en, es, fr, it, nl, pl, pt, ro, ru)
├── messages/ # i18n catalogs (da, de, en, es, fi, fr, hr, it, nl, pl, pt, ro, ru, sv, tr)
├── specs/ # Specification files
├── AGENTS.md # AI assistant instructions
├── CHANGELOG.md # Version history
Expand Down
8 changes: 5 additions & 3 deletions backend/app/routers/admin_dashboard_banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

router = APIRouter(prefix="/api/admin/dashboard-banner", tags=["admin"])

_TRANSLATION_PROMPT = """Translate this dashboard announcement faithfully into all ten requested locales.
Return en, es, fr, pt, de, it, ru, nl, pl, and ro. Preserve the original tone and meaning.
_TRANSLATION_PROMPT = """Translate this dashboard announcement faithfully into all fifteen requested locales.
Return en, es, fr, pt, de, it, ru, nl, pl, ro, tr, sv, da, fi, and hr. Preserve the original tone and meaning.
Do not add claims, details, formatting, Markdown, or HTML. Every field must contain plain text only.
For the source locale, copy each provided field exactly.

Expand All @@ -31,7 +31,9 @@
"""


@router.get("", response_model=DashboardBannerAdminResponse | None)
@router.get(
"", response_model=DashboardBannerAdminResponse | None, response_model_exclude_unset=True
)
@limiter.limit("60/minute")
async def get_dashboard_banner(
request: Request,
Expand Down
28 changes: 28 additions & 0 deletions backend/app/routers/lessons.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,34 @@
"good_pronunciation": "Telaffuzunuz iyi!",
"target_phrase": "Hedef ifade şuydu: {answer}",
},
"sv": {
"correct": "Rätt!",
"correct_answer": "Rätt svar: {answer}",
"free_write_unavailable": "Det gick inte att bedöma det skriftliga svaret just nu.",
"good_pronunciation": "Bra uttal!",
"target_phrase": "Målfrasen var: {answer}",
},
"da": {
"correct": "Rigtigt!",
"correct_answer": "Det rigtige svar er: {answer}",
"free_write_unavailable": "Det skriftlige svar kunne ikke vurderes lige nu.",
"good_pronunciation": "God udtale!",
"target_phrase": "Målfrasen var: {answer}",
},
"fi": {
"correct": "Oikein!",
"correct_answer": "Oikea vastaus: {answer}",
"free_write_unavailable": "Kirjoitettua vastausta ei voitu arvioida juuri nyt.",
"good_pronunciation": "Hyvä ääntäminen!",
"target_phrase": "Tavoiteilmaus oli: {answer}",
},
"hr": {
"correct": "Točno!",
"correct_answer": "Točan odgovor: {answer}",
"free_write_unavailable": "Pisani odgovor trenutačno nije bilo moguće ocijeniti.",
"good_pronunciation": "Dobar izgovor!",
"target_phrase": "Ciljani izraz bio je: {answer}",
},
}


Expand Down
32 changes: 17 additions & 15 deletions backend/app/schemas/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@

from app.core.config import settings

# Native-language codes accepted when creating users through the admin API.
# These match the registration and profile options, not the study-language catalog.
SUPPORTED_LANGUAGES = {
"en",
"es",
"fr",
"pt",
"de",
"it",
"zh",
"ja",
"ko",
"ar",
"ru",
"nl",
"pl",
"ro",
"tr",
"da", # Danish
"de", # German
"en", # English
"es", # Spanish
"fi", # Finnish
"fr", # French
"hr", # Croatian
"it", # Italian
"nl", # Dutch
"pl", # Polish
"pt", # Portuguese
"ro", # Romanian
"ru", # Russian
"sv", # Swedish
"tr", # Turkish
}


Expand Down
8 changes: 8 additions & 0 deletions backend/app/schemas/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def validate_password_strength(v: str) -> str:
"pl",
"ro",
"tr",
"sv",
"da",
"fi",
"hr",
}

SUPPORTED_TARGET_LANGUAGES: set[str] = {
Expand Down Expand Up @@ -67,6 +71,10 @@ def get_available_languages() -> list[str]:
"ro",
"ru",
"tr",
"sv",
"da",
"fi",
"hr",
}

VALID_LEARNING_GOALS: set[str] = {
Expand Down
21 changes: 18 additions & 3 deletions backend/app/schemas/dashboard_banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

from pydantic import BaseModel, ConfigDict, Field, field_validator

DashboardBannerLocale = Literal["en", "es", "fr", "pt", "de", "it", "ru", "nl", "pl", "ro"]
DashboardBannerLocale = Literal[
"en", "es", "fr", "pt", "de", "it", "ru", "nl", "pl", "ro", "tr", "sv", "da", "fi", "hr"
]


class DashboardBannerTranslation(BaseModel):
Expand All @@ -24,7 +26,7 @@ def strip_nonblank_text(cls, value: str) -> str:
return value


class DashboardBannerTranslations(BaseModel):
class DashboardBannerStoredTranslations(BaseModel):
model_config = ConfigDict(extra="forbid")

en: DashboardBannerTranslation
Expand All @@ -37,6 +39,19 @@ class DashboardBannerTranslations(BaseModel):
nl: DashboardBannerTranslation
pl: DashboardBannerTranslation
ro: DashboardBannerTranslation
tr: DashboardBannerTranslation | None = None
sv: DashboardBannerTranslation | None = None
da: DashboardBannerTranslation | None = None
fi: DashboardBannerTranslation | None = None
hr: DashboardBannerTranslation | None = None


class DashboardBannerTranslations(DashboardBannerStoredTranslations):
tr: DashboardBannerTranslation
sv: DashboardBannerTranslation
da: DashboardBannerTranslation
fi: DashboardBannerTranslation
hr: DashboardBannerTranslation


class DashboardBannerTranslateRequest(DashboardBannerTranslation):
Expand All @@ -57,7 +72,7 @@ class DashboardBannerUpdate(BaseModel):

class DashboardBannerPublicResponse(BaseModel):
revision: int
translations: DashboardBannerTranslations
translations: DashboardBannerStoredTranslations


class DashboardBannerAdminResponse(DashboardBannerPublicResponse):
Expand Down
Loading
Loading