## Context Язык локализованных полей вывода (`title`, режиссёр) сейчас задаётся неявно и в двух местах: промпт LLM говорит модели вернуть «каноническое название» без указания языка (модель решает сама), а клиент TMDB жёстко берёт локаль из `[metadata.tmdb].language` с дефолтом `ru-RU`. Это два несогласованных рычага для одного решения. Требуется один явный переключатель, единый для детектора и метабаз. Затрагиваемые пакеты: `internal/config` (новое поле), `internal/recognize` (промпт), `internal/metadata` (TMDB-клиент), проброс из `cmd/jellybit` в recognizer и TMDB. Конвенция конфига — `docs/conventions/config.md` (самодокументируемый `config.example.toml`, валидация на старте). ## Goals / Non-Goals **Goals:** - Одно top-level поле `language` (`ru`|`en`, дефолт `en`) — единственный источник языка локализованного вывода. - `title` от LLM и локаль TMDB (поиск + credits) следуют этому полю. - `original_title` не затрагивается ни при каком значении. **Non-Goals:** - Гарантированная локализация имён режиссёров (провайдеры их почти не переводят — best-effort). - Языки помимо `ru`/`en`, автоопределение языка по контенту, per-раздача override. - Локаль TVDB/TVMaze: TVDB extended не параметризуем языком в текущем клиенте, TVMaze — англоязычный; вне охвата этого изменения (охват — LLM и TMDB). ## Decisions **1. Место конфига — `[general].language`, значения `ru`|`en`, дефолт `en`.** Поле кросс-каттинг (и recognition, и metadata-match) и по классу — это presentation-настройка отображения, ровно как уже живущий в `[general]` `timezone` («таймзона отображения времени»). Селим рядом с ним — единый дом таких настроек, не плодим второй паттерн места. Значения — короткие языковые коды, а не локали (`ru-RU`): локаль — диалект конкретного провайдера, его выводит сам провайдер (см. решение 2). Значение хранится голой строкой, как остальные enum-подобные поля конфига (`llm.type`, `log.format`) — проект осознанно не типизирует их. Валидация: значение вне `ru`/`en` → ошибка старта (fail-fast, **по образцу `llm.type`**, а не permissive `log.level`), форма сообщения — `unsupported language %q (supported: ru, en)`. Нормализацию «пусто → `en`» держим НЕ в `validate()` (он чист — только собирает ошибки), а в аксессоре `Config` по образцу `DisplayLocation()`; `Default()` при этом сеет `"en"`. Альтернативы отклонены: `[recognition].language` назвала бы поле по одному из двух потребителей; top-level рядом с `timezone` не согласуется с уже сложившимся домом presentation-настроек. **2. `[metadata.tmdb].language` удаляется; локаль выводит провайдер.** В общий слой (config/cmd) едет абстрактный `ru`/`en`; диалект TMDB `ru-RU`/`en-US` выводит сам `internal/metadata/tmdb.go` — тотальным `switch` с default-веткой `en-US` (defense in depth: невозможный вход не роняет запрос без локали). Так знание диалекта принадлежит тому, кто на нём говорит: второй локализуемый провайдер (TVDB с иным синтаксисом локали) добавит свой маппинг у себя, а не расширит общий слой. Остаточный `tmdbDefaultLanguage = "ru-RU"` и его fallback в `NewTMDB` **удаляются** — это скрытый второй дефолт, противоречащий заявленному `en-US`. Второго способа задать язык не остаётся (нет рассинхрона). Ломающее изменение конфига переворачивает дефолт локали TMDB `ru-RU`→`en-US`; фиксируем в proposal и миграции. Альтернатива «оставить `[metadata.tmdb].language` как override» отклонена: две ручки для одного решения противоречат «второго способа быть не должно». **3. Директива языка в промпте — только для `title`, с fallback на оригинал.** Добавляем в промпт распознавания явную строку: вернуть `title` на языке `language`, при отсутствии перевода — на языке оригинала (не выдумывать). Семантически совпадает с поведением TMDB `language` (локализованное поле с fallback на оригинал), поэтому обе стороны согласованы. Язык прокидывается в `buildMessages`/`systemPrompt` из конфига через `Recognizer`. **4. Режиссёр — та же локаль в запрос credits, best-effort.** Клиент TMDB уже передаёт `language` в поиск; тем же значением параметризуем запрос credits (`Director`). Имена людей TMDB локализует не всегда — где перевода нет, приходит оригинал; это не ошибка и не проваливает выборку (best-effort уже закреплён спекой metadata-match). ## Risks / Trade-offs - [Имена режиссёров редко локализованы у провайдеров] → best-effort по спеке: при отсутствии перевода — оригинал; ожидание зафиксировано в proposal, отказа не вызывает. - [Ломающее изменение конфига: удаление `[metadata.tmdb].language` + флип дефолта на `en-US`] → go-toml молча игнорирует неизвестный ключ, поэтому деплой с оставленным `[metadata.tmdb].language = "ru-RU"` тихо переключится на английский. Решение: НЕ вводим отклонение неизвестных ключей (лишняя машинерия ради единственного деплоя), а прописываем обязательный ручной шаг в миграции ниже. Существующие «русские» деплои выставляют `[general].language = "ru"` и убирают старый ключ. - [Дефолт `en` переименует имена папок медиатеки на английский] → косметика: Jellyfin идентифицирует контент по id-тегу метабазы (TVDB/IMDb в имени папки), а не по языку названия, поэтому распознавание не ломается. `original_title` (ось поиска в базах) от `language` не зависит. Осознанный дефолт. - [LLM может проигнорировать языковую директиву на слабой модели] → директива best-effort, как и весь недоверенный вывод LLM; безопасность по-прежнему на валидации и гейте матча, не на языке `title`. ## Migration Plan 1. Выкатка бинаря с новым полем; дефолт `en` активен сразу. 2. Деплой, где ожидались русские названия, добавляет `[general].language = "ru"` в конфиг и удаляет `[metadata.tmdb].language` (обязательный ручной шаг — валидатор про удалённый ключ не предупреждает). 3. Откат — вернуть прежний бинарь; `language` в конфиге игнорируется старой версией (unknown-поле go-toml не роняет парсинг), `[metadata.tmdb].language` при откате нужно вернуть, если он был.