Settings tab: add v3f type
[oweals/minetest.git] / builtin / mainmenu / tab_settings.lua
1 --Minetest
2 --Copyright (C) 2015 PilzAdam
3 --
4 --This program is free software; you can redistribute it and/or modify
5 --it under the terms of the GNU Lesser General Public License as published by
6 --the Free Software Foundation; either version 2.1 of the License, or
7 --(at your option) any later version.
8 --
9 --This program is distributed in the hope that it will be useful,
10 --but WITHOUT ANY WARRANTY; without even the implied warranty of
11 --MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 --GNU Lesser General Public License for more details.
13 --
14 --You should have received a copy of the GNU Lesser General Public License along
15 --with this program; if not, write to the Free Software Foundation, Inc.,
16 --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 local FILENAME = "settingtypes.txt"
19
20 local CHAR_CLASSES = {
21         SPACE = "[%s]",
22         VARIABLE = "[%w_%-%.]",
23         INTEGER = "[-]?[%d]",
24         FLOAT = "[-]?[%d%.]",
25         FLAGS = "[%w_%-%.,]",
26 }
27
28 -- returns error message, or nil
29 local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
30         -- comment
31         local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
32         if comment then
33                 if settings.current_comment == "" then
34                         settings.current_comment = comment
35                 else
36                         settings.current_comment = settings.current_comment .. "\n" .. comment
37                 end
38                 return
39         end
40
41         -- clear current_comment so only comments directly above a setting are bound to it
42         -- but keep a local reference to it for variables in the current line
43         local current_comment = settings.current_comment
44         settings.current_comment = ""
45
46         -- empty lines
47         if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then
48                 return
49         end
50
51         -- category
52         local stars, category = line:match("^%[([%*]*)([^%]]+)%]$")
53         if category then
54                 table.insert(settings, {
55                         name = category,
56                         level = stars:len() + base_level,
57                         type = "category",
58                 })
59                 return
60         end
61
62         -- settings
63         local first_part, name, readable_name, setting_type = line:match("^"
64                         -- this first capture group matches the whole first part,
65                         --  so we can later strip it from the rest of the line
66                         .. "("
67                                 .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name
68                                 .. CHAR_CLASSES.SPACE
69                                 .. "%(([^%)]*)%)"  -- readable name
70                                 .. CHAR_CLASSES.SPACE
71                                 .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type
72                                 .. CHAR_CLASSES.SPACE .. "?"
73                         .. ")")
74
75         if not first_part then
76                 return "Invalid line"
77         end
78
79         if name:match("secure%.[.]*") and not allow_secure then
80                 return "Tried to add \"secure.\" setting"
81         end
82
83         if readable_name == "" then
84                 readable_name = nil
85         end
86         local remaining_line = line:sub(first_part:len() + 1)
87
88         if setting_type == "int" then
89                 local default, min, max = remaining_line:match("^"
90                                 -- first int is required, the last 2 are optional
91                                 .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "?"
92                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "?"
93                                 .. "(" .. CHAR_CLASSES.INTEGER .. "*)"
94                                 .. "$")
95
96                 if not default or not tonumber(default) then
97                         return "Invalid integer setting"
98                 end
99
100                 min = tonumber(min)
101                 max = tonumber(max)
102                 table.insert(settings, {
103                         name = name,
104                         readable_name = readable_name,
105                         type = "int",
106                         default = default,
107                         min = min,
108                         max = max,
109                         comment = current_comment,
110                 })
111                 return
112         end
113
114         if setting_type == "string" or setting_type == "noise_params"
115                         or setting_type == "key" or setting_type == "v3f" then
116                 local default = remaining_line:match("^(.*)$")
117
118                 if not default then
119                         return "Invalid string setting"
120                 end
121                 if setting_type == "key" and not read_all then
122                         -- ignore key type if read_all is false
123                         return
124                 end
125
126                 table.insert(settings, {
127                         name = name,
128                         readable_name = readable_name,
129                         type = setting_type,
130                         default = default,
131                         comment = current_comment,
132                 })
133                 return
134         end
135
136         if setting_type == "bool" then
137                 if remaining_line ~= "false" and remaining_line ~= "true" then
138                         return "Invalid boolean setting"
139                 end
140
141                 table.insert(settings, {
142                         name = name,
143                         readable_name = readable_name,
144                         type = "bool",
145                         default = remaining_line,
146                         comment = current_comment,
147                 })
148                 return
149         end
150
151         if setting_type == "float" then
152                 local default, min, max = remaining_line:match("^"
153                                 -- first float is required, the last 2 are optional
154                                 .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "?"
155                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "?"
156                                 .. "(" .. CHAR_CLASSES.FLOAT .. "*)"
157                                 .."$")
158
159                 if not default or not tonumber(default) then
160                         return "Invalid float setting"
161                 end
162
163                 min = tonumber(min)
164                 max = tonumber(max)
165                 table.insert(settings, {
166                         name = name,
167                         readable_name = readable_name,
168                         type = "float",
169                         default = default,
170                         min = min,
171                         max = max,
172                         comment = current_comment,
173                 })
174                 return
175         end
176
177         if setting_type == "enum" then
178                 local default, values = remaining_line:match("^(.+)" .. CHAR_CLASSES.SPACE .. "(.+)$")
179
180                 if not default or values == "" then
181                         return "Invalid enum setting"
182                 end
183
184                 table.insert(settings, {
185                         name = name,
186                         readable_name = readable_name,
187                         type = "enum",
188                         default = default,
189                         values = values:split(",", true),
190                         comment = current_comment,
191                 })
192                 return
193         end
194
195         if setting_type == "path" then
196                 local default = remaining_line:match("^(.*)$")
197
198                 if not default then
199                         return "Invalid path setting"
200                 end
201
202                 table.insert(settings, {
203                         name = name,
204                         readable_name = readable_name,
205                         type = "path",
206                         default = default,
207                         comment = current_comment,
208                 })
209                 return
210         end
211
212         if setting_type == "flags" then
213                 local default, possible = remaining_line:match("^"
214                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. ""
215                                 .. "(" .. CHAR_CLASSES.FLAGS .. "+)"
216                                 .. "$")
217
218                 if not default or not possible then
219                         return "Invalid flags setting"
220                 end
221
222                 table.insert(settings, {
223                         name = name,
224                         readable_name = readable_name,
225                         type = "flags",
226                         default = default,
227                         possible = possible,
228                         comment = current_comment,
229                 })
230                 return
231         end
232
233         return "Invalid setting type \"" .. setting_type .. "\""
234 end
235
236 local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure)
237         -- store this helper variable in the table so it's easier to pass to parse_setting_line()
238         result.current_comment = ""
239
240         local line = file:read("*line")
241         while line do
242                 local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure)
243                 if error_msg then
244                         core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"")
245                 end
246                 line = file:read("*line")
247         end
248
249         result.current_comment = nil
250 end
251
252 -- read_all: whether to ignore certain setting types for GUI or not
253 -- parse_mods: whether to parse settingtypes.txt in mods and games
254 local function parse_config_file(read_all, parse_mods)
255         local builtin_path = core.get_builtin_path() .. DIR_DELIM .. FILENAME
256         local file = io.open(builtin_path, "r")
257         local settings = {}
258         if not file then
259                 core.log("error", "Can't load " .. FILENAME)
260                 return settings
261         end
262
263         parse_single_file(file, builtin_path, read_all, settings, 0, true)
264
265         file:close()
266
267         if parse_mods then
268                 -- Parse games
269                 local games_category_initialized = false
270                 local index = 1
271                 local game = gamemgr.get_game(index)
272                 while game do
273                         local path = game.path .. DIR_DELIM .. FILENAME
274                         local file = io.open(path, "r")
275                         if file then
276                                 if not games_category_initialized then
277                                         local translation = fgettext_ne("Games"), -- not used, but needed for xgettext
278                                         table.insert(settings, {
279                                                 name = "Games",
280                                                 level = 0,
281                                                 type = "category",
282                                         })
283                                         games_category_initialized = true
284                                 end
285
286                                 table.insert(settings, {
287                                         name = game.name,
288                                         level = 1,
289                                         type = "category",
290                                 })
291
292                                 parse_single_file(file, path, read_all, settings, 2, false)
293
294                                 file:close()
295                         end
296
297                         index = index + 1
298                         game = gamemgr.get_game(index)
299                 end
300
301                 -- Parse mods
302                 local mods_category_initialized = false
303                 local mods = {}
304                 get_mods(core.get_modpath(), mods)
305                 for _, mod in ipairs(mods) do
306                         local path = mod.path .. DIR_DELIM .. FILENAME
307                         local file = io.open(path, "r")
308                         if file then
309                                 if not mods_category_initialized then
310                                         local translation = fgettext_ne("Mods"), -- not used, but needed for xgettext
311                                         table.insert(settings, {
312                                                 name = "Mods",
313                                                 level = 0,
314                                                 type = "category",
315                                         })
316                                         mods_category_initialized = true
317                                 end
318
319                                 table.insert(settings, {
320                                         name = mod.name,
321                                         level = 1,
322                                         type = "category",
323                                 })
324
325                                 parse_single_file(file, path, read_all, settings, 2, false)
326
327                                 file:close()
328                         end
329                 end
330         end
331
332         return settings
333 end
334
335 local settings = parse_config_file(false, true)
336 local selected_setting = 1
337
338 local function get_current_value(setting)
339         local value = core.setting_get(setting.name)
340         if value == nil then
341                 value = setting.default
342         end
343         return value
344 end
345
346 local function create_change_setting_formspec(dialogdata)
347         local setting = settings[selected_setting]
348         local formspec = "size[10,5.2,true]" ..
349                         "button[5,4.5;2,1;btn_done;" .. fgettext("Save") .. "]" ..
350                         "button[3,4.5;2,1;btn_cancel;" .. fgettext("Cancel") .. "]" ..
351                         "tablecolumns[color;text]" ..
352                         "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
353                         "table[0,0;10,3;info;"
354
355         if setting.readable_name then
356                 formspec = formspec .. "#FFFF00," .. fgettext(setting.readable_name)
357                                 .. " (" .. core.formspec_escape(setting.name) .. "),"
358         else
359                 formspec = formspec .. "#FFFF00," .. core.formspec_escape(setting.name) .. ","
360         end
361
362         formspec = formspec .. ",,"
363
364         local comment_text = ""
365
366         if setting.comment == "" then
367                 comment_text = fgettext_ne("(No description of setting given)")
368         else
369                 comment_text = fgettext_ne(setting.comment)
370         end
371         for _, comment_line in ipairs(comment_text:split("\n", true)) do
372                 formspec = formspec .. "," .. core.formspec_escape(comment_line) .. ","
373         end
374
375         if setting.type == "flags" then
376                 formspec = formspec .. ",,"
377                                 .. "," .. fgettext("Please enter a comma seperated list of flags.") .. ","
378                                 .. "," .. fgettext("Possible values are: ")
379                                 .. core.formspec_escape(setting.possible:gsub(",", ", ")) .. ","
380         elseif setting.type == "noise_params" then
381                 formspec = formspec .. ",,"
382                                 .. "," .. fgettext("Format: <offset>, <scale>, (<spreadX>, <spreadY>, <spreadZ>), <seed>, <octaves>, <persistence>") .. ","
383                                 .. "," .. fgettext("Optionally the lacunarity can be appended with a leading comma.") .. ","
384         elseif setting.type == "v3f" then
385                 formspec = formspec .. ",,"
386                                 .. "," .. fgettext_ne("Format is 3 numbers separated by commas and inside brackets.") .. ","
387         end
388
389         formspec = formspec:sub(1, -2) -- remove trailing comma
390
391         formspec = formspec .. ";1]"
392
393         if setting.type == "bool" then
394                 local selected_index
395                 if core.is_yes(get_current_value(setting)) then
396                         selected_index = 2
397                 else
398                         selected_index = 1
399                 end
400                 formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
401                                 .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";"
402                                 .. selected_index .. "]"
403
404         elseif setting.type == "enum" then
405                 local selected_index = 0
406                 formspec = formspec .. "dropdown[0.5,3.5;3,1;dd_setting_value;"
407                 for index, value in ipairs(setting.values) do
408                         -- translating value is not possible, since it's the value
409                         --  that we set the setting to
410                         formspec = formspec ..  core.formspec_escape(value) .. ","
411                         if get_current_value(setting) == value then
412                                 selected_index = index
413                         end
414                 end
415                 if #setting.values > 0 then
416                         formspec = formspec:sub(1, -2) -- remove trailing comma
417                 end
418                 formspec = formspec .. ";" .. selected_index .. "]"
419
420         elseif setting.type == "path" then
421                 local current_value = dialogdata.selected_path
422                 if not current_value then
423                         current_value = get_current_value(setting)
424                 end
425                 formspec = formspec .. "field[0.5,4;7.5,1;te_setting_value;;"
426                                 .. core.formspec_escape(current_value) .. "]"
427                                 .. "button[8,3.75;2,1;btn_browser_path;" .. fgettext("Browse") .. "]"
428
429         else
430                 -- TODO: fancy input for float, int, flags, noise_params, v3f
431                 local width = 10
432                 local text = get_current_value(setting)
433                 if dialogdata.error_message then
434                         formspec = formspec .. "tablecolumns[color;text]" ..
435                         "tableoptions[background=#00000000;highlight=#00000000;border=false]" ..
436                         "table[5,4;5,1;error_message;#FF0000,"
437                                         .. core.formspec_escape(dialogdata.error_message) .. ";0]"
438                         width = 5
439                         if dialogdata.entered_text then
440                                 text = dialogdata.entered_text
441                         end
442                 end
443                 formspec = formspec .. "field[0.5,4;" .. width .. ",1;te_setting_value;;"
444                                 .. core.formspec_escape(text) .. "]"
445         end
446         return formspec
447 end
448
449 local function handle_change_setting_buttons(this, fields)
450         if fields["btn_done"] or fields["key_enter"] then
451                 local setting = settings[selected_setting]
452                 if setting.type == "bool" then
453                         local new_value = fields["dd_setting_value"]
454                         -- Note: new_value is the actual (translated) value shown in the dropdown
455                         core.setting_setbool(setting.name, new_value == fgettext("Enabled"))
456
457                 elseif setting.type == "enum" then
458                         local new_value = fields["dd_setting_value"]
459                         core.setting_set(setting.name, new_value)
460
461                 elseif setting.type == "int" then
462                         local new_value = tonumber(fields["te_setting_value"])
463                         if not new_value or math.floor(new_value) ~= new_value then
464                                 this.data.error_message = fgettext_ne("Please enter a valid integer.")
465                                 this.data.entered_text = fields["te_setting_value"]
466                                 core.update_formspec(this:get_formspec())
467                                 return true
468                         end
469                         if setting.min and new_value < setting.min then
470                                 this.data.error_message = fgettext_ne("The value must be greater than $1.", setting.min)
471                                 this.data.entered_text = fields["te_setting_value"]
472                                 core.update_formspec(this:get_formspec())
473                                 return true
474                         end
475                         if setting.max and new_value > setting.max then
476                                 this.data.error_message = fgettext_ne("The value must be lower than $1.", setting.max)
477                                 this.data.entered_text = fields["te_setting_value"]
478                                 core.update_formspec(this:get_formspec())
479                                 return true
480                         end
481                         core.setting_set(setting.name, new_value)
482
483                 elseif setting.type == "float" then
484                         local new_value = tonumber(fields["te_setting_value"])
485                         if not new_value then
486                                 this.data.error_message = fgettext_ne("Please enter a valid number.")
487                                 this.data.entered_text = fields["te_setting_value"]
488                                 core.update_formspec(this:get_formspec())
489                                 return true
490                         end
491                         core.setting_set(setting.name, new_value)
492
493                 elseif setting.type == "flags" then
494                         local new_value = fields["te_setting_value"]
495                         for _,value in ipairs(new_value:split(",", true)) do
496                                 value = value:trim()
497                                 if not value:match(CHAR_CLASSES.FLAGS .. "+")
498                                                 or not setting.possible:match("[,]?" .. value .. "[,]?") then
499                                         this.data.error_message = fgettext_ne("\"$1\" is not a valid flag.", value)
500                                         this.data.entered_text = fields["te_setting_value"]
501                                         core.update_formspec(this:get_formspec())
502                                         return true
503                                 end
504                         end
505                         core.setting_set(setting.name, new_value)
506
507                 else
508                         local new_value = fields["te_setting_value"]
509                         core.setting_set(setting.name, new_value)
510                 end
511                 core.setting_save()
512                 this:delete()
513                 return true
514         end
515
516         if fields["btn_cancel"] then
517                 this:delete()
518                 return true
519         end
520
521         if fields["btn_browser_path"] then
522                 core.show_file_open_dialog("dlg_browse_path", fgettext_ne("Select path"))
523         end
524
525         if fields["dlg_browse_path_accepted"] then
526                 this.data.selected_path = fields["dlg_browse_path_accepted"]
527                 core.update_formspec(this:get_formspec())
528         end
529
530         return false
531 end
532
533 local function create_settings_formspec(tabview, name, tabdata)
534         local formspec = "tablecolumns[color;tree;text;text]" ..
535                                         "tableoptions[background=#00000000;border=false]" ..
536                                         "table[0,0;12,4.5;list_settings;"
537
538         local current_level = 0
539         for _, entry in ipairs(settings) do
540                 local name
541                 if not core.setting_getbool("main_menu_technical_settings") and entry.readable_name then
542                         name = fgettext_ne(entry.readable_name)
543                 else
544                         name = entry.name
545                 end
546
547                 if entry.type == "category" then
548                         current_level = entry.level
549                         formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",,"
550
551                 elseif entry.type == "bool" then
552                         local value = get_current_value(entry)
553                         if core.is_yes(value) then
554                                 value = fgettext("Enabled")
555                         else
556                                 value = fgettext("Disabled")
557                         end
558                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
559                                         .. value .. ","
560
561                 elseif entry.type == "key" then
562                         -- ignore key settings, since we have a special dialog for them
563
564                 else
565                         formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. ","
566                                         .. core.formspec_escape(get_current_value(entry)) .. ","
567                 end
568         end
569
570         if #settings > 0 then
571                 formspec = formspec:sub(1, -2) -- remove trailing comma
572         end
573         formspec = formspec .. ";" .. selected_setting .. "]" ..
574                         "button[4,4.5;3,1;btn_change_keys;".. fgettext("Change keys") .. "]" ..
575                         "button[10,4.5;2,1;btn_edit;" .. fgettext("Edit") .. "]" ..
576                         "button[7,4.5;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" ..
577                         "checkbox[0,4.5;cb_tech_settings;" .. fgettext("Show technical names") .. ";"
578                                         .. dump(core.setting_getbool("main_menu_technical_settings")) .. "]"
579
580         return formspec
581 end
582
583 local function handle_settings_buttons(this, fields, tabname, tabdata)
584         local list_enter = false
585         if fields["list_settings"] then
586                 selected_setting = core.get_table_index("list_settings")
587                 if  core.explode_table_event(fields["list_settings"]).type == "DCL" then
588                         -- Directly toggle booleans
589                         local setting = settings[selected_setting]
590                         if setting.type == "bool" then
591                                 local current_value = get_current_value(setting)
592                                 core.setting_setbool(setting.name, not core.is_yes(current_value))
593                                 core.setting_save()
594                                 return true
595                         else
596                                 list_enter = true
597                         end
598                 else
599                         return true
600                 end
601         end
602
603         if fields["btn_edit"] or list_enter then
604                 local setting = settings[selected_setting]
605                 if setting.type ~= "category" then
606                         local edit_dialog = dialog_create("change_setting", create_change_setting_formspec,
607                                         handle_change_setting_buttons)
608                         edit_dialog:set_parent(this)
609                         this:hide()
610                         edit_dialog:show()
611                 end
612                 return true
613         end
614
615         if fields["btn_restore"] then
616                 local setting = settings[selected_setting]
617                 if setting.type ~= "category" then
618                         core.setting_set(setting.name, setting.default)
619                         core.setting_save()
620                         core.update_formspec(this:get_formspec())
621                 end
622                 return true
623         end
624
625         if fields["btn_change_keys"] then
626                 core.show_keys_menu()
627                 return true
628         end
629
630         if fields["cb_tech_settings"] then
631                 core.setting_set("main_menu_technical_settings", fields["cb_tech_settings"])
632                 core.setting_save()
633                 core.update_formspec(this:get_formspec())
634                 return true
635         end
636
637         return false
638 end
639
640 tab_settings = {
641         name = "settings",
642         caption = fgettext("Settings"),
643         cbf_formspec = create_settings_formspec,
644         cbf_button_handler = handle_settings_buttons,
645 }
646
647 local function create_minetest_conf_example()
648         local result = "#    This file contains a list of all available settings and their default value for minetest.conf\n" ..
649                         "\n" ..
650                         "#    By default, all the settings are commented and not functional.\n" ..
651                         "#    Uncomment settings by removing the preceding #.\n" ..
652                         "\n" ..
653                         "#    minetest.conf is read by default from:\n" ..
654                         "#    ../minetest.conf\n" ..
655                         "#    ../../minetest.conf\n" ..
656                         "#    Any other path can be chosen by passing the path as a parameter\n" ..
657                         "#    to the program, eg. \"minetest.exe --config ../minetest.conf.example\".\n" ..
658                         "\n" ..
659                         "#    Further documentation:\n" ..
660                         "#    http://wiki.minetest.net/\n" ..
661                         "\n"
662
663         local settings = parse_config_file(true, false)
664         for _, entry in ipairs(settings) do
665                 if entry.type == "category" then
666                         if entry.level == 0 then
667                                 result = result .. "#\n# " .. entry.name .. "\n#\n\n"
668                         else
669                                 for i = 1, entry.level do
670                                         result = result .. "#"
671                                 end
672                                 result = result .. "# " .. entry.name .. "\n\n"
673                         end
674                 else
675                         if entry.comment ~= "" then
676                                 for _, comment_line in ipairs(entry.comment:split("\n", true)) do
677                                         result = result .."#    " .. comment_line .. "\n"
678                                 end
679                         end
680                         result = result .. "#    type: " .. entry.type
681                         if entry.min then
682                                 result = result .. " min: " .. entry.min
683                         end
684                         if entry.max then
685                                 result = result .. " max: " .. entry.max
686                         end
687                         if entry.values then
688                                 result = result .. " values: " .. table.concat(entry.values, ", ")
689                         end
690                         if entry.possible then
691                                 result = result .. " possible values: " .. entry.possible:gsub(",", ", ")
692                         end
693                         result = result .. "\n"
694                         result = result .. "# " .. entry.name .. " = ".. entry.default .. "\n\n"
695                 end
696         end
697         return result
698 end
699
700 local function create_translation_file()
701         local result = "// This file is automatically generated\n" ..
702                         "// It conatins a bunch of fake gettext calls, to tell xgettext about the strings in config files\n" ..
703                         "// To update it, refer to the bottom of builtin/mainmenu/tab_settings.lua\n\n" ..
704                         "fake_function() {\n"
705
706         local settings = parse_config_file(true, false)
707         for _, entry in ipairs(settings) do
708                 if entry.type == "category" then
709                         local name_escaped = entry.name:gsub("\"", "\\\"")
710                         result = result .. "\tgettext(\"" .. name_escaped .. "\");\n"
711                 else
712                         if entry.readable_name then
713                                 local readable_name_escaped = entry.readable_name:gsub("\"", "\\\"")
714                                 result = result .. "\tgettext(\"" .. readable_name_escaped .. "\");\n"
715                         end
716                         if entry.comment ~= "" then
717                                 local comment_escaped = entry.comment:gsub("\n", "\\n")
718                                 comment_escaped = comment_escaped:gsub("\"", "\\\"")
719                                 result = result .. "\tgettext(\"" .. comment_escaped .. "\");\n"
720                         end
721                 end
722         end
723         result = result .. "}\n"
724         return result
725 end
726
727 if false then
728         local file = io.open("minetest.conf.example", "w")
729         if file then
730                 file:write(create_minetest_conf_example())
731                 file:close()
732         end
733 end
734
735 if false then
736         local file = io.open("src/settings_translation_file.cpp", "w")
737         if file then
738                 file:write(create_translation_file())
739                 file:close()
740         end
741 end