Система локализации отвечает за извлечение человекочитаемых текстовых строк, чтобы они могли быть переведены другими серверами, если они того пожелают. Основной 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 = Stack is already full.
Этот пример определяет сообщение с именем comp-stack-already-full и значением "Stack is already full.".
Использование этого messageId в C# коде:
Loc.GetString("comp-stack-already-full")
Вернёт строку "Stack is already full.", которую вы затем можете использовать для всплывающих сообщений, UI и так далее.
Пример 2 (Сообщение с переменными):
traitor-user-was-a-traitor = {$user} was a traitor.
Не весь текст так прост, как "Stack is already full.". Часто вам нужно, чтобы часть текста изменялась. К сожалению, учитывая, что языки имеют разную грамматику (SVO, SOV и т.д.), вы не можете просто сделать
var text = "Bob" + Loc.GetString("traitor-user-was-a-traitor");
Помните, выше показано, чего не надо делать!
и надеяться получить "Bob was a traitor."; это может сработать для английского (язык 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 was a traitor.".
Пример 3 (Множественное число, род и другие языковые проблемы)
humanoid-character-profile-summary =
This is {$ent}. {GENDER($ent) ->
[male] He is
[female] She is
*[other] They are
} {$age} years old.
Вы, вероятно, знаете хотя бы один язык, где структура предложения или слова варьируется в зависимости от количества, рода или другой особенности объекта. Если вы читаете этот документ, то самым простым примером будет английский!
Сообщение humanoid-character-profile-summary используется на экране выбора персонажа в лобби и описывает имя, пол и возраст вашего персонажа, поэтому оно, очевидно, должно меняться в зависимости от имени, пола и возраста персонажа!
имя и возраст просты, так как они не меняют структуру предложения. Имя подразумевается, когда вы передаёте EntityUid в качестве параметра, а грамматический род можно получить с помощью функции GENDER(). Возраст мы передаём здесь вручную.
Однако с полом всё сложнее. В английском пол человека влияет на то, какие местоимения используются в предложениях, которые ссылаются на него; нам нужно, чтобы ‘He’, ‘She’ или ‘They’ были выбраны соответствующим образом.
К счастью, Fluent поддерживает ‘selectors’, которые должны быть знакомы всем, кто когда-либо использовал switch/case или match в других языках программирования. Переменная переключается или сопоставляется с рядом ветвей, и если совпадение найдено, выполняется эта ветвь.
Селекторы Fluent ничем не отличаются: в зависимости от переключаемой переменной предложение изменяется, чтобы соответствовать наиболее подходящей ветви.
- Строковые переменные сопоставляются со строковыми ветвями
[male], [female] и т.д.
- Числовые переменные сопоставляются с числами
[1], [2] и т.д., а также специальными категориями [zero], [one], [two], [few], [many], которые представляют ‘категорию множественности CLDR’ для числа.
- Это используется для обработки множественного числа:
- 1 minute, 2+ minutes (английское множественное число)
- 1 минута, 2-4 минуты и 5+ минут (чешское множественное число)
Пример 4 (Fluent Functions)
Очевидно, что приведённый выше пример с родом немного раздражает в написании и особенно раздражает необходимость вызывать его в C#. К счастью, у нас есть некоторые fluent функции, которые облегчают эту задачу. Fluent функции вызываются внутри фигурных скобок {} так же, как и переменные, и вызываются с переменными в качестве аргументов. Функции часто используются несколько раз подряд над результатами других функций.
Используя функции, приведённый выше пример кода выглядит просто так:
humanoid-character-profile-summary = This is {$ent}. {SUBJECT($ent)} {CONJUGATE-BE($ent)} {$age} years old.
Краткий обзор функций
Самый простой для понимания — CAPITALIZE, который просто капитализирует первую букву того, что передано. Это чаще всего используется для изменения результатов, возвращаемых другими функциями.
Функции GENDER() и PROPER() возвращают грамматический род (мужской, женский, общий, средний) и свойство собственного имени entity соответственно.
Также существуют функции для определения определённых и неопределённых артиклей, которые должна иметь entity: это THE, который возвращает ‘the’, если entity является именем собственным, и ничего в противном случае, и INDEFINITE, который возвращает ‘a’ или ‘an’ в зависимости от некоторых сложных правил.
hugging-success-generic = You hug {THE($target)}.
hugging-success-generic-others = { CAPITALIZE(THE($user)) } hugs {THE($target)}.
Существуют и другие функции для автоматического определения различных местоимений на основе грамматического рода переданной entity — мужской, женский, общий (they) или средний (it). К ним относятся:
SUBJECT($ent) — he, she, they, it
OBJECT($ent) — him, her, them, it
POSS-PRONOUN($ent) — his, hers, theirs, its
POSS-ADJ($ent) — his, her, their, its
REFLEXIVE($ent) — himself, herself, themselves, itself
Наконец, существуют функции для спряжения некоторых специальных глаголов в зависимости от рода:
CONJUGATE-BE($ent) — (they) are, (he/she/it) is
CONJUGATE-HAVE($ent) — (they) have, (he/she/it) has
CONJUGATE-BASIC($ent, first, second) — (they) , (he/she/it) , например, CONJUGATE-BASIC($ent, "run", "runs") (they run, he/she/it runs)
Эти функции в совокупности создают довольно сложные FTL-строки, но они будут читаться идеально каждый раз, независимо от того, какая entity используется.
Например, в hands-system.ftl:
# Examine text after when they're holding something (in-hand)
comp-hands-examine = { CAPITALIZE(THE(SUBJECT($user))) } { CONJUGATE-BE($user) } holding { INDEFINITE($item) } { $item }.
Единственное уникальное слово в этой строке — ‘holding’! Но в любом случае, это всегда будет приводить к корректно выглядящей строке, независимо от того, кто пользователь или что за предмет:
# Example output strings
Sarah Collins is holding a wrench.
The corgi is holding an apple.
The rats are holding a piece of cheese.
Вам следует стараться использовать эти функции всякий раз, когда это возможно, чтобы создавать динамические и 100% корректные строки. Если вы локализуете на другой язык, подумайте, какие аналоги этих функций существуют, и реализуйте их самостоятельно, поскольку они, очевидно, очень специфичны для английского.
Локализация прототипов
Это не для использования в апстриме. Если вы создаёте апстрим-контент, пожалуйста, используйте поля name/description.
Это сделано для упрощения переопределения переводов без редактирования основных данных игры.
- type: entity
id: RedOxygenTank
name: oxygen tank
description: A tank of oxygen. This one is red.
Итак, вы знаете, как локализовать C# код, но как локализовать YAML?
В общем, это будет так же просто, как:
someYaml: some-message-id
Но для entity у нас есть код, позволяющий упростить чтение и написание локализаций.
- type: entity
id: RedOxygenTank
name: red-oxygen-tank-name
description: red-oxygen-tank-desc
red-oxygen-tank-name = oxygen tank
red-oxygen-tank-desc = A tank of oxygen. This one is red.
Хотя вы могли бы сделать что-то вроде вышеуказанного, это немного повторяемо: мы знаем, что локализуем entity красного кислородного баллона, так зачем указывать это снова в каждом сообщении?
Встречайте: Attributes
Fluent позволяет добавлять дополнительную информацию к сообщениям. Вы можете использовать их для описания свойств текста, таких как род или множественность слова.
Мы используем систему атрибутов для прикрепления сообщений… к сообщениям! На стороне C# id прототипа entity, например RedOxygenTank, преобразуется в messageId ent-RedOxygenTank. Этот messageId используется для имени entity и имеет атрибут .desc, который используется для описания entity.
- type: entity
id: RedOxygenTank
ent-RedOxygenTank = oxygen tank
.desc = A tank of oxygen. This one is red.
Смотрите! Никакого YAML-определения для name или desc!
Советы
- ОТСТУПЫ ПРОБЕЛАМИ, НЕ ТАБАМИ
- Fluent воспринимает табы буквально, поэтому их нельзя использовать для отступов
- Чтобы начать строку сообщения с тега форматирования, такого как
[bold], вам нужно
экранировать открывающую квадратную скобку:
my-formatted-message =
{"["}bold]some bold text[/bold]
- Syntax Guide Fluent.
- Good Practices Fluent.
- Специфика SS14: мы рекомендуем добавлять префикс ко всем сообщениям, соответствующий контексту, в котором они используются. Это помогает сохранить уникальность messageId (требование) и также служит для создания “пространства имён” для сообщений.
Например, сообщения, определённые для
StackComponent, должны начинаться с comp-stack-
- Чтобы применить языковой пакет к игре, вам нужно отредактировать Shared/EntryPoint.cs.
- Мы рекомендуем искать
Loc.GetString в коде, чтобы найти все переводимые тексты.
Последнее изменение 21 июня 2026 г.