343 lines
14 KiB
Lua

-- -------------------------------------------------------------------------------------------------------------------------------------------------------------
-- RAILUALIB TRANSLATION MODULE
-- Requests and organizes translations for localised strings.
-- dependencies
local event = require("__RaiLuaLib__.lualib.event")
local migration = require("__RaiLuaLib__.lualib.migration")
-- locals
local math_floor = math.floor
local string_gsub = string.gsub
local string_lower = string.lower
local table_sort = table.sort
-- object
local translation = {}
-- internal events
translation.start_event = event.get_id("translation_start")
translation.finish_event = event.get_id("translation_finish")
translation.canceled_event = event.get_id("translation_canceled")
-- converts a localised string into a format readable by the API
-- basically just spits out the table in string form
local function serialise_localised_string(t)
local output = "{"
if type(t) == "string" then return t end
for _,v in pairs(t) do
if type(v) == "table" then
output = output..serialise_localised_string(v)
else
output = output.."\""..v.."\", "
end
end
output = string_gsub(output, ", $", "").."}"
return output
end
-- translate 50 entries per tick
local function translate_batch(e)
local __translation = global.__lualib.translation
local iterations = math_floor(50 / __translation.active_translations_count)
if iterations < 1 then iterations = 1 end
local players = __translation.players
-- for each player that is doing a translation
for _,pi in ipairs(e.registered_players) do
local pt = players[pi]
local request_translation = game.get_player(pi).request_translation
local next_index = pt.next_index
local finish_index = next_index + iterations
local strings = pt.strings
local strings_len = pt.strings_len
-- request translations for the next n strings
for i=next_index,finish_index do
if i > strings_len then
-- deregister this event for this player
event.disable("translation_translate_batch", pi)
goto continue
end
request_translation(strings[i])
end
-- update next index
pt.next_index = finish_index + 1
::continue::
end
end
-- sorts a translated string into its appropriate dictionaries
local function sort_translated_string(e)
local __translation = global.__lualib.translation
local player_data = __translation.players[e.player_index]
local active_translations = player_data.active_translations
local localised = e.localised_string
local serialised = serialise_localised_string(localised)
-- check if the string actually exists in the registry. if it does not, then another mod requested this translation as well and it was already sorted.
local string_registry = player_data.string_registry[serialised]
if string_registry then
-- for each dictionary that requested this string
for dictionary_name, internal_names in pairs(string_registry) do
local data = active_translations[dictionary_name]
-- extra sanity check
if data then
-- remove from registry index
data.registry_index[serialised] = nil
data.registry_index_size = data.registry_index_size - 1
-- check if the string was successfully translated
local success = e.translated
local result = e.result
local include_failed_translations = data.include_failed_translations
if not include_failed_translations and (not success or result == "") then
log(dictionary_name..": key "..serialised.." was not successfully translated, and will not be included in the output.")
else
-- do this only if the result will be the same for all internal names
if success then
-- add to lookup table
data.lookup[string_lower(result)] = internal_names
-- add to sorted results table
data.sorted_translations[#data.sorted_translations+1] = data.lowercase_sorted_translations and string_lower(result) or result
end
-- for every internal name that this string applies do
for i=1,#internal_names do
local internal = internal_names[i]
-- set result to internal name if the translation failed and the option is active
if not success and include_failed_translations then
result = internal
-- add to lookup and sorted_translations tables here, as each iteration will have a different name
local lookup = data.lookup[result]
if lookup then
lookup[#lookup+1] = internal
else
data.lookup[result] = {internal}
end
data.sorted_translations[#data.sorted_translations+1] = result
end
-- add to translations table
if data.translations[internal] then
error("Duplicate key ["..internal.."] in dictionary: "..dictionary_name)
else
data.translations[internal] = result
end
end
end
-- check if this dictionary has finished translation
if data.registry_index_size == 0 then
-- sort sorted results table
table_sort(data.sorted_translations)
-- decrement active translation counters
__translation.active_translations_count = __translation.active_translations_count - 1
player_data.active_translations_count = player_data.active_translations_count - 1
-- raise finished event with the output tables
event.raise(translation.finish_event, {player_index=e.player_index, dictionary_name=dictionary_name, lookup=data.lookup,
sorted_translations=data.sorted_translations, translations=data.translations})
-- remove from active translations table
player_data.active_translations[dictionary_name] = nil
-- check if the player is done translating
if player_data.active_translations_count == 0 then
-- deregister events from this player
event.disable("translation_translate_batch", e.player_index)
event.disable("translation_sort_result", e.player_index)
-- remove player's translation table
__translation.players[e.player_index] = nil
end
end
else
error("Data for dictionary: "..dictionary_name.." for player: "..e.player_index.." does not exist!")
end
end
-- remove from string registry
player_data.string_registry[serialised] = nil
end
end
translation.serialise_localised_string = serialise_localised_string
-- begin translating strings
function translation.start(player, dictionary_name, data, options)
options = options or {}
local __translation = global.__lualib.translation
local player_data = __translation.players[player.index]
-- create player table if it doesn't exist
if not player_data then
__translation.players[player.index] = {
active_translations = {}, -- contains data for each dictionary that is being translated
active_translations_count = 0, -- count of translations that this player is performing
next_index = 1, -- index of the next string to be translated
string_registry = {}, -- contains data on where a translation should be placed
strings = {}, -- contains the actual localised string objects to be translated
strings_len = 0 -- length of the strings table, for use in on_tick to avoid extraneous logic
}
player_data = __translation.players[player.index]
-- reset if the translation is already running
elseif player_data.active_translations[dictionary_name] then
log("Cancelling and restarting translation of "..dictionary_name.." for "..player.name)
translation.cancel(player, dictionary_name)
end
-- create local references
local string_registry = player_data.string_registry
local strings = player_data.strings
local registry_index = {} -- contains a table of keys that represent all the places in the string index that this dictionary has a place in
-- add data to translation tables
for i=1,#data do
local t = data[i]
local localised = t.localised
local serialised = serialise_localised_string(localised)
-- check for this string in the global string registry
local registry_entry = string_registry[serialised]
if registry_entry then
-- check if this dictionary has been added to this registry yet
if registry_index[serialised] then
local our_registry = registry_entry[dictionary_name]
our_registry[#our_registry+1] = t.internal
else
registry_index[serialised] = true
registry_entry[dictionary_name] = {t.internal}
end
else
-- this is a new string, so add it to the strings table and create the registry
strings[#strings+1] = localised
string_registry[serialised] = {[dictionary_name]={t.internal}}
registry_index[serialised] = true
end
end
-- set new strings table length
player_data.strings_len = #strings
-- add this dictionary"s data to the player"s table
player_data.active_translations[dictionary_name] = {
-- string registry index
registry_index = registry_index,
registry_index_size = table_size(registry_index), -- used to determine when the translation has finished
-- options
lowercase_sorted_translations = options.lowercase_sorted_translations,
include_failed_translations = options.include_failed_translations,
-- output
lookup = {},
sorted_translations = {},
translations = {}
}
-- increment active translations counters, register on_tick and sort result handlers
__translation.active_translations_count = __translation.active_translations_count + 1
player_data.active_translations_count = player_data.active_translations_count + 1
-- raise translation start event
event.raise(translation.start_event, {player_index=player.index, dictionary_name=dictionary_name})
-- register events, if needed
event.enable("translation_translate_batch", player.index)
event.enable("translation_sort_result", player.index)
end
-- cancel a translation
function translation.cancel(player, dictionary_name)
local __translation = global.__lualib.translation
local player_data = __translation.players[player.index]
local translation_data = player_data.active_translations[dictionary_name]
if not translation_data then
log("Tried to cancel a translation that isn't running!")
return
end
log("Canceling translation of ["..dictionary_name.."] for player ["..player.name.."]")
-- remove this dictionary from the string registry
local string_registry = player_data.string_registry
for key,_ in pairs(translation_data.registry_index) do
local key_registry = string_registry[key]
key_registry[dictionary_name] = nil
if table_size(key_registry) == 0 then
string_registry[key] = nil
end
end
-- decrement active translation counters
__translation.active_translations_count = __translation.active_translations_count - 1
player_data.active_translations_count = player_data.active_translations_count - 1
-- raise canceled event with the output tables
event.raise(translation.canceled_event, {player_index=player.index, dictionary_name=dictionary_name})
-- remove from active translations table
player_data.active_translations[dictionary_name] = nil
-- check if the player is done translating
if player_data.active_translations_count == 0 then
-- deregister events for this player
event.disable("translation_sort_result", player.index)
-- only deregister this if it's actually registered
if event.is_enabled("translation_translate_batch", player.index) then
event.disable("translation_translate_batch", player.index)
end
-- remove player's translation table
__translation.players[player.index] = nil
end
end
-- cancels all translations for a player
function translation.cancel_all_for_player(player)
local __translation = global.__lualib.translation
local player_translations = __translation.players[player.index].active_translations
for name,_ in pairs(player_translations) do
translation.cancel(player, name)
end
end
-- cancels ALL translations for this mod
function translation.cancel_all()
for i,t in pairs(global.__lualib.translation.players) do
local player = game.get_player(i)
for name,_ in pairs(t.active_translations) do
translation.cancel(player, name)
end
end
end
-- register conditional events
event.register_conditional{
translation_translate_batch = {id=defines.events.on_tick, handler=translate_batch, options={skip_validation=true, suppress_logging=true}},
translation_sort_result = {id=defines.events.on_string_translated, handler=sort_translated_string, options={skip_validation=true, suppress_logging=true}},
}
-- set up global
event.on_init(function()
-- this requires the event module so the lualib table will already exist
global.__lualib.translation = {
active_translations_count = 0,
players = {}
}
translation.retranslate_all_event = remote.call("railualib_translation", "retranslate_all_event")
end)
event.on_load(function()
translation.retranslate_all_event = remote.call("railualib_translation", "retranslate_all_event")
end)
event.on_configuration_changed(function()
migration.run(global.__lualib.__version, {
["0.2.6"] = function()
-- remove unneeded translation tables
local players = global.__lualib.translation.players
for i,t in pairs(players) do
if t.active_translations_count == 0 then
players[i] = nil
end
end
end
})
end)
-- cancel all translations for the player when they leave or are removed
event.register({defines.events.on_pre_player_left_game, defines.events.on_pre_player_removed}, function(e)
local player_translation = global.__lualib.translation.players[e.player_index]
if player_translation and player_translation.active_translations_count > 0 then
translation.cancel_all_for_player(game.get_player(e.player_index))
end
end)
return translation