Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Tools/langtool/src/main.rs
5972 views
1
use std::io;
2
3
use std::collections::BTreeMap;
4
5
mod section;
6
use section::{Section, line_value};
7
8
mod inifile;
9
use inifile::IniFile;
10
11
mod chatgpt;
12
use clap::Parser;
13
14
mod util;
15
16
use crate::{chatgpt::ChatGPT, section::split_line, util::ask_letter};
17
18
#[derive(Parser, Debug)]
19
struct Args {
20
#[command(subcommand)]
21
cmd: Command,
22
#[arg(short, long)]
23
dry_run: bool,
24
#[arg(short, long)]
25
verbose: bool,
26
// gpt-5, gpt-5-mini, gpt-5-nano, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, o3, o4-mini, gpt-4o, gpt-4o-realtime-preview
27
#[arg(short, long, default_value = "gpt-4o-mini")]
28
model: String,
29
}
30
31
#[derive(Parser, Debug)]
32
enum Command {
33
CopyMissingLines {
34
#[arg(short, long)]
35
dont_comment_missing: bool,
36
},
37
ListUnknownLines {},
38
CommentUnknownLines {},
39
RemoveUnknownLines {},
40
AddNewKey {
41
section: String,
42
key: String,
43
},
44
AddNewKeyAI {
45
section: String,
46
key: String,
47
extra: Option<String>,
48
#[arg(short, long)]
49
overwrite_translated: bool,
50
},
51
AddNewKeyValueAI {
52
section: String,
53
key: String,
54
value: String,
55
extra: Option<String>,
56
#[arg(short, long)]
57
overwrite_translated: bool,
58
},
59
AddNewKeyValue {
60
section: String,
61
key: String,
62
value: String,
63
},
64
MoveKey {
65
old: String,
66
new: String,
67
key: String,
68
},
69
CopyKey {
70
old_section: String,
71
new_section: String,
72
key: String,
73
},
74
DupeKey {
75
section: String,
76
old: String,
77
new: String,
78
},
79
RenameKey {
80
section: String,
81
old: String,
82
new: String,
83
},
84
SortSection {
85
section: String,
86
},
87
RemoveKey {
88
section: String,
89
key: String,
90
},
91
GetNewKeys,
92
ImportSingle {
93
filename: String,
94
section: String,
95
key: String,
96
},
97
CheckRefKeys,
98
FixupRefKeys, // This was too big a job.
99
FinishLanguageWithAI {
100
language: String,
101
section: Option<String>,
102
},
103
RemoveLinebreaks {
104
section: String,
105
key: String,
106
},
107
ApplyRegex {
108
section: String,
109
key: String,
110
pattern: String,
111
replacement: Option<String>,
112
},
113
SplitKey {
114
section: String,
115
key: String,
116
},
117
}
118
119
fn copy_missing_lines(
120
reference_ini: &IniFile,
121
target_ini: &mut IniFile,
122
comment_missing: bool,
123
) -> io::Result<()> {
124
for reference_section in &reference_ini.sections {
125
// Insert any missing full sections.
126
if !target_ini.insert_section_if_missing(reference_section) {
127
if let Some(target_section) = target_ini.get_section_mut(&reference_section.name) {
128
for line in &reference_section.lines {
129
target_section.insert_line_if_missing(line);
130
}
131
132
//target_section.remove_lines_if_not_in(reference_section);
133
if comment_missing {
134
target_section.comment_out_lines_if_not_in(reference_section);
135
}
136
}
137
} else {
138
// Note: insert_section_if_missing will copy the entire section,
139
// no need to loop over the lines here.
140
println!("Inserted missing section: {}", reference_section.name);
141
}
142
}
143
Ok(())
144
}
145
146
enum UnknownLineAction {
147
Remove,
148
Comment,
149
Log,
150
}
151
152
fn deal_with_unknown_lines(
153
reference_ini: &IniFile,
154
target_ini: &mut IniFile,
155
action: UnknownLineAction,
156
) -> io::Result<()> {
157
for reference_section in &reference_ini.sections {
158
if let Some(target_section) = target_ini.get_section_mut(&reference_section.name) {
159
match action {
160
UnknownLineAction::Remove => {
161
target_section.remove_lines_if_not_in(reference_section)
162
}
163
UnknownLineAction::Comment => {
164
target_section.comment_out_lines_if_not_in(reference_section)
165
}
166
UnknownLineAction::Log => {
167
let unknown_lines = target_section.get_lines_not_in(reference_section);
168
if !unknown_lines.is_empty() {
169
println!("Unknown lines in section [{}]:", target_section.name);
170
for line in unknown_lines {
171
println!(" {}", line);
172
}
173
}
174
}
175
}
176
}
177
}
178
Ok(())
179
}
180
181
fn print_keys_if_not_in(
182
reference_ini: &IniFile,
183
target_ini: &mut IniFile,
184
header: &str,
185
) -> io::Result<()> {
186
for reference_section in &reference_ini.sections {
187
if let Some(target_section) = target_ini.get_section_mut(&reference_section.name) {
188
let keys = target_section.get_keys_if_not_in(reference_section);
189
if !keys.is_empty() {
190
println!("{} ({})", reference_section.name, header);
191
for key in &keys {
192
println!("- {key}");
193
}
194
}
195
}
196
}
197
Ok(())
198
}
199
200
fn move_key(target_ini: &mut IniFile, old: &str, new: &str, key: &str) -> io::Result<()> {
201
if let Some(old_section) = target_ini.get_section_mut(old) {
202
if let Some(line) = old_section.remove_line(key) {
203
if let Some(new_section) = target_ini.get_section_mut(new) {
204
new_section.insert_line_if_missing(&line);
205
} else {
206
println!("No new section {new}");
207
}
208
} else {
209
println!("No key {key} in section {old}");
210
}
211
} else {
212
println!("No old section {old}");
213
}
214
Ok(())
215
}
216
217
fn copy_key(target_ini: &mut IniFile, old: &str, new: &str, key: &str) -> io::Result<()> {
218
if let Some(old_section) = target_ini.get_section_mut(old) {
219
if let Some(line) = old_section.get_line(key) {
220
if let Some(new_section) = target_ini.get_section_mut(new) {
221
new_section.insert_line_if_missing(&line);
222
} else {
223
println!("No new section {new}");
224
}
225
} else {
226
println!("No key {key} in section {old}");
227
}
228
} else {
229
println!("No old section {old}");
230
}
231
Ok(())
232
}
233
234
fn remove_key(target_ini: &mut IniFile, section: &str, key: &str) -> io::Result<()> {
235
if let Some(old_section) = target_ini.get_section_mut(section) {
236
old_section.remove_line(key);
237
} else {
238
println!("No section {section}");
239
}
240
Ok(())
241
}
242
243
fn remove_linebreaks(target_ini: &mut IniFile, section: &str, key: &str) -> io::Result<()> {
244
if let Some(old_section) = target_ini.get_section_mut(section) {
245
old_section.remove_linebreaks(key);
246
} else {
247
println!("No section {section}");
248
}
249
Ok(())
250
}
251
252
fn add_new_key(
253
target_ini: &mut IniFile,
254
section: &str,
255
key: &str,
256
value: &str,
257
overwrite_translated: bool,
258
) -> io::Result<()> {
259
if let Some(section) = target_ini.get_section_mut(section) {
260
if !overwrite_translated {
261
if let Some(existing_value) = section.get_value(key)
262
&& existing_value != key
263
{
264
// This one was already translated. Skip it.
265
println!(
266
"Key '{key}' already has a translated value '{existing_value}', skipping."
267
);
268
return Ok(());
269
}
270
}
271
section.insert_line_if_missing(&format!("{key} = {value}"));
272
} else {
273
println!("No section {section}");
274
}
275
Ok(())
276
}
277
278
fn check_keys(target_ini: &IniFile) -> io::Result<()> {
279
for section in &target_ini.sections {
280
let mut mismatches = Vec::new();
281
282
if section.name == "DesktopUI" {
283
// ignore the ampersands for now
284
continue;
285
}
286
287
for line in &section.lines {
288
if let Some((key, value)) = split_line(line) {
289
if key != value {
290
mismatches.push((key, value));
291
}
292
}
293
}
294
295
if !mismatches.is_empty() {
296
println!("[{}]", section.name);
297
for (key, value) in mismatches {
298
println!(" {key} != {value}");
299
}
300
}
301
}
302
Ok(())
303
}
304
305
fn fixup_keys(target_ini: IniFile, dry_run: bool) -> io::Result<()> {
306
for section in &target_ini.sections {
307
let mut mismatches = Vec::new();
308
309
if section.name == "DesktopUI"
310
|| section.name == "MappableControls"
311
|| section.name == "PostShaders"
312
{
313
// ignore the ampersands for now, also mappable controls, we don't want to change those strings.
314
continue;
315
}
316
317
for line in &section.lines {
318
if let Some((key, value)) = split_line(line)
319
&& key != value
320
{
321
mismatches.push((key, value));
322
}
323
}
324
325
if !mismatches.is_empty() {
326
println!("[{}]", section.name);
327
for (key, value) in mismatches {
328
if (key.len() as i32 - value.len() as i32).abs() > 15 {
329
println!(" (skipping {key} = {value} (probably an alias))");
330
continue;
331
}
332
if value.contains(r"\n") {
333
println!(" (skipping {key} = {value} (line break))");
334
continue;
335
}
336
if value.contains("×") || value.contains("\"") {
337
println!(" (skipping {key} = {value} (symbol))");
338
continue;
339
}
340
if key.contains("ardboard") {
341
println!(" (skipping {key} = {value} (cardboard))");
342
continue;
343
}
344
if key.contains("translators") {
345
continue;
346
}
347
348
let _ = cli_clipboard::set_contents(format!("\"{key}\""));
349
350
match ask_letter(&format!(" '{key}' != '{value}' ? >\n"), "ynrd") {
351
'y' => execute_command(
352
Command::RenameKey {
353
section: section.name.clone(),
354
old: key.to_string(),
355
new: value.to_string(),
356
},
357
None,
358
dry_run,
359
false,
360
),
361
'r' => {
362
println!("reverse fixup not supported yet");
363
}
364
'q' => {
365
println!("Cancelled! Quitting.");
366
return Ok(());
367
}
368
'd' => execute_command(
369
Command::RemoveKey {
370
section: section.name.clone(),
371
key: key.to_string(),
372
},
373
None,
374
dry_run,
375
false,
376
),
377
378
'n' => {}
379
_ => {
380
println!("Invalid response, ignoring.");
381
}
382
}
383
}
384
}
385
}
386
Ok(())
387
}
388
389
fn finish_language_with_ai(
390
target_language: &str,
391
target_ini: &mut IniFile,
392
ref_ini: &IniFile,
393
section: Option<&str>,
394
ai: &ChatGPT,
395
dry_run: bool,
396
) -> anyhow::Result<()> {
397
println!("Finishing language with AI");
398
println!(
399
"Step 1: Compare all strings in the section with the matching strings from the reference."
400
);
401
402
let sections: Vec<Section> = if let Some(section_name) = section {
403
vec![target_ini.get_section(section_name).unwrap().clone()]
404
} else {
405
target_ini.sections.to_vec()
406
};
407
408
let base_prompt = format!(
409
"Please translate the below list of strings from US English to {target_language}.
410
After the strings to translate, there are related already-translated strings that may help for context.
411
Note that the strings are UI strings for my PSP emulator application.
412
Also, please output similarly to the input, with section headers and key=value pairs. The section name
413
is not to be translated.
414
415
Here are the strings to translate:
416
"
417
);
418
let suffix = " Do not output any text before or after the list of translated strings, do not ask followups.
419
'undo state' means a saved state that's been saved so that the last save state operation can be undone.
420
DO NOT translate strings like DDMMYYYY, MMDDYYYY and similar technical letters and designations. Not even
421
translating the individual letters, they need to be kept as-is.
422
'JIT using IR' should be interpreted as 'JIT, with IR'.
423
Don't translate strings about 'load undo state' or 'save undo state', also not about savestate slots.
424
IMPORTANT! 'Notification screen position' means the position on the screen where notifications are displayed,
425
not the position of a 'notification screen', no such thing.
426
%1 is a placeholder for a number or word, do not change it, just make sure it ends up in the right location.
427
A 'driver manager' is a built-in tool to manage drivers, not a human boss. Same goes for other types of manager.
428
The '=' at the end of the lines to translate is not part of the translation keys.
429
";
430
431
for section in sections {
432
let Some(ref_section) = ref_ini.get_section(&section.name) else {
433
println!("Section '{}' not found in reference file", section.name);
434
continue;
435
};
436
let mut alias_map = BTreeMap::new();
437
let mut alias_inverse_map = BTreeMap::new();
438
for line in &ref_section.lines {
439
if let Some((key, value)) = split_line(line) {
440
// We actually process almost everything here, we could check for case but we don't
441
// since the aliased case is better.
442
if key != value {
443
println!("Saving alias: {key} = {value}");
444
alias_map.insert(key, value.to_string());
445
alias_inverse_map.insert(value.to_string(), key);
446
}
447
}
448
}
449
450
// When just testing aliases.
451
// return Ok(());
452
453
let mut untranslated_keys = vec![];
454
let mut translated_keys = vec![];
455
for line in &section.lines {
456
if let Some((key, value)) = split_line(line) {
457
if let Some(ref_value) = ref_section.get_value(key) {
458
if value == ref_value {
459
// Key not translated.
460
// However, we need to reject some things that the AI likes to mishandle.
461
if value.to_uppercase() == value {
462
println!(
463
"Skipping untranslated key '{}' with uppercase value '{}'",
464
key, value
465
);
466
continue;
467
}
468
untranslated_keys.push((key, ref_value));
469
} else {
470
translated_keys.push((key, value));
471
}
472
} else {
473
println!(
474
"Key '{}' not found in reference section '{}'",
475
key, ref_section.name
476
);
477
}
478
}
479
}
480
481
println!(
482
"[{}]: Found {} untranslated keys",
483
section.name,
484
untranslated_keys.len()
485
);
486
if untranslated_keys.is_empty() {
487
continue;
488
}
489
490
for (key, ref_value) in &untranslated_keys {
491
println!(" - '{} (ref: '{}')", key, ref_value);
492
}
493
494
// Here you would call the AI to translate the keys.
495
let section_prompt = format!(
496
"{base_prompt}\n\n[{}]\n{}\n\n\n\nBelow are the already translated strings for context, don't re-translate these:\n\n{}\n\n{}",
497
section.name,
498
untranslated_keys
499
.iter()
500
.map(|(k, _v)| format!("{} = ", alias_map.get(k).unwrap_or(&k.to_string())))
501
.collect::<Vec<String>>()
502
.join("\n"),
503
translated_keys
504
.iter()
505
.map(|(k, v)| format!("{} = {}", k, v))
506
.collect::<Vec<String>>()
507
.join("\n"),
508
suffix
509
);
510
println!("[{}] AI prompt:\n{}", section.name, section_prompt);
511
if !dry_run {
512
println!("Running AI translation...");
513
let response = ai
514
.chat(&section_prompt)
515
.map_err(|e| anyhow::anyhow!("chat failed: {e}"))?;
516
println!("AI response:\n{}", response);
517
// Now we just need to merge the AI response into the target_ini.
518
let parsed_response = IniFile::parse_string(&response)
519
.map_err(|e| anyhow::anyhow!("Failed to parse AI response: {e}"))?;
520
if parsed_response.sections.is_empty() {
521
println!("No sections found in AI response! bad!");
522
}
523
let target_section = target_ini.get_section_mut(&section.name).unwrap();
524
for parsed_section in parsed_response.sections {
525
if parsed_section.name == section.name {
526
println!("Merging AI response for section '{}'", parsed_section.name);
527
for line in &parsed_section.lines {
528
if let Some((key, value)) = split_line(line) {
529
// Put the key through the inverse alias map.
530
let original_key = alias_inverse_map.get(key).unwrap_or(&key);
531
print!("Updating '{}': {}", original_key, value);
532
if key != *original_key {
533
println!(" ({})", key);
534
} else {
535
println!();
536
}
537
if !target_section.set_value(original_key, value, Some("AI translated"))
538
{
539
println!("Failed to update '{}'", original_key);
540
}
541
}
542
}
543
} else {
544
println!("Mismatched section name '{}'", parsed_section.name);
545
}
546
}
547
}
548
}
549
550
Ok(())
551
}
552
553
fn rename_key(target_ini: &mut IniFile, section: &str, old: &str, new: &str) -> io::Result<()> {
554
if let Some(section) = target_ini.get_section_mut(section) {
555
section.rename_key(old, new);
556
} else {
557
println!("No section {section}");
558
}
559
Ok(())
560
}
561
562
fn apply_regex(
563
target_ini: &mut IniFile,
564
section: &str,
565
key: &str,
566
pattern: &str,
567
replacement: &str,
568
) -> io::Result<()> {
569
if let Some(section) = target_ini.get_section_mut(section) {
570
section.apply_regex(key, pattern, replacement);
571
} else {
572
println!("No section {section}");
573
}
574
Ok(())
575
}
576
577
fn split_key(target_ini: &mut IniFile, section: &str, key: &str) -> io::Result<()> {
578
if let Some(section) = target_ini.get_section_mut(section) {
579
section.split_key(key);
580
} else {
581
println!("No section {section}");
582
}
583
Ok(())
584
}
585
586
fn dupe_key(target_ini: &mut IniFile, section: &str, old: &str, new: &str) -> io::Result<()> {
587
if let Some(section) = target_ini.get_section_mut(section) {
588
section.dupe_key(old, new);
589
} else {
590
println!("No section {section}");
591
}
592
Ok(())
593
}
594
595
fn sort_section(target_ini: &mut IniFile, section: &str) -> io::Result<()> {
596
if let Some(section) = target_ini.get_section_mut(section) {
597
section.sort();
598
} else {
599
println!("No section {section}");
600
}
601
Ok(())
602
}
603
604
fn generate_prompt(filenames: &[String], section: &str, value: &str, extra: &str) -> String {
605
let languages = filenames
606
.iter()
607
.map(|filename| filename.split_once(".").unwrap().0)
608
.collect::<Vec<&str>>()
609
.join(", ");
610
611
let base_str = format!("Please translate '{value}' from US English to all of these languages: {languages}.
612
Output in json format, a single dictionary, key=value. Include en_US first (the original string).
613
For context, the string will be in the translation section '{section}', and these strings are UI strings for my PSP emulator application.
614
Keep the strings relatively short, don't let them become more than 40% longer than the original string.
615
Do not output any text before or after the list of translated strings, do not ask followups.
616
{extra}");
617
618
base_str
619
}
620
621
fn parse_response(response: &str) -> Option<BTreeMap<String, String>> {
622
// Try to find JSON in the response (it might have other text around it)
623
let response = response.trim();
624
625
// Look for JSON object boundaries
626
let start = response.find('{')?;
627
let end = response.rfind('}')? + 1;
628
let json_str = &response[start..end];
629
630
// Parse the JSON into a BTreeMap
631
match serde_json::from_str::<BTreeMap<String, serde_json::Value>>(json_str) {
632
Ok(json_map) => {
633
let mut result = BTreeMap::new();
634
for (key, value) in json_map {
635
// Convert JSON values to strings
636
let string_value = match value {
637
serde_json::Value::String(s) => s,
638
_ => value.to_string().trim_matches('"').to_string(),
639
};
640
result.insert(key, string_value);
641
}
642
Some(result)
643
}
644
Err(e) => {
645
eprintln!("Failed to parse JSON response: {}", e);
646
eprintln!("JSON string was: {}", json_str);
647
None
648
}
649
}
650
}
651
652
fn main() {
653
let opt = Args::parse();
654
655
let api_key = std::env::var("OPENAI_API_KEY").ok();
656
let ai = api_key.map(|key| chatgpt::ChatGPT::new(key, opt.model));
657
658
// TODO: Grab extra arguments from opt somehow.
659
// let args: Vec<String> = vec![]; //std::env::args().skip(1).collect();
660
execute_command(opt.cmd, ai.as_ref(), opt.dry_run, opt.verbose);
661
}
662
663
fn execute_command(cmd: Command, ai: Option<&ChatGPT>, dry_run: bool, verbose: bool) {
664
let root = "../../assets/lang";
665
let reference_ini_filename = "en_US.ini";
666
667
let mut reference_ini =
668
IniFile::parse_file(&format!("{root}/{reference_ini_filename}")).unwrap();
669
670
let mut filenames = Vec::new();
671
if filenames.is_empty() {
672
// Grab them all.
673
for path in std::fs::read_dir(root).unwrap() {
674
let path = path.unwrap();
675
if path.file_name() == reference_ini_filename {
676
continue;
677
}
678
let filename = path.file_name();
679
let filename = filename.to_string_lossy();
680
if !filename.ends_with(".ini") {
681
continue;
682
}
683
filenames.push(path.file_name().to_string_lossy().to_string());
684
}
685
}
686
687
let mut single_ini_section: Option<Section> = None;
688
if let Command::ImportSingle {
689
filename,
690
section,
691
key: _,
692
} = &cmd
693
{
694
if let Ok(single_ini) = IniFile::parse_file(filename) {
695
if let Some(single_section) = single_ini.get_section("Single") {
696
single_ini_section = Some(single_section.clone());
697
} else {
698
println!("No section {section} in {filename}");
699
}
700
} else {
701
println!("Failed to parse {filename}");
702
return;
703
}
704
}
705
706
if let Command::FinishLanguageWithAI { language, section } = &cmd {
707
if let Some(ai) = &ai {
708
let target_ini_filename = format!("{root}/{language}.ini");
709
let mut target_ini = IniFile::parse_file(&target_ini_filename).unwrap();
710
finish_language_with_ai(
711
language,
712
&mut target_ini,
713
&reference_ini,
714
section.as_deref(),
715
ai,
716
dry_run,
717
)
718
.unwrap();
719
if !dry_run {
720
println!("Writing modified file for target language: {}", language);
721
target_ini.write().unwrap();
722
}
723
} else {
724
println!("FinishLanguageWithAI: AI key not set, skipping AI command.");
725
}
726
return;
727
}
728
729
// This is a bit ugly, but we need to generate the AI response before processing files.
730
let ai_response = if let Command::AddNewKeyAI {
731
section,
732
key,
733
extra,
734
overwrite_translated: _,
735
} = &cmd
736
{
737
match generate_ai_response(ai, &filenames, section, key, extra) {
738
Some(value) => value,
739
None => return,
740
}
741
} else if let Command::AddNewKeyValueAI {
742
section,
743
key: _, // We don't need the key here, it's used later when writing to the ini file.
744
value,
745
extra,
746
overwrite_translated: _,
747
} = &cmd
748
{
749
match generate_ai_response(ai, &filenames, section, value, extra) {
750
Some(value) => value,
751
None => return,
752
}
753
} else {
754
None
755
};
756
757
for filename in &filenames {
758
let reference_ini = &reference_ini;
759
if filename == "langtool" {
760
// Get this from cargo run for some reason.
761
continue;
762
}
763
let target_ini_filename = format!("{root}/{filename}");
764
if verbose {
765
println!("Langtool processing {target_ini_filename}");
766
}
767
768
let mut target_ini = IniFile::parse_file(&target_ini_filename).unwrap();
769
770
match cmd {
771
Command::ApplyRegex {
772
ref section,
773
ref key,
774
ref pattern,
775
ref replacement,
776
} => {
777
apply_regex(
778
&mut target_ini,
779
section,
780
key,
781
pattern,
782
replacement.as_ref().unwrap_or(&"".to_string()),
783
)
784
.unwrap();
785
}
786
Command::SplitKey {
787
ref section,
788
ref key,
789
} => {
790
split_key(&mut target_ini, section, key).unwrap();
791
}
792
Command::FinishLanguageWithAI {
793
language: _,
794
section: _,
795
} => {}
796
Command::FixupRefKeys => {}
797
Command::CheckRefKeys => {}
798
Command::CopyMissingLines {
799
dont_comment_missing,
800
} => {
801
copy_missing_lines(reference_ini, &mut target_ini, !dont_comment_missing).unwrap();
802
}
803
Command::CommentUnknownLines {} => {
804
deal_with_unknown_lines(reference_ini, &mut target_ini, UnknownLineAction::Comment)
805
.unwrap();
806
}
807
Command::RemoveUnknownLines {} => {
808
deal_with_unknown_lines(reference_ini, &mut target_ini, UnknownLineAction::Remove)
809
.unwrap();
810
}
811
Command::ListUnknownLines {} => {
812
deal_with_unknown_lines(reference_ini, &mut target_ini, UnknownLineAction::Log)
813
.unwrap();
814
}
815
Command::GetNewKeys => {
816
print_keys_if_not_in(reference_ini, &mut target_ini, &target_ini_filename).unwrap();
817
}
818
Command::SortSection { ref section } => sort_section(&mut target_ini, section).unwrap(),
819
Command::RenameKey {
820
ref section,
821
ref old,
822
ref new,
823
} => rename_key(&mut target_ini, section, old, new).unwrap(),
824
Command::AddNewKey {
825
ref section,
826
ref key,
827
} => add_new_key(&mut target_ini, section, key, key, false).unwrap(),
828
Command::AddNewKeyAI {
829
ref section,
830
ref key,
831
extra: _,
832
overwrite_translated,
833
} => {
834
let lang = filename.split_once('.').unwrap().0;
835
if let Some(ai_response) = &ai_response {
836
// Process it.
837
if let Some(translated_string) = ai_response.get(lang) {
838
println!("{lang}:");
839
add_new_key(
840
&mut target_ini,
841
section,
842
key,
843
&format!("{translated_string} # AI translated"),
844
overwrite_translated,
845
)
846
.unwrap();
847
} else {
848
println!("Language {lang} not found in response. Bailing.");
849
return;
850
}
851
}
852
}
853
Command::AddNewKeyValueAI {
854
ref section,
855
ref key,
856
value: _, // was translated above
857
extra: _,
858
overwrite_translated,
859
} => {
860
let lang = filename.split_once('.').unwrap().0;
861
if let Some(ai_response) = &ai_response {
862
// Process it.
863
if let Some(translated_string) = ai_response.get(lang) {
864
println!("{lang}:");
865
add_new_key(
866
&mut target_ini,
867
section,
868
key,
869
&format!("{translated_string} # AI translated"),
870
overwrite_translated,
871
)
872
.unwrap();
873
} else {
874
println!("Language {lang} not found in response. Bailing.");
875
return;
876
}
877
}
878
}
879
Command::AddNewKeyValue {
880
ref section,
881
ref key,
882
ref value,
883
} => add_new_key(&mut target_ini, section, key, value, false).unwrap(),
884
Command::MoveKey {
885
ref old,
886
ref new,
887
ref key,
888
} => {
889
move_key(&mut target_ini, old, new, key).unwrap();
890
}
891
Command::CopyKey {
892
// Copies between sections
893
ref old_section,
894
ref new_section,
895
ref key,
896
} => {
897
copy_key(&mut target_ini, old_section, new_section, key).unwrap();
898
}
899
Command::DupeKey {
900
ref section,
901
ref old,
902
ref new,
903
} => {
904
dupe_key(&mut target_ini, section, old, new).unwrap();
905
}
906
Command::RemoveKey {
907
ref section,
908
ref key,
909
} => {
910
remove_key(&mut target_ini, section, key).unwrap();
911
}
912
Command::RemoveLinebreaks {
913
ref section,
914
ref key,
915
} => {
916
remove_linebreaks(&mut target_ini, section, key).unwrap();
917
}
918
Command::ImportSingle {
919
filename: _,
920
ref section,
921
ref key,
922
} => {
923
let lang_id = filename.strip_suffix(".ini").unwrap();
924
if let Some(single_section) = &single_ini_section {
925
if let Some(target_section) = target_ini.get_section_mut(section) {
926
if let Some(single_line) = single_section.get_line(lang_id) {
927
if let Some(value) = line_value(&single_line) {
928
println!(
929
"Inserting value {value} for key {key} in section {section} in {target_ini_filename}"
930
);
931
if !target_section.insert_line_if_missing(&format!(
932
"{key} = {value} # AI translated"
933
)) {
934
// Didn't insert it, so it exists. We need to replace it.
935
target_section.set_value(key, value, Some("AI translated"));
936
}
937
}
938
} else {
939
println!("No lang_id {lang_id} in single section");
940
}
941
} else {
942
println!("No section {section} in {target_ini_filename}");
943
}
944
} else {
945
println!("No section {section} in {filename}");
946
}
947
}
948
}
949
950
if !dry_run {
951
target_ini.write().unwrap();
952
}
953
}
954
955
println!("Langtool processing reference {reference_ini_filename}");
956
957
// Some commands also apply to the reference ini.
958
match cmd {
959
Command::ApplyRegex {
960
ref section,
961
ref key,
962
ref pattern,
963
ref replacement,
964
} => {
965
apply_regex(
966
&mut reference_ini,
967
section,
968
key,
969
pattern,
970
replacement.as_ref().unwrap_or(&"".to_string()),
971
)
972
.unwrap();
973
}
974
Command::FinishLanguageWithAI {
975
language: _,
976
section: _,
977
} => {}
978
Command::CheckRefKeys => check_keys(&reference_ini).unwrap(),
979
Command::FixupRefKeys => fixup_keys(reference_ini.clone(), dry_run).unwrap(),
980
Command::AddNewKey {
981
ref section,
982
ref key,
983
} => {
984
add_new_key(&mut reference_ini, section, key, key, false).unwrap();
985
}
986
Command::AddNewKeyAI {
987
ref section,
988
ref key,
989
ref extra,
990
overwrite_translated,
991
} => {
992
if ai_response.is_some() {
993
let _ = extra;
994
add_new_key(&mut reference_ini, section, key, key, overwrite_translated).unwrap();
995
}
996
}
997
Command::AddNewKeyValueAI {
998
ref section,
999
ref key,
1000
ref value,
1001
extra,
1002
overwrite_translated,
1003
} => {
1004
if ai_response.is_some() {
1005
let _ = extra;
1006
add_new_key(
1007
&mut reference_ini,
1008
section,
1009
key,
1010
value,
1011
overwrite_translated,
1012
)
1013
.unwrap();
1014
}
1015
}
1016
Command::AddNewKeyValue {
1017
ref section,
1018
ref key,
1019
ref value,
1020
} => {
1021
add_new_key(&mut reference_ini, section, key, value, false).unwrap();
1022
}
1023
Command::SortSection { ref section } => sort_section(&mut reference_ini, section).unwrap(),
1024
Command::RenameKey {
1025
ref section,
1026
ref old,
1027
ref new,
1028
} => {
1029
if old == new {
1030
println!("WARNING: old == new");
1031
}
1032
rename_key(&mut reference_ini, section, old, new).unwrap();
1033
}
1034
Command::MoveKey {
1035
ref old,
1036
ref new,
1037
ref key,
1038
} => {
1039
move_key(&mut reference_ini, old, new, key).unwrap();
1040
}
1041
Command::CopyKey {
1042
// between sections
1043
ref old_section,
1044
ref new_section,
1045
ref key,
1046
} => {
1047
copy_key(&mut reference_ini, old_section, new_section, key).unwrap();
1048
}
1049
Command::DupeKey {
1050
// Inside a section, preserving a value
1051
ref section,
1052
ref old,
1053
ref new,
1054
} => {
1055
dupe_key(&mut reference_ini, section, old, new).unwrap();
1056
}
1057
Command::SplitKey {
1058
ref section,
1059
ref key,
1060
} => {
1061
split_key(&mut reference_ini, section, key).unwrap();
1062
}
1063
Command::RemoveKey {
1064
ref section,
1065
ref key,
1066
} => {
1067
remove_key(&mut reference_ini, section, key).unwrap();
1068
}
1069
Command::RemoveLinebreaks {
1070
ref section,
1071
ref key,
1072
} => {
1073
remove_linebreaks(&mut reference_ini, section, key).unwrap();
1074
}
1075
Command::CopyMissingLines {
1076
dont_comment_missing: _,
1077
} => {}
1078
Command::ListUnknownLines {} => {}
1079
Command::CommentUnknownLines {} => {}
1080
Command::RemoveUnknownLines {} => {}
1081
Command::GetNewKeys => {}
1082
Command::ImportSingle {
1083
filename: _,
1084
section: _,
1085
key: _,
1086
} => {}
1087
}
1088
1089
if !dry_run {
1090
reference_ini.write().unwrap();
1091
}
1092
}
1093
1094
fn generate_ai_response(
1095
ai: Option<&ChatGPT>,
1096
filenames: &[String],
1097
section: &str,
1098
key: &str,
1099
extra: &Option<String>,
1100
) -> Option<Option<BTreeMap<String, String>>> {
1101
let prompt = generate_prompt(
1102
filenames,
1103
section,
1104
key,
1105
&extra.clone().unwrap_or("".to_string()),
1106
);
1107
println!("generated prompt:\n{prompt}");
1108
Some(if let Some(ai) = &ai {
1109
println!("Using AI for translation...");
1110
let response = ai
1111
.chat(&prompt)
1112
.map_err(|e| anyhow::anyhow!("chat failed: {e}"))
1113
.unwrap();
1114
println!("AI response: {response}");
1115
if let Some(parsed) = parse_response(&response) {
1116
println!("Parsed: {:?}", parsed);
1117
1118
if parsed.len() < filenames.len() {
1119
println!(
1120
"Not enough languages generated! {} vs {}",
1121
parsed.len(),
1122
filenames.len()
1123
);
1124
}
1125
1126
Some(parsed)
1127
} else {
1128
println!("Failed to parse AI response, not doing anything.");
1129
return None;
1130
}
1131
} else {
1132
println!("AI key not set, skipping AI command.");
1133
return None;
1134
})
1135
}
1136
1137