В этой инструкции я расскажу о своей библиотеке Poyglot для локализации строк на Java и покажу, как сделать Minecraft-плагин мультиязычным.
Базовое введение
Добавьте в проект репозиторий:
maven {
name = "densyRepositorySnapshots"
url = uri("https://repo.densy.org/snapshots")
}
Добавьте в проект зависимости:
implementation "org.densy.polyglot:api:1.1.2-SNAPSHOT"
implementation "org.densy.polyglot:core:1.1.2-SNAPSHOT"
Начнём с основ библиотеки.
import org.densy.polyglot.api.Translation;
import org.densy.polyglot.api.context.TranslationContext;
import org.densy.polyglot.core.context.BaseTranslationContext;
import org.densy.polyglot.core.parameter.TrParameters;
import org.densy.polyglot.core.provider.YamlFileProvider;
import java.io.File;
import static org.densy.polyglot.common.language.LocaleLanguage.*;
public class Main {
public static void main(String[] args) {
TranslationContext context = new BaseTranslationContext();
Translation translation = context.createTranslation(new YamlFileProvider(
new File("lang"), context.getLanguageStandard()
));
System.out.println(translation.translate(EN_GB, "message.key", TrParameters.keyed().put("key", "Value")));
}
}
Разберёмся, что здесь происходит.
-
Создаётся контекст переводов. Контекст хранит глобальные параметры и переводы, доступные во всех созданных Translation.
Например, можно добавить глобальные переводы:common.yes=Да common.no=Нети использовать их в других строках:
message.key={common.yes} или {common.no}Результат будет:
Да или Нет.Пример добавления глобальных значений:
context.addGlobalTranslation(EN_GB, "common.yes", "No"); context.addGlobalTranslation(EN_GB, "common.yes", "Нет"); context.addGlobalParameter("version", "1.1.2-SNAPSHOT"); -
Создаётся объект перевода с использованием провайдера.
Провайдеры загружают локализации из внешнего источника. Сейчас доступны три файловых провайдера:
YamlFileProvider- Провайдер переводов из.yamlфайлов.JsonFileProvider- Провайдер переводов из.jsonфайлов.PropertiesFileProvider- Провайдер для key=Value файлов (.properties,.props,.iniи другие).
Пример структуры:
lang/ en_GB.properties ru_RU.properties ua_UK.propertiesЕсли загрузка из файлов не нужна, можно создать пустой перевод и заполнить его вручную:
Translation translation = context.createTranslation(); // Пустой перевод, в который мы теперь можем добавить переводы translation.addTranslation(EN_GB, "messages.first", "First message"); translation.addTranslation(EN_GB, "messages.second", "Second message"); translation.addTranslation(RU_RU, "messages.first", "Первое сообщение"); translation.addTranslation(RU_RU, "messages.second", "Второе сообщение"); -
Получение строки по ключу.
translation.translate(EN_GB, "message.key", TrParameters.keyed().put("key", "Value")); // Передаем параметры ключ=Значение translation.translate(EN_GB, "message.key", "John Doe", 25); // Передаем параметры массивом translation.translate(EN_US, "message.key"); // Без параметров
Этого достаточно, чтобы перейти к созданию мультиязычного плагина.
Создание мультиязычного плагина
Пример простого плагина с локализацией:
package com.example;
import cn.nukkit.Player;
import cn.nukkit.event.EventHandler;
import cn.nukkit.event.Listener;
import cn.nukkit.event.player.PlayerJoinEvent;
import cn.nukkit.event.player.PlayerJumpEvent;
import cn.nukkit.plugin.PluginBase;
import org.densy.polyglot.api.Translation;
import org.densy.polyglot.api.context.TranslationContext;
import org.densy.polyglot.api.parameter.TranslationParameters;
import org.densy.polyglot.core.context.BaseTranslationContext;
import org.densy.polyglot.core.language.BaseLanguage;
import org.densy.polyglot.core.parameter.TrParameters;
import org.densy.polyglot.core.provider.PropertiesFileProvider;
import java.io.File;
public class Main extends PluginBase implements Listener {
private Translation translation;
@Override
public void onLoad() {
// Сохраняем языковые файлы
// Я рекомендую в папке с переводами сделать файл, languages.json
// или тому подобный, в котором будут прописаны языки для сохранения
this.saveResource("en_GB.properties");
this.saveResource("ru_RU.properties");
}
@Override
public void onEnable() {
TranslationContext context = new BaseTranslationContext();
this.translation = context.createTranslation(new PropertiesFileProvider(
new File("lang"), context.getLanguageStandard()
));
this.getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage(tr(player, "messages.hello", TrParameters.keyed().put("name", player.getName())));
}
@EventHandler
public void onPlayerJump(PlayerJumpEvent event) {
Player player = event.getPlayer();
player.sendMessage(tr(player, "messages.jump"));
}
private String tr(Player player, String key) {
return tr(player, key, null);
}
private String tr(Player player, String key, TranslationParameters parameters) {
return translation.translate(BaseLanguage.parseLanguage(player.getLoginChainData().getLanguageCode()), key, parameters);
}
}
Теперь плагин отправляет игроку сообщения на его языке при входе и прыжке. Чтобы сделать код удобнее, вынесем локализацию в отдельный класс.
Создадим класс Lang:
package com.example.util;
import cn.nukkit.Player;
import cn.nukkit.command.CommandSender;
import cn.nukkit.plugin.Plugin;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.densy.polyglot.api.Translation;
import org.densy.polyglot.api.context.TranslationContext;
import org.densy.polyglot.api.language.Language;
import org.densy.polyglot.api.parameter.TranslationParameters;
import org.densy.polyglot.core.context.BaseTranslationContext;
import org.densy.polyglot.core.language.BaseLanguage;
import org.densy.polyglot.core.parameter.KeyedTranslationParameters;
import org.densy.polyglot.core.parameter.TrParameters;
import org.densy.polyglot.core.provider.PropertiesFileProvider;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Objects;
public final class Lang {
private static Language defaultLanguage;
private static Translation translation;
/**
* Инициализируем переводы.
*/
public static void init(Plugin plugin) {
defaultLanguage = BaseLanguage.parseLanguage(plugin.getConfig().getString("language"));
// Создаем папку для переводов
File folder = new File(plugin.getDataFolder() + "/lang/");
if (!folder.exists() && !folder.mkdirs()) {
throw new IllegalArgumentException("Failed to create folder " + folder.getAbsolutePath());
}
// Сохраняем файлы переводов указанные в languages.json в таком формате:
// [
// "en_GB",
// "ru_RU"
// ]
try (var stream = Lang.class.getResourceAsStream("/lang/languages.json");
var reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(stream, "Resource /lang/languages.json not found")))) {
List<String> languages = new Gson().fromJson(reader, new TypeToken<List<String>>() {
}.getType());
for (String language : languages) {
plugin.saveResource("lang/" + language + ".properties");
}
} catch (IOException e) {
throw new RuntimeException("Failed to save language files");
}
TranslationContext context = new BaseTranslationContext();
translation = context.createTranslation(new PropertiesFileProvider(folder, context.getLanguageStandard()));
translation.setDefaultLanguage(defaultLanguage);
}
/**
* Переводим строку для языка по умолчанию без параметров.
*/
public static String tr(String key) {
return translation.translate(defaultLanguage, key);
}
/**
* Переводим строку для языка по умолчанию с параметрами.
*/
public static String tr(String key, TranslationParameters parameters) {
return translation.translate(defaultLanguage, key, parameters);
}
/**
* Переводим строку для языка игрока или другого отправителя.
*/
public static String tr(CommandSender sender, String key, TranslationParameters parameters) {
if (sender instanceof Player player) {
return translation.translate(BaseLanguage.parseLanguage(player.getLoginChainData().getLanguageCode()), key, parameters);
} else {
return translation.translate(defaultLanguage, key, parameters);
}
}
/**
* Переводим строку для языка игрока или другого отправителя с массивом параметров.
*/
public static String tr(CommandSender sender, String key, Object... parameters) {
return tr(sender, key, TrParameters.array(parameters));
}
}
Инициализируем его в onEnable и используем в плагине:
package com.example;
import cn.nukkit.Player;
import cn.nukkit.event.EventHandler;
import cn.nukkit.event.Listener;
import cn.nukkit.event.player.PlayerJoinEvent;
import cn.nukkit.event.player.PlayerJumpEvent;
import cn.nukkit.plugin.PluginBase;
import com.example.polyglot.util.Lang;
import org.densy.polyglot.core.parameter.TrParameters;
public class Main extends PluginBase implements Listener {
@Override
public void onLoad() {
this.saveDefaultConfig();
}
@Override
public void onEnable() {
Lang.init(this);
this.getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage(Lang.tr(player, "messages.hello", TrParameters.keyed().put("name", player.getName())));
}
@EventHandler
public void onPlayerJump(PlayerJumpEvent event) {
Player player = event.getPlayer();
player.sendMessage(Lang.tr(player, "messages.jump", player.getName()));
}
}
Структура ресурсов:
resources/
lang/
en_GB.properties
ru_RU.properties
languages.json
config.yml
plugin.yml
Пример переводов:
messages.hello=Привет, {name}!
messages.jump=Ты подпрыгнул, {0}!
Дополнительные возможности библиотеки
-
Языковые стандарты.
Polyglot из коробки поддерживает два формата:
locale:en_GB,ru_RU,de_DE(по умолчанию)simple: трёхбуквенные ISO 639-1 (eng,rus,ger)
Установка стандарта:
context.setLanguageStandard(new LocaleLanguageStandard()); // или context.setLanguageStandard(new SimpleLanguageStandard()); -
Стратегия отсутствующей локализации.
translation.setFallbackStrategy(FallbackStrategy.prefix("prefix.")); // Результат: prefix.missing.key translation.setFallbackStrategy(FallbackStrategy.prefix(".suffix")); // Результат: missing.key.suffix translation.setFallbackStrategy( missing -> "Missing locale (" + missing + ")" ); // Результат: Missing locale (missing.key) -
Стратегия отсутствия языка.
Можно указать, какой язык использовать вместо отсутствующего:
translation.setLanguageStrategy(LanguageStrategy.mappingsBuilder() .put(LocaleLanguage.UK_UA, LocaleLanguage.RU_RU) .put(LocaleLanguage.EN_US, LocaleLanguage.EN_GB) .build());Или задать язык по умолчанию:
translation.setLanguageStrategy( LanguageStrategy.defaultResult(LocaleLanguage.EN_GB) ); -
Стандартные языки.
Модуль common содержит готовые наборы языков:
package org.densy.polyglot.common.language; import org.densy.polyglot.core.language.BaseLanguage; /** * Built-in languages in standard locale format. */ public interface LocaleLanguage { BaseLanguage EN_US = new BaseLanguage("en", "US"); BaseLanguage EN_GB = new BaseLanguage("en", "GB"); BaseLanguage DE_DE = new BaseLanguage("de", "DE"); BaseLanguage ES_ES = new BaseLanguage("es", "ES"); BaseLanguage ES_MX = new BaseLanguage("es", "MX"); ...package org.densy.polyglot.common.language; import org.densy.polyglot.core.language.BaseLanguage; /** * Built-in languages in a simple format. */ public interface SimpleLanguage { BaseLanguage ENG = new BaseLanguage("eng"); BaseLanguage GER = new BaseLanguage("ger"); BaseLanguage SPA = new BaseLanguage("spa"); BaseLanguage FRA = new BaseLanguage("fra"); BaseLanguage ITA = new BaseLanguage("ita"); ... -
Форматтеры позволяют обрабатывать результат перевода. Пример пост-процессора для удаления escape-символов:
package org.densy.polyglot.core.formatter; import org.densy.polyglot.api.formatter.context.TranslationFormatContext; import org.densy.polyglot.api.formatter.TranslationFormatter; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Post-processor formatter that removes escape sequences. * This should be the LAST formatter in the chain. * <p> * Converts: * - \{param} -> {param} * - \\{param} -> \{param} * - \\\{param} -> \{param} * - \\\\{param} -> \\{param} * <p> * In general: removes one backslash from each pair/sequence of backslashes */ public class EscapeSequenceFormatter implements TranslationFormatter { private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\\\+"); @Override public String format(String text, TranslationFormatContext context) { Matcher matcher = ESCAPE_PATTERN.matcher(text); StringBuilder result = new StringBuilder(); while (matcher.find()) { String backslashes = matcher.group(0); int count = backslashes.length(); // Remove one slash from each pair; if the number is odd, round down. // \\ -> \ // \\\ -> \ // \\\\ -> \\ int resultCount = count / 2; String replacement = "\\".repeat(resultCount); matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); } matcher.appendTail(result); return result.toString(); } }Подключение:
translation.addFormatter(new EscapeSequenceFormatter());
Исходный код и использование
Исходный код: GitHub - DensyDev/Polyglot: Polyglot is an advanced and multifunctional library for localizing strings
Библиотека распространяется под лицензией MIT — вы можете свободно использовать, изменять и встраивать её в свои проекты.
Буду признателен за
на репозиторий и если вы поделитесь библиотекой и этой инструкцией с другими.
