This tutorial will walk you through adding localization to your addon.

What is localization?

Localization is the process of making sure that your addon works in all locales (regions of the world where different languages are used), and that people who don’t speak your language can still understand and use your addon.

Sometimes localization is required for an addon to even work in another locale. For example, if you’ve written an addon to play a sound when you mouse over a Peacebloom, your addon needs to know what text is shown in the tooltip in other languages. The tooltip shows “Peacebloom” in your English game client, but it shows a different word or phrase in a Spanish game client, or a Chinese one. If your addon only looks for the word “Peacebloom”, it won’t work in other languages. By localizing your addon, you can make it look for the right word or phrase for the language in which its user is playing WoW.

Other times, your addon might work fine in other locales, but still isn’t very usable, because all the text is in English and someone who only speaks French can’t understand it. For example, if your addon has an options window, localization would mean adding translations of the text shown for those options, so that German and Russian users can understand what the options do just as easily as English users.

If you simply want to copy and paste some code, feel free to do that. If you want to learn more about how it works, read on after the code for details.

Step 1: Create a translation table

Add a file to your addon named “Localization.lua" containing this:

local _, namespace = ...

local L = setmetatable({}, { __index = function(t, k)
	local v = tostring(k)
	rawset(t, k, v)
	return v
end })

namespace.L = L

local LOCALE = GetLocale()

if LOCALE == "enUS" then
	-- The EU English game client also
	-- uses the US English locale code.
return end

if LOCALE == "deDE" then
	-- German translations go here
	L["Hello!"] = "Hallo!"
return end

if LOCALE == "frFR" then
	-- French translations go here
	L["Hello!"] = "Bonjour!"
return end

if LOCALE == "esES" or LOCALE == "esMX" then
	-- Spanish translations go here
	L["Hello!"] = "¡Hola!"
return end

if LOCALE == "ptBR" then
	-- Brazilian Portuguese translations go here
	L["Hello!"] = "Olá!"
	-- Note that the EU Portuguese WoW client also
	-- uses the Brazilian Portuguese locale code.
return end

if LOCALE == "ruRU" then
	-- Russian translations go here
	L["Hello!"] = "Привет!"
return end

if LOCALE == "koKR" then
	-- Korean translations go here
	L["Hello!"] = "안녕하세요!"
return end

if LOCALE == "zhCN" then
	-- Simplified Chinese translations go here
	L["Hello!"] = "您好!"
return end

if LOCALE == "zhTW" then
	-- Traditional Chinese translations go here
	L["Hello!"] = "您好!"
return end

Be sure to save this file with UTF8 encoding, so that non-Latin characters (such as those used in Russian, Korean, and Chinese, and even the accented letters in French, etc.) are preserved properly.

Add the path to this file in your addon’s TOC file, above any other files where localization will be needed.

Step 2: Localize text using the translation table

First, get a reference to the translation table at the top of each file where you will need to use it:

local ADDON_NAME, namespace = ...
local L = namespace.L

Then, anywhere you would show a string of text to the user:

DEFAULT_CHAT_FRAME:AddMessage("Hello!")
MyFontString:SetText("Hello!")
local text = string.format("You have %d cats and a %s.", 5, "dog")

… look up the string in your translation table and show the resulting value instead:

DEFAULT_CHAT_FRAME:AddMessage(L["Hello!"])
MyFontString:SetText(L["Hello!"])
local text = string.format(L["You have %d cats and a %s."], 5, L["dog"])

This way, the same code can show different text for users in different languages. For example, assuming your addon included translations for these strings, the above block of code would show a German user “Hallo!“ and “Du hast 5 Katzen und einen Hund.” — but “¡Hola!“ and “Tienes 5 gatos y un perro.” for a Spanish user.

Step 3: Getting translations

Often, if you simply add a note to your addon’s download page indicating that you’re looking for translations, and for which languages, people who use your addon and speak one of those languages will happily supply you with translations.

There are also people who may not use your addon personally, but are active in a language-specific WoW community. For example, there is a large Chinese WoW community with many people who just like translating addons so that other Chinese players can use them.

Simple words and phrases can often be translated with a machine-translation service like Google Translate, or be constructed by looking at the Blizzard global strings for other locales. (Though, if the complete word or phrase you want to use already exists as a global string, you should just use the global string in your addon, instead of copying and pasting the translated strings into your translation table.)

If your addon is hosted on Curse, you can use their localization app to provide a web-based method for accepting translations. If you are using the Curse packaging system, it can automatically insert new translations into your addon. Otherwise, you can easily export new translations and copy/paste them into your addon.

How it works

The core of this simple localization solution is a metatable, a special feature of Lua tables that allows you to specify custom behaviors on the table.

In this case, we are using a metatable index so that when we look up a key in the translation table that doesn’t exist, instead of returning a nil value, a custom function runs that transforms the given key name into a string, adds the given key name to the translation table with the string as its value, and then returns the string.

The beauty of a metatable is that it does not require any special checks or extra function calls every time you want to show a localized string. The index function only runs when we look up a key that doesn’t already have a value; under normal circumstances, getting a translation requires only a simple table lookup.

1.	local L = setmetatable({}, {
2.    __index = function(t, k)
3.		   local v = tostring(k)
4.		   rawset(t, k, v)
5.		   return v
6.	   end
7. })

Here we can see the translation table code from above. Let’s walk through it line by line and find out how it works.

  1. Line 1:
    • We define the local variable L, which holds the value returned by the setmetatable function, which we call here with two arguments, both of which are tables.
    • The first argument is an empty table, and is the “real” value of L.
    • The second argument is also a table; this is the “metatable” we are setting on the “real” table.
  2. Line 2:
    • We define a key in the metatable, whose name is __index, and whose value is a function that accepts two arguments, which we will call t and k. These names are commonly used in programming to refer to tables and keys.
    • The __index function is a metamethod that Lua will automatically call if we try to look up a value in the “real” table that does not exist there.
    • If we look up a value that does exist, then the metatable is completely ignored, and the value is returned normally.
  3. Line 3:
    • The index function converts the requested key name into a string, and assigns that string to the variable v, a common name for values.
  4. Line 4:
    • The value contained in v is added to the table t (in this case, t == L) under the key k (in this case, the English value of the localized string that was requested).
    • The rawset Lua function does the same thing as t[k] = v would do, except that it bypasses any metamethods related to adding keys or setting values. In this case, our metatable doesn't have any such metamethods, but it’s a good habit to use rawset when working with metatables, to avoid problems.
  5. Line 5:
    • Our function returns the string v. To the code that looked for a value in L this is indistinguishable from finding a pre-existing value at L[k].
  6. Line 6:
    • Our __index metamethod ends.
  7. Line 7:
    • Our metatable ends, and our setmetatable function call ends.
  8. The rest of the code simply checks for the user’s current language, using the [B]GetLocale[/B] WoW API function, and then adds the appropriate translations to the table.

    We don’t need to specifically add the English words and phrases, because those are the defaults. If we look up L["Goodbye!"] while playing WoW in English, the first time causes the __index metamethod to add a “Goodbye!” key to the translation table with the value “Goodbye!”, and any subsequent lookups will find the “Goodbye!” key and its value. If we were to add English words and phrases, it would just be a lot of lines like this:

    L["Hello!"] = "Hello!"
    L["Goodbye!"] = "Goodbye!"
    

    Not very exciting, and not very useful, so we can just skip it and let the metatable magic take care of it.

    This also means that when you add a new word or phrase to your addon, it won’t instantly break the addon for users in other languages. Of course, without actual translations, the new feature you added the word for might not work (because it’s looking for “Peacebloom” instead of “Botão-da-paz” or “Мироцвет”) or users in other languages might not be able to understand what the new text means (because it says “Turn left!” instead of “Bieg links ab!” or “좌회전!”), but at least your addon will still load, and won’t confuse your users with cryptic Lua error messages.