Как сделать мультиязычный сервер Minecraft

В этой инструкции я расскажу о своей библиотеке 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")));
    }
}

Разберёмся, что здесь происходит.

  1. Создаётся контекст переводов. Контекст хранит глобальные параметры и переводы, доступные во всех созданных 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");
    
  2. Создаётся объект перевода с использованием провайдера.

    Провайдеры загружают локализации из внешнего источника. Сейчас доступны три файловых провайдера:

    • 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", "Второе сообщение"); 
    
  3. Получение строки по ключу.

    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}!

Дополнительные возможности библиотеки

  1. Языковые стандарты.

    Polyglot из коробки поддерживает два формата:

    • locale: en_GB, ru_RU, de_DE (по умолчанию)
    • simple: трёхбуквенные ISO 639-1 (eng, rus, ger)

    Установка стандарта:

    context.setLanguageStandard(new LocaleLanguageStandard());
    // или
    context.setLanguageStandard(new SimpleLanguageStandard());
    
  2. Стратегия отсутствующей локализации.

    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)
    
  3. Стратегия отсутствия языка.

    Можно указать, какой язык использовать вместо отсутствующего:

    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)
    );
    
  4. Стандартные языки.

    Модуль 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");
        ...
    
  5. Форматтеры позволяют обрабатывать результат перевода. Пример пост-процессора для удаления 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 — вы можете свободно использовать, изменять и встраивать её в свои проекты.

Буду признателен за :star: на репозиторий и если вы поделитесь библиотекой и этой инструкцией с другими.

4 лайка

Этот пост написан для @Dinner_Bone для его AstraAuction и чтобы популяризировать свою библиотеку. Так что поставьте звездочку на репозиторий пж пж пж
image

лень читать лан пок

ok

как много всего

1 лайк