Navigation menu

WoW:Localizing an addon: Difference between revisions

Jump to navigation Jump to search
→‎localization.core.lua: break comment into multiple lines
m (Typo correction)
(→‎localization.core.lua: break comment into multiple lines)
Line 1: Line 1:
The locale is what makes it possible to know what language the interface is using and thus determine which specific code is going to be executed in a localized addon.
{{UIHowTo}}
Blizzard releases many localized versions of the World of Warcraft client, each version translating the game to a particular language. Players in some regions may also install additional [http://www.wow-europe.com/en/downloads/elp/ language packs], allowing them to play World of Warcraft in a desired language.


typically, this is used in a [[API GetLocale|GetLocale]] block:
Depending on the functionality they provide to the player, addons may need to be ''internationalized'' in order to work on clients that use a different locale than the addon author's, and ''localized'' in order to present themselves to the user in the user's desired language.


Code:
== Internationalization ==
    if (GetLocale() == "frFR") then
The [[World of Warcraft API]] stays consistent across all client localizations -- all the normal API functions are available regardless of the client's locale. This should in theory enable any addon to run on any WoW client; however if the addon relies on the names of spells, abilities, NPCs, items, professions, or even the text or format strings of system messages, it may not function properly when those are translated to another language.
        -- things for the french client
    else
        -- for the rest, usually English since it's the default language
    end


== Variables ==
There are three main approaches to solving this problem:
; Ignore the whole problem : Your addon will work for the locale it was written for, somewhat limiting its target audience.
; Localize all text that would be localized by the client. : This would involve maintaining a list of translations to all supported languages of all translatable strings which your addon requires; for instance, you could keep the information that "Frostbolt" is called "Frostblitz" in german, and "Ледяная стрела" in Russian.
: This method has a disadvantage: you'll end up distributing and maintaining massive lists of localizations, only a small subset of which is only ever useful to a particular user.
; Avoid using text that would be localized by the client. : Rather than finding (or using) a {{item|Hearthstone}} by its name, you could instead use its [[item id]]: 6948. Such work-arounds exists for most things that can break an addon after being translated.
{| class="darktable"
! Rather than... !! Use... !! Acquire by...
|-
| Item names || Item IDs || Available as part of the item string; or look up on database sites.
|-
| NPC names || NPC IDs || Can be recovered from unit [[API UnitGUID|GUID]]s; or look up on database sites.
|-
| Quest names || Quest IDs || Part of quest links; or look up on database sites.
|-
| Spell, ability, and<br/>profession names || Spell IDs || ID is part of the spell link, and available on database sites.<br />You can use {{api|GetSpellInfo}}(id) to get the localized name of the spell/ability/profession.
|-
| Zone names || - || Can get names from continent map coordinates; continent IDs are not localized.
|}
You may also be able to recognize system messages without requiring explicit localization of your string patterns by deriving the patterns from the format strings found in FrameXML/GlobalStrings.lua. This is slightly complicated by the fact that some languages may use the "%2$s %1$s" syntax to change the order in which format arguments appear in the string; you may wish to consider using [http://www.wowace.com/addons/libdeformat-3-0/ LibDeformat-3.0] to perform that conversion automatically.


The whole deal doing a successful addon localization is simply using the translated interface texts (as translated ingame) in the language you want to enable for the element you want to use (texts, item names, spells, etc.).
In summary, to avoid your addon malfunctioning when run on a client of another locale, avoid using hard-coded text to identify spells, items or NPCs the names of which would be localized by the client, and be mindful of using the order of return values for some functions that return lists of names (as those tend to be in alphabetical order, which may vary with localization).


So, whenever texts (well almost) are used, they should be replaced by variables which will be set in a GetLocale block in order to give them the correct value for each locale
=== A word on return value ordering ===
Some API functions that return lists or take indices internally sort their data alphabetically. When the client is localized, the order of returned values or correspondence between return values and indices may change.


Currently, it is best to put these in a file separate from the main code (usually name localization.lua for easy reference but any name will do).
Examples of this behavior would include the character's spellbook (where the spells are alphabetically sorted within each school) and the list of zones on a particular continent (zones are alphabetically sorted in the returned list). It would be dangerous to rely on the order of those return values alone.


That file must then be declared in the XML part of the addon, just before the other scripts that compose it, example:
== Localization ==
If a player is using a localized client, they would presumably also want addons to be localized to that language. To accomplish this, all text presented to the user should be translatable; this section discusses strategies for addons to support localizations, and simplify creation of new localizations.


=== Localization in the .toc file ===
A number of [[TOC format#Official tags|official .toc tags]] support localization by appending a locale suffix to a tag name. This mechanism allows addons to present a localized name and description of the addon in the addons interface. The following example illustrates this, by adding a french ("frFR" locale) title and description to the Gatherer addon:
Title: Gatherer
Title-frFR: Gatherer (en Francais, in French)
Notes: Gatherer, displays stuff you gather in your minimap
Notes-frFR: Gatherer, affiche les objets recoltes dans la minicarte


Code:
=== Making an addon localizable ===
    <Script file="localization.lua"/>
To begin with, you need to ensure that every time your addon displays a piece of text to the user, that piece of text ''can'' be localized. Consider the two pieces of code below:
    <Script file="GatherIcons.lua"/>
{|
| valign="top"| <pre>fontString:SetText("Hello World!")</pre>
| valign="top"| <pre>local L = MyLocalizationTable;
fontString:SetText(L["Hello World!"]);</pre>
|}
Code on the left could not be localized without editing the file containing it; while the code in the right wraps the output in a table lookup -- so if the value associated with the "Hello World!" key in MyLocalizationTable was altered, the text output would change. We can use a metatable to allow you to write localizable code without worrying about supplying a default localization:
local function defaultFunc(L, key)
  -- If this function was called, we have no localization for this key.
  -- We could complain loudly to allow localizers to see the error of their ways, but, for now, just return the key as its own localization. This allows you to avoid writing the default localization out explicitly.
  return key;
end
MyLocalizationTable = setmetatable({}, {__index=defaultFunc});
This method allows you to add an easy localization mechanism to your addons, requiring minimal additional code.


If this is a modification on an existing addon and you want to submit it to the author, it's better to use English for the variables names and to prefix them with the addon name (to prevent conflicts with existing variables from the game or from other addons), keep the original strings and put them in variables too so that the author can match them with his original code (don't forget to comment).
=== Adding translations ===
 
Suppose we then wanted to translate the "Hello World!" string from the example above to the client's language. The locale of the client is returned by {{api|GetLocale}}(). To add localizations to the addon, we could add new files along the lines of the following:
Example:
local L = MyLocalizationTable;
 
if GetLocale() == "frFR" then
Code:
  L["Hello World!"] = "Hello France!";
    if (skillName == "Herbalism") then
end
        GatherSkills.herbs = skillRank;
To run correctly, those localization files need to be loaded after MyLocalizationTable is created, but before localization is actually used.
    elseif (skillName == "Mining") then
        GatherSkills.mining = skillRank;
    end
 
becomes:
 
Code:
    if (skillName == GATHERER_TRADE_HERBALISM) then
        GatherSkills.herbs = skillRank;
    elseif (skillName == GATHERER_TRADE_MINING) then
        GatherSkills.mining = skillRank;
    end
 
the GATHERER_TRADE_HERBALISM and GATHERER_TRADE_MINING variables are declared in the localization.lua file as follow:
 
 
Code:
    if( GetLocale() == "frFR" ) then
        -- french part
        GATHERER_TRADE_HERBALISM="Botanique";
        GATHERER_TRADE_MINING="Minage";
    else
        -- default, English
        GATHERER_TRADE_HERBALISM="Herbalism";
        GATHERER_TRADE_MINING="Mining";
    end
 
As you could see in the example, you simply replace the original string between double quotes by the corresponding variable.
 
Warning: If you need to use a localized string in the xml files, you need to keep the double quotes around the variable name.
 
Example:
 
Code:
    [FontString name="bcTM_PopupTitle" inherits="GameFontHighlight" text="TRACKMENU_TOOLTIP_MENU_TITLE"]
 
will allow to use the content of the TRACKMENU_TOOLTIP_MENU_TITLE variable for this xml element.
 
== Grammar and case sensitivity ==
 
This part is usually only useful if the addon has to process sentences (extracted from the chat windows), such as in processing to keep specific parts of the string.
In that case it can be necessary to add a GetLocale block since words are not always ordered in the same manner in various languages.
 
Example:
 
Quote:
    copper vein => veine de cuivre 
 
 
 
another thing linked to this is that since word placement may be different, uppercases may be applied on different words.
 
Examples:
 
Quote:
    Damaged chest => Coffre endommagé
    Small damaged chest => Petit coffre endommagé
 
== Accents, Special Chars and Umlauts ==
 
This is somewhat delicate as wow deals with accents directly as unicode which means 2 things:
 
First, it will not recognize an accent in a localized lua textfile if it's not in unicode and thus will not be able to match it to the accent that can be seen in the game interface.
 
In the previous example, if you try to match "endommagé" with the same string provided by the game, it will fail.
 
Second, since these are unicode char, they're actually coded on several distinct characters (usually 2), which has to be taken into account for string manipulations based on length.
 
The use of a UTF-8 compatible editor is a must if you're dealing with accented characters, otherwise they'll probably be converted to the local charset equivalent which will not match the ingame data.
 
For a list of codes corresponding to various accentuated characters see
http://www.allegro-c.de/unicode/zcodes.htm (page is in German but the table allows to find what's needed quite easily).


For our earlier chest example, a string that will match the ingame data would look like:
=== Character Encoding in World of Warcraft ===
World of Warcraft supports UTF-8 encoded strings of unicode characters; in UTF-8 encoding, ASCII characters can be represented using 1 byte, while most unicode characters typically require multi-byte sequences. You can simply use a UTF-8 enabled text editor, and save unicode-containing .lua files using UTF-8 encoding.


Quote:
Alternatively, you can manually encode the multi-byte sequences required by UTF-8 using Lua escape sequences. The table below shows escape sequences for some european characters:
    Coffre endommag\195\169 
 
Some corresponding codes :
     à : \195\160    è : \195\168    ì : \195\172    ò : \195\178    ù : \195\185
     à : \195\160    è : \195\168    ì : \195\172    ò : \195\178    ù : \195\185
     á : \195\161    é : \195\169    í : \195\173    ó : \195\179    ú : \195\186
     á : \195\161    é : \195\169    í : \195\173    ó : \195\179    ú : \195\186
Line 118: Line 79:
     æ : \195\166                                    ø : \195\184
     æ : \195\166                                    ø : \195\184
     ç : \195\167                                    œ : \197\147
     ç : \195\167                                    œ : \197\147
   
     Ä : \195\132   Ö : \195\150   Ü : \195\156   ß : \195\159
     Ä : \195\132
    Ö : \195\150
    Ü : \195\156
    ß : \195\159


== Unicode ==
If you don't know the UTF-8 sequence for a particular character, you can compute it from the unicode character code, or by [http://www.utf8-chartable.de/ looking up the desired character online].


WOW can recognize unicode characters directly in any .lua file, if this file is saved in unicode format. The exact format to use is "utf-8".
=== Check the GlobalStrings.lua ===
Some strings, particularly those used by Blizzard in FrameXML, are already localized by the client. You can view the full list of such strings by reading the FrameXML/GlobalStrings.lua file.


In order to edit any text file in utf-8, you need a text editor which supports utf-8. By default, the normal encoding for text files is usually iso8859-1. Some older editors might use the ANSI character set instead though.
== An Example ==
The addon below displays a message every time the PLAYER_ENTERING_WORLD event fires. The message is localized according to the client localization; new locales can be added relatively easily by duplicating the localization.de.lua file.


My preference in text file editing goes to [http://www.editpadpro.com/editpadlite.html EditPad Lite]. By default, it edits using the ANSI charset, but it can convert the whole buffer to UTF-8 using the Convert/Unicode/ANSI->UTF-8 function. There are of course lots of other commercial software around which are able to save files in UTF-8 encoding.
<div style="max-height: 400px; margin: 0.5em; border: 0.2em solid black; overflow: auto; padding: 0.2em"><!--
This code in the middle of an article to illustrate a point; but it shouldn't fill everything. put it in a scrollable box -->
=== LocaleDemo.toc ===
## Title: Hello World
## Title-frFR: Hello France
## Title-deDE: Hello Germany
## Description: Displays a message greeting the world
## Description-frFR: Displays a message greeting France
## Description-deDE: Displays a message greeting Germany
localization.core.lua
localization.de.lua
localization.fr.lua
LocaleDemo.lua


If you don't know the UTF-8 sequence, you can convert it from UTF-16 manually by converting it to Binary and using the following table (ref. [http://www.unicode.org/reports/tr26/]):
=== localization.core.lua ===
local addonName, L = ...; -- Let's use the private table passed to every .lua file to store our locale
local function defaultFunc(L, key)
  -- If this function was called, we have no localization for this key.
  -- We could complain loudly to allow localizers to see the error of their ways,
  -- but, for now, just return the key as its own localization. This allows you to
  -- avoid writing the default localization out explicitly.
  return key;
end
setmetatable(L, {__index=defaultFunc});


  UTF-16 Code Unit 1st Byte  2nd Byte  3rd Byte
=== localization.fr.lua ===
  000000000xxxxxxx 0xxxxxxx
  local _, L = ...;
   00000yyyyyxxxxxx  110yyyyy  10xxxxxx
  if GetLocale() == "frFR" then
  zzzzyyyyyyxxxxxx  1110zzzz  10yyyyyy 10xxxxxx
   L["Hello World!"] = "Hello France!";
  end


After you convert the UTF-16 code to Binary (remember it starts out in Hexadecimal), separate the bytes out, then convert them Decimal.  You then type them as sequences followed by "\".  Ex: \195\145
=== localization.de.lua ===
 
local _, L = ...;
The latest Unicode charts can be found here: http://www.unicode.org/charts/ Remember that not all codes are supported by the WoW client.
if GetLocale() == "deDE" then
 
  L["Hello World!"] = "Hello Germany!";
== The single quote case ==
  end
 
Single quote is a bit of a special case, it also has it's equivalent in unicode but this is actually not always the case ingame, has to be checked experimentally.
 
Examples:
    D\195\169couverte d'herbes ==> that match with game data
    Veine d'argent ==> this one used not to match, not true anymore in 1.2.2-4196 though. 
    Nefarian's Lair ==> the apostrophe is actually the UTF8 sequence \239\191\189
 
As a guideline, especially check anything coming from chat windows since that's where the special coding seems (might not be true anymore for 1.2.2-4196 versions and up) to be used most often instead of the standard single quote.
 
Note: the Unicode equivalent of the single quote is encoded with 3 UTF-8 bytes:
    ’ : \226\128\153 (UCS 2019 -- RIGHT SINGLE QUOTATION MARK)
 
It can for instance be found in the frFR locale game data for the Cri d’intimidation warrior ability (at least in 1.8.4).
 
== Check the GlobalStrings.lua ==
For some strings, Blizzard does the job for you, check the contents of the GlobalStrings.lua file (extracted from the Interface.mpq for the basis and from the patch.mpq for the latest up to date). This file contains predefined variables whose values are directly in the client's language.
 
== A word on table ordering ==
 
Most tables in WoW are organized alphabetically which means that the order the entries appear in the table depends on the translation of the individual items that compose it.
 
Example:
zone names in English clients (UK and US) for Kalimdor continent:
    Ashenvale, Azshara, Darkshore, etc.
 
in the french client you have the following order:
    Ashenvale, Azshara, Un'Goro Crater (translated as "Cratère d'Un'Goro"), etc.
 
3rd entry represent a different zone.
 
A static table supplying, say scaling values for the minimap based on zone order, would have to be "localized" in the sense that the table need to be sorted differently according to the client's locale to give correct values.
 
== Spells in the spellbook ==
 
The same holds true for spells in the spellbook; they are in alphabetical order based on the locale.
 
The first 8 spells in a hunter's spellbook:
: This hunter has [[Enchanting]] and [[Herbalist]] profs.
 
<pre>-- Get spells 1-8
for n = 1, 8 do
    SpellsTable[n] = GetSpellName(n)
end</pre>
 
enUS client
<pre>SpellsTable = {
    "Attack", -- [1]
    "Basic Campfire", -- [2]
    "Beast Training", -- [3]
    "Cooking", -- [4]
    "Disenchant", -- [5]
    "Dodge", -- [6]
    "Dual Wield", -- [7]
    "Enchanting", -- [8]
}</pre>
esES client
<pre>SpellsTable = {
    "Adiestramiento de bestias", -- [1] (Beast Training)
    "Atacar", -- [2] (Attack)
    "Buscar hierbas", -- [3] (Find Herbs)
    "Cocina", -- [4] (Cooking)
    "Desencantar", -- [5] (Disenchant)
    "Doble empuñadura", -- [6] (Multi-Shot)
    "Encantamiento", -- [7] (Enchanting)
    "Espíritu de fuego fatuo", -- [8] (Basic Campfire)
}</pre>
 
 
In other words, GetSpellName(n) doesn't return the same value on all clients.
 
== Moderate yourself ==
 
Not everything must have a translation, some things will not work anymore if they are translated. A classical example would be lua objects name (variables usually, functions occasionally) or xml objects (components names defined in the .xml file) that are reconstructed in order to be accessed.
 
Example:
    getglobal(EN_DUR_FRAME..FrameName.."SlotLeft"):Show();
 
Here, we're getting an XML object to display it, the object name itself being rebuilt by appending together the variables EN_DUR_FRAME, FrameName and the "SlotLeft" character string.
 
Since the objects names are hard-coded in the .xml file, a translation in one of the variable used will result in a failure getting the object in localized clients.
 
== Final touch, the .toc file ==
 
The .toc file allows localization by adding entries postfixed by the locale they should appear on:
 
Example:
 
Quote:
## Title: Gatherer
## Title-frFR: Gatherer (en Francais, in French)
## Notes: Gatherer, displays stuff you gather in your minimap
## Notes-frFR: Gatherer, affiche les objets recoltes dans la minicarte  
 
 
 
The -frFR postfix indicate to a French localized client that a translation is available and should be preferred to the default (which most of the time correspond to English), if no "localized" entry exists for a specific client, the default one is displayed.
 
Addendum (patch 1.4.0): in patch 1.4.0 a slightly cryptic comment from Slouken on the US board announce that they left the decimal point to the European value (ie a comma). The impact of this is that table index that uses a strings containing a dot do not work anymore, since that behaviour didn't exist before patch 1.4.0 on European version, caution would be avoid using dots in text index for table.
 
Example: prior to patch this table was fine.
    ["FR 1.4.0"] = ...
 
Post patch, trying to get the index content would result in an error because it was translated by the game engine to "FR 1,4,0" which of course doesn't match the index up there.
 
== Some tips ==
 
Here are some of Sarf's tips that can be used to keep addons localized during maintenance and updates.
 
See the [[Localization Info]]rmation page for lists of known localized strings (zones, channels, etc.)
=== Do not use item names ===
 
Q: How do you refer to items if not by their names?
 
A: By using their item ids.


Thanks to CastOptions.lua for the origin of this code snippet:
=== LocaleDemo.lua ===
 
  local _, L = ...;
  function MyAddOn_ExtractItemID(link)
local f = CreateFrame("Frame");
_, _, id = string.find(link, "Hitem:(.+):%d+:%d+:%d+%\124");
f:RegisterEvent("PLAYER_ENTERING_WORLD");
return id;
f:SetScript("OnEvent", function()
  print(L["Hello World!"]);
  end
  end
</div>


If you are wondering how to get item links, check out the [[World of Warcraft API#Item Functions|Global API - Item Functions]].
== Tips and common pitfalls ==
 
* It is a generally good idea to separate localizable strings from your addon code if you intend to make your addon localizable -- concentrating localization in a few files makes it more approachable.
=== Use predefined, global names for skills ===
* Avoid using names to identify things you can identify by ID instead (this would include items, spells, quests, NPCs, etc). IDs do not need to be localized, saving you work, and allowing your addon to work on a client of a different locale, even if only by using the original alnguage.
 
* Not everything must have a translation, and some things will break if translated. For instance, the names of the World of Warcraft API functions, XML elements, etc, do not need to be translated, since those names are hard-coded in either the client or FrameXML, and do not change with localizations.
By using predefined, global names for skills you can change the skill on-the-fly as well as not have to care what the name actually is!
 
Do remember to keep rank and spell name separate, however. This allows for easy integration with [[API GetSpellName|GetSpellName(spellID, "bookType")]].
 
=== Separate localization code/variables from normal code ===
 
Usually, one file per language / locale is about right.  
The naming convention is often localization<SEPERATOR><LOCALE>.lua
 
Examples (German localization):
 
localization.de.lua
 
or
 
localization_de.lua
 
You can have empty localization file, or you can just put this into them:
 
if ( GetLocale() == "deDE" ) then
    -- todo
end


== Some links ==
== Some links ==
 
* A complete [http://forums.wowace.com/showthread.php?t=15610 thread] where to download and upload updated v3.0.1+ WotLK GlobalStrings for all ten languages actually recognized (enUS enGB esES esMX deDE frFR ruRU zhCN koKR zhTW)  
*A complete [http://forums.wowace.com/showthread.php?t=15610 thread] where to download and upload updated v3.0.1+ WotLK GlobalStrings for all ten languages actually recognized (enUS enGB esES esMX deDE frFR ruRU zhCN koKR zhTW)  
* A github [http://github.com/tekkub/wow-globalstrings/tree/master repository] for storing community provided GlobalStrings.lua
*A github [http://github.com/tekkub/wow-globalstrings/tree/master repository] for storing community provided GlobalStrings.lua
* [http://wow.playhard.ru/Tools/ Tools] for help in russian localization
*The now [http://oldsvn.wowace.com/wowace/trunk/GlobalStrings old GlobalStrings project] on WoWace (outdated, expect a new one soon)
<references/>
*[http://wow.playhard.ru/Tools/ Tools] for help in russian localization
 
{{AlsoSee|[[HOWTO: Localize an addon easily]]}}
[[Category:HOWTOs|Localize an AddOn]]