Repository navigation
Implementing auto-generated timelineReplace for Oopsyraidsy #999
Description
This is a follow-up issue after #947.
Feature(s) Requested
gen_oopsy_timeline_replace.ts automatically generates Oopsyraidsy's timelineReplace.
Draft: https://github.com/Jaehyuk-Lee/cactbot/blob/oopsy-i18n-data/util/gen_oopsy_timeline_replace.ts
See auto-generated examples
See: Jaehyuk-Lee@fd7ffad
CLI Output
$ node --loader=ts-node/esm util/gen_oopsy_timeline_replace.ts
(node:34236) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
(node:34236) [DEP0180] DeprecationWarning: fs.Stats constructor is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Alert: ui/oopsyraidsy/data/05-shb/raid/e12s.ts: Multiple candidates for 'Ice Pillar' in cn: 冰柱, 氷柱
Alert: Using existing translation: '冰柱'
Alert: ui/oopsyraidsy/data/05-shb/raid/e12s.ts: Multiple candidates for 'Ice Pillar' in tc: 冰柱, 氷柱
Alert: Using existing translation: '冰柱'
Success: Updated: 13, Skipped: 242How this work
- Search and process all data files from
ui/oopsyraidsy/data(process repeatedly file by file) - Find all
sourceandtarget. Capture with regex/(?:source|target):\s*(?:['"]([^'"]+)['"]|\[((?:['"][^'"]+['"],?\s*)+)\])/g - If there are no
sourceandtarget, skip the file. - Fetch BNpcName data from xivapi, and ffxiv-datamining-ko / cn / tc
- Map "English BNpcName" → "BNpcName ID" (works case-insensitive)
- Map "BNpcName ID" → "localized BNpcName"
Handling edge cases
| New Data | Multiple Candidates | Existing Translation | Action |
|---|---|---|---|
| ✓ Found | ✗ Single | - | Use new data |
| ✓ Found | ✓ Multiple | ✓ Exists | Keep existing + console.warn() |
| ✓ Found | ✓ Multiple | ✗ None | Output 'candidate1 / candidate2' (human review) |
| ✗ Not found | - | - | Keep existing |
Example: Multiple Candidates Found
The English name "Lindwurm" has multiple Korean translations in the datamining:
- ID 130: "린드부름" (Lindwurm variant 1)
- ID 14378: "린드블룸" (Lindwurm variant 2)
If existing translation exists:
// Existing in file:
'Lindwurm': '린드블룸',
// Script finds: ['린드부름', '린드블룸']
// Action: Keep existing '린드블룸', output console.warn()If no existing translation:
// No existing translation in file
// Script finds: ['린드부름', '린드블룸']
// Action: Output '린드부름 / 린드블룸' (requires human review)Example: Multiple Candidates + No Existing Translation
When the script finds multiple translation candidates for a source name, but no existing translation exists in the file:
-
Output all candidates joined by " / "
'Lindwurm': '린드부름 / 린드블룸',
-
Output
console.warn()with details[WARNING] example.ts: Multiple candidates for 'Lindwurm' in ko: 린드부름, 린드블룸 No existing translation found. Manual review required.
-
Human review required
- The generated output is syntactically valid TypeScript
- However, the regex pattern
'린드부름 / 린드블룸'won't match actual game logs - Developer must manually choose the correct translation for that specific encounter
- After review, update to single value:
'Lindwurm': '린드블룸',
When the script couldn't translate all source and target
missingTranslation: true, will be automatically added.
...(allTranslated ? [] : [` 'missingTranslations': true,`]),Question for Deutsch
This question is resolved. Fixed by Jaehyuk-Lee@bae9985
#,Singular,Plural,Adjective,PossessivePronoun,StartsWithVowel,Unknown0,Pronoun,Article
9876,Alchemist[p] des 2. Ordens,Alchemisten[p] des 2. Ordens,6,1,0,1,0,0
9877,Ghomoro-Golem,Ghomoro-Golems,4,1,0,1,0,0
9878,Konstrukt[p] 2,Konstrukte[p] 2,4,2,0,1,2,0
9879,Granate,Granaten,3,1,0,1,1,0
9880,Patriarch[p] des 2. Ordens Za Da,Patriarchen[p] des 2. Ordens Za Da,6,1,0,1,0,0
9881,Felsspalter[p] des 2. Ordens,Felsspalter[p] des 2. Ordens,4,1,0,1,0,0
9882,Stimme[p] von Xeven,Stimme[p] von Xeven,3,2,0,1,1,0
9883,Stimme[p] von Stanik,Stimme[p] von Stanik,3,2,0,1,1,0
9884,Stimme[p] von Isolde,Stimme[p] von Isolde,3,2,0,1,1,0Some BNpcName have [p], [a], or [t].
Candidate 1:
{
'locale': 'de',
'replaceSync': {
'Chiseled Sculpture': 'Abbild eines Mannes',
'Ice Pillar': 'Eissäule',
'Beastly Sculpture': 'Abbild eines Löwen',
'Regal Sculpture': 'Abbild eines großen Löwen',
},
},Candidate 2:
{
'locale': 'de',
'replaceSync': {
'Chiseled Sculpture': 'Abbild[p] eines Mannes',
'Ice Pillar': 'Eissäule',
'Beastly Sculpture': 'Abbild[p] eines Löwen',
'Regal Sculpture': 'Abbild[p] eines großen Löwen',
},
},Which one is the right one? Should all [x] text be removed? The gen_oopsy_timeline_replace is using Abbild eines Löwen (Candidate 1) for now.
Activity
Jaehyuk-Lee commentedon Feb 8, 2026
This script also found out that the original translation was not exactly right.
Example:
FIX:
netRegex works case-insensitive
resources/regexes.tswas addingioption to regex.cactbot/resources/regexes.ts
Lines 778 to 791 in f0e09d6
So this change doesn't matter.
Bing-su commentedon Feb 8, 2026
The
[x]pattern exists only in German, and it is sufficient to perform the following substitution.{ "[t]": "(?:der|die|das)", "[a]": "(?:e|er|es|en)", "[A]": "(?:e|er|es|en)", "[p]": "" }cactbot/util/gen_hunt_data.ts
Lines 106 to 130 in f0e09d6
Bing-su commentedon Feb 8, 2026
I recommend considering exceptions for cases where the results retrieved via the API start with
_rsv_.Jaehyuk-Lee commentedon Feb 8, 2026
I didn't implement that exception point because the script can't find the "BNpcName ID" with rsv data.
ui/oopsyraidsy/datasourceandtarget. Capture with regex/(?:source|target):\s*(?:['"]([^'"]+)['"]|\[((?:['"][^'"]+['"],?\s*)+)\])/gsourceandtarget, skip the file._rsv_data will remain untranslated, and the script will markmissingTranslation: true.And I don't think there will be cases where only Korean (or other languages) uses
_rsv_data while English uses plain text because the ffxiv version across all region is going to be the same soon.Jaehyuk-Lee commentedon Feb 8, 2026
👍 I could find some examples from raidboss data.
I'll implement the same logic here.
Fixed by Jaehyuk-Lee@bae9985
Jaehyuk-Lee commentedon Feb 8, 2026
I implemented the regex when the translation has multiple candidates.
Before:
After:
code change:
Jaehyuk-Lee@8bcaeed
Bing-su commentedon Feb 8, 2026
RSV-decrypted data is transmitted to cactbot. Meanwhile, xivapi retains the data in encrypted RSV state.
example: BNpcName r12s
lindschratJaehyuk-Lee commentedon Feb 8, 2026
Yes, I mean, assume there is a trigger like this.
This script tries to find
Lindschratfrom xivapi, but it can't find one.Because it's written as
_rsv_14380_-1_1_0_0_S64755250_E64755250instead ofLindschratin the 'BNpcName' sheet.Script fails to make a
replaceSync, it finds some translation is missing, and adds'missingTranslations': true,.You can't auto-translate rsv data. So, the script let the developer to translate it manually.
Bing-su commentedon Feb 8, 2026
You're right. I misunderstood. I'm sorry.
Jaehyuk-Lee commentedon Feb 8, 2026
Instead, I added explicit alert log for missing translations via commit Jaehyuk-Lee@4c35443. You'll get the log like this:
I added a sample trigger with
source: 'Lindschrat'ine10s.tsto test this log.I manually added
Lindschrattranslation todeandkobefore running the script, those are not logged as missing translation.