-
Notifications
You must be signed in to change notification settings - Fork 21
/
dictionary.lua
467 lines (424 loc) · 14.1 KB
/
dictionary.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
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