z_mirror/flib

git clone https://git.raiguard.me/z_mirror/flib

Caleb HeuerFix dictionary module using old table module functions6cd6928

trunk
13.8 KiB457 linesraw
1local flib_array = require("__flib__.array")
2local flib_gui = require("__flib__.gui")
3local mod_gui = require("__core__.lualib.mod-gui")
4
5--- @class flib.DictionaryStorage
6--- @field init_ran boolean
7--- @field raw table<string, flib.Dictionary>
8--- @field raw_count integer
9--- @field to_translate string[]
10--- @field translated table<string, table<string, flib.TranslatedDictionary>?>
11--- @field wip flib.DictionaryWipData?
12
13--- @class flib.DictionaryWipData
14--- @field dict string
15--- @field dicts table<string, flib.TranslatedDictionary>
16--- @field finished boolean
17--- @field key string?
18--- @field last_batch_end flib.DictionaryTranslationRequest?
19--- @field language string
20--- @field received_count integer
21--- @field requests table<uint, flib.DictionaryTranslationRequest>
22--- @field request_tick uint
23--- @field translator LuaPlayer
24
25--- Utilities for creating dictionaries of localised string translations.
26--- ```lua
27--- local flib_dictionary = require("__flib__.dictionary")
28--- ```
29--- @class flib_dictionary : event_handler
30local flib_dictionary = {}
31
32local request_timeout_ticks = (60 * 5)
33
34--- @param init_only boolean?
35--- @return flib.DictionaryStorage
36local function get_data(init_only)
37  if not storage.__flib or not storage.__flib.dictionary then
38    error("Dictionary module was not properly initialized - ensure that all lifecycle events are handled.")
39  end
40  local data = storage.__flib.dictionary
41  if init_only and data.init_ran then
42    error("Dictionaries cannot be modified after initialization.")
43  end
44  return data
45end
46
47--- @param language string
48--- @return LuaPlayer?
49local function get_translator(language)
50  for _, player in pairs(game.players) do
51    if player.connected and player.locale == language then
52      return player
53    end
54  end
55end
56
57--- @param data flib.DictionaryStorage
58local function update_gui(data)
59  local wip = data.wip
60  for _, player in pairs(game.players) do
61    local frame_flow = mod_gui.get_frame_flow(player)
62    local window = frame_flow.flib_translation_progress
63    if wip then
64      if not window then
65        _, window = flib_gui.add(frame_flow, {
66          type = "frame",
67          name = "flib_translation_progress",
68          style = mod_gui.frame_style,
69          direction = "vertical",
70          {
71            type = "label",
72            style = "frame_title",
73            caption = { "gui.flib-translating-dictionaries" },
74            tooltip = { "gui.flib-translating-dictionaries-description" },
75          },
76          {
77            type = "frame",
78            name = "pane",
79            style = "inside_shallow_frame_with_padding",
80            --- @diagnostic disable-next-line: missing-fields
81            style_mods = { top_padding = 8 },
82            direction = "vertical",
83          },
84        })
85      end
86      local pane = window.pane --[[@as LuaGuiElement]]
87      local mod_flow = pane[script.mod_name]
88      if not mod_flow then
89        _, mod_flow = flib_gui.add(pane, {
90          type = "flow",
91          name = script.mod_name,
92          style_mods = { vertical_align = "center", top_margin = 4, horizontal_spacing = 8 },
93          {
94            type = "label",
95            style = "caption_label",
96            --- @diagnostic disable-next-line: missing-fields
97            style_mods = { minimal_width = 130 },
98            caption = { "?", { "mod-name." .. script.mod_name }, script.mod_name },
99            ignored_by_interaction = true,
100          },
101          { type = "empty-widget", style = "flib_horizontal_pusher" },
102          { type = "label", name = "language", style = "bold_label", ignored_by_interaction = true },
103          {
104            type = "progressbar",
105            name = "bar",
106            --- @diagnostic disable-next-line: missing-fields
107            style_mods = { top_margin = 1, width = 100 },
108            ignored_by_interaction = true,
109          },
110          {
111            type = "label",
112            name = "percentage",
113            style = "bold_label",
114            --- @diagnostic disable-next-line: missing-fields
115            style_mods = { width = 24, horizontal_align = "right" },
116            ignored_by_interaction = true,
117          },
118        })
119      end
120      local progress = wip.received_count / data.raw_count
121      mod_flow.language.caption = wip.language
122      mod_flow.bar.value = progress --[[@as double]]
123      mod_flow.percentage.caption = tostring(math.min(math.floor(progress * 100), 99)) .. "%"
124      mod_flow.tooltip =
125        { "", (wip.dict or { "gui.flib-finishing" }), "\n" .. wip.received_count .. " / " .. data.raw_count }
126    else
127      if window then
128        local mod_flow = window.pane[script.mod_name]
129        if mod_flow then
130          mod_flow.destroy()
131        end
132        if #window.pane.children == 0 then
133          window.destroy()
134        end
135      end
136    end
137  end
138end
139
140--- @param data flib.DictionaryStorage
141--- @return boolean success
142local function request_next_batch(data)
143  local raw = data.raw
144  local wip = data.wip --[[@as flib.DictionaryWipData]]
145  if wip.finished then
146    wip.last_batch_end = nil
147    return false
148  end
149  wip.last_batch_end = { language = wip.language, dict = wip.dict, key = wip.key }
150  local requests, strings = {}, {}
151  for i = 1, game.is_multiplayer() and 5 or 50 do
152    local string
153    repeat
154      wip.key, string = next(raw[wip.dict], wip.key)
155      if not wip.key then
156        wip.dict = next(raw, wip.dict)
157        if not wip.dict then
158          -- We are done!
159          wip.finished = true
160        end
161      end
162    until string or wip.finished
163    if wip.finished then
164      break
165    end
166    local request = { dict = wip.dict, key = wip.key }
167    requests[i] = request
168    strings[i] = string
169  end
170
171  if not requests[1] then
172    return false -- Finished
173  end
174
175  local translator = wip.translator
176  if not translator.valid or not translator.connected or translator.locale ~= wip.language then
177    local new_translator = get_translator(wip.language)
178    if new_translator then
179      wip.translator = new_translator
180    else
181      -- Cancel this translation
182      data.wip = nil
183      return false
184    end
185  end
186
187  local ids = wip.translator.request_translations(strings)
188  if not ids then
189    return false
190  end
191  for i = 1, #ids do
192    wip.requests[ids[i]] = requests[i]
193  end
194  --- @diagnostic disable-next-line: missing-fields
195  wip.request_tick = game.tick
196
197  update_gui(data)
198
199  return true
200end
201
202--- @param data flib.DictionaryStorage
203local function handle_next_language(data)
204  if not next(data.raw) then
205    -- This can happen if handle_next_language is called during on_init or on_configuration_changed
206    return
207  end
208  while not data.wip and #data.to_translate > 0 do
209    local next_language = table.remove(data.to_translate, 1)
210    if next_language then
211      local translator = get_translator(next_language)
212      if translator then
213        -- Start translation
214        local dicts = {}
215        for name in pairs(data.raw) do
216          dicts[name] = {}
217        end
218        --- @type flib.DictionaryWipData
219        data.wip = {
220          dict = next(data.raw),
221          dicts = dicts,
222          finished = false,
223          --- @type string?
224          key = nil,
225          language = next_language,
226          received_count = 0,
227          --- @type table<uint, flib.DictionaryTranslationRequest>
228          requests = {},
229          request_tick = 0,
230          translator = translator,
231        }
232        request_next_batch(data)
233      end
234    end
235  end
236end
237
238-- Events
239
240flib_dictionary.on_player_dictionaries_ready = script.generate_event_name()
241--- Called when a player's dictionaries are ready to be used. Handling this event is not required.
242--- @class flib.on_player_dictionaries_ready: EventData
243--- @field player_index uint
244
245-- Lifecycle handlers
246
247function flib_dictionary.on_init()
248  if not storage.__flib then
249    storage.__flib = {}
250  end
251  --- @type flib.DictionaryStorage
252  storage.__flib.dictionary = {
253    init_ran = false,
254    player_language_requests = {},
255    player_languages = {},
256    raw = {},
257    raw_count = 0,
258    to_translate = {},
259    translated = {},
260    wip = nil,
261  }
262  for player_index, player in pairs(game.players) do
263    if player.connected then
264      flib_dictionary.on_player_joined_game({
265        name = defines.events.on_player_joined_game,
266        tick = game.tick,
267        --- @cast player_index uint
268        player_index = player_index,
269      })
270    end
271  end
272end
273
274flib_dictionary.on_configuration_changed = flib_dictionary.on_init
275
276function flib_dictionary.on_tick()
277  local data = get_data()
278  if not data.init_ran then
279    data.init_ran = true
280  end
281
282  handle_next_language(data)
283
284  local wip = data.wip
285  if not wip then
286    return
287  end
288
289  if game.tick - wip.request_tick > request_timeout_ticks then
290    local request = wip.last_batch_end
291    if not request then
292      -- TODO: Remove WIP because we actually finished somehow? This should never happen I think
293      error("We're screwed")
294    end
295    wip.dict = request.dict
296    wip.finished = false
297    wip.key = request.key
298    wip.requests = {}
299    request_next_batch(data)
300    update_gui(data)
301  end
302end
303
304--- @param e EventData.on_string_translated
305function flib_dictionary.on_string_translated(e)
306  local data = get_data()
307  local id = e.id
308
309  handle_next_language(data)
310
311  local wip = data.wip
312  if not wip then
313    return
314  end
315
316  local request = wip.requests[id]
317  if request then
318    wip.requests[id] = nil
319    wip.received_count = wip.received_count + 1
320    if e.translated then
321      wip.dicts[request.dict][request.key] = e.result
322    end
323  end
324
325  while wip and not next(wip.requests) and not request_next_batch(data) do
326    if wip.finished then
327      data.translated[wip.language] = wip.dicts
328      data.wip = nil
329      for player_index, player in pairs(game.players) do
330        if player.locale == wip.language then
331          script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = player_index })
332        end
333      end
334    end
335    handle_next_language(data)
336    update_gui(data)
337    wip = data.wip
338  end
339end
340
341--- @param e EventData.on_player_joined_game
342function flib_dictionary.on_player_joined_game(e)
343  local player = game.get_player(e.player_index) --- @unwrap
344  if not player then
345    return
346  end
347  local language = player.locale
348  local data = get_data()
349  if data.translated[language] then
350    script.raise_event(flib_dictionary.on_player_dictionaries_ready, { player_index = e.player_index })
351    return
352  elseif data.wip and data.wip.language == language then
353    return
354  elseif flib_array.contains(data.to_translate, language) then
355    return
356  end
357  table.insert(data.to_translate, language)
358  handle_next_language(data)
359  update_gui(data)
360end
361
362flib_dictionary.on_player_locale_changed = flib_dictionary.on_player_joined_game
363
364--- Handle all non-bootstrap events with default event handlers. Will not overwrite any existing handlers. If you have
365--- custom handlers for on_tick, on_string_translated, or on_player_joined_game, ensure that you call the corresponding
366--- module lifecycle handler..
367function flib_dictionary.handle_events()
368  for id, handler in pairs(flib_dictionary.events) do
369    if not script.get_event_handler(id) then
370      script.on_event(id, handler)
371    end
372  end
373end
374
375--- For use with `__core__/lualib/event_handler`. Pass `flib_dictionary` into `handler.add_lib` to
376--- handle all relevant events automatically.
377flib_dictionary.events = {
378  [defines.events.on_player_joined_game] = flib_dictionary.on_player_joined_game,
379  [defines.events.on_player_locale_changed] = flib_dictionary.on_player_locale_changed,
380  [defines.events.on_string_translated] = flib_dictionary.on_string_translated,
381  [defines.events.on_tick] = flib_dictionary.on_tick,
382}
383
384-- Dictionary creation
385
386--- Create a new dictionary. The name must be unique.
387--- @param name string
388--- @param initial_strings flib.Dictionary?
389function flib_dictionary.new(name, initial_strings)
390  local data = get_data(true)
391  local raw = data.raw
392  if raw[name] then
393    error("Attempted to create dictionary '" .. name .. "' twice.")
394  end
395  raw[name] = initial_strings or {}
396  if initial_strings then
397    data.raw_count = data.raw_count + table_size(initial_strings)
398  end
399end
400
401--- Add the given string to the dictionary.
402--- @param dict_name string
403--- @param key string
404--- @param localised LocalisedString
405function flib_dictionary.add(dict_name, key, localised)
406  local data = get_data(true)
407  local raw = data.raw[dict_name]
408  if not raw then
409    error("Dictionary '" .. dict_name .. "' does not exist.")
410  end
411  if not raw[key] then
412    data.raw_count = data.raw_count + 1
413  end
414  raw[key] = localised
415end
416
417--- Get all dictionaries for the player. Will return `nil` if the player's language has not finished translating.
418--- @param player_index uint
419--- @return table<string, flib.TranslatedDictionary>?
420function flib_dictionary.get_all(player_index)
421  local player = game.get_player(player_index)
422  if not player then
423    return
424  end
425  return get_data().translated[player.locale]
426end
427
428--- Get the specified dictionary for the player. Will return `nil` if the dictionary has not finished translating.
429--- @param player_index uint
430--- @param dict_name string
431--- @return flib.TranslatedDictionary?
432function flib_dictionary.get(player_index, dict_name)
433  local data = get_data()
434  if not data.raw[dict_name] then
435    error("Dictionary '" .. dict_name .. "' does not exist.")
436  end
437  local language_dicts = flib_dictionary.get_all(player_index) or {}
438  return language_dicts[dict_name]
439end
440
441--- @class flib.DictionaryLanguageRequest
442--- @field player LuaPlayer
443--- @field tick uint
444
445--- @class flib.DictionaryTranslationRequest
446--- @field language string
447--- @field dict string
448--- @field key string
449
450--- Localised strings identified by an internal key. Keys must be unique and language-agnostic.
451--- @alias flib.Dictionary table<string, LocalisedString>
452
453--- Translations are identified by their internal key. If the translation failed, then it will not be present. Locale
454--- fallback groups can be used if every key needs a guaranteed translation.
455--- @alias flib.TranslatedDictionary table<string, string>
456
457return flib_dictionary