aboutsummaryrefslogtreecommitdiff
path: root/scripts/langs.js
blob: b2f5287bb84b861af038d6d049e51906298d2267 (plain)
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
const fs = require("fs");
const dialog = require("enquirer");
const flat = require("flattenizer");
const args = require("minimist")(process.argv.slice(2), {
	boolean: [
		"help",
		"check",
		"format",
		"localize"
	],

	default: {
		"help": false,
		"check": false,
		"format": false,
		"localize": false
	}
})

console = require("../src/modules/console");

// help message
if (args["help"]) {
	console.log(`options:
  --help                  shows this help message
  --check                 checks for incorrectly formatted lang files
                          and missing localizations
  --format                formats all lang files correctly if the files
                          can be read and parsed
  --localize              allows you add missing incorrectly
                          localizations, and edit old ones
	`.trim()) // the trim removes the last blank newline

	process.exit(0);
}

// move into `scripts` folder, makes sure all file system requests work
// identically to `require()`
process.chdir(__dirname);

// get list of files in `src/lang/`, except for `maintainers.json` these
// should all be language files
let langs = fs.readdirSync("../src/lang");

// get the English language file and flatten it
let lang = flat.flatten(require("../src/lang/en.json"));

// formats all files automatically, nothing too fancy, it ignores
// `en.json` however, as its manually edited.
let format = (logging = true) => {
	// run through langs
	langs.forEach((locale_file) => {
		// ignore these files
		if (locale_file == "en.json"
			|| locale_file == "maintainers.json") {
			return;
		}

		// path to lang file
		let file_path = "../src/lang/" + locale_file;

		try {
			// attempt read, parse and flatten `file_path`
			let json = flat.flatten(
				JSON.parse(fs.readFileSync(file_path))
			)

			// sort `json`
			json = Object.fromEntries(
				Object.entries(json).sort()
			)

			// delete keys that are only found in `locale_file` but not
			// in the English localization file, if something doesn't
			// exist in the English localization file, then it shouldn't
			// exist at all!
			for (let i in json) {
				if (! lang[i]) {
					delete json[i];
				}
			}

			json = flat.unflatten(json);

			// attempt to stringify earlier JSON, with default
			// formatting, directly into `file_path`
			fs.writeFileSync(
				file_path, JSON.stringify(json, null, "\t")
			)
		}catch(err) {
			// something went wrong!
			console.error("Couldn't format: " + locale_file);
		}
	})

	console.ok("Formatted all localization files.");
}

// starts up a prompt interface to edit localization strings, letting
// you both add missing ones, and change old ones
let localize = async () => {
	// check if there's any missing keys
	let problems = check(false);

	// this'll have the `choices` for language picking prompt
	let lang_list = [];

	// run through langs
	langs.forEach((locale_file) => {
		// ignore these files
		if (locale_file == "en.json"
			|| locale_file == "maintainers.json") {
			return;
		}

		// default value
		let lang_name = locale_file;

		// are we missing any keys? if so, add a "(missing)" label at
		// the end of the language name
		let missing = (problems[lang_name] && problems[lang_name].length);
		if (missing) {
			lang_name += ` (missing: ${missing})`;
		}

		// add to list of langs
		lang_list.push(lang_name);
	})

	// prompt for which lang to edit
	let picked_lang = await new dialog.AutoComplete({
		choices: lang_list,
		message: "Pick a language to edit:",
	}).run()

	// remove extra labels after the lang file name itself
	picked_lang = picked_lang.replace(/ \(.*/, "");

	// this'll contain the languages flattened contents
	let lang_keys;

	try {
		// attempt to read, parse and flatten the language file
		lang_keys = flat.flatten(require("../src/lang/" + picked_lang));
	}catch (err) {
		// something went wrong!
		console.error("Couldn't read and parse language file");
		process.exit(1);
	}

	// should we just show the keys that are missing, or everything?
	let just_missing = false;

	// get just the flattened keys of the language
	let keys = Object.keys(lang_keys);

	// are there any missing keys?
	if (problems[picked_lang].length) {
		// prompt for whether we should only show missing keys
		just_missing = await new dialog.Confirm({
			message: "Add just missing keys without editing all keys?",
		}).run()

		// if we should just show missing keys, remove all other keys,
		// if we're allowed to show other keys, then we'll at least add
		// the missing keys
		if (just_missing) {
			keys = problems[picked_lang];
		} else {
			keys = [
				...problems[picked_lang],
				...keys,
			]
		}
	}

	// add "Save changes" option
	keys = [
		"Save changes",
		...keys
	]

	// add "(missing)" label to missing keys
	for (let i = 0; i < keys.length; i++) {
		if (! just_missing && problems[picked_lang].includes(keys[i])) {
			keys[i] = keys[i] + " \x1b[91m(missing)\x1b[0m";
		}
	}

	// this'll hold the flattened edits we make
	let edited_keys = {};

	// starts the process of editing a key
	let edit_key = async () => {
		// prompt for which key to edit
		let key_to_edit = await new dialog.AutoComplete({
			limit: 15,
			choices: [...keys],
			message: "Pick a key to edit:"
		}).run()

		// if "Save changes" was picked then return all the edits we've
		// made and stop prompting for new edits
		if (key_to_edit == "Save changes") {
			return edited_keys;
		}

		// strip labels from chosen key name
		key_to_edit = key_to_edit.split(" ")[0];

		// prompt for what to set the key to
		let edited_key = await new dialog.Input({
			type: "input",
			message: `Editing: ${key_to_edit}\n` +
			"  Original string: " + lang[key_to_edit] + "\n"
		}).run()

		// add the edited key in `edited_keys`
		edited_keys[key_to_edit] = edited_key;

		// add "(edited)" to the label of this key
		for (let i = 0; i < keys.length; i++) {
			if (keys[i].split(" ")[0] == key_to_edit) {
				keys[i] = key_to_edit + " \x1b[94m(edited)\x1b[0m";
			}
		}

		// clear screen and ask for the next edit to be made
		console.clear();
		return edit_key();
	}

	// start the process of key editing, whenever the below function
	// returns with the list of edits, it also only first returns when
	// all the changes have been made and "Save changes" has been
	// selected in the menu
	let changes = await edit_key();

	console.clear();

	try {
		// merge edits and original lang file, then unflatten them
		let final_json = flat.unflatten({
			...lang_keys,
			...changes
		})

		// attempt to write `final_json` to the language file
		fs.writeFileSync(
			"../src/lang/" + picked_lang,
			JSON.stringify(final_json, null, "\t")
		)

		console.ok("Saved changes: " + picked_lang);

		// check for changes
		check(true);

		// format everything
		format(true);
	}catch(err) {
		// something went wrong!
		console.error("Failed to save changes: " + picked_lang);
	}
}

// checks whether or not language files are missing any keys, and
// whether they're even parseable.
//
// an object will be returned containing information about each file and
// which, if any, keys are missing from them
let check = (logging = true) => {
	// this'll contain the missing keys for all the files, if any
	let problems = {};

	// this'll be changed to `true` if any errors at any point arise
	let has_problems = false;

	// get list of maintainers for each language
	let maintainers = require("../src/lang/maintainers.json");

	// run through langs
	langs.forEach((locale_file) => {
		// ignore this file, it's not a language file
		if (locale_file == "maintainers.json") {return}

		// this'll contain missing keys
		let missing = [];

		// this'll contain the flattened language file contents
		let locale = false;

		// this is the list of maintainers for this language
		let lang_maintainers = maintainers.list[
			locale_file.replace(/\..*$/, "")
		]

		// attempt read, parse and flatten language file
		try {
			locale = flat.flatten(require("../src/lang/" + locale_file));
		}catch(err) {
			// we couldn't parse it!
			if (logging) {
				has_problems = true;
				console.error(`!! ${locale_file} is not formatted right !!`);
			}

			return;
		}

		// run through keys, and note ones that are missing from
		// `en.json` in this lang
		for (let i in lang) {
			if (! locale[i]) {
				missing.push(i);
			}
		}

		// add missing keys to `problems`
		problems[locale_file] = missing;

		// was there any missing keys?
		if (missing.length > 0) {
			// this is a problem
			has_problems = true;

			// do nothing if we're not supposed to log anything
			if (! logging) {
				return;
			}

			// log language file with missing keys
			console.error(`${locale_file} is missing:`);

			// log missing keys
			for (let i in missing) {
				console.error(`  ${missing[i]}`);
			}

			// spacing
			console.log();

			// log the maintainers for this language
			console.log("Maintainers: ");
			for (let i in lang_maintainers) {
				console.log(`  ${lang_maintainers[i]}`);
			}

			console.log("\n");
		}
	})

	// if no problems occurred, and we can log things, then print that
	// everything went just fine!
	if (! has_problems && logging) {
		console.ok("All localizations are complete and parseable.");
	}

	return problems;
}

// run `check()` if `--check()` is set
if (args["check"]) {
	let has_problems = false;

	// check localizations, and set `has_problems` depending on whether
	// any localization files have problems
	Object.values(check()).forEach((item) => {
		if (item.length) {
			has_problems = true;
		}
	});

	// exit with the correct exit code
	if (has_problems) {
		process.exit(1);
	} else {
		process.exit();
	}
}

// run `format()` if `--format` is set
if (args["format"]) {
	format();
}

// run `localize()` if `--localize` is set
if (args["localize"]) {
	localize();
}