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

Gettext — это система локализации, глубоко укоренившаяся в проект GNU и сопутствующие ему архитектурные решения. Fluent Project рассматривает gettext как хороший пример полноценной низкоуровневой платформонезависимой экосистемы библиотек и инструментов для управления полным циклом выпуска продукта с файлами локализации в удобочитаемом формате. В то же время парадигма Fluent приводит нас к другим архитектурным решениям в важных локализационных аспектах, которые, в свою очередь, приводят к совершенно разным API и жизненным циклам.

Другими словами, gettext — отличный проект, но мы не разделяем его взгляды на подход к локализации.

Вот основные различия между gettext и Fluent:
gettext Fluent
Идентификатор сообщения исходная строка предоставляется разработчиком
Привязка аргументов позиционная* на основе ключей
Аннулирование перевода нечёткие совпадения изменение идентификатора
Хранение данных удобочитаемый формат (.po) или компилированный формат (.mo) удобочитаемый формат (.ftl)
Внешние аргументы нет богатая поддержка
Поддержка множественного числа special-cased часть общего синтаксиса селектора вариантов
Широта поддержки множественного числа На усмотрение разработчика, влияет на все переводы На усмотрение локализатора, влияет только на конкретную локаль
Создан для Языков семейства С* веба, современных клиентских языков
Ссылки на сообщения определяется разработчиком определяется локализатором
Шаблоны сообщений необходимы (.pot) нет
Комментарии локализаторов нет* полная поддержка
Восстановление после ошибок хрупкая сильная логика восстановления
Составные сообщения нет значение + атрибут на сообщение
Двунаправленные тексты нет двунаправленная изоляция
Международное форматирование нет явное и неявное

Договорённость


Самое важное различие gettext и Fluent — идентификатор сообщения. В gettext решили использовать исходную строку (обычно на английском) в качестве идентификатора. Этот выбор кажется простым, но в дальнейшем он навязывает множество ограничений.

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

Во-вторых, это усложняет введение нескольких сообщений с одинаковым текстом на исходном языке, которые должны переводиться по-разному. Например, текст для кнопки «Open» и для отметки «Open» могут переводиться по-разному, поскольку первый текст — команда, а второй — описание. В gettext есть опциональная контекстная строка msgctxt для различия строк с одинаковым исходным сегментом. Такой подход возлагает ответственность за распознавание подобных ситуаций на разработчиков, что противоречит принципу разделения интересов.

Fluent не рекомендует использовать тексты повторно именно по этой причине. Отделение исходного текста от других переводов также важно для нашей способности вводить составные сообщения (которые содержат несколько строк для одной единицы перевода, привязанной к одному виджету пользовательского интерфейса) и для ссылок на сообщения на основе идентификатора.

Fluent устанавливает «договорённость» между разработчиками и локализаторами. Разработчик вводит уникальный идентификатор и набор переменных (кол-во непрочитанных соообщений, имя пользователя и т.п.), а локализатор, используя синтаксиc Fluent, решает, как именно сконструировать текст сообщения для этого идентификатора.

Разработчик не должен беспокоиться о детальной реализации переводов подобных сообщений. Всё, что нужно разработчику, это чтобы на запрос строки по конкретному идентификатору он получил одну строчку текста, подходящего для конкретного места в UI.

Варианты сообщений


В gettext поддерживается небольшой набор функций для интернационализации, в частности — для множественных чисел. Но такой синтаксис для множественных чисел является особым случаем, дополнением к стандартному синтаксису gettext, и его сложно масштабировать для других случаев, требующих вариативности.

Fluent поддерживает базовую концепцию вариативности строк, которую можно использовать с селекторами. Обычно правило множественного числа и будет таким селектором, но в зависимости от грамматических особенностей языка, могут быть и другие, такие как пол, склонение или даже окружение — например, время суток или операционная система. Синтаксис Fluent позволяет локализаторам учитывать все эти особенности и создавать текст, точно подходящий под ситуацию.

Внешние аргументы


Gettext не поддерживает внешние аргументы. Другими словами, нельзя задать форматирование параметров — чисел, дат. Для форматирования параметров в gettext рекомендуется возвращать строку, которую в дальнейшем передадут в printf или запустят String.prototype.replace на результирующей строке.

Во Fluent поддержка внешних аргументов находится в самой основе синтаксиса. Внешние аргументы не только интерполируются, но и используются в качестве параметров для селектора, а так же могут передаваться во встроенные функции. Это позволяет локализаторам создавать гораздо более точные тексты для конкретных случаев. Вдобавок ко всему, Fluent размещает маркеры FSI / PDI вокруг объектов, чтобы защитить изоляцию направленности в двунаправленном тексте, и запрещает любые манипуляции с конечными строками, снижая нагрузку на разработчиков.

Изолирование ответственности


Кроме того, способ, которым gettext обрабатывает правила для множественных чисел, требует от разработчика системы выбора, будет ли сообщение многовариантным сообщением или единственной строкой. С точки зрения Fluent, разработчик не должен заниматься подобными вопросами. Во многих случаях, когда в английском достаточно одного варианта, в других языках нужно добавлять варианты со множественными числами.

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

В результате Fluent хранит каждый перевод отдельно, без «утечки» требований одного языка на другие, и сохраняет все переводы «непрозрачными» для разработчика, которому не нужно беспокоиться о том, какие функции локализаторам могут понадобиться для данной строки.

Аннулирование перевода


В цикле разработки можно выделить три ситуации, когда перевод «аннулируется» (станет недействительным) по отношению к оригиналу:

  • Незначительное изменение: не влияет на перевод (исправление пунктуации, опечатки).
  • Среднее изменение: влияет на конструкцию сообщения, но не отменяет корректности связанного перевода (например, Show All Bookmarks -> Show Bookmarks Manager).
  • Значительное изменение: новый смысл предложения (Click to save -> Click to open).

По архитектурным причинам, gettext объединяет все три уровня в одно состояние под названием нечёткое совпадение (fuzzy). Любое изменение исходной строки (хоть полное, хоть незначительное) приводит к аннулированию переводов.

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

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

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

Формат данных


В gettext использует три формата файлов — *.po, *.pot и *.mo. Это влияет на внедрение gettext в производственный цикл, добавляя этапы вроде извлечения и компиляции сообщений.

Fluent использует единый формат файла *.ftl, что упрощает внедрение и не требует дополнительных шагов, которые могут привести к расхождению в данных.

Поддержка Юникода


Gettext можно кодировать в UTF-8. В целом, на этом поддержка Unicode заканчивается. В нём используется свой набор данных для множественных чисел, не умеет работать с форматированием дат и чисел, не помогает в работе с двунаправленными текстами.

Fluent активно использует стандартизованные библиотеки и алгоритмы CLDR, ICU и ECMA402, аккуратно объединяя локализацию и интернационализацию.

Заключение


Мы верим, что API и синтаксис Fluent представляют собой существенное улучшение по сравнению с gettext, и мы рекомендуем использовать их для международного ПО.

Больше о Fluent


Комментарии (32)


  1. Displacer
    07.05.2019 09:00
    +2

    Так связки есть только к JavaScript, Rust и Python, или всё же есть Си-шный API? Всё-таки универсальная библиотека по хорошему должна быть доступна из любых языков.


    1. wtigga Автор
      07.05.2019 09:13

      Судя по всему, пока только вышеперечисленные.


    1. maxzhurkin
      07.05.2019 09:20
      +2

      Так не бывает: сопровождение неограниченного списка языков разработчиком просто невозможно — требуется участие причастных к самому языку (целевому), а тут от разработчика мало что зависит кроме выбора языка разработки, который широко используется для создания биндингов или поддержки промежуточных биндингов для такого языка.
      В этом смысле да, требуются биндинги/API на C


    1. vyo
      07.05.2019 17:10

      Так на чём сама либа написана? Или это вовсе не биндинги, а именно полноценные реализации имелись в виду?


      1. maxzhurkin
        08.05.2019 16:18

        ECMAScript, кажется


        1. vyo
          08.05.2019 16:23

          Тогда на C я бы очень хотел отдельную реализацию, а не биндинги. Пихать интерпретатор полноценного языка (особенно в C, когда это дело порой нужно залить на микроконтроллер) ради одной либы — не самая хорошая идея ИМХО.


          1. maxzhurkin
            08.05.2019 16:26

            Что-то мне подсказывает, что gettext местами тоже жирноват для микроконтроллеров


            1. vyo
              08.05.2019 16:30

              Спорить тут не буду, самому аж интересно стало, можно ли его впихнуть на ESP32 (с SD картой для файлов). Но остаюсь при мнении, что проге на C лучше уж использовать и либы на C.


      1. mayorovp
        08.05.2019 16:41

        Ну, fluent-rs — это именно что реализация.


  1. GCU
    07.05.2019 10:32
    +1

    Комментарии локализаторов: нет

    Ну как же www.gnu.org/software/gettext/manual/html_node/Modifying-Comments.html#Modifying-Comments

    Создан для: Языков семейства С

    Вот перечень поддерживаемых типов файлов/языков для xgettext
    C, C++, ObjectiveC, PO, Shell, Python, Lisp, EmacsLisp, librep, Scheme, Smalltalk, Java, JavaProperties, C#, awk, YCP, Tcl, Perl, PHP, GCC-source, NXStringTable, RST, Glade, Lua, JavaScript, Vala, Desktop

    Привязка аргументов: позиционная

    Это к gettext лишь косвенно относится (там нет форматтера аргументов, используется стандартный) и зависит от языка программы — в Python прекрасно работают ключи.

    В gettext использует три формата файлов — *.po, *.pot и *.mo. Это влияет на внедрение gettext в производственный цикл, добавляя этапы вроде извлечения и компиляции сообщений.

    По факту .po и .pot это один и тот-же формат, .pot и .mo автоматически генерируемые, их не нужно хранить в репозитории. Компиляция это ещё и проверка синтаксиса, а автоматическое извлечение сообщений — это наиболее полезная функция в gettext.
    xgettext + msgmerge это как-раз то, что избавляет от головной боли при работе с большим числом сообщений. Пока Fluent сам по себе не предлагает решения этих проблем, с каждым id нужно возиться вручную.

    P.S. Fluent гораздо лучше .po в качестве формата, но xgettext отнюдь не так плох, и исторически хорошо себя зарекомендовал как достаточно простое и удобное решение, для большинства случаев вполне достаточное. Лучшее враг хорошего :)


    1. wtigga Автор
      07.05.2019 10:57

      Я не настоящий разработчик, поэтому мне сложно судить о технической стороне вопроса. Но с точки зрения локализатора, я полностью разделяю мнение авторов fluent про болезненность подхода «оригинал = идентификатор». Надеюсь, что у fluent тоже появится механизм для извлечения сообщений и превращения их в ID в коде, тогда все (разработчики, переводчики) будут рады.


      1. GCU
        07.05.2019 11:45

        Ну тогда по технической части мелкие неточности, кроме уже указанных

        Идентификатор сообщения: исходная строка

        Обычно да, но есть ещё msgctx точнее было бы написать контекст + исходная строка

        не умеет работать с форматированием дат и чисел
        Fluent активно использует стандартизованные библиотеки и алгоритмы CLDR, ICU

        В большинстве случаев форматированием дат и чисел уже занимаются функции ОС/браузера/окружения, которые как раз и используют ICU(CLDR) и gettext этому никак не мешает. В том же JavaScript этим обычно занимается Intl, и он существует независимо от Fluent.

        Шаблоны сообщений: необходимы (.pot)

        Но ведь .ftl и есть тот самый шаблон. Непосредственно для переводчиков сам .pot не нужен — после генерации и обновления .po файлов его можно удалить, его не хранят.
        Технически он есть, вот такой временный генерируемый файл. Но после настройки сборки о существовании .pot или .mo можно забыть и работать только с обновляемыми .po

        Не раскрыта тема устаревших переводов, .po может их хранить/накапливать и «воскрешать» в случае необходимости — вот такая «память переводов» из коробки :)


        1. wtigga Автор
          07.05.2019 11:57

          На всякий случай напомню, что это перевод, а не моя оригинальная статья :-)

          В большинстве случаев форматированием дат и чисел уже занимаются функции ОС/браузера/окружения, которые как раз и используют ICU(CLDR) и gettext этому никак не мешает. В том же JavaScript этим обычно занимается Intl, и он существует независимо от Fluent.

          Имхо, это некорректный подход. Не обязательно локаль браузера/ОС соответствует локали сайта/продукта, и получается мешанина из, например, английского текста и китайской даты. Следовательно — плохой UX.


          1. GCU
            07.05.2019 12:09

            Не обязательно локаль браузера/ОС соответствует локали сайта/продукта, и получается мешанина из, например, английского текста и китайской даты.

            Эмм… я не писал о том что нужно обязательно использовать текущую локаль браузера/ОС. Обычно ОС поддерживает вполне солидный набор локалей, можно выбрать наиболее подходящую, не обязательно текущую/по умолчанию.
            При взаимодействии с другими программами в GNU/Linux например скорее всего они (другие программы) будут использовать текущую локаль системы и для консистентности продукт должен работать так-же


            1. wtigga Автор
              07.05.2019 12:19

              Я имел в виду такую ситуацию:
              Есть программа, UI на русском. В системе выбрана английская локаль США. Если программа будет полагаться на форматирование чисел по локали системы, то получится, что в русском интерфейсе цифры будут отформатированы на американский манер. Или я не прав, и можно заставить софт (с gettext) использовать именно правила форматирования для русского языка, независимо от локали системы?


              1. GCU
                07.05.2019 12:56

                Да, можно форматировать дату на любой локали, которая поддерживается ОС независимо от текущей. Это никак не относится к gettext, обычно это стандартная библиотека.

                P.S. Для Python gettext — стандартная библиотека.


                1. wtigga Автор
                  07.05.2019 13:23

                  Понятно, спасибо!


    1. wtigga Автор
      07.05.2019 11:26

      Спасибо за дополнение в комментарии. За это люблю habr — очень много полезного можно почерпнуть из обсуждений.


      1. GCU
        07.05.2019 11:59
        +1

        Я как-то упустил из виду что это перевод :) извините.
        Официальная позиция разработчиков Fluent вполне ожидаема — и маркетинг им не чужд.


    1. immaculate
      07.05.2019 13:18
      -1

      Да что там говорить, если очень многие не могут осилить даже gettext. Конкретно на Хабре раз в 3-6 месяцев появляется очередная статья об очередном «гениальном» способе перевода, который является примитивной костыльной надстройкой над каким-нибудь Excel/CSV.


      А тут какой-то новый инструмент, по которому не найти ни капли информации, да еще не знаю, поддерживается ли хоть одним инструментом для переводчиков, таким как Weblate...


      Кроме того, часть перечисленных недостатков не являются недостатками gettext. Например, про изменение исходных текстов и потерю переводов: в gettext есть встроенные средства для того, чтобы не терять переводы (fuzzy), а кроме того, наверное, в идеальном случае инструмент перевода должен интегрироваться с Translation Memory что для gettext, что для любого другого формата.


      1. wtigga Автор
        07.05.2019 13:23

        Fuzzy отдельным абзацем упомянут как неидеальный (по мнению разработчиков fluent) способ сохранения переводов. И я с ними согласен.


  1. ColdPhoenix
    07.05.2019 15:59

    Насколько знаю gettext не запрещает использовать идентификаторы, как строки.
    Хотя это и не принято.


    Выглядит проект интересно, но, где примеры? на сайт зашел… там пример напоминает просто форматирование. локализации не увидел(по крайней мере на главной).
    нужны примеры на всех уровнях, а так же более широкую поддержку ЯП.


    1. vyo
      07.05.2019 16:17

      > Насколько знаю gettext не запрещает использовать идентификаторы, как строки.

      Вот пользователи `C` локали спасибо скажут вам за id без значений вместо строк. Плюс предупреждения о несовпадении переносов строк/символов форматирования (`%s`, `%d`, etc.). Последнее, кстати, ИМХО плюс в сторону строк против id.


    1. mayorovp
      07.05.2019 16:48

      "Просто форматирование" — это когда форматные строки зашиты в коде. А когда они все вынесены в отдельный файл, выбираемый в зависимости от языка — это уже локализация.


      1. ColdPhoenix
        07.05.2019 18:30

        Так я и говорю, нужны примеры как это работает в коде. Именно пример локализации через Fluent.


  1. Cykooz
    07.05.2019 21:49
    +2

    В живую Fluent я ещё не использовал, но зато пользовался gettext. И после того, что я узнал про возможности Fluent, использовать gettext у меня больше нет желания.
    Я программист и мой первый шок от gettext был когда я понял, что для получения перевода с поддержкой множественных форм, я должен в коде использовать отдельную функцию и передать ей варианты написания куска текста, зависящего от числа. Как так? Зачем я, как программист, должен думать про множественные формы?
    Позднее мне стало понятно почему так получилось в gettext — потому что он разрешает использовать текст как id сообщения, а значит должен уже в коде приложения иметь всю информацию необходимую для генерации текста, даже если вообще нет ни каких переводов.
    В Fluent с этим всё становится легко и просто. Для получения перевода в коде используется только одна функция, и мне не надо при этом думать про множественные формы даже для одного (например английского) языка. А ещё в Fluent (в отличии от gettext) можно в рамках одного сообщения использовать несколько разных чисел для выбора множественной формы разных частей строки. А можно использовать вообще не числа, а например пол «персонажа» и вообще любой другой «селектор», который может повлиять на перевод.
    С Fluent в принципе не нужны инструменты для извлечения «строк» из кода, не надо их мержить с уже имеющимися переводами, т.к. в коде есть только id сообщений. Тут конечно есть минус — можно забыть добавить id в переводы если у вас нет тестов в приложении, которые вероятно выдадут ошибку при запросе не существующего перевода.
    По поводу инструментов для работы с переводами — у Mozilla есть в гитхабе опен-сорсное веб-приложение для совместной работы над переводами (они используют его для перевода своих проектов), название не помню, но в одной с предыдущих статей про Fluent на хабре оно упоминалось.


    1. wtigga Автор
      08.05.2019 05:45

      Зачем я, как программист, должен думать про множественные формы?


      Вот это ключевая ценность Fluent. Пусть локализаторы/переводчики/UX-writer'ы думают, как писать контент, им за это платят.


      1. GCU
        08.05.2019 10:24
        -1

        Разработчик всё равно должен передать все «селекторы» из кода, просто дальнейшая логика работы по построению сообщения сброшена на «локализаторов/переводчиков/UX-writerов». Гибкость Fluent усложняет оценку затрат на переводы и тестирование/контроль качества.
        Для всяких энтузиастов и опенсорса это хорошее решение, но в мире коммерческих переводов, где цена зависит от объема текста — маловероятно что переводчики будут заморачиваться с логикой без дополнительных капиталовложений. Почему переводчик на русский должен писать целую пачку сообщений с учётом комбинаций числа и рода, в то время когда в английском это была лишь пара строк?
        (Переведут «Сообщений: X» и хватит :) )
        gettext довольно строго регламентирует plural, изначально требуя все формы, необходимые с учётом языка. Хотя это и дубово — это фиксированный и заранее известный объем работы для переводчиков и QA. А вот креатив с Fluent уже не так прост и предсказуем.


        1. wtigga Автор
          08.05.2019 10:53
          +1

          Зависит от того, как построен процесс. Если текст отдают на аутсорс переводчику, которому платят за объём, то вы совершенно правы. И там переводчику всё равно, плывёт у вас UI или не плывёт.
          А если это штатный UX-копирайтер или локализатор — то объёмы могут быть незначительные. Я работал и в роли аутсорса, и в роли штатного локализатора, поэтому знаю, о чём говорю.
          Но в обоих случаях у программиста обычно нет знаний такого количества человеческих языков, чтобы во всех нужных местах подставить селекторы и прочее.


          1. GCU
            08.05.2019 11:26

            Про «селекторы» я имел ввиду, что для того, чтобы логика построения в Fluent работала, разработчики всё равно должны передать число, род в саму систему — в контекст сообщения. Вместо явной привязки к конкретному сообщению эти дополнительные данные привязываются к сущности в Fluent являющимися контекстом для сообщений. Разработчик должен откуда-то взять род/число/социальный статус/возраст (и прочие «селекторы») и передать их в Fluent — должен знать что они там нужны (пусть даже и не понимать как и зачем они используются).

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

            Переводчик не сможет использовать селектор по роду/числу, если этих данных у него нет. А передать их может только разработчик.

            Не все компании могут себе позволить держать штатных переводчиков/локализаторов :(.


            1. wtigga Автор
              08.05.2019 11:35

              Разработчик должен откуда-то взять род/число/социальный статус/возраст


              Всё правильно: если данных негде взять, то будет один вариант. А если есть — разработчик просто передаёт этот параметр, и дальше локализатор решает, как быть.

              Не все компании могут себе позволить держать штатных переводчиков/локализаторов :(

              А кто с этим спорит? Не бывает одного решения, которое идеально подойдёт для всех кейсов.

              Объясню на примере моего любимого AliExpress, где я бы очень хотел внедрить Fluent. У нас тут поддержка 18 языков, но только пять из них — штатными копирайтерами, остальное — аутсорс. Используя флюент, мы могли бы сделать красиво хотя бы пять языков, а остальные оставить в безопасном generic варианте без учёта вариативности. Но мы не можем: либо всё, либо ничего. А правила plural в каком-нибудь арабском и русском значительно отличаются, поэтому нужно городить кучу селекторов со стороны разработчиков, а у них и так работы хватает.


              1. GCU
                08.05.2019 12:06
                +1

                Возможность сделать красиво целых 5 языков — это замечательно.
                Кстати комментарий очень полезный — он как раз наглядно показывает зачем и почему Fluent :). Удачи в этом нелёгком деле.

                P.S. При работе только с аутсорсом преимущества Fluent не так хорошо выражены. На примере с plural gettext явно выдаёт нужное количество форм на перевод, но всё равно иногда (не часто) переводчиков приходится дополнительно просить, чтобы различные формы вообще использовали в переводе o_O.

                P.P.S. Доля сообщений/текстов, требующих дополнительной языко-зависимой логики построения обычно не так велика, чтобы это было какой-то острой проблемой, но всё зависит от типа приложений.