-- Colloquy i18n (internationalisation)
-- these functions look up tokens in a specific language, and return
-- them, optionally including some parameters.
--
-- It reads from a table called 'lang' keyed on language name, such
-- as "en-gb" etc. Each of these is a table, keyed on token. The
-- value can then be either a string, or a function. If it is a
-- string, parameters are subsitited into the string. Place holders
-- in the string are $n where n is a number from 0 to 9 for keying
-- purposes. If the value is a function, then the function is called,
-- passing it the values as parameters, and it takes the string the
-- function returns as the text.
-- example:
-- lang["en-gb"] = {
-- hello = "Hello, $0!",
-- connect = "$0 has connected from $1.",
-- goodbye = function(a, b, c)
-- return format("Wibble %s splat %s goo %s.", a, b, c)
-- end
-- }
--
-- then:
-- getmsg(lang["en-gb"], lang["en-gb"], "hello", "Bob") = "Hello, Bob!"
-- getmsg(lang["en-gb"], lang["en-gb"], "connect", "Bob", "localhost") =
-- "Bob has connected from localhost."
-- getmsg(lang["en-gb"], lang["en-gb"], "goodbye", "Moo", "Oink", Cluck") =
-- "Wibble Moo splat Oink goo Cluck"
--
-- Tokens can include the result of expanding another token, such that if
-- the token "moo" is defined as "Hello $0, welcome to ${Oink,Pig Meat}" and
-- the token "Oink" is defined as "I love $0" then "moo" might expand to
-- "Hello Bob, welcome to 'I love Pig Meat'"
--
-- The getmsg function takes a language to look a token up in, a fallback
-- language if the first one doesn't contain the token, the token name,
-- and the parameters for it. If no token is found, it returns a string
-- with each parameter seperated by spaces.
lang = {}
local loaded = {}
unfoundToken = "(none)"
msgCache = {} -- pre-expanded strings, keyed on these values \n seperated:
-- pLang, fLang, token, param1, param2, ... paramN
-- Also contains number -> key mappings for random replacement
-- once the cache reaches a maximum size.
msgCacheSize = 0
msgCacheMax = 256
msgCacheLastRemoved = 0
msgCacheHits = 0
msgCacheMisses = 0
randomseed(0) -- naughty, but handy while debugging
function msgCacheState()
for i = 1, msgCacheSize do
print(i .. ": " .. gsub(msgCache[i], "\n", ".") .. "=" .. msgCache[msgCache[i]] .. "\n")
end
end
function flushMsgCache()
msgCache = {}
msgCacheSize = 0
end
function searchMsgCache(pLang, fLang, token, params)
local key = format("%s\n%s\n%s\n", pLang.NAME, fLang.NAME, token)
for i = 1, getn(params) do
key = key .. format("%s\n", params[i])
end
if( msgCache[key] ) then
msgCacheHits = msgCacheHits + 1
else
msgCacheMisses = msgCacheMisses + 1
end
return msgCache[key]
end
function insertMsgCache(msg, pLang, fLang, token, params)
local pos
if( msgCacheSize >= msgCacheMax ) then
-- we need to randomly select an entry to remove
pos = random(msgCacheMax)
msgCacheLastRemoved = pos
msgCache[msgCache[pos]] = nil -- remove the key/value pair
else
msgCacheSize = msgCacheSize + 1
pos = msgCacheSize
end
local key = format("%s\n%s\n%s\n", pLang.NAME, fLang.NAME, token)
for i = 1, getn(params) do
key = key .. format("%s\n", params[i])
end
msgCache[pos] = key
msgCache[key] = msg
end
local defaultToken = function(...)
local r = "Unknown token '" .. unfoundToken .. "': "
for i = 1, getn(arg) do
r = r .. arg[i]
if( i < getn(arg) ) then
r = r .. " "
end
end
return r
end
evalsubtoken = function(pLang, fLang, ft)
-- takes "${token,params...}"
local t = {}
local f;
if( strsub(ft, 1, 2) == "${" and strsub(ft, -1, -1) == "}" ) then
-- yes, we need to fiddle this.
gsub(ft, "([^%,%$%{%}]+)%,?", function(a) tinsert(%t, a) end)
f = tremove(t, 1)
tinsert(t, 1, f)
tinsert(t, 1, fLang)
tinsert(t, 1, pLang)
return call(getmsg, t)
else
return ft
end
end
local expandDollars = function(ft, arg)
for i = 0, getn(arg or {}) - 1 do
-- we can't use gsub here, because this may contain information from a user,
-- and as such might contain gsub magic characters.
-- search for "$n", where n is the number i, and replace it with arg[i + 1]
local l = strfind(ft, "$" .. i, 1, 1)
if( l ) then
ft = strsub(ft, 1, l - 1) .. arg[i + 1] .. strsub(ft, l + 2, -1)
end
end
return ft
end
local subtext = function(pLang, fLang, ft, arg)
local expandDollars = %expandDollars
ft = gsub(ft, "(%$%{[%w%d%s$,]+%})", function(x) return evalsubtoken(%pLang, %fLang, %expandDollars(x,%arg)) end)
return expandDollars(ft, arg)
end
function getmsg(pLang, fLang, token, ...)
-- pLang: Primary search language
-- fLang: Fallback search language
-- token: Token to search for
-- ... : Parameters for token
pLang = pLang or getlang(colloquy.lang)
fLang = fLang or pLang
local cached = searchMsgCache(pLang, fLang, token, arg)
if( cached ) then return cached end
local ft, cL
-- search pLang for the token, falling back to each of its parents if
-- it's not there.
cL = pLang
repeat
ft = cL[token]
if( not ft and cL.PARENT ) then
cL = getlang(cL.PARENT)
else
cL = nil
end
until( ft or not cL )
-- if we've still not found it, try looking in fLang for it, falling
-- back to each of its parents if it's not there.
if( not ft ) then
cL = fLang
repeat
ft = cL[token]
if( not ft and cL.PARENT ) then
cL = getlang(cL.PARENT)
else
cL = nil
end
until( ft or not cL )
end
-- give up, and give them the default token.
unfoundToken = token
ft = ft or %defaultToken
if( type(ft) == "string" ) then
local r = %subtext(pLang, fLang, ft, arg)
insertMsgCache(r, pLang, fLang, token, arg)
return r
elseif( type(ft) == "function" ) then
ft = call(ft, arg)
ft = %subtext(pLang, fLang, ft)
return ft
else
-- god knows what went wrong. Return the default.
return "unknown token type: " .. call(%defaultToken, arg)
end
end
function getlang(plang)
if( %loaded[plang] ~= nil ) then
return %loaded[plang]
else
-- OK, it's not loaded. Let's load it, after having flushed
-- the msgCache
flushMsgCache()
local n = gsub(plang, "[%/%.]", "")
local f = openfile(colloquy.langs .. n .. ".lua", "r")
if( not f ) then
return nil
else
closefile(f)
dofile(colloquy.langs .. n .. ".lua")
%loaded[n] = lang[n]
return lang[n]
end
end
end
function reload(plang)
%loaded[plang] = nil
getlang(plang)
for i, v in colloquy.connections do
if( type(i) == "table" ) then
if( v.lang.NAME == plang ) then
v.lang = %loaded[plang]
end
end
end
end
function gm(conn, token, ...)
tinsert(arg, 1, token)
tinsert(arg, 1, getlang(colloquy.lang))
tinsert(arg, 1, conn.lang)
return call(getmsg, arg)
end