Many developers tend to forget that LoTRO is an international application supporting three client languages, English, German and French (and to a limited extent, a fourth language, Russian). When developing a plugin for the general public, it is a good idea to separate out all of the text strings. There are two basic approaches to this, one is to create one table and load it with only the currently selected language strings. The other is to use a table that has a separate index for the language. Either way, all references to static text should provide some means of translating them to each of the three client languages. If you aren't comfortable translating your interface, ask someone on the forums to assist you, there are many friendly multi-lingual people that will be glad to help you.
A more troublesome problem arises when saving and reloading data. Since the user has the option to change which language his client is supporting and not only the character sets but the numeric formatting is different between English and German/French, this can cause some serious problems. The first problem of supporting the UTF-8 characters for non-English clients has several solutions, I have chosen to implement a variant of the patch originally published by Vindar. This patch creates a wrapper for the standard Turbine data save and load methods which encodes the data before saving it and decodes it when it is reloaded so that UTF-8 characters are properly saved and loaded. The Vindar Patch can be found on LoTROInterface.com at http://www.lotrointerface.com/downlo...anclients.html
The Vindar Patch does not by itself solve the numeric formatting problem. Fortunately, there is a fairly simple solution to this. Since numeric data is saved as strings via the Vindar Patch, you can coerce the string into the correct format when reloading. Create a global variable to track whether european formatting or english formatting is currently in use:
Code:
euroFormat=(tonumber("1,000")==1); -- will be true if the number is formatted with a comma for decimal place, false otherwise
-- now create a function for automatically converting a number in string format to its correct numeric value
if euroFormat then
function euroNormalize(value)
return tonumber((string.gsub(value,"%.",",")));
end
else
function euroNormalize(value)
return tonumber((string.gsub(value,",",".")));
end
end
then whenever you load a saved numeric value, force it to the current number format. The following example assumes data was stored for the current character
Code:
Opacity=1; -- default
local settings=PatchDataLoad( Turbine.DataScope.Character, "Settings");
if settings~=nil then
Opacity=euroNormalize(settings.Opacity);
end
Note that PatchDataLoad is the wrapper for the PluginData.Load method from the Vindar Patch. Not only will this allow saving and loading data in the DE/FR clients, this has the added benefit of automatically adjusting the numeric format if a client changes from DE/FR to EN or EN to DE/FR between saving and loading the data.
Another internationalization issue that can arise is automatically detecting the current client locale setting. The built in function GetLocale() returns the operating system locale, NOT the current client application locale. Lua authors developed a workaround but Turbine eventually responded by implementing the Turbine.Engine:GetLanguage() method which returns one of the Turbine.Language values:
Turbine.Language.Invalid=0
Turbine.Language.English=2
Turbine.Language.EnglishGB=268 435457 (0x10000001)
Turbine.Language.French=268435 459 (0x10000003)
Turbine.Language.German=268435 460 (0x10000004)
Turbine.Language.Russian=26843 5463 (0x10000007)
The different clients have different chat commands and chat messages - for instance in the french client, you don't use "/plugins load HelloWorld", you would instead use "/plugins charger HelloWorld". This can become a significant issue when creating quickslot alias commands or when using the Chat object to trap incomming messages. You can use the locale test above to determine the running client and then create the appropriate command.
Another issue has to do with creating resource strings. Some people have created their .lua files with UTF8 encoding with success, but I prefer a slightly more "brute force" approach. Anywhere that I need a special character, I simply concatenate the appropriate character codes to generate the desired character. For instance to generate a cedilla (the french "c" with a squiggle under it which indicates a soft c) I use "\195\167" - see the example below. The "" character escapes the character code and the cedilla is character code 195 + character code 167. This is essentially the same as string.char(195)..string.char( 167).
The last issue has to do with the lack of text metrics in LoTRO Lua. Different languages will need different amounts of space for their translation of a string. In most programming languages, you would simply use a function to determine how much space a string requires. Unfortunately, Turbine did not provide us with such a luxury. However, there is a workaround using the Visibility attribute of a bound scroll bar and a non-multiline label control. Set the label control's width to a small number (a good estimate for the 20 point fonts would be 8 pixels per character in the string) and then slowly increase the width of the label (I usually use increments of 8 to save time) until the scrollbar:IsVisible() returns false. Once the scrollbar detects that it no longer needs to be rendered, you will know that you have enough room for the text. Ideally, you create one such label with a bound scrollbar and re-use it as needed to test all strings. You should probably hide the label and the scrollbar off canvass by setting their top properties to a negative value. By obtaining the metrics, you will know if you need to increase the size of a control or possibly indicate that text has been cropped.
This leads me to the last of the "Hello World" samples. This one will remember where it was loaded and will display the message in the correct client language (of course, you'll have to close and restart the client, selecting a different language to test it
)
Code:
import "Turbine"; -- the base Turbine namespace
import "Turbine.UI"; -- this will expose the label control that we will implement
import "Turbine.UI.Lotro"; -- this will expose the standard window that we will implement
locale = "en";
if Turbine.Shell.IsCommand("hilfe") then
locale = "de";
elseif Turbine.Shell.IsCommand("aide") then
locale = "fr";
end
strings={}; -- create a table for the string resources - note, this would usually be generated in a separate .lua file
strings["en"]={}; -- create the English resource string table
strings["en"][1]="Hello World Window"
strings["en"][2]="Hello World"
strings["en"][3]="English"
strings["de"]={}; -- create the German resource string table
strings["de"][1]="Hallo Welt Fenster"
strings["de"][2]="Hallo Welt"
strings["de"][3]="Deutsch"
strings["fr"]={}; -- create the French resource string table
strings["fr"][1]="Fen\195\170tre Bonjour tout le monde"
strings["fr"][2]="Bonjour tout le monde"
strings["fr"][3]="Fran\195\167aise";
function UnloadPlugin()
if HelloWindow~=nil then
local settings={}
settings["top"]=HelloWindow:GetTop();
settings["left"]=HelloWindow:GetLeft();
Turbine.PluginData.Save(Turbine.DataScope.Account, "HelloWorld", settings);
end
end
HelloWindow=Turbine.UI.Lotro.Window(); -- we call the constructor of the standard window object to create an instance
local x,y=Turbine.UI.Display:GetWidth()/2-100,Turbine.UI.Display:GetHeight()/2-100;
local settings=Turbine.PluginData.Load(Turbine.DataScope.Account, "HelloWorld");
if settings~=nil then
if settings["top"]~=nil then y=settings["top"] end
if settings["left"]~=nil then x=settings["left"] end
end
HelloWindow.loaded=false;
HelloWindow:SetWantsUpdates(false);
HelloWindow.Update=function()
if not HelloWindow.loaded then
HelloWindow.loaded=true;
Plugins["HelloWorld"].Unload = function(self,sender,args)
UnloadPlugin();
end
HelloWindow:SetWantsUpdates(false);
end
end
if locale=="fr" then
HelloWindow:SetSize(350,200);
elseif locale=="de" then
HelloWindow:SetSize(260,200);
else
HelloWindow:SetSize(280,200);
end
HelloWindow:SetPosition(x,y);
HelloWindow:SetText(strings[locale][1]); -- assigns the title bar text
HelloWindow.Message=Turbine.UI.Label(); -- create a label control to display our message
HelloWindow.Message:SetParent(HelloWindow); -- sets the label as a child of the main window
HelloWindow.Message:SetSize(HelloWindow:GetWidth()-20,20); -- sets the message size
HelloWindow.Message:SetPosition(10,90); -- places the message control in the vertical middle of the window with a 10 pixel left and right border
HelloWindow.Message:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter); -- centers the text in the message control both horizontally and vertically
HelloWindow.Message:SetText(strings[locale][2]); -- sets the actual message text
HelloWindow:SetWantsUpdates(true);
HelloWindow:SetVisible(true); -- display the window (windows are not visible by default)