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

9.1 KiB
Raw Blame History

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-RUen-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 при откате нужно вернуть, если он был.