Skip to content

Локализация (i18n)

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

Локализация/интернационализация (принятое в экосистеме сокращение — i18n, от internationalization) означает показ текста вашего дополнения на разных языках. Каждый кусочек текста, который показывает ваше дополнение, живёт в JSON-файлах, упакованных внутри jar — по одному файлу на язык. (.jar — единственный zip-подобный файл, в который упаковано дополнение Kotlin/Java, как tarball npm-пакета; вы никогда не открываете его вручную.)

Обязательно ли мне это делать?

Да, хотя бы немного. Вы можете жёстко зашить английские строки прямо в ваши Svelte-компоненты — но двум видам текста больше негде жить: заголовки разрешений (показываемые на странице Разрешения панели) и строки журнала активности (показываемые на странице Активность панели) могут приходить только из файлов локализации. Так что даже дополнению только на английском нужен en-US.json. Вы встретите оба ниже.

Настройте файлы локализации

Внутри проекта вашего дополнения (того, что создало Начало работы) создайте папку src/main/resources/locales/, если её ещё нет, и добавьте файл en-US.json, содержащий просто {}. (resources — имя Gradle для некодовых файлов — JSON, изображений и подобного — которые упаковываются в jar. Несмотря на название, это не только для изображений.)

Вы получаете по одному файлу на язык, где имя файла — код этого языка:

src/main/resources/locales/
├─ en-US.json
├─ tr.json
└─ ru.json

Обязателен только en-US.json. Добавляйте другие языки (tr.json, ru.json, …), когда будете готовы переводить.

Контрольная точка

Теперь у вас должен быть src/main/resources/locales/en-US.json на диске, содержащий {}.

  • Формат файла: каждый файл — обычный, валидный .json — без комментариев и без завершающих запятых (любое из них делает весь файл невалидным), и сохраняйте его как UTF-8, чтобы турецкие или русские символы не превратились в искажённый текст в стиле ö.
  • Имя файла — код локали: en-US.json — код en-US, tr.jsontr и так далее. Pano принимает короткие коды вроде en-US или tr — те же коды, что вы задаёте как язык сайта (смотрите Конфигурацию).

Какой язык видит посетитель. У сайта есть язык по умолчанию (настройка locale), и когда админ это разрешает, каждый посетитель может выбрать свой из языков, которые вы поставляете. Чтобы протестировать ваш tr.json, переключите свой язык на турецкий в Панель → Настройки → Платформа → Предпочтения (или через селектор языка сайта) и обновите.

en-US — запасной вариант

Если ключ отсутствует для языка посетителя, Pano откатывается к en-US.json. Если ключ отсутствует и там тоже, посетитель видит на экране сырой путь ключа — что-то вроде plugins.pano-plugin-shoutbox.widget.title вместо реальных слов. Держите en-US.json присутствующим и полным: это страховочная сеть для каждого другого языка, а сырой ключ на экране — ваш первый признак того, что он неполон.

Пространство имён — где живут ваши ключи

Платформа отдаёт каждый написанный вами ключ под plugins.<pluginId>.<key>. Для нашего примера дополнения Shoutbox (его pluginIdpano-plugin-shoutbox — значение, которое вы задали в gradle.properties при создании дополнения, смотрите Начало работы) ключ по имени widget.title становится plugins.pano-plugin-shoutbox.widget.title во время поиска.

Вы никогда не пишете этот длинный префикс — ни в ваших JSON-файлах, ни в компонентах. Внутри вашего JSON ключи остаются без префикса (widget.title), а Pano добавляет часть plugins.<pluginId>. автоматически. Единственное место, где префикс реально появляется, — небольшой помощник в main.js (показан ниже), и boilerplate уже его содержит. Это автоматическое добавление префикса — то, что не даёт вашим ключам когда-либо столкнуться с ключами основной платформы или другого дополнения.

Определите ваш первый ключ

Кастомные ключи — обычные пары ключ–значение, которые показывает ваш UI. Поместите их в en-US.json, вложенными как угодно:

json
{
  "widget": {
    "title": "Latest shouts",
    "empty": "No shouts yet — be the first!"
  },
  "welcome-message": "Welcome back, {username}!"
}

Вложенные объекты становятся путями с точками. title выше живёт внутри widget, так что вы ищете его как единственный ключ widget.title (не пишите буквальный ключ "widget.title"). Аналогично, empty внутри widget — это ключ widget.empty.

Плейсхолдеры используют одинарные фигурные скобки. {username} — слот, который вы заполните позже из вашего компонента (показано в следующем разделе). Плейсхолдеры отрисовываются с одинарными скобками ({username}), потому что UI использует svelte-i18n. Бэкенд-шаблоны писем — единственное исключение — они используют двойные скобки; смотрите одинарные против двойных скобок ниже.

Покажите ключ в компоненте (Svelte)

Чтобы показать перевод в компоненте, импортируйте помощник _ из main.js вашего дополнения — boilerplate уже его поставляет:

svelte
<script>
  import { _ } from '../main.js';
</script>

<h2>{$_('widget.title')}</h2>

Три вещи, на которых спотыкается начинающий читатель, все нормальные:

  • Почему он называется _? _ — принятое в svelte-i18n имя для его функции перевода — один символ, потому что вы печатаете его постоянно.
  • Импортируйте _, используйте $_. Префикс $ — способ Svelte читать текущее значение хранилища (хранилище — реактивная коробка значений Svelte). Вы импортируете _; используете его как $_.
  • Поправьте ../ под глубину вашего файла. import { _ } from '../main.js' предполагает, что ваш компонент сидит ровно на одну папку ниже main.js. В более глубоко вложенном маршруте добавьте больше ../ (например, ../../main.js), иначе импорт даст 404.

{$_('widget.title')} ищет plugins.pano-plugin-shoutbox.widget.title за вас.

Контрольная точка

Компонент теперь отрисовывает Latest shouts. Если вы вместо этого видите сырой plugins.pano-plugin-shoutbox.widget.title, ключ отсутствует или написан с ошибкой в en-US.json.

Заполнение плейсхолдера

У welcome-message выше был слот {username}. Чтобы заполнить его, передайте второй аргумент — svelte-i18n называет его options — с вашими значениями плейсхолдеров под values:

svelte
<p>{$_('welcome-message', { values: { username: 'Ada' } })}</p>

Это отрисовывает Welcome back, Ada!. Имена под values должны совпадать с именами {...} в строке. Если вы совсем забудете аргумент values, посетитель увидит на экране буквальный {username}.

Множественное число: «1 shout» против «5 shouts»

Не изобретайте два ключа и if. svelte-i18n понимает синтаксис сообщений ICU, так что единственный ключ обрабатывает оба — например {count, plural, one {# shout} other {# shouts}} — и вы вызываете его как $_('shout-count', { values: { count: n } }). Смотрите документацию по форматированию svelte-i18n.

Как работает помощник _ (пропустите, если торопитесь)

Вам не нужно понимать этот сниппет, чтобы использовать _main.js из boilerplate уже его содержит, так что просто подтвердите, что он там. Но вот он:

js
// src/main.js
import { derived } from 'svelte/store';
import { _ as i18n } from '@panomc/sdk/utils/language';

export const pluginId = 'pano-plugin-shoutbox';
export const _ = derived(i18n, ($t) => (key, options) => $t(`plugins.${pluginId}.${key}`, options));

Хранилище — реактивная коробка значений Svelte; derived означает «пересчитывать всякий раз, когда меняется то, от чего я завишу» — так что когда посетитель переключает язык, каждый $_(...) на странице обновляется сам. @panomc/sdk упаковывается boilerplate (смотрите Разработку фронтенда). Скопируйте как есть. Это единственное место, где префикс plugins.<pluginId>. из ранее реально написан.

Доступ к ключам вне вашего пространства имён

Если вам нужен ключ, который не под пространством имён вашего дополнения, импортируйте собственное хранилище svelte-i18n — import { _ } from '@panomc/sdk/utils/language' — и передайте полный путь сами (например, $_('plugins.pano-plugin-shoutbox.widget.title')). _ в вашем main.js — это просто то же хранилище с добавленным префиксом.

Когда мои правки появляются? Мгновенно в режиме разработки, иначе только после пересборки

Теперь, когда у вас есть ключ на экране, вот как ваши правки его доходят до браузера. Это зависит от режима разработки — переключателя, который вы включили в Начале работы (Панель → Настройки платформы, ключ конфигурации development-mode). Если вы его пропустили, сделайте это сначала.

При включённом режиме разработки и вашем дополнении, клонированном в папку plugins/<pluginId>/ вашего работающего сервера Pano (папку, где выполняется сервер, называют экземпляром), Pano читает ваши locales/*.json вживую с диска на каждый запрос. Отредактируйте файл, обновите (F5), и новый текст появляется — без пересборки, ровно как Svelte UI.

При выключенном режиме разработки или в релизном jar работающий сервер читает только копию ваших файлов локализации, упакованную внутри jar — он никогда не смотрит на вашу папку исходников, так что редактирование файла на диске ничего не делает, пока вы не пересоберёте jar:

bash
./gradlew build -Pnoui

(gradlew — скрипт-обёртка Gradle, уже находящийся в вашем проекте — запускайте его из корня проекта, что и означает ведущий ./. -Pnoui пропускает пересборку UI для экономии времени.)

Контрольная точка

Теперь вы должны увидеть jar в build/libs/ (для локальной сборки — pano-plugin-shoutbox-local-build.jar). Скопируйте этот файл в папку plugins/ вашего экземпляра Pano, заменив старый, затем перезапустите Pano — остановите работающий процесс и запустите его снова (смотрите Начало работы). Отключение и повторное включение дополнения в панели не перезагружает содержимое jar.

Зачем два режима?

В продакшене локализации поставляются внутри jar, чтобы дополнение оставалось одним самодостаточным файлом. Режим разработки меняет это на чтение вживую с диска, чтобы вы могли итерировать над переводами без пересборки — по той же причине он переотдаёт ваш Svelte UI на каждый запрос. Смотрите Начало работы для полной таблицы «горячее против пересборки».

Разрешения (только если ваше дополнение определяет разрешения)

Если ваше дополнение определяет разрешение, дайте ему читаемый человеком заголовок и описание, чтобы админы могли понять его на странице Разрешения панели. Пропустите этот раздел, если у вашего дополнения нет разрешений.

Определение (Kotlin):

kotlin
@PermissionDefinition
class ManageShoutboxPermission : PanelPermission("fa-bullhorn")

Строка @PermissionDefinition — это Kotlin-аннотация — маркер, который Pano сканирует, чтобы ваш класс регистрировался автоматически; скопируйте её точно (детали в Разработке бэкенда). fa-bullhorn — имя иконки Font Awesome, показываемой рядом с разрешением в панели.

Перевод (в вашем файле локализации):

json
{
  "permissions": {
    "MANAGE_SHOUTBOX": {
      "title": "Manage Shoutbox",
      "description": "Allows managing shouts shown on the home page."
    }
  }
}

Вы не выбираете ключ MANAGE_SHOUTBOX — он выводится из имени класса по фиксированному правилу: отбросьте суффикс Permission, затем преобразуйте в UPPER_SNAKE_CASE.

  • ManageShoutboxPermissionMANAGE_SHOUTBOX

Контрольная точка

Откройте Панель → Разрешения. Ваша запись теперь должна читаться как Manage Shoutbox с её описанием, вместо сырого ключа MANAGE_SHOUTBOX.

Журналы активности (только если ваше дополнение записывает активность)

Если ваше дополнение записывает админские действия, определите шаблон для каждого под activity-logs. Эти строки появляются на странице Активность панели. Пропустите этот раздел, если ваше дополнение не записывает активность.

Определение (Kotlin):

kotlin
class CreatedShoutLog(
    userId: Long,
    username: String,
    pluginId: String,
    message: String,
) : PluginActivityLog(
    userId = userId,
    pluginId = pluginId,
    details = JsonObject().put("target", message).put("username", username),
)

Для перевода важна только строка details: каждый ключ, который вы put в неё — здесь target и username — становится {placeholder}, который вы можете использовать в тексте. Остальное (userId, pluginId, конструкторная сантехника) — бэкенд-связка, рассмотренная в Разработке бэкенда.

Перевод (в вашем файле локализации):

json
{
  "activity-logs": {
    "CREATED_SHOUT": "<b>{username}</b> posted a shout: {target}."
  }
}

Ключ следует тому же правилу, что и разрешения, но отбрасывает суффикс Log:

  • CreatedShoutLogCREATED_SHOUT

Плейсхолдеры {username} и {target} заполняются из JSON details журнала — класс Kotlin поставляет данные, а эта запись локализации поставляет формулировку.

Можно ли использовать HTML вроде <b> в моих строках?

Здесь — да: <b>...</b> работает, потому что страница Активность отрисовывает именно эти строки как HTML. Это не общее разрешение помещать HTML в каждую строку локализации. В обычных кастомных ключах, показываемых через {$_(...)}, svelte-i18n возвращает сырую строку, и Svelte экранирует её, так что <b> появился бы как буквальный текст <b> на экране. Держите HTML-разметку только в строках журнала активности.

Контрольная точка

Выполните действие, затем откройте Панель → Активность. Отрисованное предложение должно появиться с именем пользователя жирным.

Один полный en-US.json

Примеры выше были фрагментами. Вот как они складываются вместе в единый файл. permissions и activity-logs сидят в корне, рядом с вашими кастомными ключами — никогда не вложены в другой объект. Pano читает эти два раздела напрямую, чтобы связать их со страницами Разрешения и Активность ядра.

json
{
  "widget": {
    "title": "Latest shouts",
    "empty": "No shouts yet — be the first!"
  },
  "welcome-message": "Welcome back, {username}!",
  "permissions": {
    "MANAGE_SHOUTBOX": {
      "title": "Manage Shoutbox",
      "description": "Allows managing shouts shown on the home page."
    }
  },
  "activity-logs": {
    "CREATED_SHOUT": "<b>{username}</b> posted a shout: {target}."
  }
}

Бэкенд (Kotlin)

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

  • Журналы активности. Когда ваше дополнение записывает админское действие, вы вставляете подкласс PluginActivityLog (смотрите Разработку бэкенда). Страница Активность отрисовывает его, ища activity-logs.<KEY> и заполняя плейсхолдеры {...} из полезной нагрузки details журнала. Локализованный текст живёт здесь, в файле локализации — никогда в Kotlin.
  • Почта. Если ваше дополнение отправляет письма, тема и тело приходят из шаблонов Handlebars. (Handlebars — язык шаблонов; помечает переменную для заполнения.) Эти шаблоны не часть ваших locales/*.json — они живут рядом с почтовым кодом вашего дополнения, а не в файлах локализации. Дополнение pano-plugin-auth-guard — эталон отправки шаблонных писем (смотрите Разработку бэкенда).

Одинарные против двойных скобок

Всё, что отрисовывает UI — кастомные ключи, заголовки разрешений, строки журнала активности — проходит через svelte-i18n и интерполируется одинарными скобками: {username}. Единственное место, где вы пишете двойные скобки , — внутри бэкенд-шаблонов писем (Handlebars). Использование неправильного стиля оставляет сырой {...} видимым на экране.

Переопределения админа и новые языки

Одна из приятных черт Pano: администраторы могут редактировать переводы вашего дополнения прямо из панели.

  • Переопределения: админ может изменить любой ключ, который вы определили — новая формулировка без трогания вашего jar.
  • Новые языки: админ может добавить язык, который ваше дополнение не поставляет, переведя ваши ключи в панели.

Эти правки хранятся Pano отдельно и переживают обновления дополнения. Чей текст побеждает? Переопределение админа всегда бьёт текст в вашем jar. Так что если вы измените свою формулировку в новом релизе, кастомизированное значение админа остаётся в силе для ключей, которые он переопределил; ваше новое значение по умолчанию показывается только для ключей, которых админ никогда не трогал. Формулировка владельца сайта никогда не перезаписывается обновлением незаметно.

Если что-то выглядит не так

Вы видитеВероятная причинаИсправление
Сырой путь ключа на экране (plugins.pano-plugin-shoutbox.widget.title)Ключ отсутствует или написан с ошибкой в языке посетителя и в en-US.jsonДобавьте или исправьте ключ в en-US.json (запасной вариант)
Буквальный {username} на экранеВы не передали значение для плейсхолдераПередайте его: $_('welcome-message', { values: { username: ... } })
Буквальный на экранеНеправильный стиль скобок для рендерераUI использует одинарные { }; только почта/Handlebars использует двойные
Правки файла локализации не появляются после обновленияРежим разработки выключен, или вы запускаете релизный jarВключите режим разработки или пересоберите jar и перезапустите Pano
Турецкий/русский текст показывается как искажение в стиле öФайл был сохранён в неправильной кодировкеПересохраните файл как UTF-8

Сломанный JSON-файл ломает все свои ключи, а не один

JSON строг: единственная завершающая запятая, случайный комментарий или незакрытая скобка делает весь файл невалидным. Когда файл не может быть распарсен, ни один из его ключей не разрешается, так что вы увидите сырые пути ключей там, где должны быть эти строки — часто по всей странице сразу. Если страница вдруг показывает пути ключей повсюду, подозревайте сначала синтаксическую ошибку JSON и прогоните файл через любой JSON-валидатор.

Куда дальше