Files
jellybit/openspec/changes/archive/2026-07-24-content-language-switch/design.md
T
av a2d8140e22 recognition: глобальный переключатель языка вывода [general].language
- новое поле [general].language (ru|en, дефолт en) — единый источник языка
  локализованного title детектора и локали запросов к метабазам; original_title
  всегда на языке оригинала
- локаль TMDB (поиск + credits) выводится из него, [metadata.tmdb].language убран
- промпт LLM явно задаёт язык title с fallback на оригинал
2026-07-24 14:09:04 +03:00

108 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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`
при откате нужно вернуть, если он был.