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