langtool: Let import-single create the key in en_US.ini too

It skipped the reference file, so adding a new string meant running
add-new-key-value first just to get the key into en_US.ini (and, as a side
effect, the English text into all 46 other files whether they were about to be
translated or not). Now an en_US line in the imported file is treated like any
other language, minus the "AI translated" comment, since that one is the string
the others were translated from rather than a translation.

So the workflow is one command plus validate. Languages left out of the import
simply don't get the key, and fall back to the English string at runtime -
copy-missing-lines is still there if you want the placeholder written out.

Instructions in AGENTS.md and the /add-string command updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-23 16:18:07 +02:00
1 parent 10bfda59ee
commit a1d205539c
3 files changed
+57 -46

No files matched your search

+16 -16
View File
@@ -13,11 +13,7 @@ Add and/or translate a PPSSPP UI string.
Follow the workflow in AGENTS.md under "Translated UI strings (assets/lang)". Run langtool from
`Tools/langtool`:
1. If an English string was given above:
`cargo run -- add-new-key-value "$1" "$2" "$3"`
That adds the key to every language file with the English text as a placeholder.
2. **Work out what the string actually means before translating it.** This is the part the tool's
1. **Work out what the string actually means before translating it.** This is the part the tool's
own AI commands can't do, and the whole reason you're doing this instead of them:
- Grep the C++ for the key to find the call site. What widget is it? A button, a checkbox
label, a tooltip, a error message?
@@ -29,29 +25,33 @@ Follow the workflow in AGENTS.md under "Translated UI strings (assets/lang)". Ru
Say briefly what you found before you start translating.
3. Write the translations to a scratch file outside the repo, in this shape:
2. Write the translations to a scratch file outside the repo, in this shape:
```ini
[Single]
en_US = Test string
sv_SE = Teststräng
lt-LT = Testeilutė
```
One line per language, named after the ini file minus the extension (`lt-LT`, `he_IL_invert`,
`zh_TW`, ...). No trailing `# comments` on those lines, they'd end up inside the translation.
Placeholders like `%1` and `%d` have to appear verbatim in the translation, in whatever position
the target language needs them.
`zh_TW`, ...), plus an `en_US` line carrying the English string itself if you were given one -
that's what creates the key in `en_US.ini`. No trailing `# comments` on those lines, they'd end
up inside the translation. Placeholders like `%1` and `%d` have to appear verbatim in the
translation, in whatever position the target language needs them.
**If you don't know a language well enough to be confident, leave it out.** The English string
stays as the fallback, which is normal and fine - much better than a confident guess that nobody
in the project can read well enough to catch.
**If you don't know a language well enough to be confident, leave it out.** A key that's missing
from a language file falls back to the English string at runtime, which is normal and fine - much
better than a confident guess that nobody in the project can read well enough to catch.
4. `cargo run -- import-single <scratch-file> "$1" "$2"`
3. `cargo run -- import-single <scratch-file> "$1" "$2"`
Note this overwrites any existing value for that key, so if the key already had human
translations, check what you're about to replace first.
translations, check what you're about to replace first. The section has to exist already.
5. `cargo run -- validate` - always, at the end. It must print `Found 0 problems.`
4. `cargo run -- validate` - always, at the end. It must print `Found 0 problems.`
Finally, report which languages you translated and which you skipped and why, and leave the changes
uncommitted for review unless asked otherwise.
uncommitted for review unless asked otherwise. If you want the languages you skipped to carry the
English string as a visible placeholder rather than just falling back to it, that's
`cargo run -- copy-missing-lines`.
+15 -11
View File
@@ -189,28 +189,32 @@ the ~47 files**, and don't run the AI commands in `Tools/langtool` either - you
which its fixed prompt can't. Do the translating yourself and let the tool do the file surgery. Run it
from `Tools/langtool`:
1. `cargo run -- add-new-key-value <Section> "<Key>" "<English string>"` adds the key to every file,
English everywhere. (Skip if the key already exists and you're only filling in translations.)
2. Work out what the string actually means before translating it: find where it's used in the C++,
1. Work out what the string actually means before translating it: find where it's used in the C++,
what any `%1`/`%d` placeholders get substituted with, how long it can be without breaking the
layout, and how neighboring keys are already phrased in each language (that's your style guide).
Then write a scratch file, one line per language, named after the ini file minus the extension:
2. Write a scratch file, one line per language, named after the ini file minus the extension, with
the English string under `en_US`:
```ini
[Single]
en_US = Test string
sv_SE = Teststräng
lt-LT = Testeilutė
```
No trailing `# comments` on those lines, they'd end up inside the translation. Placeholders have to
survive verbatim. If you don't know a language well enough, leave it out - it keeps the English
string, which is the normal fallback, and that's much better than a confident guess.
3. `cargo run -- import-single <scratch-file> <Section> "<Key>"` writes them in, tagged
`# AI translated`. Note that it overwrites existing values for that key, so take care with keys
that already have human translations.
survive verbatim. If you don't know a language well enough, leave it out - a key that's missing from
a language file falls back to the English string at runtime, which is much better than a confident
guess.
3. `cargo run -- import-single <scratch-file> <Section> "<Key>"` writes them all in, including a new
key in en_US.ini, tagged `# AI translated` except for the en_US line, which is the string the rest
were translated from. Note it overwrites existing values for that key, so take care with keys that
already have human translations, and that the section has to exist already - langtool won't create
one.
4. `cargo run -- validate` at the end, always. It checks that placeholders survived and exits
non-zero if anything is off.
The other mechanical jobs (renaming and moving keys, sorting sections, copying missing lines to all
files) are langtool commands too - prefer them over editing the ini files by hand.
Optionally follow up with `cargo run -- copy-missing-lines` to give the languages you skipped the
English string as a placeholder. The other mechanical jobs (renaming and moving keys, sorting
sections) are langtool commands too - prefer them over editing the ini files by hand.
## Headless and unittest builds
+26 -19
View File
@@ -1032,31 +1032,38 @@ fn execute_command(cmd: Command, ai: Option<&Ai>, dry_run: bool, verbose: bool)
ref section,
ref key,
} => {
if !is_reference {
let lang_id = filename.strip_suffix(".ini").unwrap();
if let Some(single_section) = &single_ini_section {
if let Some(target_section) = target_ini.get_section_mut(section) {
if let Some(single_line) = single_section.get_line(lang_id) {
if let Some(value) = line_value(&single_line) {
println!(
"Inserting value {value} for key {key} in section {section} in {target_ini_filename}"
);
if !target_section.insert_line_if_missing(&format!(
"{key} = {value} # AI translated"
)) {
// Didn't insert it, so it exists. We need to replace it.
target_section.set_value(key, value, Some("AI translated"));
}
let lang_id = filename.strip_suffix(".ini").unwrap();
// en_US is in here too, if the file has a line for it - that one is the English
// string the others were translated from, not a translation, so no comment on it.
let comment = if is_reference {
None
} else {
Some("AI translated")
};
if let Some(single_section) = &single_ini_section {
if let Some(target_section) = target_ini.get_section_mut(section) {
if let Some(single_line) = single_section.get_line(lang_id) {
if let Some(value) = line_value(&single_line) {
println!(
"Inserting value {value} for key {key} in section {section} in {target_ini_filename}"
);
let line = match comment {
Some(comment) => format!("{key} = {value} # {comment}"),
None => format!("{key} = {value}"),
};
if !target_section.insert_line_if_missing(&line) {
// Didn't insert it, so it exists. We need to replace it.
target_section.set_value(key, value, comment);
}
} else {
println!("No lang_id {lang_id} in single section");
}
} else {
println!("No section {section} in {target_ini_filename}");
println!("No lang_id {lang_id} in single section");
}
} else {
println!("No section {section} in {filename}");
println!("No section {section} in {target_ini_filename}");
}
} else {
println!("No section {section} in {filename}");
}
}
}