mirror of
https://github.com/factoriolib/flib.git
synced 2025-09-04 17:00:12 +00:00
468 lines
14 KiB
Lua
468 lines
14 KiB
Lua
if ... ~= "__flib__.dictionary" then
|
|
return require("__flib__.dictionary")
|
|
end
|
|
|
|
local gui = require("__flib__.gui")
|
|
local mod_gui = require("__core__.lualib.mod-gui")
|
|
local table = require("__flib__.table")
|
|
|
|
--- @class flib.DictionaryStorage
|
|
--- @field init_ran boolean
|
|
--- @field raw table<string, flib.Dictionary>
|
|
--- @field raw_count integer
|
|
--- @field to_translate string[]
|
|
--- @field translated table<string, table<string, flib.TranslatedDictionary>?>
|
|
--- @field wip flib.DictionaryWipData?
|
|
|
|
--- @class flib.DictionaryWipData
|
|
--- @field dict string
|
|
--- @field dicts table<string, flib.TranslatedDictionary>
|
|
--- @field finished boolean
|
|
--- @field key string?
|
|
--- @field last_batch_end flib.DictionaryTranslationRequest?
|
|
--- @field language string
|
|
--- @field received_count integer
|
|
--- @field requests table<uint, flib.DictionaryTranslationRequest>
|
|
--- @field request_tick uint
|
|
--- @field translator LuaPlayer
|
|
|
|
--- Utilities for creating dictionaries of localised string translations.
|
|
--- ```lua
|
|
--- local flib_dictionary = require("__flib__.dictionary")
|
|
--- ```
|
|
--- @class flib_dictionary
|
|
local flib_dictionary = {}
|
|
|
|
local request_timeout_ticks = (60 * 5)
|
|
|
|
--- @param init_only boolean?
|
|
--- @return flib.DictionaryStorage
|
|
local function get_data(init_only)
|
|
if not storage.__flib or not storage.__flib.dictionary then
|
|
error("Dictionary module was not properly initialized - ensure that all lifecycle events are handled.")
|
|
end
|
|
local data = storage.__flib.dictionary
|
|
if init_only and data.init_ran then
|
|
error("Dictionaries cannot be modified after initialization.")
|
|
end
|
|
return data
|
|
end
|
|
|
|
--- @param language string
|
|
--- @return LuaPlayer?
|
|
local function get_translator(language)
|
|
for _, player in pairs(game.players) do
|
|
if player.connected and player.locale == language then
|
|
return player
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param data flib.DictionaryStorage
|
|
local function update_gui(data)
|
|
local wip = data.wip
|
|
for _, player in pairs(game.players) do
|
|
local frame_flow = mod_gui.get_frame_flow(player)
|
|
local window = frame_flow.flib_translation_progress
|
|
if wip then
|
|
if not window then
|
|
_, window = gui.add(frame_flow, {
|
|
type = "frame",
|
|
name = "flib_translation_progress",
|
|
style = mod_gui.frame_style,
|
|
direction = "vertical",
|
|
{
|
|
type = "label",
|
|
style = "frame_title",
|
|
caption = { "gui.flib-translating-dictionaries" },
|
|
tooltip = { "gui.flib-translating-dictionaries-description" },
|
|
},
|
|
{
|
|
type = "frame",
|
|
name = "pane",
|
|
style = "inside_shallow_frame_with_padding",
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
style_mods = { top_padding = 8 },
|
|
direction = "vertical",
|
|
},
|
|
})
|
|
end
|
|
local pane = window.pane --[[@as LuaGuiElement]]
|
|
local mod_flow = pane[script.mod_name]
|
|
if not mod_flow then
|
|
_, mod_flow = gui.add(pane, {
|
|
type = "flow",
|
|
name = script.mod_name,
|
|
style_mods = { vertical_align = "center", top_margin = 4, horizontal_spacing = 8 },
|
|
{
|
|
type = "label",
|
|
style = "caption_label",
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
style_mods = { minimal_width = 130 },
|
|
caption = { "?", { "mod-name." .. script.mod_name }, script.mod_name },
|
|
ignored_by_interaction = true,
|
|
},
|
|
{ type = "empty-widget", style = "flib_horizontal_pusher" },
|
|
{ type = "label", name = "language", style = "bold_label", ignored_by_interaction = true },
|
|
{
|
|
type = "progressbar",
|
|
name = "bar",
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
style_mods = { top_margin = 1, width = 100 },
|
|
ignored_by_interaction = true,
|
|
},
|
|
{
|
|
type = "label",
|
|
name = "percentage",
|
|
style = "bold_label",
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
style_mods = { width = 24, horizontal_align = "right" },
|
|
ignored_by_interaction = true,
|
|
},
|
|
})
|
|
end
|
|
local progress = wip.received_count / data.raw_count
|
|
mod_flow.language.caption = wip.language
|
|
mod_flow.bar.value = progress --[[@as double]]
|
|
mod_flow.percentage.caption = tostring(math.min(math.floor(progress * 100), 99)) .. "%"
|
|
mod_flow.tooltip =
|
|
{ "", (wip.dict or { "gui.flib-finishing" }), "\n" .. wip.received_count .. " / " .. data.raw_count }
|
|
else
|
|
if window then
|
|
local mod_flow = window.pane[script.mod_name]
|
|
if mod_flow then
|
|
mod_flow.destroy()
|
|
end
|
|
if #window.pane.children == 0 then
|
|
window.destroy()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param data flib.DictionaryStorage
|
|
--- @return boolean success
|
|
local function request_next_batch(data)
|
|
local raw = data.raw
|
|
local wip = data.wip --[[@as flib.DictionaryWipData]]
|
|
if wip.finished then
|
|
wip.last_batch_end = nil
|
|
return false
|
|
end
|
|
wip.last_batch_end = { language = wip.language, dict = wip.dict, key = wip.key }
|
|
local requests, strings = {}, {}
|
|
for i = 1, game.is_multiplayer() and 5 or 50 do
|
|
local string
|
|
repeat
|
|
wip.key, string = next(raw[wip.dict], wip.key)
|
|
if not wip.key then
|
|
wip.dict = next(raw, wip.dict)
|
|
if not wip.dict then
|
|
-- We are done!
|
|
wip.finished = true
|
|
end
|
|
end
|
|
until string or wip.finished
|
|
if wip.finished then
|
|
break
|
|
end
|
|
local request = { dict = wip.dict, key = wip.key }
|
|
requests[i] = request
|
|
strings[i] = string
|
|
end
|
|
|
|
if not requests[1] then
|
|
return false -- Finished
|
|
end
|
|
|
|
local translator = wip.translator
|
|
if not translator.valid or not translator.connected or translator.locale ~= wip.language then
|
|
local new_translator = get_translator(wip.language)
|
|
if new_translator then
|
|
wip.translator = new_translator
|
|
else
|
|
-- Cancel this translation
|
|
data.wip = nil
|
|
return false
|
|
end
|
|
end
|
|
|
|
local ids = wip.translator.request_translations(strings)
|
|
if not ids then
|
|
return false
|
|
end
|
|
for i = 1, #ids do
|
|
wip.requests[ids[i]] = requests[i]
|
|
end
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
wip.request_tick = game.tick
|
|
|
|
update_gui(data)
|
|
|
|
return true
|
|
end
|
|
|
|
--- @param data flib.DictionaryStorage
|
|
local function handle_next_language(data)
|
|
if not next(data.raw) then
|
|
-- This can happen if handle_next_language is called during on_init or on_configuration_changed
|
|
return
|
|
end
|
|
while not data.wip and #data.to_translate > 0 do
|
|
local next_language = table.remove(data.to_translate, 1)
|
|
if next_language then
|
|
local translator = get_translator(next_language)
|
|
if translator then
|
|
-- Start translation
|
|
local dicts = {}
|
|
for name in pairs(data.raw) do
|
|
dicts[name] = {}
|
|
end
|
|
--- @type flib.DictionaryWipData
|
|
data.wip = {
|
|
dict = next(data.raw),
|
|
dicts = dicts,
|
|
finished = false,
|
|
--- @type string?
|
|
key = nil,
|
|
language = next_language,
|
|
received_count = 0,
|
|
--- @type table<uint, flib.DictionaryTranslationRequest>
|
|
requests = {},
|
|
request_tick = 0,
|
|
translator = translator,
|
|
}
|
|
request_next_batch(data)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Events
|
|
|
|
flib_dictionary.on_player_dictionaries_ready = script.generate_event_name()
|
|
--- Called when a player's dictionaries are ready to be used. Handling this event is not required.
|
|
--- @class flib.on_player_dictionaries_ready: EventData
|
|
--- @field player_index uint
|
|
|
|
-- Lifecycle handlers
|
|
|
|
function flib_dictionary.on_init()
|
|
if not storage.__flib then
|
|
storage.__flib = {}
|
|
end
|
|
--- @type flib.DictionaryStorage
|
|
storage.__flib.dictionary = {
|
|
init_ran = false,
|
|
player_language_requests = {},
|
|
player_languages = {},
|
|
raw = {},
|
|
raw_count = 0,
|
|
to_translate = {},
|
|
translated = {},
|
|
wip = nil,
|
|
}
|
|
for player_index, player in pairs(game.players) do
|
|
if player.connected then
|
|
flib_dictionary.on_player_joined_game({
|
|
name = defines.events.on_player_joined_game,
|
|
tick = game.tick,
|
|
--- @cast player_index uint
|
|
player_index = player_index,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
flib_dictionary.on_configuration_changed = flib_dictionary.on_init
|
|
|
|
function flib_dictionary.on_tick()
|
|
local data = get_data()
|
|
if not data.init_ran then
|
|
data.init_ran = true
|
|
end
|
|
|
|
handle_next_language(data)
|
|
|
|
local wip = data.wip
|
|
if not wip then
|
|
return
|
|
end
|
|
|
|
if game.tick - wip.request_tick > request_timeout_ticks then
|
|
local request = wip.last_batch_end
|
|
if not request then
|
|
-- TODO: Remove WIP because we actually finished somehow? This should never happen I think
|
|
error("We're screwed")
|
|
end
|
|
wip.dict = request.dict
|
|
wip.finished = false
|
|
wip.key = request.key
|
|
wip.requests = {}
|
|
request_next_batch(data)
|
|
update_gui(data)
|
|
end
|
|
end
|
|
|
|
--- @param e EventData.on_string_translated
|
|
function flib_dictionary.on_string_translated(e)
|
|
local data = get_data()
|
|
local id = e.id
|
|
|
|
handle_next_language(data)
|
|
|
|
local wip = data.wip
|
|
if not wip then
|
|
return
|
|
end
|
|
|
|
local request = wip.requests[id]
|
|
if request then
|
|
wip.requests[id] = nil
|
|
wip.received_count = wip.received_count + 1
|
|
if e.translated then
|
|
wip.dicts[request.dict][request.key] = e.result
|
|
end
|
|
end
|
|
|
|
while wip and not next(wip.requests) and not request_next_batch(data) do
|
|
if wip.finished then
|
|
data.translated[wip.language] = wip.dicts
|
|
data.wip = nil
|
|
for player_index, player in pairs(game.players) do
|
|
if player.locale == wip.language then
|
|
script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = player_index })
|
|
end
|
|
end
|
|
end
|
|
handle_next_language(data)
|
|
update_gui(data)
|
|
wip = data.wip
|
|
end
|
|
end
|
|
|
|
--- @param e EventData.on_player_joined_game
|
|
function flib_dictionary.on_player_joined_game(e)
|
|
local player = game.get_player(e.player_index) --- @unwrap
|
|
if not player then
|
|
return
|
|
end
|
|
local language = player.locale
|
|
local data = get_data()
|
|
if data.translated[language] then
|
|
script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = e.player_index })
|
|
return
|
|
elseif data.wip and data.wip.language == language then
|
|
return
|
|
elseif table.find(data.to_translate, language) then
|
|
return
|
|
end
|
|
table.insert(data.to_translate, language)
|
|
handle_next_language(data)
|
|
update_gui(data)
|
|
end
|
|
|
|
flib_dictionary.on_player_locale_changed = flib_dictionary.on_player_joined_game
|
|
|
|
--- Handle all non-bootstrap events with default event handlers. Will not overwrite any existing handlers. If you have
|
|
--- custom handlers for on_tick, on_string_translated, or on_player_joined_game, ensure that you call the corresponding
|
|
--- module lifecycle handler..
|
|
function flib_dictionary.handle_events()
|
|
for id, handler in pairs({
|
|
[defines.events.on_tick] = flib_dictionary.on_tick,
|
|
[defines.events.on_string_translated] = flib_dictionary.on_string_translated,
|
|
[defines.events.on_player_joined_game] = flib_dictionary.on_player_joined_game,
|
|
}) do
|
|
if
|
|
not script.get_event_handler(id --[[@as uint]])
|
|
then
|
|
script.on_event(id, handler)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- For use with `__core__/lualib/event_handler`. Pass `flib_dictionary` into `handler.add_lib` to
|
|
--- handle all relevant events automatically.
|
|
flib_dictionary.events = {
|
|
[defines.events.on_player_joined_game] = flib_dictionary.on_player_joined_game,
|
|
[defines.events.on_player_locale_changed] = flib_dictionary.on_player_locale_changed,
|
|
[defines.events.on_string_translated] = flib_dictionary.on_string_translated,
|
|
[defines.events.on_tick] = flib_dictionary.on_tick,
|
|
}
|
|
|
|
-- Dictionary creation
|
|
|
|
--- Create a new dictionary. The name must be unique.
|
|
--- @param name string
|
|
--- @param initial_strings flib.Dictionary?
|
|
function flib_dictionary.new(name, initial_strings)
|
|
local data = get_data(true)
|
|
local raw = data.raw
|
|
if raw[name] then
|
|
error("Attempted to create dictionary '" .. name .. "' twice.")
|
|
end
|
|
raw[name] = initial_strings or {}
|
|
if initial_strings then
|
|
data.raw_count = data.raw_count + table_size(initial_strings)
|
|
end
|
|
end
|
|
|
|
--- Add the given string to the dictionary.
|
|
--- @param dict_name string
|
|
--- @param key string
|
|
--- @param localised LocalisedString
|
|
function flib_dictionary.add(dict_name, key, localised)
|
|
local data = get_data(true)
|
|
local raw = data.raw[dict_name]
|
|
if not raw then
|
|
error("Dictionary '" .. dict_name .. "' does not exist.")
|
|
end
|
|
if not raw[key] then
|
|
data.raw_count = data.raw_count + 1
|
|
end
|
|
raw[key] = localised
|
|
end
|
|
|
|
--- Get all dictionaries for the player. Will return `nil` if the player's language has not finished translating.
|
|
--- @param player_index uint
|
|
--- @return table<string, flib.TranslatedDictionary>?
|
|
function flib_dictionary.get_all(player_index)
|
|
local player = game.get_player(player_index)
|
|
if not player then
|
|
return
|
|
end
|
|
return get_data().translated[player.locale]
|
|
end
|
|
|
|
--- Get the specified dictionary for the player. Will return `nil` if the dictionary has not finished translating.
|
|
--- @param player_index uint
|
|
--- @param dict_name string
|
|
--- @return flib.TranslatedDictionary?
|
|
function flib_dictionary.get(player_index, dict_name)
|
|
local data = get_data()
|
|
if not data.raw[dict_name] then
|
|
error("Dictionary '" .. dict_name .. "' does not exist.")
|
|
end
|
|
local language_dicts = flib_dictionary.get_all(player_index) or {}
|
|
return language_dicts[dict_name]
|
|
end
|
|
|
|
--- @class flib.DictionaryLanguageRequest
|
|
--- @field player LuaPlayer
|
|
--- @field tick uint
|
|
|
|
--- @class flib.DictionaryTranslationRequest
|
|
--- @field language string
|
|
--- @field dict string
|
|
--- @field key string
|
|
|
|
--- Localised strings identified by an internal key. Keys must be unique and language-agnostic.
|
|
--- @alias flib.Dictionary table<string, LocalisedString>
|
|
|
|
--- Translations are identified by their internal key. If the translation failed, then it will not be present. Locale
|
|
--- fallback groups can be used if every key needs a guaranteed translation.
|
|
--- @alias flib.TranslatedDictionary table<string, string>
|
|
|
|
return flib_dictionary
|