## ADDED Requirements ### Requirement: Локализованное название кандидата TVDB Кандидат TVDB SHALL нести локализованное название в поле `Title` и название на языке оригинала в поле `OriginalTitle`. Язык локализации задаёт та же глобальная настройка `language`, что и для TMDB, — правило единственного источника языка живёт в требовании «Локаль запроса к TMDB» и здесь не переписывается. Источник локализованного названия — блок переводов в ответе поиска TVDB (карта «код языка → название»). `OriginalTitle` SHALL брать primary name записи — это название на языке оригинала и ось сравнения при матче. Фолбэк SHALL быть тотальным: если перевода на нужный язык нет, его значение пусто после обрезки пробелов или блока переводов нет вовсе, `Title` SHALL быть равен primary name. Пустого `Title` при непустом primary name быть SHALL NOT. В `Title` SHALL попадать значение перевода после обрезки пробелов. Ключ языка SHALL искаться регистронезависимо: молчаливый фолбэк из-за регистра ключа неотличим от отсутствия перевода и в эксплуатации не диагностируется. Если условию отвечает несколько ключей, выбор SHALL быть детерминированным — один и тот же ответ провайдера обязан давать один и тот же `Title`. Локаль TVDB SHALL влиять только на разбор ответа и SHALL NOT сужать выдачу поиска: параметр языка в запрос поиска не передаётся. Причина — в `docs/research/tvdb-search-translations.md`; здесь заказано поведение, а не устройство чужого API. Тем самым разница с TMDB намеренна: у TMDB локаль едет в запрос, у TVDB читается из ответа. Негодная форма блока переводов (блок пришёл не картой, значения не строки) SHALL приводить к тому же тотальному фолбэку, а не проваливать разбор ответа поиска целиком: ответ метабазы — недоверенный вход. Подозрение на иную форму ответа SHALL оставлять диагностический след. Признаком служит **отсутствие во всей выдаче хотя бы одного ключа языка ожидаемого вида**, а не неудача разбора блока: `null`, пустая карта и словарь кодов другого вида разбираются без ошибки и потому признаком быть SHALL NOT. Штатное «перевода на этот язык нет» (ключи ожидаемого вида есть, нужного среди них нет) следа оставлять SHALL NOT — иначе сигнал неотличим от рутины. Уровень следа задают конвенции логирования проекта и здесь не нормируются; требуется наблюдаемость, а не конкретный уровень. Настоящее требование не изменяет условий подтверждения матча — они заданы требованиями «Подтверждение матча и каноническое имя» и «Безгодовой второй проход сверки как fallback». Заполнение `OriginalTitle` расширяет множество названий кандидата, по которым идёт сравнение, с одного до двух. Исход гейта при этом монотонным SHALL NOT считаться: там, где сильный кандидат был один, их может стать двое, и тогда подтверждённого матча нет, а записи уходят кандидатами в review — по действующему требованию, без исключений для TVDB. #### Scenario: Перевод на язык настройки есть - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** поиск возвращает запись с primary name `哪吒之魔童降世` и переводом `rus` = «Нэчжа» - **THEN** `Candidate.Title` = «Нэчжа» - **AND** `Candidate.OriginalTitle` = `哪吒之魔童降世` #### Scenario: Перевода на язык настройки нет — фолбэк на оригинал - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** поиск возвращает запись с primary name `Fargo` и переводами без ключа `rus` - **THEN** `Candidate.Title` = `Fargo` - **AND** `Candidate.OriginalTitle` = `Fargo` #### Scenario: Перевод есть, но пустой - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** поиск возвращает запись с primary name `Fargo` и переводом `rus` из одних пробелов - **THEN** `Candidate.Title` = `Fargo` - **AND** `Candidate.OriginalTitle` = `Fargo` #### Scenario: Язык по умолчанию — английский - **GIVEN** TVDB включён, глобальный `language` не задан в конфиге - **WHEN** поиск возвращает запись с переводами `eng` и `rus` - **THEN** `Candidate.Title` берётся из перевода `eng` #### Scenario: Ключ перевода в другом регистре - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** поиск возвращает запись с переводом под ключом `RUS` вместо `rus` - **THEN** `Candidate.Title` берётся из этого перевода #### Scenario: Блок переводов отсутствует или пришёл негодной формой - **GIVEN** TVDB включён с любым значением `language` - **WHEN** поиск возвращает записи без блока переводов либо с блоком, который не разбирается картой - **THEN** `Candidate.Title` = primary name записи - **AND** `Candidate.OriginalTitle` = primary name записи - **AND** разбор ответа поиска не проваливается, кандидаты возвращаются - **AND** остаётся диагностический след о подозрении на иную форму ответа #### Scenario: Коды языка не того вида — след остаётся - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** вся выдача поиска несёт блоки переводов с ключами другого вида (например, двухбуквенными), либо `null`, либо пустые - **THEN** `Candidate.Title` = primary name у каждой записи - **AND** остаётся диагностический след о подозрении на иную форму ответа #### Scenario: Перевода на язык нет — следа не остаётся - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** выдача несёт блоки переводов с ключами ожидаемого вида, но без нужного языка - **THEN** `Candidate.Title` = primary name у каждой записи - **AND** диагностического следа не остаётся: это штатный исход, а не подозрение #### Scenario: Запрос поиска не сужается языком - **GIVEN** TVDB включён, глобальный `language` = `ru` - **WHEN** выполняется поиск - **THEN** строка запроса поиска не содержит параметра языка - **AND** строка запроса совпадает с той, что уходит при `language` = `en` #### Scenario: Перевод сделал сильных кандидатов двумя — матч не подтверждён - **GIVEN** план с `title` «Нэчжа» и `original_title` `Ne Zha`, год известен - **WHEN** поиск TVDB возвращает две записи в пределах года ±1: одну с primary name `哪吒之魔童降世` и переводом `rus` «Нэчжа», другую с primary name `Ne Zha` - **THEN** гейт даёт двух сильных кандидатов вместо одного - **AND** подтверждённого матча нет, обе записи уходят кандидатами в review