Coder Social home page Coder Social logo

cubbossa / tinytranslations Goto Github PK

View Code? Open in Web Editor NEW
17.0 2.0 0.0 1.56 MB

A translation framework for minecraft servers

License: MIT License

Java 96.63% HTML 3.37%
bukkit language minecraft paper plugin spigot translation translatable annotation component minimessage yml kyori

tinytranslations's Introduction

TinyTranslations

A translation framework to translate chat messages. This framework builds upon Kyori Components and the MiniMessage format.

Join the Discord Server for support

Wiki

UML Overview

UML Overview

Maven

Install the following repository and dependency in your pom.xml or your build.gradle. Make sure to use the latest version.

<repositories>
    <repository>
        <id>Translations</id>
        <url>https://nexus.leonardbausenwein.de/repository/maven-public/</url>
    </repository>
</repositories>
<dependencies>
    <dependency>
        <groupId>de.cubbossa</groupId>
        <!-- alternatively TinyTranslations-paper -->
        <artifactId>TinyTranslations-bukkit</artifactId>
        <version>[version]</version>
    </dependency>
</dependencies>

Shading

When shading, it is highly recommended to relocate the resource within your plugin. This assures that no other plugin loads outdated Translations classes before your plugin can load the latest classes. Occurring errors would potentially disable your plugin on startup.

<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.4</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <relocations>
                            <relocation>
                                <pattern>de.cubbossa.tinytranslations</pattern>
                                <shadedPattern>[yourpluginpath].libs.tinytranslations</shadedPattern>
                            </relocation>
                        </relocations>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

or with gradle:

tasks.shadowJar {
    minimize()
    relocate 'de.cubbossa.tinytranslations', '[yourpluginpath].libs.tinytranslations'
}

Dependencies (Spigot Libraries)

Your server must find and load the Kyori Adventure classes for Translations to work. Either use TinyTranslations-paper to use the Adventure classes provided by paper or use TinyTranslations-bukkit to include them as shaded dependencies.

Overview

Translations are split into global server-wide translations and local application translations. Translations exist in a treelike structure. The global root Translations instance allows to provide translation strings and styles for all following Translations instances. Each Translation instance can be forked into a child, which then uses all styles and messages of its parent but has some encapsulated translations on its own. Mostly, there will be one global Translations instance and one per plugin.

Example of the Server folder structure and how translations are included:

/Server
  /plugins

    /lang
      global_styles.properties # <--- global styling rules
      en-US.properties # <--- global messages (like the server name)

    /YourPlugin
      /lang
        styles.properties # <--- application only styles
        en-US.properties # <--- application only messages

Styles are a way to create new tag resolvers

# We use opening tags to define simple styles.
text_l="<white>"
text="<gray>"
text_d="<dark-gray>"
# Or slot based styles for more complex patterns
# The list-el example will render "<list-el>abc</list-el>" as "- abc", where the "-" is gray and "abc" is white.
list_el="<gray>- </gray><white>{slot}</white>\n"
# the url tag renders only a short version but opens the whole url on click.
#     https://docs.advntr.dev/minimessage/format.html
# becomes
#     https://docs.advntr.dev/min...tml
# The first occurring '/' after the domain separates the tail, which will show its first and last three letters.
url="<blue><u><click:open_url:"{slot}"><hover:show_text:"Click to open url"><shorten_url>{slot}</shorten_url></hover></click></u></blue>"

Messages can be stored in many ways, like SQL, Yaml or properties. In a properties file, messages would look like this:

some.example.message="<text-light>Some light text <aqua>that can also be styled directly</aqua></text-light>"
some.example.reference="An embedded message: {msg:some.example.message}"

As you can see in the example, messages can be embedded into each other, which allows you to simply create own messages and use them all over the place. Why is this useful? Think of the following example from my plugin:

# c-brand is a style for the main plugin color.
# bg and bg-dark are global styles.
prefix="<primary>PathFinder </primary><bg_dark>| </bg_dark><bg>"
other.message="{msg:prefix}Hello."

Prefix is not a message that is enforced by the plugin. Users can simply create the entry, and it will be loaded by the plugin and embedded in other messages.

Setup

import de.cubbossa.tinytranslations.MessageBuilder;

class Messages {
    public static final Message PREFIX = new MessageBuilder("prefix")
            .withDefault("<gradient:#ff0000:#ffff00:#ff0000>My Awesome Plugin</gradient>")
            .build();
    public static final Message NO_PERM = new MessageBuilder("no_perm")
            .withDefault("<prefix_negative>No permissions!</prefix_negative>")
            .build();
}


class ExamplePlugin extends JavaPlugin {

    Translations translations;

    public void onEnable() {
        // create a Translations instance for your plugin 
        translations = BukkitTinyTranslations.application(this);

        // define the storage types for your plugins locale
        translations.setMessageStorage(new PropertiesMessageStorage(new File(getDataFolder(), "/lang/")));
        translations.setStyleStorage(new PropertiesStyleStorage(new File(getDataFolder(), "/lang/styles.properties")));

        // register all your messages to your Translations instance
        // a message cannot be translated without a Translations instance, which works as
        // messageTranslator.
        translations.addMessages(messageA, messageB, messageC);
        translations.addMessage(messageD);
        // just load all public static final messages declared in Messages.class
        translations.addMessages(TinyTranslations.messageFieldsFromClass(Messages.class));

        // They will not overwrite pre-existing values.
        // You only need to save values that you assigned programmatically, like from a
        // message builder. You can also create a de.properties resource and save it as file instead.
        // Then there is no need to write the german defaults to file here.
        translations.saveLocale(Locale.ENGLISH);
        translations.saveLocale(Locale.GERMAN);

        // load all styles and locales from file. This happens for all parent translations,
        // so all changes to the global styles and translations will apply too.
        translations.loadStyles();
        translations.loadLocales();
    }

    public void onDisable() {
        // close open Translations instance
        translations.close();
    }
}

Messages

Messages can easily be set up as statics. Locale files can be generated from a class that holds Messages as static members.

A fully defined message:

public static final Message ERR_NO_PLAYER = new MessageBuilder("error.must_be_player")
        .withDefault("<prefix_negative>No player found: '{input}'.</prefix_negative>")
        .withTranslation(Locale.GERMAN, "<prefix_negative>Spieler nicht gefunden: '{input}'.</prefix_negative>")
        .withComment("Used to indicate if no player was found - who would have thought :P")
        .withPlaceholder("input", "The used input that was supposed to be a playername.")
        .build();

Or maybe just

public static final Message ERR_NO_PERM = new MessageBuilder("error.no_perm")
        .withDefault("<prefix_negative>No permission!</prefix_negative>")
        .build();

Add Messages to Translations

Don't forget to register all messages to your application!!

translations.addMessage(Messages.ERR_NO_PLAYER);
translations.addMessage(Messages.ERR_NO_PERM);
// or
translations.addMessages(Messages.ERR_NO_PLAYER, Messages.ERR_NO_PERM);
// or just:
translations.addMessages(TranslationsFramework.messageFieldsFromClass(Messages.class));

Build Messages from Translations directly

If you use your translations instance to create a message, it will automatically be added to your translations.

ERR_NO_PERM = translations.message("error.no_perm");

ERR_NO_PERM = translations.messageBuilder("error.no_perm")
        .withDefault("<prefix_negative>No permission!</prefix_negative>")
        .build();

Message as Component

Messages are implementations of the TranslatableComponent interface, which means that they automatically render in the player client locale.

// on Paper server:
player.sendMessage(Messages.ERR_NO_PLAYER);
player.sendMessage(Messages.ERR_NO_PLAYER.insertString("input", args[0]));

// on Spigot server:
BukkitTinyTranslations.sendMessage(player, Messages.ERR_NO_PLAYER);
BukkitTinyTranslations.sendMessageIfNotEmpty(player, Messages.ERR_NO_PLAYER);

Other Formats

You can also format a Message into any other format with like so:

Message ERR_NO_PERM = new MessageBuilder("err.no_perm")
        .withDefault("<prefix_negative>No permissions!</prefix_negative>")
        .build();

String s = ERR_NO_PERM.toString(MessageEncoding.LEGACY_PARAGRAPH);
// -> §cNo permissions!

String s = ERR_NO_PERM.toString(MessageEncoding.LEGACY_AMPERSAND);
// -> &cNo permissions!

String s = ERR_NO_PERM.toString(MessageEncoding.PLAIN);
// -> No permissions!

String s = ERR_NO_PERM.toString(MessageEncoding.NBT);
// -> {"text":"No permissions!","color":"red"}

tinytranslations's People

Contributors

bale1017 avatar cubbossa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

tinytranslations's Issues

Common Global Message Provider

Each shaded TinyTranslation will create its own GlobalTranslator instance. This is okay, but it would be very good if actually the highest version of those instances serves as global and the other globals connect to it via any communication system. They cannot simply reference each other because they technically are different classes. They should, however, be always all be able to parse the latest syntax. What happens in the plugin directory does not be up to date

NullPointerException in YmlMessageStorage loading

java.lang.NullPointerException: Cannot invoke "Object.toString()" because "o" is null
        at de.cubbossa.tinytranslations.storage.yml.YamlMessageStorage.lambda$readMessages$0(YamlMessageStorage.java:65) ~[PvPCooldown-1.0.jar:?]
        at java.util.HashMap.forEach(HashMap.java:1429) ~[?:?]
        at de.cubbossa.tinytranslations.storage.yml.YamlMessageStorage.readMessages(YamlMessageStorage.java:59) ~[PvPCooldown-1.0.jar:?]
        at de.cubbossa.tinytranslations.MessageTranslatorImpl.loadLocale(MessageTranslatorImpl.java:337) ~[PvPCooldown-1.0.jar:?]

Should check if value was not null when loading yml mapping

Context Sensitive Tags

<darker> <brighter> <invert> are all tags that could potentially be context sensitive so they don't only apply to explicit color tags within their content.

// currently only darkens b, could also darken a. Must therefore know about its parent
<darker> a <red>b</red></darker>

With MiniMessage it is not possible to gain information about parent components and even NanoMessage compiler could include the functionality due to tag resolvers.

Maybe remember regions and replace them with darkened components in the end 🤷

Precompilation

For now, the framework is limited to parse <gray>{slot}</gray> whenever a style with this nanomessage representation is being used. This is not the most performant way, since static parts of the string are being parsed over and over again.

To reduce redundant parsing, static messages could be cached. Also, maybe self-closing tags could be parsed into "future components" that need applying values to complete. Then, static components around placeholders could be parsed once.

page <page/>/<pages/>
became

text("page ").append(placeholder(String key, Queue<String>)).append(text("/")).append(placeholder(String key, Queue<String>));

Then traverse and for each placeholder insert appropriate value.

Framework Usage

Can you please code one plugin, or anything to give more explanation how to exactly use this framework, i know Devotions already exists, but i need some simple & straight forward example to help me out easily integrate this in my plugin.

Object Resolving locally

The ObjectTagResolverMap is an attribute of the global NanoMessage. instead, every translations should have its way of converting objects while also inheriting resolvers from parent

Object Resolving with inheritance

If adding a class that extends collection as resolving, "size" property could be resolved from collection. So actually the loop should collect all implemented classes and resolve in the correct order to allow overriding.

List Slot to apply element format

Instead of making lists a placeholder, they could be a style that defines how to render one element.
Maybe even a default name for the element, like el or element. Resolving like in .insertObject

<my_items:'\n'><gray>- {el:amount}x {el:material}</gray></my_items>

File collisions in shaded jars

When the plugin provides en.properties and global does too, both files collide in the target jar. Therefore, global files should have prefix

Whitespaces in Tags, Placeholders and Choices

There is need for a strict handling of whitespace within tags.
I will make the parser follow these rules (_ meaning possible whitespace)

<tag(_:_attr_)*></tag>
<selfclosing(_:_attr_)*/>
{_ph(_:_attr_)*_}
{_choice_?(_:_opt_)+_}

Ownership for static Message fields

Since Message objects are unmodifiable, constants like

public static final Message PREFIX = Message.message("prefix");

could match every MessageTranslator instance. A mechanism that binds these fields to a Translator to create unique translation keys would improve the work with static fields.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.