/************************************************************************************/ import loadScripts from "./loadScripts.js"; import statParserFR from "./import-stat-2.js"; /************************************************************************************/ let compmod = "wfrp4e-core"; const vo_conditions = { "ablaze": "Ablaze", "bleeding": "Bleeding", "blinded": "Blinded", "broken": "Broken", "deafened": "Deafened", "entangled": "Entangled", "fatigued": "Fatigued", "poisoned": "Poisoned", "prone": "Prone", "stunned": "Stunned", "surprised": "Surprised", "unconscious": "Unconscious", "grappling": "Grappling", "fear": "Fear", "defeated": "Defeated" } // Map English characteristic names (as they appear in spell formulas) to WFRP4e abbreviations const __CHAR_EN_TO_ABBR = { "weapon skill": "ws", "ballistic skill": "bs", "strength": "s", "toughness": "t", "initiative": "i", "agility": "ag", "dexterity": "dex", "intelligence": "int", "willpower": "wp", "fellowship": "fel" } /************************************************************************************/ export class WFRP4FrTranslation { static parseSpellContent(spell) { if (spell.system.range?.value) { spell.system.range.value = this.processSpellContent(spell.system.range.value) } if (spell.system.damage?.value) { spell.system.damage.value = this.processSpellContent(spell.system.damage.value) } if (spell.system.duration?.value) { spell.system.duration.value = this.processSpellContent(spell.system.duration?.value) } if (spell.system.range?.value) { spell.system.range.value = this.processSpellContent(spell.system.range?.value) } if (spell.system.target?.value) { spell.system.target.value = this.processSpellContent(spell.system.target?.value) } } static processSpellContent(value) { if (value == "") return ""; // Hop ! if (value == "Touch") return "Berührung"; // Hop ! if (value == "You") return "Du"; // Hop ! if (value == "Instant") return "Sofort"; // Hop ! let translw = value; let re = /(.*)\s+[Bb]onus\s*(.*)/i; // (.*) at end captures modifiers like "+4" let res = re.exec(value); let unit = ""; if (res) { // Test " Bonus " pattern if (res[1]) { // We have char name, then convert it const charEN = res[1].trim().toLowerCase(); const abbr = __CHAR_EN_TO_ABBR[charEN]; if (abbr && game.wfrp4e?.config?.characteristicsBonus?.[abbr]) { // Use the localized German bonus name from config (already resolved by localizeConfig at i18nInit) translw = game.wfrp4e.config.characteristicsBonus[abbr]; } else { translw = game.i18n.localize(res[1].trim()); translw = translw + "bonus"; } } unit = res[2]; // may be "+4", "-2", "2", "yards", etc. } else { re = /(\d+) (\w+)/i; res = re.exec(value); if (res) { // Test : " " pattern translw = res[1]; unit = res[2]; } else { // Test re = /(\w+) (\w+)/i; res = re.exec(value); if (res) { // Test : " " pattern translw = game.i18n.localize(res[1].trim()); unit = res[2]; } } } if (unit == "hour") unit = "stunde"; if (unit == "hours") unit = "stunden"; if (unit == "days") unit = "tage"; if (unit == "yard") unit = "meter"; if (unit == "yards") unit = "meter"; if (unit == "Bonus") { // Another weird management console.log("Translating bonus", unit); translw = translw + " Bonus"; } else if (unit && /^[+\-*\/]/.test(unit)) { translw += unit; // No space before operators like "+4" } else { translw += " " + unit; } return translw; } } /************************************************************************************/ Hooks.once('init', () => { // Check various settings in the installation game.modules.forEach((module, id) => { if (id == "wfrp4e-core" && module.active) { compmod = "wfrp4e-core"; } }); game.wfrp4efr = { compmod: compmod, vo_conditions: vo_conditions } game.wfrp4e.apps.StatBlockParser.parseStatBlock = async function (statString, type = "npc") { return statParserFR(statString, type); } console.log("WFRP4E-DE | Loading Babele translation module ..."); loadScripts(); }); /************************************************************************************/ // Babele registration must happen in Babele's own "babele.init" hook (fires before // Babele builds its runtime session/mapping layers). Registering in Foundry's // "init" hook is too late for Babele 2.8+: once the runtime session is installed, // late register()/registerConverters() calls are silently rejected, leaving the // pack "loaded" (JSON parsed) but with no effective mapping applied - which is // exactly the "everything stays English except journals" symptom. // See: https://gitlab.com/riccisi/foundryvtt-babele/-/raw/master/DEVELOPER_MIGRATION_2_8_0.md Hooks.once('babele.init', () => { if (typeof game.babele !== 'undefined') { game.babele.register({ module: 'wh4-de-translation', lang: 'de', dir: 'compendium' }) // Pre-define functions for cross-converter calls (Babele 2.8+ wraps converters // as objects, so game.babele.converters.X() no longer works as a plain call) const career_skills_fn = (skills_list) => { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("skill") //DEBUG: console.log( "Thru here ...", compendium, skills_list); if (skills_list) { let i; let len = skills_list.length; let re = /(.*)\((.*)\)/i; for (i = 0; i < len; i++) { skills_list[i] = skills_list[i].trim(); for (let compData of validCompendiums) { let translItem = game.babele.translate(compData.metadata.id, { name: skills_list[i], type: "skill" }, true) let transl = translItem?.name || undefined if (!transl) transl = skills_list[i] if (transl == skills_list[i]) { let res = re.exec(skills_list[i]); if (res) { let subword = game.i18n.localize(res[2].trim()); let s1 = res[1].trim() + " ()"; translItem = game.babele.translate(compData.metadata.id, { name: s1, type: "skill" }, true) let translw = translItem?.name || undefined if (translw && translw != s1) { let res2 = re.exec(translw); transl = res2[1].trim() + " (" + subword + ")"; } else { s1 = res[1].trim() + " ( )"; translItem = game.babele.translate(compData.metadata.id, { name: s1, type: "skill" }, true) translw = translItem?.name || undefined if (translw) { let res2 = re.exec(translw); transl = res2[1].trim() + " (" + subword + ")"; } else { transl = res[1].trim() + " (" + subword + ")"; } } } } skills_list[i] = transl; if (translItem?.system) break; } } } return skills_list }; const career_careergroup_fn = (value) => { if (value == 'Slayer') return "Slayer"; if (value == 'Druidic Priest') return "Druide"; let validCompendiums = game.wfrp4e.tags.getPacksWithTag("career"); for (let compData of validCompendiums) { let newName = game.babele.translate(compData.metadata.id, { name: value }).name; if (!newName) newName = value; return newName; } ui.notifications.error("Das Übersetzen der Karriere " + value + " ist nicht möglich. Sie ist wahrscheinlich nicht übersetzt.", { permanent: true }); return value; }; game.babele.registerConverters({ "career_skills": career_skills_fn, "role_skills": (skills) => { let skills_list = skills.split(','); let skillsFR = career_skills_fn(skills_list); return skillsFR.join(', ') }, "process_effects": (effectsData, translations, data, tc, tc_translations) => { //console.log("Effects :", effectsData, translations, data, tc, tc_translations) for (let e of effectsData) { // Foundry v13 requires name; migrate legacy data where only label was stored if (e.name == null) e.name = e.label || ""; let origName = e.name // Symptom effects have their own name (Fever, Malaise, etc.) — don't overwrite with the parent item name if (e.flags?.wfrp4e?.symptom) { let symName = e.name; let gravity = ""; if (symName.includes("(") && symName.includes(")")) { let re = /(.*) +\((.*)\)/i; let res = re.exec(symName); if (res) { symName = res[1].trim(); gravity = " (" + game.i18n.localize(res[2].trim()) + ")"; } } e.name = game.i18n.localize(symName) + gravity; } else { e.name = tc_translations.name || game.i18n.localize(e.name) || e.label || "" } if (e.flags?.wfrp4e?.scriptData) { for (let script of e.flags.wfrp4e.scriptData) { if (script?.label) { // Quand le label du script est strictement identique au nom de l'item concerné if (script.label.toLowerCase() == origName.toLowerCase()) { script.label = e.name } else if (script.label.toLowerCase().includes("tests to affect")) { script.label = script.label.replace("Tests to affect", "Würfe zum Beeinflussen von ") } else if (script.label.toLowerCase().includes("using torn muscle")) { script.label = script.label.replace("Using Torn Muscle", "Verwende gerissenen Muskel ") } else { script.label = game.i18n.localize(script.label) } } } } } return effectsData }, "resultConverter": (results, translated) => { //console.log("STUF PARSING", results, translated) if (translated) { for (let data of results) { if (translated[`${data.range[0]}-${data.range[1]}`]) { data.description = translated[`${data.range[0]}-${data.range[1]}`] } } return results } // Auto patch if (results[0].description.includes("wfrp4e-core.career-descriptions")) { results[0].description = "wfrp4e-core.journals" } if (results[0].description.includes("wfrp4e-core.journal")) { for (let data of results) { let career = data.description.match(/{(.*)}/) //console.log(">>>>>", career) if (career?.[1]) { let careerFR = career_careergroup_fn(career[1]) data.description = data.description.replace(career[1], careerFR) } } } if (results?.[0].documentUuid) { // Babele 2.8+ handles documentUuid resolution via its own mapping system return results } return results }, "npc_details": (details) => { //console.log("DETAILS: ", details); let newDetails = foundry.utils.duplicate(details); if (details.species?.value) newDetails.species.value = game.i18n.localize(details.species.value); if (details.gender?.value) newDetails.gender.value = game.i18n.localize(details.gender.value); if (details.class?.value) newDetails.class.value = game.i18n.localize(details.class.value); return newDetails; }, "career_talents": (talents_list) => { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("talent") let i; if (talents_list) { let len = talents_list.length; let re = /(.*)\((.*)\)/i; for (i = 0; i < len; i++) { for (let compData of validCompendiums) { let translItem = game.babele.translate(compData.metadata.id, { name: talents_list[i] }, true); // Specific case management if (talents_list[i] === "Trapper") { translItem = game.babele.translate(compData.metadata.id, { name: "a7v422EZcOUUC20X" }, true); } if (talents_list[i] === "Inspiring") { translItem = game.babele.translate(compData.metadata.id, { name: "WCXnFSV4WOSmzzc4" }, true); } //console.log("Search talent name:", compData.metadata.id, talents_list[i], translItem); let transl = translItem?.name || undefined if (!transl) transl = talents_list[i] if (transl == talents_list[i]) { let res = re.exec(talents_list[i]); if (res) { let subword = game.i18n.localize(res[2].trim()); let s1 = res[1].trim(); // No () in talents table translItem = game.babele.translate(compData.metadata.id, { name: s1 }, true) let translw = translItem?.name || undefined //console.log("Search talent name:", compData.metadata.id, s1, translw); if (translw && translw != s1) { transl = translw.trim() + " (" + subword + ")"; } } } talents_list[i] = transl; if (translItem?.system) break; } } } return talents_list; }, "npc_characteristics": (chars) => { // Auto-convert char names in the sheet for (let key in chars) { let char = chars[key]; //console.log("Was here !", key, char ); let abrev = char["abrev"]; let toTransl = "CHAR." + abrev; if (game.i18n.localize(toTransl) != toTransl) { // Manages unknown language char["label"] = game.i18n.localize("CHAR." + abrev); char["abrev"] = game.i18n.localize("CHARAbbrev." + abrev); } } return chars; }, "bestiary_traits": (beast_traits, translations) => { if (!beast_traits) { console.log("No beast traits found here ...") return beast_traits } //console.log("TRANS:", beast_traits) // Normalize: Foundry v13 requires ActiveEffect.name; migrate legacy v12 data for (let trait_en of beast_traits) { if (trait_en.effects) { for (let eff of trait_en.effects) { if (eff.name == null) eff.name = eff.label || ""; } } } for (let trait_en of beast_traits) { let special = ""; let nbt = ""; let name_en = trait_en.name.replace(/\r/g, '').trim(); // strip \r (including internal) in some traits name if (!trait_en.name || trait_en.name.length == 0) { console.log("Wrong item name found!!!!") continue } //console.log(">>>>>>>> Parsing", trait_en.name) if (trait_en.type == "trait") { //console.log("Trait translation", compmod, trait_en) if (name_en.includes("Tentacles")) { // Process specific Tentacles case let re = /(.d*)x Tentacles/i; let res = re.exec(name_en); if (res?.[1]) nbt = res[1] + "x "; name_en = "Tentacles"; } else if (name_en.includes("(") && name_en.includes(")")) { // Then process specific traits name with (xxxx) inside let re = /(.*) \((.*)\)/i; let res = re.exec(name_en); if (!res) { console.warn("WFRP4E-DE | bestiary_traits: regex failed for trait:", name_en); } else { name_en = res[1]; // Get the root traits name special = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword } } let validCompendiums = game.wfrp4e.tags.getPacksWithTag("trait") for (let compData of validCompendiums) { let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system: { description: { value: trait_en.system.description.value } } }, true) if (trait_fr?.name && trait_fr?.name != name_en) { trait_fr.name = trait_fr.name || trait_en.name trait_en.name = nbt + trait_fr.name + special; if (trait_en.system?.description?.value && trait_fr.system?.description?.value) { trait_en.system.description.value = trait_fr.system.description.value; } if (trait_en?.system?.specification && isNaN(trait_en.system.specification?.value)) { // This is a string, so translate it //console.log("Translating : ", trait_en.system.specification.value); trait_en.system.specification.value = game.i18n.localize(trait_en.system.specification.value.trim()); } break // Translation has been found, skip other compendiums } } } else if (trait_en.type == "skill") { if (name_en.includes("(") && name_en.includes(")")) { // Then process specific skills name with (xxxx) inside let re = /(.*) +\((.*)\)/i; let res = re.exec(name_en); if (!res) { console.warn("WFRP4E-DE | bestiary_traits: regex failed for skill:", name_en); } else { name_en = res[1].trim(); // Get the root skill name special = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword } } let validCompendiums = game.wfrp4e.tags.getPacksWithTag("skill") for (let compData of validCompendiums) { let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system: { description: { value: trait_en.system.description.value } } }, true) //console.log(">>>>> Skill ?", name_en, special, trait_fr.name, trait_fr); if (trait_fr?.name && trait_fr?.name != name_en) { trait_fr.name = trait_fr.name || name_en trait_en.name = trait_fr.name + special; trait_en.system.description.value = trait_fr.system.description.value; break; // Translation has been found, skip other compendiums } } } else if (trait_en.type == "prayer") { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("prayer") for (let compData of validCompendiums) { let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system: { description: { value: trait_en.system.description.value } } }, true) if (trait_fr?.name && trait_fr?.name != name_en) { //DEBUG : console.log(">>>>> Prayer ?", name_en, special, trait_fr.name ); WFRP4FrTranslation.parseSpellContent(trait_en) trait_fr.name = trait_fr.name || name_en trait_en.name = trait_fr.name + special; if (trait_fr.system?.description?.value) { trait_en.system.description.value = trait_fr.system.description.value; } break; } } } else if (trait_en.type == "spell") { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("spell") for (let compData of validCompendiums) { let trait_fr = game.babele.translate(compData.metadata.id, { name: name_en, system: { description: { value: trait_en.system.description.value } } }, true) if (trait_fr?.name && trait_fr?.name != name_en) { //DEBUG : console.log(">>>>> Spell ?", name_en, special, trait_fr); WFRP4FrTranslation.parseSpellContent(trait_en) trait_fr.name = trait_fr.name || name_en trait_en.name = trait_fr.name + special; if (trait_fr.system?.description?.value) { trait_en.system.description.value = trait_fr.system.description.value; } break; } } } else if (trait_en.type == "talent") { if (name_en.includes("(") && name_en.includes(")")) { // Then process specific skills name with (xxxx) inside let re = /(.*) +\((.*)\)/i; let res = re.exec(name_en); if (!res) { console.warn("WFRP4E-DE | bestiary_traits: regex failed for talent:", name_en); } else { name_en = res[1].trim(); // Get the root talent name, no parenthesis this time... special = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword } } let validCompendiums = game.wfrp4e.tags.getPacksWithTag("talent") for (let compData of validCompendiums) { if (name_en === "Trapper") { name_en = "a7v422EZcOUUC20X" } if (name_en === "Inspiring") { name_en = "WCXnFSV4WOSmzzc4" } trait_en.name = name_en // Reset the name to the original one let trait_fr = game.babele.translate(compData.metadata.id, trait_en, true) if (trait_fr?.name && trait_fr?.name != name_en) { trait_fr.name = trait_fr.name || name_en // Security since babele v10 //console.log(">>>>> Talent ?", trait_fr, name_en, special, trait_fr.name); if (trait_fr.name && (trait_fr.name == "Sprinter" || trait_fr.name != name_en)) { // Talent translated! trait_en.name = trait_fr.name.trim() + special if (trait_fr.system?.tests?.value) { // Why ??? trait_en.system.tests.value = trait_fr.system.tests.value; } if (trait_fr.system?.description?.value) { // Why ??? trait_en.system.description.value = trait_fr.system.description.value; } } break; } } } else if (trait_en.type == "career") { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("career") for (let compData of validCompendiums) { let career_fr = game.babele.translate(compData.metadata.id, trait_en, true); if (career_fr?.name && career_fr?.name != name_en) { trait_en.name = career_fr.name || trait_en.name // DEBG: console.log(">>>>> Career ?", career_fr.name ); trait_en.system = foundry.utils.duplicate(career_fr.system); break; } } } else if (trait_en.type == "vehicleRole") { let validCompendiums = game.wfrp4e.tags.getPacksWithTag("vehicleRole") for (let compData of validCompendiums) { let role_fr = game.babele.translate(compData.metadata.id, trait_en, true); if (role_fr?.name && role_fr?.name != name_en) { trait_en.name = role_fr.name || trait_en.name // DEBG: console.log(">>>>> Role ?", role_fr.name ); trait_en.system = foundry.utils.duplicate(role_fr.system); break; } } } else if (trait_en.type == "trapping" || trait_en.type == "weapon" || trait_en.type == "armour" || trait_en.type == "container" || trait_en.type == "money") { let validCompendiums = game.wfrp4e.tags.getPacksWithTag(["trapping"], ["weapon", "armour", "container", "money"]) for (let compData of validCompendiums) { let trapping_fr = game.babele.translate(compData.metadata.id, { name: name_en, system: { description: { value: trait_en.system.description.value } } }, true); if (trapping_fr?.name && trapping_fr?.name != name_en) { //console.log(">>>>> Trapping ?", name_en, trapping_fr.name); trait_en.name = trapping_fr.name || trait_en.name if (trapping_fr.system?.description?.value) { trait_en.system.description.value = trapping_fr.system.description.value } break; } } } } //console.log(">>>>>>>> { let ret = value if (value) { ret = game.i18n.localize(value.trim()); if (!ret) ret = value } return ret }, "trapping_qualities_flaws": (value) => { if (value) { let newQF = []; //console.log("ATOUTS", value, typeof(value)); let list = value if (typeof (value) == "string") { let myList = value.split(",") list = [] for (let l of myList) { list.push({ name: l.trim() }) } } for (let i = 0; i < list.length; i++) { newQF[i] = foundry.utils.duplicate(list[i]) if (newQF[i].name == "Trap Blade") { newQF[i].name = "TrapBlade"; // Auto-patch, without space! //console.log("PATCHED", trim); } let oldName = newQF[i].name newQF[i].name = game.i18n.localize(oldName) if (!newQF[i].name) newQF[i].name = oldName } return newQF; } }, // Search back in careers the translated name of the groupe (as it is the name of the level career itself) "career_careergroup": career_careergroup_fn, "mutations_modifier": (value) => { // This is really UGLYYYY i know, but i started like this and discovered afterward that many strings were not easy to automate... Sorry :) //console.log("Parsing mutation :", value); if (!value) return; value = value.toLowerCase(); value = value.replace("gain a broken condition if you fail a test derived from ", "Du erhälst einen Demoralisiert-Zustand wenn dir ein Wurf misslingt in "); value = value.replace("weapon skill", "Kampfgeschick"); value = value.replace("ballistic skill", "Ballisitsche Fertigkeit"); value = value.replace("strength", "Stärke"); value = value.replace("toughness", "Widerstand"); value = value.replace("agility", "Gewandheit"); value = value.replace("dexterity", "Geschicklichkeit"); value = value.replace("willpower", "Willenskraft"); value = value.replace("fellowship", "Charisma"); value = value.replace("initiative", "Initiative"); value = value.replace("intelligence", "Intelligenz"); value = value.replace("armor points to the head", "RP am Kopf"); value = value.replace("subject to frenzy", "unterliegt Raserei"); value = value.replace("you do not scar", "Du bildest keine Narben"); value = value.replace("movement", "Bewegung"); value = value.replace("armor points to all locations", "RP am ganzen Körper"); value = value.replace("to any test when alone", "auf jeden Wurf, wenn du alleine bsit"); value = value.replace("track", "Spurenlesen"); value = value.replace("to any test not hurting another", "auf jeden Wurf, um anderen keine Schmerzen zuzufügen"); value = value.replace("on tests to hurt", "auf jeden Wurf, um anderen Schmerzen zuzufügen") value = value.replace("to all language tests when speaking", "auf alle Sprache-Würfe beim Sprechen"); value = value.replace("on perception tests involving sight", "auf Wahrnehmungs-Würfe, bei denen es um die Sicht geht"); value = value.replace("to all Sociabilité tests", "auf alle gesellschaftlichen Würfe"); return value; }, "talent_name": (name, translation) => { console.log("NAME !!!", name, translation) }, "effects": (effects, translations) => { if (!effects) return; if (!translations) return; for (let i = 0; i < effects.length; i++) { let effect = effects[i]; //console.log("EFFECT LABEL1:", effect ); effect.name = translations['label' + i]; } return effects }, "diseases_effects": (effects, translations) => { if (!effects) return; for (const element of effects) { let effect = element; let name = effect.name || effect.label; let gravity = ""; if (name.includes("(") && name.includes(")")) { // Then process specific skills name with (xxxx) inside let re = /(.*) +\((.*)\)/i; let res = re.exec(name); name = res[1].trim(); // Get the gravity gravity = " (" + game.i18n.localize(res[2].trim()) + ")"; // And the special keyword } effect.name = game.i18n.localize(name) + gravity; } }, // Auto-translate duration "spells_duration_range_target_damage": (value) => { return WFRP4FrTranslation.processSpellContent(value); }, // Auto-translate disease duration units "disease_duration_unit": (unit) => { return game.i18n.localize(unit).capitalize(); } }); } }); /*---------------------------------------------------------------------*/ Hooks.once('ready', () => { import("https://www.uberwald.me/fvtt_appcount/count-class-ready.js").then(moduleCounter => { console.log("ClassCounter loaded", moduleCounter) moduleCounter.ClassCounter.registerUsageCount("wh4-de-translation") }).catch(err => console.log("No stats available, giving up.") ) });