Skip to content

Making use of injection functionality

BlackyPaw edited this page Aug 1, 2016 · 1 revision

Please note that this guide assumes that you have read and fully understood the previous guide 'Building a simple translated plugin using I18N'. If you have not yet done so we strongly advise you to do so before reading this guide as it will build upon the contents of its predecessor.

The final remark inside the previous guide already hinted at some functionality which allows for writing even shorter code whilst achieving the very same results one may get by using the .translateDirect( ... ) method of a Localizer instance. Said functionality is called "Injection" and will be discussed below.

The term "Injection" describes the automated replacement of text contained in network packets with translated messages. This replacement is performed for each player into his / her native language. Unfortunately, not all platforms provide a standardized way of interfering with Minecraft's protocol in this way and obviously doing so will make code a lot more version-dependant. This is the reason why not all platform adapters support injection functionality and this is also the reason why one must ensure to have to correct version of I18N installed. Currently, only the platform adapter for Spigot has support for automated translation injection which is why everything that follows will assume that you are working with Spigot.

Whereever you would normally specify static text such as when creating a scoreboard, setting a team name, attaching a displayname to an itemstack via its ItemMeta or on a sign you pass a String to the Bukkit API. When using injection you are passing in specially encoded strings which will be decoded by the packet interceptor that will perform the actual text replacement. Thus, you can in fact use injection whereever you would normally pass in static text.

Let's get started with a very basic example:

InjectionAwareI18N<UUID> i18n = I18NSpigotAdapter.getI18N();
PropertyTranslationStorage storage = ...;
// Load languages here:
InjectionAwareLocalizer localizer = i18n.createLocalizer( storage );

// Somewhere else:
player.sendMessage( i18n.inject( "example-message" ) );

[en.properties]

example-message = Hello, world!

In fact almost everything in the above example should mostly be clear if you have read the previous guide. You may have noticed that we are no longer using I18N<UUID> and Localizer as our variable types but InjectionAwareI18N<UUID> and InjectionAwareLocalizer instead. This is I18N's way of notifying you that injection functionality is available: if your platform adapter does not return an I18N> instance but rather a InjectionAwareI18N> you will know just by looking at the method signature that injection is supported on your target platform (the InjectionAware classes are basically just extensions to the base classes I18N and Localizer). A InjectionAwareLocalizer grants you access to yet another function called .inject( ... ) which we only need to pass in the name of the message we would like translate; no locale needed! This is where part of the injection magic comes into play: the packet interceptor which will do the final text replacement for you will auto-detect a player's locale on its own. This simplifies the otherwise bulky .translateDirect( ... ) invocation to a mere specification of the desired message's name. Simple, isn't it? Let's take a look at some other nice examples of injection:

Multilingual Signs:

// This will display text on a sign in each player's native language
Sign sign = ...;
sign.setLine( 0, localizer.inject( "example-message" ) );
sign.update();

Multilingual Scoreboard:

// You can translate team prefixes, suffixes, objectives and even score entries:
Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard();
					
team = scoreboard.registerNewTeam( "team" );
team.setPrefix( this.localizer.inject( "scoreboard.prefix" ) );
team.setSuffix( this.localizer.inject( "scoreboard.suffix" ) );
team.setDisplayName( this.localizer.inject( "scoreboard.team" ) );
					
Objective objective = scoreboard.registerNewObjective( "test", "dummy" );
objective.setDisplaySlot( DisplaySlot.SIDEBAR );
objective.setDisplayName( this.localizer.inject( "scoreboard.objective" ) );
					
Score score = objective.getScore( this.localizer.inject( "scoreboard.entry" ) );
score.setScore( 15 );

player.setScoreboard( scoreboard );

Multilingual Itemstacks:

// Preamble: This will ONLY work when not in Creative mode (!)
// Basically this will create an item stack which - whenever the player's update gets resent -
// will change its display name to the player's current language. If put into a container such
// as a chest, every player will view its display name in his / her native language. Fancy, isn't it?
ItemStack stack = new ItemStack( Material.APPLE, 1 );
ItemMeta  meta  = stack.getItemMeta();
meta.setDisplayName( this.localizer.inject( "item-name" ) );
stack.setItemMeta( meta );
player.getInventory().addItem( stack );

Already hyped a little? Unfortunately there are some drawbacks: Injection will get quite tricky as soon as additional arguments come into play. The main problem lies within the fact that it is not predictable when the underlying network packets will actually be sent to the clients and when all clients has been sent a certain packets. This circumstance prevents I18N from knowing when to release memory allocated for storing the additional arguments - if used. This is the reason why you will not be given a raw String when invoking .inject( ... ) with additional arguments: instead you will be given a InjectionHandle. In order to prevent memory leaks you will have to manually dispose of these handles using .dispose() as soon as you can be sure that all network packets have been sent or until some other condition is reached that guarantees that the arguments do no longer need to be cached, e.g. the server is shutting down, switching states, etc. Also, due to the fact how Mojang decided to implement the Creative mode you should avoid giving players who are playing in Creative mode any ItemStacks with injected names. The whole internals would be too long to explain at this point but just be aware of this issue when using .inject() with ItemStacks.

So what to learn from all this: .inject() is a very useful feature if available and if you do NOT have a need for additional arguments. If you still need additional arguments, try to circumvent to problem using the bulky .translateDirect() version or accept the pain of InjectionHandles (if you only allocate a few you shouldn't run into any problems after all).