z_mirror/flib
git clone https://git.raiguard.me/z_mirror/flib
6cd6928
trunk
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 ) 37if not storage . __flib or not storage . __flib . dictionary then 38error ( "Dictionary module was not properly initialized - ensure that all lifecycle events are handled." ) 39end 40local data =storage . __flib . dictionary 41if init_only and data . init_ran then 42error ( "Dictionaries cannot be modified after initialization." ) 43end 44return data 45end 46 47--- @param language string 48--- @return LuaPlayer? 49local function get_translator ( language ) 50for _ , player in pairs ( game . players ) do 51if player . connected and player . locale == language then 52return player 53end 54end 55end 56 57--- @param data flib.DictionaryStorage 58local function update_gui ( data ) 59local wip =data . wip 60for _ , player in pairs ( game . players ) do 61local frame_flow =mod_gui . get_frame_flow ( player ) 62local window =frame_flow . flib_translation_progress 63if wip then 64if not window then 65_ , window =flib_gui . add ( frame_flow , { 66type ="frame" , 67name ="flib_translation_progress" , 68style =mod_gui . frame_style , 69direction ="vertical" , 70{ 71type ="label" , 72style ="frame_title" , 73caption ={ "gui.flib-translating-dictionaries" } , 74tooltip ={ "gui.flib-translating-dictionaries-description" } , 75} , 76{ 77type ="frame" , 78name ="pane" , 79style ="inside_shallow_frame_with_padding" , 80--- @diagnostic disable-next-line: missing-fields 81style_mods ={ top_padding =8 } , 82direction ="vertical" , 83} , 84} ) 85end 86local pane =window . pane --[[@as LuaGuiElement]] 87local mod_flow =pane [ script . mod_name ] 88if not mod_flow then 89_ , mod_flow =flib_gui . add ( pane , { 90type ="flow" , 91name =script . mod_name , 92style_mods ={ vertical_align ="center" , top_margin =4 , horizontal_spacing =8 } , 93{ 94type ="label" , 95style ="caption_label" , 96--- @diagnostic disable-next-line: missing-fields 97style_mods ={ minimal_width =130 } , 98caption ={ "?" , { "mod-name." .. script . mod_name } , script . mod_name } , 99ignored_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{ 104type ="progressbar" , 105name ="bar" , 106--- @diagnostic disable-next-line: missing-fields 107style_mods ={ top_margin =1 , width =100 } , 108ignored_by_interaction =true , 109} , 110{ 111type ="label" , 112name ="percentage" , 113style ="bold_label" , 114--- @diagnostic disable-next-line: missing-fields 115style_mods ={ width =24 , horizontal_align ="right" } , 116ignored_by_interaction =true , 117} , 118} ) 119end 120local progress =wip . received_count / data . raw_count 121mod_flow . language . caption =wip . language 122mod_flow . bar . value =progress --[[@as double]] 123mod_flow . percentage . caption =tostring ( math . min ( math . floor ( progress * 100 ), 99 )) .. "%" 124mod_flow . tooltip = 125{ "" , ( wip . dict or { "gui.flib-finishing" } ), "\n" .. wip . received_count .. " / " .. data . raw_count } 126else 127if window then 128local mod_flow =window . pane [ script . mod_name ] 129if mod_flow then 130mod_flow . destroy () 131end 132if # window . pane . children == 0 then 133window . destroy () 134end 135end 136end 137end 138end 139 140--- @param data flib.DictionaryStorage 141--- @return boolean success 142local function request_next_batch ( data ) 143local raw =data . raw 144local wip =data . wip --[[@as flib.DictionaryWipData]] 145if wip . finished then 146wip . last_batch_end =nil 147return false 148end 149wip . last_batch_end ={ language =wip . language , dict =wip . dict , key =wip . key } 150local requests , strings ={} , {} 151for i =1 , game . is_multiplayer () and 5 or 50 do 152local string 153repeat 154wip . key , string =next ( raw [ wip . dict ], wip . key ) 155if not wip . key then 156wip . dict =next ( raw , wip . dict ) 157if not wip . dict then 158-- We are done! 159wip . finished =true 160end 161end 162until string or wip . finished 163if wip . finished then 164break 165end 166local request ={ dict =wip . dict , key =wip . key } 167requests [ i ] =request 168strings [ i ] =string 169end 170 171if not requests [ 1 ] then 172return false -- Finished 173end 174 175local translator =wip . translator 176if not translator . valid or not translator . connected or translator . locale ~= wip . language then 177local new_translator =get_translator ( wip . language ) 178if new_translator then 179wip . translator =new_translator 180else 181-- Cancel this translation 182data . wip =nil 183return false 184end 185end 186 187local ids =wip . translator . request_translations ( strings ) 188if not ids then 189return false 190end 191for i =1 , # ids do 192wip . requests [ ids [ i ]] =requests [ i ] 193end 194--- @diagnostic disable-next-line: missing-fields 195wip . request_tick =game . tick 196 197update_gui ( data ) 198 199return true 200end 201 202--- @param data flib.DictionaryStorage 203local function handle_next_language ( data ) 204if not next ( data . raw ) then 205-- This can happen if handle_next_language is called during on_init or on_configuration_changed 206return 207end 208while not data . wip and # data . to_translate > 0 do 209local next_language =table . remove ( data . to_translate , 1 ) 210if next_language then 211local translator =get_translator ( next_language ) 212if translator then 213-- Start translation 214local dicts ={} 215for name in pairs ( data . raw ) do 216dicts [ name ] ={} 217end 218--- @type flib.DictionaryWipData 219data . wip ={ 220dict =next ( data . raw ), 221dicts =dicts , 222finished =false , 223--- @type string? 224key =nil , 225language =next_language , 226received_count =0 , 227--- @type table<uint, flib.DictionaryTranslationRequest> 228requests ={} , 229request_tick =0 , 230translator =translator , 231} 232request_next_batch ( data ) 233end 234end 235end 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 () 248if not storage . __flib then 249storage . __flib ={} 250end 251--- @type flib.DictionaryStorage 252storage . __flib . dictionary ={ 253init_ran =false , 254player_language_requests ={} , 255player_languages ={} , 256raw ={} , 257raw_count =0 , 258to_translate ={} , 259translated ={} , 260wip =nil , 261} 262for player_index , player in pairs ( game . players ) do 263if player . connected then 264flib_dictionary . on_player_joined_game ( { 265name =defines . events . on_player_joined_game , 266tick =game . tick , 267--- @cast player_index uint 268player_index =player_index , 269} ) 270end 271end 272end 273 274flib_dictionary . on_configuration_changed =flib_dictionary . on_init 275 276function flib_dictionary . on_tick () 277local data =get_data () 278if not data . init_ran then 279data . init_ran =true 280end 281 282handle_next_language ( data ) 283 284local wip =data . wip 285if not wip then 286return 287end 288 289if game . tick - wip . request_tick > request_timeout_ticks then 290local request =wip . last_batch_end 291if not request then 292-- TODO: Remove WIP because we actually finished somehow? This should never happen I think 293error ( "We're screwed" ) 294end 295wip . dict =request . dict 296wip . finished =false 297wip . key =request . key 298wip . requests ={} 299request_next_batch ( data ) 300update_gui ( data ) 301end 302end 303 304--- @param e EventData.on_string_translated 305function flib_dictionary . on_string_translated ( e ) 306local data =get_data () 307local id =e . id 308 309handle_next_language ( data ) 310 311local wip =data . wip 312if not wip then 313return 314end 315 316local request =wip . requests [ id ] 317if request then 318wip . requests [ id ] =nil 319wip . received_count =wip . received_count + 1 320if e . translated then 321wip . dicts [ request . dict ][ request . key ] =e . result 322end 323end 324 325while wip and not next ( wip . requests ) and not request_next_batch ( data ) do 326if wip . finished then 327data . translated [ wip . language ] =wip . dicts 328data . wip =nil 329for player_index , player in pairs ( game . players ) do 330if player . locale == wip . language then 331script . raise_event ( flib_dictionary . on_player_dictionaries_ready , { player_index =player_index } ) 332end 333end 334end 335handle_next_language ( data ) 336update_gui ( data ) 337wip =data . wip 338end 339end 340 341--- @param e EventData.on_player_joined_game 342function flib_dictionary . on_player_joined_game ( e ) 343local player =game . get_player ( e . player_index ) --- @unwrap 344if not player then 345return 346end 347local language =player . locale 348local data =get_data () 349if data . translated [ language ] then 350script . raise_event ( flib_dictionary . on_player_dictionaries_ready , { player_index =e . player_index } ) 351return 352elseif data . wip and data . wip . language == language then 353return 354elseif flib_array . contains ( data . to_translate , language ) then 355return 356end 357table . insert ( data . to_translate , language ) 358handle_next_language ( data ) 359update_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 () 368for id , handler in pairs ( flib_dictionary . events ) do 369if not script . get_event_handler ( id ) then 370script . on_event ( id , handler ) 371end 372end 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 ) 390local data =get_data ( true ) 391local raw =data . raw 392if raw [ name ] then 393error ( "Attempted to create dictionary '" .. name .. "' twice." ) 394end 395raw [ name ] =initial_strings or {} 396if initial_strings then 397data . raw_count =data . raw_count + table_size ( initial_strings ) 398end 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 ) 406local data =get_data ( true ) 407local raw =data . raw [ dict_name ] 408if not raw then 409error ( "Dictionary '" .. dict_name .. "' does not exist." ) 410end 411if not raw [ key ] then 412data . raw_count =data . raw_count + 1 413end 414raw [ 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 ) 421local player =game . get_player ( player_index ) 422if not player then 423return 424end 425return 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 ) 433local data =get_data () 434if not data . raw [ dict_name ] then 435error ( "Dictionary '" .. dict_name .. "' does not exist." ) 436end 437local language_dicts =flib_dictionary . get_all ( player_index ) or {} 438return 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