Перейти к содержимому
XBSL (1C:Element)
Русский
Esc
↑↓navigate↵open⌘Jpreview
На этой странице

Перевод проекта

Перевод исходников на английские написания: что отвечает по данным платформы, что по словарю проекта, как считается покрытие и по какому условию падает CI.

1С:Элемент двуязычен. У каждого ключевого слова, ключа метаданных, типа, члена и значения перечисления есть английское написание, и проект, написанный ими, компилируется так же, как русский. xbsl translate переписывает проект целиком. Платформенную половину он берёт из извлечённых данных платформы, а собственную половину проекта – из словаря, который ведёт команда.

xbsl translate e1c/app                          # только отчёт: покрытие и что осталось
xbsl translate e1c/app --coverage               # плюс разбивка по объектам метаданных
xbsl translate e1c/app --missing gaps.yaml      # непереведённый остаток заготовкой словаря
xbsl translate e1c/app --out build            # записать переведённое дерево (в build/e1c/app)
xbsl translate e1c/app --out build --strict   # ненулевой выход, пока не полно

Что чем переводится

Платформенная половина берётся из данных, переводом её не получают. Ключевые слова берут форму того же регистра: Если становится If, если становится if. Ключ yaml получает английское написание, объявленное его классом метамодели, поэтому одно и то же слово в разных узлах может выглядеть по-разному. Значение перечисления ищется внутри своего перечисления: по всему словарю одному русскому слову отвечает несколько английских. Выражение типа сохраняет форму и фасеты (.Ссылка становится .Reference) – и в yaml, и в коде, всюду, где парсер читает тип: в параметре, объявлении, конструкторе, приведении, аргументе типа. То же слово после точки в другом месте – это уже член. У приёмника, который держит фасет объекта проекта или чья цепочка дальше загружает запись, член-ссылка тоже пишется словом фасета, Reference. А свойство-ссылка надписи или картинки пишется как Link. Значение свойства с объединённым типом, совпавшее с членом объединения (Авто в Авто|Число), берёт написание этого члена из платформенной пары типов. Внутри блоков Запрос{ ... } отвечает словарь языка запросов, а не общий. Имя, которого данные не знают, остаётся как написано и попадает в отчёт как пробел данных: переводчик не угадывает. Владельца он тоже не угадывает: звено цепочки, объявленное как A|B, держит одно из двух, а код не говорит, какое, – цепочка на нём кончается, и слово после неё читается без владельца. Интерполяция строки – это код метода вокруг неё, вместе с цепочками: "%{Объект.Товары.Граница()}" читается ровно так же, как то же выражение вне кавычек.

Половина проекта берётся из словаря. Всё, что проект назвал сам, переводят люди: объекты, методы, реквизиты, компоненты форм, ключи словарей, файлы ресурсов и значение по умолчанию перечисления – голое или квалифицированное своим перечислением, как Состояния.Открыт в States.Open. Туда же идёт каждая кириллическая строка комментария – и в модуле, и в файле ресурсов. Планов три:

version: 1
language: en

# tokens: идентификатор целиком -> идентификатор целиком (файл ресурса – основой имени)
tokens:
    Задачи: Tasks
    Значок: Icon

# phrases: строка комментария -> её перевод
phrases:
    "Задача помечается выполненной.": "The task is marked done."

Ключ фразы – одна строка комментария как есть: маркер и отступ сняты, экранирования нет вовсе. План literals ниже пишется ровно наоборот, и по ошибке между планами переезжает экранированная кавычка. --set и translate_set записывают такой ключ тем написанием, которое сработает, и называют поправку в normalized. Так же снимаются пробелы по краям значения: отступ строки переводчик ставит сам. Перезапись, которая меняет у значения одни пробелы, помечена в rewritten признаком whitespace_only. Ключ на двух строках они отвергают: каждую строку комментария переводчик ищет отдельно.

Отступ – четыре пробела, так пишет сам инструмент. Чужой отступ словарь тоже сохраняет: при записи он копирует отступ у соседних записей секции. Поэтому файл, заведённый двумя пробелами, после правки из панели или ключом --set остаётся правильным.

Третий план – literals – про строковые литералы кода:

literals:
    "Файл не загружен": "The file was not uploaded"
    "Не заполнено поле \"Наименование\"": "The \"Name\" field is empty"
    "Не разобрано тело: %{Описание}": "Could not parse the body: %{Описание}"

Литерал – это данные, и переводчик их не угадывает: он заменяет ровно то, что команда перечислила. Ключ и значение пишутся так, как текст стоит в исходнике между кавычками, с тем же экранированием, что и в коде. Значение, которое не является правильным телом литерала, словарь отвергает при загрузке. Интерполяция в значении пишется как в исходнике: имя внутри неё переведёт сам движок. Литерал внутри Запрос{}, Образец{} и других разрешимых литералов не трогается: там это код, а не данные.

Есть два места, где текст и код перемешаны, и план литералов обслуживает их особо. Первое – имя именованной группы образца, (?<Имя>...). Это имя проекта: код читает группу обратно по нему, вызовом Группа("Имя"). Обе стороны берут написание из одного источника – сначала из плана литералов, затем обычным разрешением имени. Разойдись они, вызов спросил бы группу, которой образец не объявлял. Второе – шаблон представления вида события журнала. Это проза с выражениями внутри: выражения переименовываются как имена, а сам текст берётся из плана литералов по всему значению. Чего план не назвал, попадает в отчёт пропусков, а не остаётся русским молча.

Суффиксы литерала длительности в коде переезжают на английские написания сами: 300мс становится 300ms, а 2д14ч30м5с6мс – 2d14h30m5s6ms. Состав русских суффиксов задан справкой типа, английский подтверждён компилятором платформы. Число, склеенное с любыми другими буквами, не трогается.

Тем же планом идёт всякое значение yaml, которое метамодель объявляет локализуемым текстом (Localizable), – всякое, а не одни представления команд. Представление – такое свойство у полусотни классов: у справочника и документа, у реквизита, измерения и ресурса, у команды, у права доступа, у значения перечисления. Рядом с ним стоят заголовки приложения (ЗаголовкиПриложения), сообщения, которыми приложение просит у пользователя разрешение (СообщенияЗапросаРазрешений), представления групп справочника (ПредставлениеГруппыСправочника), ПредставлениеАктивного и ПредставлениеНеактивного у переключаемой команды, а также шаблоны представления события журнала – ШаблонПредставления и соседние ШаблонПредставленияНачала, ШаблонПредставленияКонца, ШаблонПредставленияОшибки. Каждый такой текст человек читает на странице, поэтому он либо назван записью целиком, либо предъявлен пропуском. Исключение – свойство Описание: это документация разработчика, она остаётся данными. Только описание с подстановкой %{...} идёт через план так же, как шаблон, и тогда его пропуск попадает в список.

Пропуск в любом из этих текстов роняет --strict: английская сборка показала бы его по-русски. Остальные пропуски плана попадают в список и проверку не роняют: строковый литерал модуля или выражения с =, текст с %{...} в дереве компонентов, описание. Отличить там данные от сообщения может только сам проект. Значение с = – это код, поэтому ключом его строки служит текст между её собственными кавычками, как в модуле.

Пометку устаревшего элемента движок переводит сам. Стандарт имен начинает заголовок такого элемента с (не используется), а naming/presentation требует от английского проекта (not used), поэтому в английском дереве на этом месте стоит английская пометка. Если пара словаря пометку опускает, переводчик ставит ее перед текстом пары. У текста без пары меняется только пометка, и он остается пропуском. Так же меняется пометка в представлении отчета, которое переводчик оставляет данными. Пару, которая начинается со своей скобки, переводчик пишет как есть, и правило укажет на нее в английском дереве.

Префикс имени устаревшего элемента движок тоже переводит сам. Стандарт начинает такое имя с Устарело, а в файле английского дерева от русского имени больше ничего не остается: устаревший элемент правило узнает там только по префиксу Deprecated. Поэтому переводчик сохраняет префикс, что бы ни написала пара. Пару, которая начинается с Deprecated, он пишет как есть, а любой другой ставит префикс спереди: из УстарелоКурсы: Rates и УстарелоКурсы: ObsoleteRates выходят DeprecatedRates и DeprecatedObsoleteRates. За именем идут файл элемента и все обращения к нему. Имя, которое только начинается с тех же букв, как длинное слово, остается таким, как в паре.

Текст, который должен читаться одинаково на обоих языках – название продукта, код, слово, которое английский взял целиком, – называют записью, повторяющей ключ значением:

literals:
    "Цена товара": "Цена товара"

Проход вернёт текст как был, план сочтёт его названным, и --strict не найдёт, о чём сообщить. Такую пару не трогают ни --unused, ни --redundant: проход ею пользуется, а платформа за неё не отвечает.

Имена переводят целиком, а не по словам. Порядок слов английского имени обратный русскому, а части русского имени склоняются, поэтому склейка пословных переводов даёт кальку. Комментарии переводятся построчно: правка соседней строки не обесценивает запись, а одна запись обслуживает все повторы. Готовый блок комментария переразбивается по ширине проекта – той же, которой пользуется правило style/line-length. Иначе перевод, ставший длиннее исходника, вылезал бы за предел. Так переразбиваются и строки // и ///, и комментарий /* ... */. У такого комментария /* остаётся в начале первой строки, а */ – в конце последней. Рамки и разделители, списки, таблицы и образцы кода остаются как были. Строки, длинные уже в исходнике, – тоже.

Строка документирующего комментария, которая открывает тег, – @параметр Коды - Коды для проверки. – сначала ищется в словаре целиком, и пара на всю строку по-прежнему главнее. Без нее строка переводится по частям: слово тега становится английским (@parameter, @returns, @throws, @see), имя после @параметр или @выбрасывает переводится так же, как в коде, ссылка, записанная именами (@см Модуль.Метод), переводится как имена, и только текст после имени – фраза, ключом которой служит сам этот текст. Параметр, переименованный в разделе токенов, тогда меняется в теге вместе с сигнатурой, а имя без пары – такой же пробел, как то же имя в коде. Переразбивка никогда не приклеивает тег к строке выше: среда разработки читает тег с начала строки, поэтому тег и строки его продолжения переразбиваются отдельным абзацем.

Строку кода, которую английские имена вытолкнули за предел, переводчик переносит так, как руководство по стилю переносит выражение: после запятой между аргументами, параметрами или элементами коллекции, после открывающей скобки такого списка или перед операцией, которая тогда открывает следующую строку. Места переноса берутся из дерева разбора, поэтому запятая между аргументами типа местом переноса не становится. Продолжение сдвигается на один отступ. Строка с комментарием после кода остаётся как была, как и строка, о которой style/line-length сообщает уже в исходнике. Проход проверяет свой результат: токены модуля и дерево разбора не должны измениться.

Словарь – это каталог yaml-файлов или один файл с именем xbsl-translation, и ищут его рядом с проектом и выше. Чтобы наполнить словарь, кладут заполненную заготовку рядом с остальными файлами. Два файла, разошедшиеся в одном ключе, отвергаются при загрузке, и так же отвергается один файл, где ключ объявлен дважды с разными переводами.

Отказ называет все такие ключи разом – секцию, ключ и перевод в каждом файле, – а не первый встреченный. Две ветки однажды закрыли одни и те же пропуски своими файлами; каждый конвейер был зелёным, а слитый словарь не загрузился: десять ключей переведены дважды, четыре – по-разному. По одному за раз это было четыре загрузки.

Место – это файл и строка, и ключ, повторённый внутри одного файла, – коллизия того же рода. Разбор yaml оставляет у повторённого ключа только последнее значение и о первом ничего не сообщает, поэтому файлы словаря читаются со строкой каждого ключа. Повтор с двумя значениями отвергается вместе с остальными конфликтами, а повтор того же значения – лишняя копия. Один и тот же ключ в двух секциях повтором не считается, а заголовок секции, написанный в файле дважды, читается как одна секция: так его читает и таблица записей. Замер на живом словаре из 167 файлов: повторов внутри файла нет, загрузка дольше не стала.

--check-duplicates сообщает о разных переводах одного ключа с кодом возврата 1, об одинаковых дублях – с кодом 0. У каждого места сохраняются файл и строка.

--against REF сравнивает рабочий словарь и ссылку через общую базу Git. Односторонние правки, удаления и отслеживаемые переименования не возвращают старую копию из базы. Разные правки одного ключа и новые дубли двух веток показываются как конфликты. Удаление против правки и неоднозначное переименование требуют разрешения. Переименование сначала добавляют в индекс: новый неотслеживаемый файл сам по себе его не доказывает. Несвязанная история также вызывает отказ.

--format json возвращает conflicts и duplicates в виде {section, key, places: [{file, line, value}]}. Объект against содержит ref, merge_base, files и added; последнее поле считает записи, выбранные со ссылки. Их места помечены REF:файл и исходными номерами строк. Проверяются переводы ключей, а не все конфликты Git в шапках и комментариях файлов.

Для --strict отдельный флаг не нужен: рабочий словарь с конфликтом не загружается. Одинаковые дубли считаются в обычной сводке.

Квалифицированная запись (Словарь.Ключ: SignIn) действует только внутри одного пространства имён. Ключу словаря локализованных строк может быть нужно написание, которого тому же слову нельзя дать в коде.

Запись с именем метода действует на его локальные имена, включая параметры. Она не переименовывает псевдонимы внутри Запрос{...}: псевдоним и обращение к полю результата получают общее написание. Переменная поймать с аннотацией Исключение тоже участвует в разрешении членов по типу. Ее свойство Причина получает платформенное написание Cause, даже если словарь задает другой перевод одноименному полю проекта.

При переносе переведенных комментариев строки с более глубоким отступом под пунктом списка остаются отдельными. Это действует и для строчных, и для блочных комментариев. Пустая строка комментария заканчивает продолжение списка; обычный абзац после нее снова может переноситься.

Имена, объявленные проектом, принадлежат проекту

Слово, которое знает и платформенный словарь, может оказаться тем, как проект назвал своё: значением перечисления, реквизитом, методом, ключом словаря. На такие имена отвечает только словарь проекта. Без этой проверки обращение уезжало бы на английское написание, пока объявление ждёт записи в словаре: yaml всё ещё объявлял бы русское значение, модуль уже звал бы английское, и сборка отвергла бы дерево. С проверкой обе половины переезжают вместе или вместе ждут одной записи.

Исключения остаются за платформой. Это встроенные элементы коллекций, которые платформа различает по имени, – стандартные код, наименование и владелец, – и фасет после точки в выражении типа.

Переопределение обработчика платформы – тоже слово платформы. Метод под @Обработчик, названный как обработчик из списков платформы, не считается именем проекта: компилятор находит переопределение по имени платформы. В модуле, который объявляет этот обработчик, объявление и вызовы метода в модуле получают английское написание из списков в данных. Модуль компонента интерфейса переопределяет то, что объявляет его база (ПослеСоздания – AfterCreate), модуль другого элемента – то, что компилятор объявляет для его вида и модуля: ПередЗаписью и ПриСозданииНаОсновании в модуле объекта справочника, Обработчик в модуле запланированного задания, ПроверитьНаличиеКлючейДоступа в модуле ключа доступа, обработчики RLS вроде ВычислитьРазрешенияДоступаДляОбъектов в модуле сущности. В остальных модулях имя переводит словарь платформы. Запись словаря проекта с другим написанием попадает в дефекты словаря, с тем же – в лишние. Пока в данных нет списков, такое имя ждет записи в словаре, как раньше.

Тип принадлежит платформе везде, где может стоять только тип: в выражении типа, в корне статического вызова и прямо перед фасетом, как в Сущность.Право. Поле, реквизит или метод проекта с тем же написанием ничего там не держат. Словарём отвечает только тип, который проект объявил под этим именем, поэтому его объявление и употребления по-прежнему переезжают вместе.

Имя в области видимости – тоже не тип. Локальная переменная, параметр, переменная цикла или поймать, параметр лямбды и свойство своего элемента модуля могут писаться как тип платформы, и тогда Имя.Член сохраняет написание объявления. Типом такое слово остаётся в статическом методе, у которого нет элемента, и в методе компонента, который компилируется только на сервере и видит лишь контекстные свойства. Код внутри строковой интерполяции принадлежит своему методу и видит те же имена, типы и записи словаря.

Файл ресурса – имя проекта во всех местах, где оно стоит. Файл дерева, свойство yaml, путь в строке и тело Ресурс{...} вместе получают слово словаря или вместе сохраняют собственное имя файла.

Картинки библиотеки платформы не относятся к файлам проекта. У каждой картинки есть русское и английское имя. Английское имя достаётся ссылке, которую разбирает компилятор: значению свойства с картинкой и телу Ресурс{...}. Пространство имён библиотеки при этом можно и не писать: Стд::Аккаунт.svg становится Std::Account.svg.

Имя без пространства имён сначала ищут среди файлов проекта: когда у проекта есть свой файл с тем же именем, ссылка ведёт на этот файл. Имя с пространством имён библиотеки всегда означает библиотеку – его для того и пишут.

Строка до библиотеки не дотягивается. Путь в строке читает ПакетРесурсов.Текущий(), то есть ресурсы своего пространства имён. Поэтому строковый литерал и значение yaml, которое схема считает текстом, остаются как написаны, даже когда буква в букву совпадают с именем картинки.

Английские имена картинок перевод берёт из раздела resource_paths файла uiterms.json в данных платформы, и на данных без этого раздела ссылка их не получает.

Метод, объявленный компонентом проекта и вызванный через узел формы, – тоже слово проекта, а встроенная команда платформенного компонента сохраняет написание словаря интерфейса.

Что не трогается

Данные. Подписи, описания и любой другой текст, который читает пользователь, остаются как написаны: их переводит штатный механизм локализации, и переведённый проект несёт те же словари. Строковые литералы тоже остаются. Исключение – код внутри их интерполяций: он разбирается заново и переводится как обычный код. Ид не меняются, потому что переведённое дерево – тот же проект, а не его копия.

Строковый литерал, совпавший с переименованным именем, попадает в предупреждения. Метод, который зовут по имени из строки, ломается молча, если переименовано только объявление.

Второе предупреждение того же рода – literal-data-value. Так помечают литерал, совпавший со значением из json-ресурса проекта, которое сдвинула запись плана литералов. Обычно такой литерал сравнивают с этими данными, при разборе начального заполнения, а сами данные перевод не трогает. После сдвига сравнение молча перестаёт срабатывать. Если литерал и правда данные, отметить это можно явно: запись со значением, равным ключу. Покрытие тогда засчитано, текст не движется, предупреждения нет. Отчёт печатает предупреждения списком: файл, строка, вид и текст.

Третье, short-pair, – о комментариях. Пара фраз ищется по одной строке комментария, а строка из одного-двух слов значит то, что из нее делает ее комментарий: пару нет. могли записать как timer. для хвоста предложения о таймере. Когда в комментарии есть новые строки, а короткую строку того же комментария перевела такая пара, отчет предупреждает на этой строке, а каждый пробел комментария в --gaps (и в translate_gaps) несет neighbors: короткие строки с их переводом, которые стоит читать вместе с новой. Строки с одним маркером, написанные одна под другой, – один комментарий, в yaml – подряд идущие строки комментария. Комментарий, все строки которого переведены, ничего не говорит.

Исключений два, и оба – это имя, записанное второй раз, вне кода.

Ключи json-ресурсов проекта, называющие поле его структуры. Структура читает json по имени поля, поэтому такой ключ и есть то же имя, записанное ещё раз. Переименовать поле и оставить ключ значит не связать ничего, причём молча: настройки чтения пропускают неизвестное свойство и инициализируют отсутствующее поле. Проект компилируется, применяется и стартует с пустыми данными. Такие ключи переезжают тем же словарём, что и поля. Значения и ключи, которых не объявляет ни одна структура, – карта по содержимому, внешний контракт – остаются как написаны. Число переименованных ключей стоит в сводке.

Литерал, который записывает путь к ресурсу – например, "Значки/%Код.svg". Перевод переименовывает файлы и каталоги ресурсов, и путь обязан следовать за ними, иначе платформа ресурс не находит. Литералом пути считается только то, что им и выглядит: он оканчивается известным суффиксом ресурса, а каждый сегмент читается как имя файла. Регулярное выражение с косыми чертами и именованными группами под это не подходит и остаётся данными.

Текст в файлах ресурсов

Комментарий в .css, .js, .html и .svg – такая же фраза, как комментарий модуля. Раньше такие файлы копировались байт в байт: русский текст молча уезжал в английскую сборку, а --strict считал файл покрытым, потому что считать там было нечего.

Перевод читает в них:

  • комментарии /* */ и // в .css и .js;
  • комментарии <!-- --> в .html и .svg;
  • текст <title> и <desc> в .svg: эти слова произносит экранный диктор, а браузер показывает их в подсказке;
  • комментарии вложенных <style> и <script>.

Ключ фразы тот же, что у комментария модуля: маркер и обрамление сняты, блок разобран по строкам. Одно предложение, написанное и в модуле, и в стилях, закрывается одной записью. Пробел находится режимом --gaps, учитывается покрытием и роняет --strict.

Селекторы, имена свойств и их значения, атрибуты, идентификаторы и текст страницы остаются как написаны. Файл читается разбором посимвольно, поэтому маркер внутри данных комментария не открывает: content: "/*", адрес в url(...) без кавычек, шаблонная строка, регулярное выражение вида /[/*]/. Разметку разбирает html-парсер стандартной библиотеки – он знает, что <!-- в значении атрибута ничего не открывает.

Две границы проведены намеренно. Комментарий с лицензией остаётся как есть: его формулировка – юридический текст, и перевод скажет не то, что сказал оригинал. Минифицированный .css или .js пропускается целиком: это сборочный вывод, от комментариев в нём остаётся одна шапка, и править там никто ничего не станет. Минифицированным считается файл, у которого самая длинная строка длиннее пятисот символов.

Записанное дерево – это репозиторий

--out КАТАЛОГ считает названный каталог корнем репозитория: проект ложится в КАТАЛОГ/{Поставщик}/{Имя}, то есть в те два имени, которые несёт переведённый дескриптор. Другой раскладки сборка не принимает: она пакует файлы по схеме {поставщик}/{имя} и каталог с другим именем отвергает. Поэтому дерево выкатывается как есть, без ручной перекладки. Имена берутся переведённые: проект, чьё имя само по себе русское слово, меняет его при переводе, и каталог следует за ним.

Каталог, который уже заканчивается этими двумя именами (--out build/acme/tasks), считается самим каталогом проекта и второй раз не вкладывается. Путь, который писали руками, пока ключа не было, продолжает работать. Дерево без дескриптора – фрагмент, переведённый отдельно – пишется туда, куда попросили. Строка журнала называет каталог, в который файлы легли на самом деле.

Повторный запуск в тот же каталог просто переписывает дерево, а сам каталог узнаётся по дескриптору Project.yaml или Проект.yaml. Каталог, занятый чужими файлами, не трогается вовсе: команда говорит, какой это каталог и что с ним делать. Отдельный файл, который записать не удалось, называется в отчёте вместе с причиной и проход не роняет. Мешает обычно остаток прошлого запуска: каталог на месте файла, файл только для чтения, файл занят другой программой. Отчёт печатается целиком, первые пять таких файлов названы поимённо, остальные сосчитаны. Запуск, которому не удалось записать дерево, возвращает ненулевой код и без --strict: дерево – это и есть его работа.

--clean: остатки прошлого запуска. Перезапись покрывает только те файлы, которые запуск пишет, и больше ничего не трогает. Поэтому файл, переименованный или снятый в исходнике, оставляет своё старое отражение в дереве вывода. Сборка берёт каталог целиком, и сирота уезжает вместе со всем остальным. Хуже того, остаток может стоять ровно на месте нужного файла: каталог вместо файла – это и есть ошибка записи выше. --clean убирает перед записью всё, чего этот запуск не пишет:

xbsl translate e1c/app --out build --clean

Ключ задают явно, потому что он удаляет файлы. Записью во временный каталог с последующей подменой это не заменить: подмена всё равно обязана удалить старое дерево, стоит вторую полную копию проекта и ломается ровно там же, где случаются ошибки записи, – на другом томе, на каталоге, занятом сборкой. Отказ каталогу с чужими файлами остаётся страховкой сверху. Чистка идёт только внутри каталога, который уже является переведённым проектом, поэтому --clean не превращается в “сотри то, на что показали”. Неудавшееся удаление называется проблемой так же, как неудавшаяся запись. Число убранного печатается в отчёте и в поле removed у json только тогда, когда убирать было что, а под числом перечисляется каждый остаток поимённо: по одному числу не понять, что именно потеряла сборка.

--dry-run: посмотреть заранее. Иначе первая чистка переведённого дерева делается вслепую. Ключ доводит работу до записи и останавливает. Дерево построено, каталог назначения проверен – занятый чужими файлами отвергается здесь так же, как отверг бы настоящий запуск, – остатки перечислены. Записывать и удалять при этом нечего.

xbsl translate e1c/app --out build --clean --dry-run
Пробный запуск: ничего не записано и не убрано
будет записано файлов: 1261 -> build/Acme/TaskBook
будет убрано остатков прошлого запуска: 2
  Формы/СтараяФорма.yaml
  Основное/Ресурсы/percent.svg

Текстовый отчёт называет первые двадцать и считает остальные. В json лежат dry_run, planned (размер дерева, которое было бы записано) и removals – все до одного. Без --out описывать нечего, и ключ отвергается, а не игнорируется.

Табличные режимы – --gaps, --entries, --table, --unused, --redundant, --drift, --suggest – дерева не пишут вовсе. Поэтому --out, --clean, --dry-run и --missing они тоже отвергают и называют запуск, который дерево пишет.

Словари локализации переворачиваются

Проект, у которого целевой язык уже лежит в секциях локализации, получает эти значения как базовые, с переведёнными ключами. Исходные значения уезжают в Локализация/<Код>/, а язык по умолчанию и язык разработки в дескрипторе следуют за этим. Ключ, у которого значения на целевом языке нет, сохраняет исходное и попадает в отчёт.

С базой меняется местами только перевод на целевой язык. Его папка находится по коду языка без учета регистра – так же, как ее находит платформа. Перевод на любой другой язык (Локализация/Vi рядом с Локализация/En) остается на своем месте: его ключи переводятся тем же словарем, что и ключи базы, а значения остаются на своем языке. Английские названия языков берутся из данных платформы, поэтому описанию проекта с вьетнамским языком запись словаря для него не нужна.

Покрытие и пробелы

--coverage печатает долю словаря по каждому объекту метаданных – это семейство yaml и xbsl с общей основой имени – и по проекту целиком. --missing пишет остаток заготовкой словаря: записи идут по убыванию частоты, у каждой стоит число вхождений и первое место, а значения пустые и готовы к заполнению.

Три счётчика разведены намеренно. Первый – покрытие словаря, число, которое команда доводит до 100%. Второй – пробелы данных платформы, их записью в словаре не заклеивают. Третий – кириллические скаляры, оставленные данными: их перечисляют, чтобы ревизор подтвердил, что это и правда данные.

Словарь ищется рядом с проектом и выше. Корень без словаря отвергается с перечнем мест поиска, а если словарь нашёлся ниже корня, отказ называет и его. --dictionary называет словарь явно, файлом или каталогом. --target называет файл, в который лягут новые записи; по умолчанию это 090-manual.yaml. Для каталога словаря передается только имя файла: путь с /, \ или буквой диска отвергается до записи любых правок. Та же проверка действует в MCP translate_set. --comment задает заголовок, с которым такой файл создается: там и место сказать, чему посвящена порция. --format json отдаёт весь отчёт машине. --no-localization-swap оставляет словари локализации как есть – это для проекта, который переводит исходники, но раскладку языков менять не хочет.

--strict даёт ненулевой выход, пока покрытие неполно или найдены проблемы. Это и нужно задаче конвейера перед выкладкой переведённой сборки. Одна из таких проблем – столкновение имён. Два разных имени одного пространства, переведённые в одно слово, ломают сборку, потому что платформа отвергает повтор имени. Увидеть это может только переводчик. Каждое столкнувшееся имя названо со своим местом: method:СтрокаРолей - 'Number' <- ... (Модуль.xbsl:2:9), ... (Модуль.xbsl:5:13). Одно только имя пространства оставило бы читателю поиск двух слов среди полутора десятков, объявленных методом, а рядом они стоят редко.

Тип платформы, который метод читает корнем статического обращения, стоит в том же пространстве имён. Локальная переменная, переведённая словом этого типа, закрывает тип в английском дереве, поэтому пара называется с обоими местами, а запись, уточнённая именем метода (Метод.Имя), разводит их.

Ещё три проблемы приходят из самого словаря. Все три нашлись на живом проекте, английская сборка которого не собиралась при покрытии 100%. Первая – запись, которая пишет член платформы так, как платформа не пишет его нигде: Важность: Severity вместо Importance у события. О ней сообщают в первом месте, где это доказывает приёмник известного типа. Там берётся платформенное написание, а приёмник, тип которого никто не назвал, получит слово из записи, и компилятор его отвергнет. Лечение одно: платформенное написание в записи. Слово, которое сама платформа пишет двумя способами, не судится: Загрузить – это Load у двоичного объекта и Upload у хранилища объектов, и запись, совпадающая с любым из них, ничего не ломает. Вторая проблема – именованный литерал, подстановки которого после перевода расходятся с подстановками ключа: %{AccountCode} там, где поле переводится как SubscriberCode. О нём сообщают с обоими списками, потому что имена внутри %{...} должны переводить те же поля на любом из двух языков.

Третья – тип, который проект объявляет под именем типа платформы, и запись, переводящая его другим словом. Выражение типа обычно берёт слово платформы поверх любого имени проекта, но тип, объявленный проектом, – исключение: его объявление и его употребления обязаны двигаться вместе, поэтому запись отвечает в каждом выражении типа с таким написанием, в том числе там, где стоит тип платформы. Узел пикера назывался Образец, запись Образец: Swatch превратила новый Образец(...) в файле, ничего про пикер не знавшем, в new Swatch(...), и сборка ответила Type "Swatch" is not defined. Никакое значение это не лечит, и лечение – не значение: тип проекта надо переименовать. Запись, повторяющая собственное написание платформы, ничего платформенного никуда не переносит и не судится.

Локализуемый текст yaml без пары в плане literals тоже роняет --strict – всякое значение, которое метамодель объявляет Localizable, кроме Описание: Представление где бы оно ни стояло, заголовки приложения, сообщение запроса разрешения, представление группы справочника, два представления переключаемой команды, шаблоны представления события журнала. Сборка такое дерево примет, но на его страницах текст останется русским. Отчёт перечисляет такие тексты с местами, а строка вердикта называет их число. Текст, который должен остаться русским, называют записью, повторяющей его значением (см. выше). Другие пропуски плана literals проверку не роняют.

Английский текст словаря

Покрытие говорит, есть ли перевод у каждого имени и каждой строки комментария, но не говорит, читается ли перевод как английский текст. Механическая правка значений – замена, прогнанная по тысячам строк, или волна, переписавшая русские ключи, – оставляет следы, которых --strict не видит. Правило translation/english-shape – warning, включено по умолчанию, область файловая – читает значения файлов словаря и сообщает о трёх таких следах:

  • окончание, приклеенное к слову, которое его не принимает: наречие, неправильное причастие или вспомогательный глагол, записанные как глагол или множественное число (onlies, gones, hases), и правильное причастие во множественном числе (loadeds);
  • страдательный залог, сразу за которым идёт именная группа без by (“a variable is shadowed the parameter”): подлежащее и дополнение остались на местах русской фразы;
  • прописные, которых нет в русском ключе: “does NOT narrow” при ключе, где ничего не выделено. Выделенное слово в русском ключе их оправдывает, как и то же латинское слово в ключе. Аббревиатуры (MB, URL) и константы не судятся вовсе.

Находка стоит на слове в файле словаря и приходит без исправления: какое слово имелось в виду, знает только автор. Судятся только файлы найденного словаря. Проект без словаря ничего не услышит, а проверка каталога, где лежат и проект, и его xbsl-translation, судит исходники и словарь вместе.

В VS Code находки видны и тогда, когда xbsl.projectRoot сужает проверки до папки проекта. Расширение передаёт файлы словаря языковому серверу при наборе, а сервер добавляет словарь в проверку всего проекта.

Словарь читают и два правила группы comment/: значение phrases – это английская строка комментария. comment/first-person сообщает о “we”, “our” или “I” в ней, а comment/emphasis-caps – о прописных ради ударения: пришедших от выделенного ключа, о приставке капсом (“UNfilled”) и об артикле во фразе капсом. Прописные, которых нет в невыделенном ключе, остаются за translation/english-shape, так что слово сообщается один раз. Литералы, токены и термины эти правила не читают. Оба выключены по умолчанию и включаются ключом --enable comment.

В редакторе

Правило conventions/missing-translation – info, выключено по умолчанию, область проектная – показывает те же пробелы на их местах. Имя или строка комментария, которых нет в словаре, дают одну находку на первое вхождение в файле. Пока словарь не найден, правило молчит, поэтому говорит оно только в проекте, который переводит исходники. Включает его --enable conventions/missing-translation, а в редакторе – настройка Linter: Enable.

Находка несёт с собой ключ словаря, его вид и подсказку, поэтому лампочка предлагает записать перевод, не выходя из файла. “Перевести как …” ставит платформенное написание одним щелчком. “Перевести …” спрашивает слово. Третье действие открывает таблицу словаря с отбором по этому ключу. После записи проект проверяется заново, перезапускать ничего не нужно.

Таблица словаря открывается командой “XBSL: словарь перевода” и показывает записи рядом с тем, что исходники ещё не покрывают: вид, ключ, перевод, число вхождений, первое место и файл словаря. Каждая запись занимает две строки, а поле перевода растянуто под остальным во всю ширину строки. Поле правится на месте. Строка поиска и переключатель “только незаполненные” сужают таблицу. Подсказка – сперва платформенное написание, иначе догадка внешнего сервиса – стоит серым внутри пустого поля и подставляется щелчком или клавишей Enter. Таблица читает --table, который за один проход по проекту отдаёт записи, пропуски и покрытие, а пишет она ключом --set. После записи ячейки проект заново не обходится: перечитывается только словарь, а счётчики шапки сдвигаются на то, что изменила эта правка. Точные числа возвращает кнопка “Перечитать”. Кнопка “Запросить машинный перевод” в этой же панели заполняет пустые поля предложениями внешнего сервиса, подробности – в разделе Машинный перевод ниже.

Наполнение словаря из инструментов

Словарь большого проекта – это тысячи записей, и вычитывать такие файлы ради одного слова долго и опасно. Поэтому все поверхности работают страницами поверх одного ядра движка.

CLI отвечает на те же вопросы, что задаёт панель:

xbsl translate e1c/app --gaps --kind token --limit 20      # чего не хватает, по убыванию частоты
xbsl translate e1c/app --entries --filter Задач            # что словарь уже говорит
xbsl translate e1c/app --table --limit 0                   # всё сразу: записи, пропуски, сводка
xbsl translate e1c/app --set правки.yaml                   # применить пачку из файла (ниже)
xbsl translate e1c/app --unused                            # пары, которых проект уже не использует
xbsl translate e1c/app --stale --filter ПодсказкаТарифа     # то же об именах одного удалённого компонента
xbsl translate e1c/app --redundant                         # пары, на которые платформа отвечает сама
xbsl translate e1c/app --drift                             # фразы, где имя названо не по паре

--table отвечает на три вопроса разом, одним проходом. Он для того и заведён, что таблица редактора спрашивает именно их. Порознь это два одинаковых обхода исходников в двух процессах и третье чтение того же словаря.

--set принимает пачку файлом в любом из двух форматов. Первый – yaml самого словаря: секции tokens, phrases и literals, то же квотирование, что в файлах словаря, а пустое значение снимает запись. Второй – список JSON [{key, value, kind}], который отдают скрипты. Пачка на сотни записей пишется так же, как сам словарь, а не одной строкой JSON в команде. Yaml читает загрузчик самого словаря, поэтому пачка, которую скрипт выгрузил с ключами в кавычках (yaml.safe_dump(..., default_style='"')), читается так же, как написанная вручную. Пачку без записей команда отвергает и называет ключи верхнего уровня, которые в файле есть.

Записывающий проход предупреждает о двух видах пар, которые видны без обхода проекта, и пару всё равно пишет. Значение, уже занятое другим ключом того же пространства, – это то самое столкновение, встреченное в момент, когда слово набирают. Ключ, написанный словом самой платформы, – второй вид: как тип, где лечит только переименование узла проекта, и как член типа платформы, где верное значение – это написание, которое даёт члену сама платформа (ЦветСсылок: LinksColor при DesignTheme.LinksColor). Это предупреждения, а не отказы: ключ, написанный именем типа платформы, становится смертельным из-за типа, который проект объявляет под тем же написанием, а увидеть такой тип может только проход по проекту – поэтому приговор выносит строгий проход, и дерево падает там. Ключ, уточнённый владельцем (<Владелец>.<Имя>), держится внутри одного пространства, в выражении типа не отвечает и не судится. translate_set несёт те же строки в collisions и platform_names, а --set печатает их в stderr рядом со счётом записанного.

--unused отвечает на вопрос, обратный --gaps. Тот показывает, чего проекту не хватает в словаре, а этот – что словарь всё ещё говорит, чего в проекте уже нет. Удаление кода оставляет за собой его имена и строки комментариев, и сказать об этом больше некому. --strict судит непокрытое, а --entries показывает, где пара объявлена, а не используется ли она. --prune снимает ровно те строки, которые только что перечислил, поэтому --kind, --filter и страница действуют и на снятие. Страницу, урезанную ключом --limit, отчёт называет отдельно: снимать “всё”, глядя на пятьдесят строк из трёх тысяч, флаг только выглядит.

--stale – тот же флаг под именем, которым вопрос обычно и задают. --filter делает ответ рабочим списком: после удаления спрашивают об именах именно этого компонента, а не обо всей истории проекта.

--since отвечает на вопрос, который задача задаёт в конце: что осталось от её правки, а не что накопилось в словаре за всю жизнь проекта. В ответе ключи, которые проект больше нигде не пишет и которые встречались только в строках, снятых этой правкой. --prune вместе с ним снимает ровно их. Ветка или коммит читаются от точки расхождения с HEAD до рабочего дерева, поэтому незакоммиченное тоже считается правкой. Диапазон A..B передаётся git как написан: так смотрят на уже слитую правку. Замер на живом проекте: без отбора 3297 сирот, из них правке принадлежат 18 – девять имён и девять строк комментариев. Один вызов вместо девятнадцати вызовов с --filter по одному имени.

Правка судится с обеих сторон. Её дифф исходников даёт снятые строки, а дифф файлов словаря – пары, которые она добавила или переписала, и это тоже кандидаты правки. Строка комментария, написанная в ветке и переписанная в той же ветке, в диффе против базы не стоит ни снятой, ни добавленной, поэтому пара первой редакции раньше оставалась в словаре навсегда: строгий проход её не судит, а --unused --since отвечал, что сирот у правки нет. Каждый кандидат по-прежнему судится против рабочего дерева, и в ответ попадает только ключ, которого проект нигде не пишет. Заголовок и блок since в --format json считают обе стороны: файлы правки, файлы словаря в её диффе и пары на их добавленных строках.

Снятые строки читает git diff. Комментарии разбираются в старой версии файла из Git: границы блока учитываются, даже если они не менялись. Относительные пути проекта и словаря отсчитываются от каталога запуска команды. Если git не ответил за минуту, режим отказывает и называет запасной путь: тот же список без git отбирают по --filter.

Обход исходников печатает прогресс в stderr каждые 200 файлов, чтобы долгий запуск было видно; stdout остаётся отчётом, и --format json там – один документ. В этом виде каждая строка unused несёт kind, key, value, file, line и scope – их и читает скрипт вместо разбора текстовых строк по двойным пробелам, которые ключ может содержать и сам.

Сужение работает как пересечение, а не как короткий путь: имя, которое проект всё ещё пишет, сиротой не станет, как бы щедро ни прочитался diff. Поэтому ответ без отбора и без --since несёт оговорку. Чтение текстовое, список описывает весь накопленный словарь и годится на просмотр, а не на снятие целиком.

Чтение текстовое, и важно, в какую сторону оно ошибается. Имя, которое встречается ещё и в прозе, может быть сочтено используемым, и тогда пара просто останется на месте. Живую пару сиротой при этом не назовут. Строка комментария читается разбором самого переводчика: маркер и обрамление снимаются ровно так же, как их снимает записывающий проход. Ключ литерала тоже берётся так, как его берёт проход. Файл yaml проходит обход самого переводчика, а тот отличает представление и шаблон от имени. Модуль разбирает лексер, поэтому строку внутри интерполяции другой строки он видит целиком. Имя ищут и в именах файлов, потому что папка и файл проходят через ту же плоскость имён. Квалифицированный ключ <Владелец>.<Имя> судится по обоим концам: в исходниках они написаны порознь, и чтение текста с точкой как одного имени объявило бы сиротой каждую такую пару.

--redundant – второе зеркало --gaps. Он показывает слово, которое словарь пишет ровно так, как его пишет сама платформа. Такая пара ничего не ломает: без неё дерево выходит слово в слово тем же. Назвать её стоит по другой причине – она отвечает вместо данных платформы, а значит, прячет за собой пробел этих данных или самого движка. В одном живом словаре так были написаны языки описания проекта, и наполовину переведённое перечисление за этой парой нашёл тест на пустом словаре, а не проект. Обычный отчёт называет их число, ни о чём не спрашивая, а --prune снимает ровно те строки, которые флаг перечислил.

В отличие от --unused здесь идёт полный проход по проекту, и вердикт опирается на свидетельство, а не на второе чтение таблиц. Пара попадает в список, только если каждое место, где она ответила, вышло бы тем же самым и без неё. Поэтому пара, которая держит хотя бы одно своё место, лишней не называется: так бывает со словом, которое платформа пишет в одной роли и не пишет в другой. Пара, которой проект не пользуется вовсе, сюда тоже не попадает: это вопрос о сиротах, и на него отвечает --unused.

--drift смотрит на фразы. Строку комментария словарь переводит целиком, вместе с именами, и ничто не связывает имя внутри перевода с разделом токенов. Токен переименовали после того, как фразу написали, или строку перевели со своим представлением об имени, – и английский комментарий называет то, чего в английском дереве нет. Раньше это оставляло один след: находку comment/unknown-name на английском дереве, у комментария, а не у записи, которую надо поправить. Режим перечисляет каждое имя из ключа фразы, у которого есть пара – токен проекта или английское написание платформы, – если перевод не несет ни одного написания пары и называет вместо него что-то неизвестное: ОбщееСклада -> StockCommon, а перевод называет WarehouseCommon, с файлом и строкой записи. Перевод, который передает имя простыми словами, ничего не называет, и в список не попадает. Пару, написанную на всю строку тега, режим судит еще и по имени после тега, какой бы формы оно ни было (параметр, названный одним простым словом, – все равно имя сигнатуры), и по слову тега: @возвращает -> @returns, а перевод называет @return. Режим читает только словарь, отвечает за секунды и в --strict не входит: дерево собирается в любом случае, а проверка отвечает именно на вопрос, соберется ли оно. Обычный отчет перевода считает такие фразы одной строкой со ссылкой на --drift, когда они есть (в json – поле dictionary_drift); код выхода от них не меняется.

--gaps показывает частоту, первые места употребления и suggestion – собственное написание платформы, если оно у неё есть. Подсказка остаётся подсказкой: имя, объявленное проектом, может намеренно требовать другого слова. Внутреннее имя платформы – класс метаданных вроде CodeAttrMd – не предлагается вовсе.

Инструменты MCP – те же шесть, для агента, который наполняет словарь:

  • translate_status – покрытие и остаток, дешёвая проверка перед любым решением; against называет git-ссылку и добавляет отчёт --check-duplicates о коллизиях с ней, где дубли, которые есть и на ссылке, только посчитаны (весь список дает full); drift считает фразы, которые перечисляет translate_drift;
  • translate_gaps – непереведённое страницами (kind, filter, limit, offset), в ответе – прочитанный dictionary; compact отдаёт по строке только {key, kind, count} – форму рабочего списка переводчика, когда полные строки не влезают в ответ;
  • translate_entries – что уже в словаре, с файлом и строкой каждой записи, чтобы новое слово согласовывалось с принятыми; по умолчанию десять строк, а compact оставляет по строке {key, kind, value} – ответ на вопрос “как переведён термин” без мест, которые давали десять килобайт на вызов по частому корню;
  • translate_unused – обратный вопрос: что словарь всё ещё говорит, а проекта уже нет; filter сужает ответ до имён одного удалённого компонента – или сразу до списка подстрок, и тогда ответ называет в unmatched те, под которые не попала ни одна сирота; since – до сирот одной правки (ветка, коммит или диапазон A..B): ключи на снятых ею строках и пары, которые она добавила в словарь, обе стороны сочтены в блоке since; prune (по умолчанию выключен) снимает все ключи, которые выбрали отборы, какой бы ни была страница, вместе со всеми их объявлениями. После снятия список по умолчанию не возвращается: removed считает вхождения, pruned.keys – пары, pruned.by_kind / pruned.by_file дают разбивку; compact=false возвращает полные строки. Просмотр остаётся полным по умолчанию, а compact=true оставляет ключ, вид, файл и строку. counts считает всех отфильтрованных кандидатов по видам; budget_seconds (по умолчанию 300) ограничивает обход исходников – по истечении ответ состоит из прочитанного, помечен partial, несёт sources со счётом прочитанных файлов из всех и note о том, как продолжить, и это список кандидатов, по которому prune ничего не делает; ответ без filter и since несёт note о том, чего стоит его чтение;
  • translate_redundant – пары, на которые платформа отвечает сама и без которых проход написал бы то же слово: обходы, прячущие пробел данных платформы или движка. filter сужает ответ, prune (по умолчанию выключен) снимает все пары, которые выбрал filter, какой бы ни была страница, и pruned.keys их считает; в отличие от translate_unused он делает полный проход – вердикт держится на местах, где запись действительно отвечала;
  • translate_drift – фразы, чей перевод называет имя не так, как его пара (список --drift): в строке имя, написания пары, имена, которые перевод называет вместо них, и файл со строкой записи; filter сужает ответ по любому из этих текстов;
  • translate_set – запись: добавить, исправить на месте или снять, обнулив значение; edits_file – пачка файлом в тех же двух форматах, что читает --set.

Каждая страница говорит, чего в ней нет. total: 72 рядом с ровно пятьюдесятью строками выглядит полным ответом, и однажды словарь, собранный по такой странице, оказался короче на двадцать две записи. Нашёл их строгий проход уже после слияния. Поэтому урезанная страница несёт truncated: true, remaining и hint со следующим offset. Весь список отдаёт limit=0, а у пропусков есть ещё --missing, который записывает весь остаток файлом заготовки словаря.

Новая запись попадает в 090-manual.yaml (или в файл из target), а уже существующая правится там, где живёт, во всех местах, если словарь объявляет ключ не один раз. Пустое значение снимает все копии, а какую копию убрать, решает человек: так их перечисляет --check-duplicates. Писатель не плодит дублей. Ключ, переведённый по-разному в двух местах – в двух файлах или дважды в одном, – словарь отвергает при загрузке, а --check-duplicates перечисляет все такие ключи, не загружая словарь.

Машинный перевод

Заполнить большой остаток вручную долго, и --suggest берёт недостающее из внешнего переводческого сервиса. Возвращается оно предложениями, а не записью: словарь не меняется, пока предложение не принято. В консоли его принимает флаг --suggest-out, в редакторе – щелчок или клавиша Enter по подсказке в таблице.

xbsl translate e1c/app --suggest                                  # отчёт: что предложил сервис
xbsl translate e1c/app --suggest --provider yandex                # выбор сервиса явно
xbsl translate e1c/app --suggest --suggest-out 080-machine.yaml   # записать план рядом со словарём
xbsl translate e1c/app --suggest --plans tokens                   # добивать только имена

Идёт такой запуск всегда по всему проекту. Ограничить его нечем, остановить на полпути нельзя, заранее оценить объём тоже нечем. При этом каждая отправленная порция – платный запрос к сервису.

--suggest-out называет файл плана, в который лягут предложенные записи. Каталог из пути отбрасывается, берётся только имя файла, и оно ложится внутрь каталога словаря. Если словарь – это один файл, отдельного плана не выходит: записи ложатся в него же.

Без --provider движок берёт единственный настроенный сервис. Если не настроен ни один, отказ называет переменные окружения, которых не хватает. Если настроено больше одного, отказ перечисляет сами сервисы – google, yandex – и просит выбрать флагом --provider. Молча движок не гадает ни в том, ни в другом случае.

Два сервиса, и они не взаимозаменяемы.

  • Yandex Translate – авторизация ключом сервисного аккаунта и идентификатором каталога (folder), нужны оба. Порция – до 10000 символов за запрос. Глоссарий он понимает: список терминов проекта уходит вместе с текстом самого запроса.
  • Google Translate – авторизация одним ключом, порция до 5000 символов. Глоссария в его API нет вовсе, поэтому написание термина накладывается уже после ответа, когда движок собирает имя из вернувшейся прозы.

Сколько бы ни позволял сервис, в одном запросе едет не больше 100 текстов. Это собственный предел движка, взятый с запасом. Порция, отвергнутая целиком за размер, стоит ровно столько же, сколько принятая, а по одной лишь сумме длин в один запрос уехало бы шестьсот однословных имён.

Ключи – в окружении, никогда в командной строке или в файле настроек. Три переменные, поимённые под каждый сервис:

  • XBSL_TRANSLATE_YANDEX_KEY, XBSL_TRANSLATE_YANDEX_FOLDER – ключ и каталог Yandex, нужны оба;
  • XBSL_TRANSLATE_GOOGLE_KEY – ключ Google, один.

В редакторе переменные окружения заводить не нужно: команда XBSL: Задать ключ машинного перевода (xbsl.translate.setKey) спрашивает, какой из трёх завести, и кладёт значение в защищённое хранилище (SecretStorage) расширения – оно передаёт его движку в окружении самого запуска --suggest, а не в настройке и не в командной строке. Настройка xbsl.translation.provider выбирает сервис, если настроены оба сразу; сам ключ ею не задаётся.

Кэш лежит в файле machine-cache.json рядом со словарём: в каталоге xbsl-translation, если словарь – каталог, иначе рядом с единственным файлом словаря. В нём хранится сырой ответ сервиса по ключу из четырёх частей: сервис, язык, отпечаток глоссария, текст. Готовое имя там не хранят: правило сборки имени и список терминов ещё изменятся, а переспрашивать сервис ради того же текста незачем. Формат JSON выбран не случайно: словарь ищет *.yaml рекурсивно и принял бы файл кэша за ещё один план перевода с повторяющимися ключами. Запись остаётся в кэше независимо от того, приняли предложение в словарь или нет. Повторный --suggest не платит за один и тот же текст дважды, даже если прошлый ответ никто не принял.

Секция terms словаря – это короткий список “русский термин -> английское написание”. Планом перевода она не является и в покрытие не входит. Из неё берут сразу две вещи: пары глоссария, которые уходят в запрос вместе с текстом (это умеет только Yandex), и написание для сборки имени. Вторая половина работает уже по ответу сервиса. Собирая идентификатор, движок сверяет каждое английское слово ответа со списком терминов без учёта регистра и ставит словарное написание, где бы слово ни стояло во фразе. Русские формы слова при этом не разбираются вовсе. Термин, вернувшийся другим английским словом – множественным числом, синонимом, – не опознаётся и остаётся таким, каким его прислал сервис. На перевод строки комментария (phrases) термины не влияют: комментарий целиком остаётся тем, что ответил сервис.

Имена собирает движок, а не сервис, и собирает по одним и тем же правилам. Сервис отвечает прозой, например “Site address”, а плану tokens нужен идентификатор. Движок убирает служебные слова (a, the, of…), подставляет написание термина, где оно есть, и склеивает остаток, начиная каждое слово с большой буквы. Имя, уже занятое другим ключом словаря, --suggest отклоняет с названной причиной. Так же он поступает с прозой, из которой не вышло собрать идентификатор. Угадывать и переписывать поверх он не станет.

Литералы в сеть не ходят вовсе. --suggest посылает сервису только пробелы tokens и phrases, то есть имена и строки комментариев целиком. Строковый литерал заполняется отдельно и только тогда, когда его текст целиком совпадает с уже принятым именем. Делается это локально, без единого запроса к сервису.

Что уходит наружу. Сервис получает только текст пробела: имя так, как оно написано в проекте, или строку комментария целиком. Ни путь файла, ни код вокруг, ни остальной проект сервис не видит. Без ключа команда не делает ни одного запроса ни к Yandex, ни к Google: она сразу отказывает и называет переменную, которой не хватает.

Итог печатается тем же способом, что и у --set. cached – сколько ответов взято из кэша, requested – сколько запрошено заново, refused – сколько отклонено, и у каждого отклонённого названа причина.

В редакторе те же три числа остаются в собственной строке сводки панели до следующего запуска. Раньше они жили только в сообщении строки состояния, которое гаснет через несколько секунд. Наведение на эту строку называет причину каждого отказа. Когда спрашивать было решительно не о чем, строка говорит об этом словами, а не тремя нулями. Когда все предложения дали локальные совпадения литералов без единого запроса, сказано и это.

Ссылки в выражениях динамического списка

В источнике динамического списка Строки.Ссылка переводится как Reference, если Строки – явно заданный псевдоним основной или присоединенной таблицы. Это относится к свойствам с выражениями внутри того же источника, включая отборы. Другие приёмники и свойства-ссылки элементов интерфейса сохраняют обычное прочтение. Локальная запись словаря проекта может переопределить написание ссылки.

Последнее обновление 2 октября 2026 г.

Эта страница была полезной?