Fluent и локализация
Система локализации извлекает удобочитаемые текстовые строки, чтобы другие серверы могли их переводить по своему желанию. Основная ветка SS14 поддерживает только английский, но другие серверы могут свободно добавлять поддержку дополнительных языков.
Локализация выполняется с помощью Project Fluent (далее просто «Fluent»). Это проект более совершенной системы локализации, изобретённый Mozilla для Firefox. Он относительно новый, но имеет заметные улучшения по сравнению с более старыми системами вроде gettext. В старых системах вроде gettext код всё ещё содержит «английскую» версию строки. Однако на практике это плохо работает, потому что английскому (к лучшему или к худшему) не хватает многих нюансов, которые могут быть в других языках. Fluent решает это тем, что в коде вообще нет английского.
Базовый обзор
Основная идея в том, что код и прототипы сами по себе не содержат текстовых строк, показываемых человеку. Весь фактический текст, представляемый человеку, вместо этого задаётся в .ftl-файлах внутри Resources/Locale/<код языка>. Так, для (американского) английского это будет space-station-14/Resources/Locale/en-US/, для французского — .../fr-FR/, и т. д…
Пример реального каталога локализации можно найти здесь — английская (US/по умолчанию) локализация SS14.
Эти локализованные текстовые строки можно получить в игре с помощью метода Loc.GetString() (и подобных).
Учтите, что полный обзор синтаксиса разметки Fluent, с примерами и живой площадкой, можно найти на его сайте (см. «syntax guide» вверху).
Практические примеры.
Пример 1 (простое сообщение):
comp-stack-already-full = Стопка уже заполнена.
Этот пример определяет сообщение с именем comp-stack-already-full со значением "Стопка уже заполнена.".
Использование этого messageId в коде C# выглядит так:
Loc.GetString("comp-stack-already-full")
Вернёт строку "Стопка уже заполнена.", которую затем можно использовать для всплывающих сообщений, UI и так далее.
Пример 2 (сообщение с переменными):
traitor-user-was-a-traitor = {$user} был предателем.
Не весь текст так прост, как "Стопка уже заполнена."; часто часть текста должна меняться. К сожалению, учитывая, что языки имеют разную грамматику (SVO, SOV, и т. д.), нельзя просто сделать
var text = "Bob" + Loc.GetString("traitor-user-was-a-traitor");
и надеяться получить "Bob был предателем.", это может сработать для английского (языка SVO), но не сработает для многих других (включая другие языки SVO!)
К счастью, Fluent был создан, чтобы справляться с этим (и многими другими проблемами).
Сообщения могут содержать переменные, которые можно использовать внутри локализованного текста в любой позиции, подходящей для языка. Часть {$user} — это Fluent-«placeable», используемый для вставки переменной $user в текст.
Запрос локализованной строки с переменными выполняется немного иначе, чем запрос сообщения без них:
Loc.GetString("traitor-user-was-a-traitor", ("user", traitor.Mind.Session.Name));
После messageId "traitor-user-was-a-traitor" идёт кортеж (..., ...), состоящий из строки и следующего за ней значения traitor.Mind.Session.Name — имени предателя.
Определение переменных в вызове Loc.GetString() таким образом позволяет поместить имя, заданное в кортеже, в локализованный текст, чтобы подставить значение.
Loc.GetString("traitor-user-was-a-traitor", ("user", "Bob"));
Так мы получаем "Bob был предателем."
Пример 3 (множественное число, род и другие языковые особенности)
humanoid-character-profile-summary =
Это {$ent}. {GENDER($ent) ->
[male] Он
[female] Она
*[other] Они
} {$age} лет.
Вы, вероятно, знаете хотя бы один язык, в котором структура предложения или слова меняется в зависимости от количества, рода или другого признака объекта. Если вы читаете этот документ, то простейшим примером будет английский!
Сообщение humanoid-character-profile-summary используется на экране выбора персонажа в лобби и описывает имя, пол и возраст вашего персонажа, поэтому очевидно, что оно должно меняться в зависимости от имени, пола и возраста персонажа!
Имя и возраст просты, поскольку они не меняют структуру предложения. Имя подставляется, когда вы передаёте EntityUid в качестве параметра, а грамматический род можно получить с помощью функции GENDER(). Возраст мы передаём здесь вручную.
Однако с полом всё сложнее. В английском пол человека влияет на то, какие местоимения используются в предложениях о нём; нам нужно, чтобы «He», «She» или «They» выбирались правильно.
К счастью, Fluent поддерживает «селекторы», которые покажутся знакомыми всякому, кто когда-либо использовал операторы switch/case или match в других языках программирования. Переменная сопоставляется с рядом ветвей, и если находится совпадение, выполняется эта ветвь.
Селекторы Fluent ничем не отличаются: в зависимости от переменной предложение меняется, чтобы соответствовать наиболее подходящей ветви.
- Строковые переменные сопоставляются со строковыми ветвями
[male], [female], etc - Числовые переменные сопоставляются с числами
[1], [2], etcи специальными категориями[zero], [one], [two], [few], [many], которые представляют «категорию множественного числа CLDR» этого числа.- Это используется для обработки форм множественного числа:
- 1 minute, 2+ minutes (английские формы множественного числа)
- 1 minuta, 2-4 minuty и 5+ minut (чешские формы множественного числа)
- Это используется для обработки форм множественного числа:
Пример 4 (функции Fluent)
Очевидно, приведённый выше вариант с родом немного раздражает в написании и особенно раздражает тем, что его приходится вызывать в C#. К счастью, у нас есть несколько функций fluent, которые упрощают дело. Функции Fluent вызываются внутри фигурных скобок {}, как и переменные, и вызываются с переменными в качестве аргументов. Функции часто используются несколько раз подряд, к результатам других функций.
С использованием функций приведённый выше пример выглядит так:
humanoid-character-profile-summary = Это {$ent}. {SUBJECT($ent)} {CONJUGATE-BE($ent)} {$age} лет.
Обзор функций
Проще всего понять CAPITALIZE, который просто делает заглавной первую букву того, что в него передано. Чаще всего он используется для изменения результатов, возвращаемых другими функциями.
Функции GENDER() и PROPER() возвращают соответственно грамматический род (мужской, женский, эпиценовый, средний) и «собственность» (proper-ness) сущности.
Существуют также функции для определения определённого и неопределённого артиклей, которые должны быть у сущности. Это функции THE, возвращающая ‘the’, если сущность является собственным именем, и ничего в противном случае, и INDEFINITE, возвращающая ‘a’ или ‘an’ в зависимости от некоторых сложных правил.
hugging-success-generic = Вы обнимаете {THE($target)}.
hugging-success-generic-others = { CAPITALIZE(THE($user)) } обнимает {THE($target)}.
Существуют и другие функции для автоматического определения различных местоимений на основе грамматического рода переданной сущности — мужского, женского, эпиценового (they) или среднего (it). К ним относятся:
SUBJECT($ent)– he, she, they, itOBJECT($ent)– him, her, them, itPOSS-PRONOUN($ent)– his, hers, theirs, itsPOSS-ADJ($ent)– his, her, their, itsREFLEXIVE($ent)– himself, herself, themselves, itself
Наконец, есть функции для спряжения некоторых особых глаголов в зависимости от рода; это:
CONJUGATE-BE($ent)– (they) are, (he/she/it) isCONJUGATE-HAVE($ent)– (they) have, (he/she/it) hasCONJUGATE-BASIC($ent, first, second)– (they) {$first}, (he/she/it) {$second}, напримерCONJUGATE-BASIC($ent, "run", "runs")(they run, he/she/it runs)
Эти функции складываются в довольно сложные FTL-строки, но они будут читаться идеально каждый раз, независимо от того, какая сущность используется.
Например, в hands-system.ftl:
# Текст осмотра, когда они что-то держат (в руке)
comp-hands-examine = { CAPITALIZE(THE(SUBJECT($user))) } { CONJUGATE-BE($user) } держит { INDEFINITE($item) } { $item }.
Единственное уникальное слово в этой строке — ‘holding!’ Но в любом случае это всегда даст правильно выглядящую строку, независимо от того, кто пользователь и что за предмет:
# Примеры выходных строк
Сара Коллинз держит гаечный ключ.
Корги держит яблоко.
Крысы держат кусок сыра.
Старайтесь использовать эти функции везде, где возможно, чтобы получать динамичные и на 100% правильные строки. Если вы локализуете на другой язык, подумайте, какие аналоги этих функций существуют, и реализуйте их сами, поскольку эти, очевидно, очень специфичны для английского.
Локализация прототипов
Это не для использования в upstream. Если вы делаете контент для upstream, пожалуйста, используйте поля name/description. Это нужно, чтобы переводам было проще переопределять вещи, не редактируя данные основной игры.
- type: entity
id: RedOxygenTank
name: oxygen tank
description: A tank of oxygen. This one is red.
Итак, вы знаете, как локализовать код C#, но как локализовать YAML? В общем случае это будет так же просто, как:
someYaml: some-message-id
Но для сущностей у нас есть код, позволяющий делать локализации более удобными для чтения и написания.
- type: entity
id: RedOxygenTank
name: red-oxygen-tank-name
description: red-oxygen-tank-desc
red-oxygen-tank-name = кислородный баллон
red-oxygen-tank-desc = Баллон с кислородом. Этот — красный.
Хотя вы могли бы сделать что-то вроде вышеописанного, это немного повторяющееся: мы же знаем, что локализуем сущность красного кислородного баллона, так зачем указывать это снова в каждом сообщении?
Встречайте: атрибуты
Fluent позволяет добавлять к сообщениям дополнительную информацию; вы можете использовать это для описания свойств текста, таких как род или число слова.
Мы используем систему атрибутов, чтобы прикреплять сообщения… к сообщениям! На стороне C# id прототипа сущности, например RedOxygenTank, преобразуется в messageId ent-RedOxygenTank; этот messageId используется для имени сущности и имеет атрибут .desc, который используется для описания сущности.
- type: entity
id: RedOxygenTank
ent-RedOxygenTank = кислородный баллон
.desc = Баллон с кислородом. Этот — красный.
Смотрите! Никакого определения YAML для name или desc!
Советы
- ДЕЛАЙТЕ ОТСТУПЫ ПРОБЕЛАМИ, А НЕ ТАБАМИ
- Fluent воспринимает табы буквально, поэтому их нельзя использовать для отступов
- Чтобы начать строку сообщения с тега форматирования вроде
[bold], нужно экранировать открывающую квадратную скобку:my-formatted-message = {"["}bold]какой-то жирный текст[/bold] - Руководство по синтаксису Fluent.
- Хорошие практики Fluent.
- Специфично для SS14: мы рекомендуем добавлять ко всем сообщениям префикс, относящийся к контексту их использования; это помогает сохранять messageId уникальными (требование), а также служит для «пространства имён» сообщений.
например, сообщения, определённые для
StackComponent, должны начинаться сcomp-stack- - Чтобы применить языковой пакет к игре, вам просто нужно отредактировать Shared/EntryPoint.cs.
- Мы рекомендуем искать
Loc.GetStringв коде, чтобы найти весь переводимый текст