Implement i18n audit/localization cleanup and sync libstorm submodule
This commit is contained in:
660
scripts/generate_i18n_catalog.py
Normal file
660
scripts/generate_i18n_catalog.py
Normal file
@@ -0,0 +1,660 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
TARGET_FUNCTION_ARGS = {
|
||||
"speak_with_history": [0],
|
||||
"screen_reader_speak": [0],
|
||||
"notify": [0],
|
||||
"learn_sounds_speak": [0],
|
||||
"learn_sounds_add_description": [1],
|
||||
"notifications_speak": [0],
|
||||
"ui_info_box": [0, 1, 2],
|
||||
"ui_question": [0, 1],
|
||||
"ui_input_box": [0, 1],
|
||||
"virtual_info_box": [0, 1, 2],
|
||||
"virtual_question": [0, 1],
|
||||
"virtual_input_box": [0, 1],
|
||||
"text_reader": [1],
|
||||
"text_reader_lines": [1],
|
||||
"text_reader_file": [1],
|
||||
}
|
||||
|
||||
INSERT_LAST_CONTEXT_HINTS = (
|
||||
"option",
|
||||
"label",
|
||||
"line",
|
||||
"prompt",
|
||||
"instruction",
|
||||
"intro",
|
||||
"reward",
|
||||
"message",
|
||||
"title",
|
||||
"menu",
|
||||
)
|
||||
|
||||
PASS_THROUGH_TEXT_FUNCTIONS = {
|
||||
"i18n_text",
|
||||
"i18n_translate_speech_message",
|
||||
}
|
||||
|
||||
SKIP_DIR_NAMES = {".git", "bloodshed", "docs", "skills", "nvgt-git"}
|
||||
SKIP_FILE_NAMES = {"crash.log"}
|
||||
|
||||
|
||||
def iter_nvgt_files() -> List[Path]:
|
||||
files: List[Path] = []
|
||||
|
||||
entrypoints = [ROOT / "draugnorak.nvgt", ROOT / "excluded_sounds.nvgt"]
|
||||
for entry in entrypoints:
|
||||
if entry.exists():
|
||||
files.append(entry)
|
||||
|
||||
source_roots = [ROOT / "src", ROOT / "libstorm-nvgt"]
|
||||
for source_root in source_roots:
|
||||
if not source_root.exists():
|
||||
continue
|
||||
for path in source_root.rglob("*.nvgt"):
|
||||
rel = path.relative_to(ROOT)
|
||||
if any(part in SKIP_DIR_NAMES for part in rel.parts):
|
||||
continue
|
||||
if path.name in SKIP_FILE_NAMES:
|
||||
continue
|
||||
files.append(path)
|
||||
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def is_identifier_char(ch: str) -> bool:
|
||||
return ch.isalnum() or ch == "_"
|
||||
|
||||
|
||||
def read_identifier_backward(text: str, before_index: int) -> str:
|
||||
i = before_index
|
||||
while i >= 0 and text[i].isspace():
|
||||
i -= 1
|
||||
end = i
|
||||
while i >= 0 and is_identifier_char(text[i]):
|
||||
i -= 1
|
||||
start = i + 1
|
||||
if end < start:
|
||||
return ""
|
||||
return text[start : end + 1]
|
||||
|
||||
|
||||
def find_matching_paren(text: str, open_index: int) -> int:
|
||||
depth = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
|
||||
for i in range(open_index, len(text)):
|
||||
ch = text[i]
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif ch == "\\":
|
||||
escape = True
|
||||
elif ch == '"':
|
||||
in_string = False
|
||||
continue
|
||||
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
continue
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
continue
|
||||
if ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
continue
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def split_top_level(expr: str, delimiter: str) -> List[str]:
|
||||
parts: List[str] = []
|
||||
depth_paren = 0
|
||||
depth_bracket = 0
|
||||
depth_brace = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
start = 0
|
||||
|
||||
for i, ch in enumerate(expr):
|
||||
if in_string:
|
||||
if escape:
|
||||
escape = False
|
||||
elif ch == "\\":
|
||||
escape = True
|
||||
elif ch == '"':
|
||||
in_string = False
|
||||
continue
|
||||
|
||||
if ch == '"':
|
||||
in_string = True
|
||||
continue
|
||||
if ch == "(":
|
||||
depth_paren += 1
|
||||
continue
|
||||
if ch == ")":
|
||||
depth_paren = max(0, depth_paren - 1)
|
||||
continue
|
||||
if ch == "[":
|
||||
depth_bracket += 1
|
||||
continue
|
||||
if ch == "]":
|
||||
depth_bracket = max(0, depth_bracket - 1)
|
||||
continue
|
||||
if ch == "{":
|
||||
depth_brace += 1
|
||||
continue
|
||||
if ch == "}":
|
||||
depth_brace = max(0, depth_brace - 1)
|
||||
continue
|
||||
|
||||
if ch == delimiter and depth_paren == 0 and depth_bracket == 0 and depth_brace == 0:
|
||||
parts.append(expr[start:i])
|
||||
start = i + 1
|
||||
|
||||
parts.append(expr[start:])
|
||||
return parts
|
||||
|
||||
|
||||
def unescape_string_literal(literal_body: str) -> str:
|
||||
result: List[str] = []
|
||||
i = 0
|
||||
while i < len(literal_body):
|
||||
ch = literal_body[i]
|
||||
if ch != "\\":
|
||||
result.append(ch)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if i + 1 >= len(literal_body):
|
||||
result.append("\\")
|
||||
break
|
||||
|
||||
nxt = literal_body[i + 1]
|
||||
if nxt == "n":
|
||||
result.append("\n")
|
||||
elif nxt == "r":
|
||||
result.append("\r")
|
||||
elif nxt == "t":
|
||||
result.append("\t")
|
||||
else:
|
||||
result.append(nxt)
|
||||
i += 2
|
||||
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def expression_to_template(expr: str) -> Optional[str]:
|
||||
expr = expr.strip()
|
||||
if not expr:
|
||||
return None
|
||||
|
||||
parts = split_top_level(expr, "+")
|
||||
template_parts: List[str] = []
|
||||
placeholder_count = 0
|
||||
|
||||
for raw_part in parts:
|
||||
part = raw_part.strip()
|
||||
if not part:
|
||||
continue
|
||||
|
||||
passthrough_expr = unwrap_passthrough_expression(part)
|
||||
if passthrough_expr is not None:
|
||||
passthrough_template = expression_to_template(passthrough_expr)
|
||||
if passthrough_template is not None:
|
||||
template_parts.append(passthrough_template)
|
||||
continue
|
||||
|
||||
if len(part) >= 2 and part[0] == '"' and part[-1] == '"':
|
||||
template_parts.append(unescape_string_literal(part[1:-1]))
|
||||
else:
|
||||
placeholder_count += 1
|
||||
template_parts.append(f"{{arg{placeholder_count}}}")
|
||||
|
||||
if not template_parts:
|
||||
return None
|
||||
|
||||
template = "".join(template_parts)
|
||||
if template.strip() == "":
|
||||
return None
|
||||
if template_literal_length(template) == 0:
|
||||
return None
|
||||
return template
|
||||
|
||||
|
||||
def unwrap_passthrough_expression(expr: str) -> Optional[str]:
|
||||
expr = expr.strip()
|
||||
match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\((.*)\)$", expr, re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
function_name = match.group(1)
|
||||
if function_name not in PASS_THROUGH_TEXT_FUNCTIONS:
|
||||
return None
|
||||
|
||||
arg_text = match.group(2)
|
||||
args = split_top_level(arg_text, ",")
|
||||
if not args:
|
||||
return None
|
||||
|
||||
return args[0].strip()
|
||||
|
||||
|
||||
def template_literal_length(template_text: str) -> int:
|
||||
literal_count = 0
|
||||
in_placeholder = False
|
||||
|
||||
for ch in template_text:
|
||||
if not in_placeholder and ch == "{":
|
||||
in_placeholder = True
|
||||
continue
|
||||
if in_placeholder and ch == "}":
|
||||
in_placeholder = False
|
||||
continue
|
||||
if not in_placeholder:
|
||||
literal_count += 1
|
||||
|
||||
return literal_count
|
||||
|
||||
|
||||
def line_number_for_index(text: str, index: int) -> int:
|
||||
return text.count("\n", 0, index) + 1
|
||||
|
||||
|
||||
def add_entry(entries: Dict[str, Dict[str, object]], template: str, source_ref: str) -> None:
|
||||
base_key = f"msg.{hashlib.sha1(template.encode('utf-8')).hexdigest()[:12]}"
|
||||
key = base_key
|
||||
suffix = 1
|
||||
while key in entries and entries[key]["value"] != template:
|
||||
key = f"{base_key}_{suffix}"
|
||||
suffix += 1
|
||||
|
||||
if key not in entries:
|
||||
entries[key] = {"value": template, "refs": [source_ref]}
|
||||
else:
|
||||
refs: List[str] = entries[key]["refs"] # type: ignore[assignment]
|
||||
if source_ref not in refs:
|
||||
refs.append(source_ref)
|
||||
|
||||
|
||||
def extract_from_call(
|
||||
entries: Dict[str, Dict[str, object]],
|
||||
receiver: str,
|
||||
function_name: str,
|
||||
args: List[str],
|
||||
source_ref_base: str,
|
||||
) -> None:
|
||||
target_arg_indexes: List[int] = []
|
||||
|
||||
if function_name in TARGET_FUNCTION_ARGS:
|
||||
target_arg_indexes = TARGET_FUNCTION_ARGS[function_name]
|
||||
elif function_name == "insert_last":
|
||||
receiver_lower = receiver.lower()
|
||||
if not any(hint in receiver_lower for hint in INSERT_LAST_CONTEXT_HINTS):
|
||||
return
|
||||
target_arg_indexes = [0]
|
||||
else:
|
||||
return
|
||||
|
||||
for arg_index in target_arg_indexes:
|
||||
if arg_index >= len(args):
|
||||
continue
|
||||
template = expression_to_template(args[arg_index])
|
||||
if not template:
|
||||
continue
|
||||
source_ref = f"{source_ref_base}:{function_name}[{arg_index}]"
|
||||
add_entry(entries, template, source_ref)
|
||||
|
||||
|
||||
def scan_file(path: Path, entries: Dict[str, Dict[str, object]]) -> None:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
i = 0
|
||||
|
||||
while i < len(text):
|
||||
ch = text[i]
|
||||
if not is_identifier_char(ch):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
start = i
|
||||
while i < len(text) and is_identifier_char(text[i]):
|
||||
i += 1
|
||||
name = text[start:i]
|
||||
|
||||
j = i
|
||||
while j < len(text) and text[j].isspace():
|
||||
j += 1
|
||||
|
||||
if j >= len(text) or text[j] != "(":
|
||||
continue
|
||||
|
||||
receiver = ""
|
||||
k = start - 1
|
||||
while k >= 0 and text[k].isspace():
|
||||
k -= 1
|
||||
if k >= 0 and text[k] == ".":
|
||||
receiver = read_identifier_backward(text, k - 1)
|
||||
|
||||
close = find_matching_paren(text, j)
|
||||
if close < 0:
|
||||
break
|
||||
|
||||
arg_text = text[j + 1 : close]
|
||||
args = split_top_level(arg_text, ",")
|
||||
|
||||
line = line_number_for_index(text, start)
|
||||
rel_path = path.relative_to(ROOT).as_posix()
|
||||
source_ref_base = f"{rel_path}:{line}"
|
||||
extract_from_call(entries, receiver, name, args, source_ref_base)
|
||||
|
||||
i = close + 1
|
||||
|
||||
|
||||
def slugify_fragment(value: str) -> str:
|
||||
slug = re.sub(r"[^a-z0-9]+", "_", value.lower())
|
||||
slug = slug.strip("_")
|
||||
return slug or "value"
|
||||
|
||||
|
||||
def add_item_registry_fragments(entries: Dict[str, Dict[str, object]]) -> None:
|
||||
item_registry_path = ROOT / "src" / "item_registry.nvgt"
|
||||
if not item_registry_path.exists():
|
||||
return
|
||||
|
||||
text = item_registry_path.read_text(encoding="utf-8", errors="replace")
|
||||
pattern = re.compile(
|
||||
r"ItemDefinition\([^,]+,\s*\"([^\"]+)\",\s*\"([^\"]+)\",\s*\"([^\"]+)\"",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
for match in pattern.finditer(text):
|
||||
plural = match.group(1)
|
||||
singular = match.group(2)
|
||||
display = match.group(3)
|
||||
|
||||
plural_slug = slugify_fragment(plural)
|
||||
singular_slug = slugify_fragment(singular)
|
||||
display_slug = slugify_fragment(display)
|
||||
|
||||
add_entry(entries, plural, f"src/item_registry.nvgt:item_plural:{plural_slug}")
|
||||
add_entry(entries, singular, f"src/item_registry.nvgt:item_singular:{singular_slug}")
|
||||
add_entry(entries, display, f"src/item_registry.nvgt:item_display:{display_slug}")
|
||||
|
||||
|
||||
def add_seed_messages(entries: Dict[str, Dict[str, object]]) -> None:
|
||||
seeds = [
|
||||
"Load Game (no saves found)",
|
||||
"No saves found.",
|
||||
"Unable to load save.",
|
||||
"Unhandled exception",
|
||||
"Really exit?",
|
||||
"Found a stone.",
|
||||
"Found a reed.",
|
||||
"Found clay.",
|
||||
]
|
||||
for value in seeds:
|
||||
add_entry(entries, value, "seed:manual")
|
||||
|
||||
|
||||
def normalize_learn_sounds_label(sound_path: Path) -> str:
|
||||
name = sound_path.stem.lower()
|
||||
name = name.replace("_", " ").replace("-", " ")
|
||||
name = re.sub(r"\s+", " ", name).strip()
|
||||
return name or "sound"
|
||||
|
||||
|
||||
def add_learn_sounds_label_seeds(entries: Dict[str, Dict[str, object]]) -> None:
|
||||
sounds_dir = ROOT / "sounds"
|
||||
if not sounds_dir.exists():
|
||||
return
|
||||
|
||||
for path in sorted(sounds_dir.rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower() not in {".ogg", ".wav"}:
|
||||
continue
|
||||
label = normalize_learn_sounds_label(path)
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
add_entry(entries, label, f"seed:learn_sounds_label:{rel}")
|
||||
|
||||
|
||||
def escape_for_ini(value: str) -> str:
|
||||
escaped = value.replace("\\", "\\\\")
|
||||
escaped = escaped.replace("\n", "\\n")
|
||||
escaped = escaped.replace("\r", "\\r")
|
||||
escaped = escaped.replace("\t", "\\t")
|
||||
return escaped
|
||||
|
||||
|
||||
def write_catalog(entries: Dict[str, Dict[str, object]], output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manual_entries: List[Tuple[str, str]] = [
|
||||
("meta.code", "en"),
|
||||
("meta.name", "English"),
|
||||
("meta.native_name", "English"),
|
||||
("system.language.select_prompt", "Select your language."),
|
||||
("system.language.selected", "Language set to {language}."),
|
||||
("system.language.english_label", "English (en)"),
|
||||
("system.ui.window_title", "Draugnorak"),
|
||||
("system.sex.male", "Male"),
|
||||
("system.sex.female", "Female"),
|
||||
("system.new_character.choose_sex", "Choose your sex."),
|
||||
("system.new_character.enter_name", "Enter your name or press Enter for random."),
|
||||
("system.new_character.save_exists_overwrite", "Save found for {name}. Overwrite?"),
|
||||
("system.load_game.option_with_metadata", "{name}, {sex}, day {day}"),
|
||||
("system.load_game.delete_confirm_base", "Are you sure you want to delete the character {name}?"),
|
||||
("system.load_game.delete_confirm_with_metadata",
|
||||
"Are you sure you want to delete the character {name} gender {sex} days {day}?"),
|
||||
("system.load_game.delete_save_heading", "Delete Save"),
|
||||
("system.load_game.delete_save_failed", "Unable to delete save."),
|
||||
("system.quick_slot.no_item_bound", "No item bound to slot {slot}."),
|
||||
("system.menu.closed", "Closed."),
|
||||
("system.menu.canceled", "Canceled."),
|
||||
("system.menu.no_options", "No options."),
|
||||
("system.menu.no_matches", "No matches for {arg1}."),
|
||||
("system.option.no", "No"),
|
||||
("system.option.yes", "Yes"),
|
||||
("system.inventory.cant_carry_any_more_item", "You can't carry any more {item}."),
|
||||
("system.inventory.menu.prompt", "Inventory menu. {option}"),
|
||||
("system.inventory.menu.no_options", "Inventory menu. No options."),
|
||||
("system.inventory.option.personal", "Personal inventory"),
|
||||
("system.inventory.option.base_storage", "Base storage"),
|
||||
("system.inventory.count_set_to_slot", "{name} count set to slot {slot}."),
|
||||
("system.inventory.need_quiver_for_arrows", "You need a quiver to carry arrows."),
|
||||
("system.search.found_item", "Found {item}."),
|
||||
("system.search.found_nothing", "Found nothing."),
|
||||
("system.pickup.item", "Picked up {item}."),
|
||||
("system.storage.window_title", "Inventory"),
|
||||
("system.storage.transfer_prompt", "{prompt} (max {max})"),
|
||||
("system.storage.deposit_how_many", "Deposit how many?"),
|
||||
("system.storage.withdraw_how_many", "Withdraw how many?"),
|
||||
("system.storage.nothing_to_deposit", "Nothing to deposit."),
|
||||
("system.storage.nothing_to_withdraw", "Nothing to withdraw."),
|
||||
("system.storage.runed_cannot_deposit", "Runed items cannot be deposited into storage."),
|
||||
("system.storage.item_full", "Storage for that item is full."),
|
||||
("system.storage.no_storage_built", "No storage built."),
|
||||
("system.storage.menu_title", "Base storage."),
|
||||
("system.storage.menu.prompt", "Base storage. {option}"),
|
||||
("system.storage.menu.no_options", "Base storage. No options."),
|
||||
("system.crafting.require.fire_within_three_clay_pot",
|
||||
"You need a fire within 3 tiles to craft a clay pot."),
|
||||
("system.crafting.require.fire_within_three_clay_pots",
|
||||
"You need a fire within 3 tiles to craft clay pots."),
|
||||
("system.crafting.require.fire_within_three_bowstring",
|
||||
"You need a fire within 3 tiles to make bowstring."),
|
||||
("system.crafting.require.altar_incense", "You need an altar to craft incense."),
|
||||
("system.crafting.require.fire_within_three_smoke_fish",
|
||||
"You need a fire within 3 tiles to smoke fish."),
|
||||
("system.crafting.require.fire_within_three_butcher", "You need a fire within 3 tiles to butcher."),
|
||||
("system.character.slot.head", "head"),
|
||||
("system.character.slot.torso", "torso"),
|
||||
("system.character.slot.arms", "arms"),
|
||||
("system.character.slot.hands", "hands"),
|
||||
("system.character.slot.legs", "legs"),
|
||||
("system.character.slot.feet", "feet"),
|
||||
("system.character.menu.prompt", "Character info. {option}"),
|
||||
("system.character.menu.no_options", "Character info. No options."),
|
||||
("system.character.pet.no_pet", "No pet."),
|
||||
("system.character.pet.abandon_confirm", "Really abandon your pet? {option}"),
|
||||
("system.character.pet.unconscious_cannot_abandon",
|
||||
"Your {pet} is unconscious. You can't abandon it right now."),
|
||||
("system.item.label.items", "items"),
|
||||
("system.item.label.item", "item"),
|
||||
("system.item.label.unknown", "Unknown"),
|
||||
("system.equipment.menu.equipped_suffix", " (equipped)"),
|
||||
("system.equipment.name.none", "None"),
|
||||
("system.equipment.name.unknown", "Unknown"),
|
||||
("system.equipment.name.spear", "Spear"),
|
||||
("system.equipment.name.stone_axe", "Stone Axe"),
|
||||
("system.equipment.name.sling", "Sling"),
|
||||
("system.equipment.name.bow", "Bow"),
|
||||
("system.equipment.name.skin_hat", "Skin Hat"),
|
||||
("system.equipment.name.skin_gloves", "Skin Gloves"),
|
||||
("system.equipment.name.skin_pants", "Skin Pants"),
|
||||
("system.equipment.name.skin_tunic", "Skin Tunic"),
|
||||
("system.equipment.name.moccasins", "Moccasins"),
|
||||
("system.equipment.name.skin_pouch", "Skin Pouch"),
|
||||
("system.equipment.name.backpack", "Backpack"),
|
||||
("system.equipment.name.fishing_pole", "Fishing Pole"),
|
||||
("system.equipment.name_plural.spears", "Spears"),
|
||||
("system.equipment.name_plural.stone_axes", "Stone Axes"),
|
||||
("system.equipment.name_plural.slings", "Slings"),
|
||||
("system.equipment.name_plural.bows", "Bows"),
|
||||
("system.equipment.name_plural.skin_hats", "Skin Hats"),
|
||||
("system.equipment.name_plural.skin_gloves", "Skin Gloves"),
|
||||
("system.equipment.name_plural.skin_pants", "Skin Pants"),
|
||||
("system.equipment.name_plural.skin_tunics", "Skin Tunics"),
|
||||
("system.equipment.name_plural.moccasins", "Moccasins"),
|
||||
("system.equipment.name_plural.skin_pouches", "Skin Pouches"),
|
||||
("system.equipment.name_plural.backpacks", "Backpacks"),
|
||||
("system.equipment.name_plural.fishing_poles", "Fishing Poles"),
|
||||
("system.equipment.name_plural.items", "Items"),
|
||||
("system.environment.tree.fell", "Tree fell!"),
|
||||
("system.environment.tree.fell_with_loot",
|
||||
"Tree fell! Got {sticks} {sticks_label}, {vines} {vines_label}, and {logs} {logs_label}."),
|
||||
("system.environment.tree.inventory_full", "Inventory full."),
|
||||
("system.crafting.menu.prompt", "Crafting menu. {option}"),
|
||||
("system.crafting.category.weapons", "Weapons"),
|
||||
("system.crafting.category.tools", "Tools"),
|
||||
("system.crafting.category.materials", "Materials"),
|
||||
("system.crafting.category.clothing", "Clothing"),
|
||||
("system.crafting.category.buildings", "Buildings"),
|
||||
("system.crafting.category.barricade", "Barricade"),
|
||||
("system.crafting.category.runes", "Runes"),
|
||||
("system.crafting.weapons.prompt", "Weapons. {option}"),
|
||||
("system.crafting.weapons.option.spear", "Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]"),
|
||||
("system.crafting.weapons.option.sling", "Sling (1 Skin, 2 Vines)"),
|
||||
("system.crafting.weapons.option.bow", "Bow (1 Stick, 1 Bowstring)"),
|
||||
("system.crafting.tools.prompt", "Tools. {option}"),
|
||||
("system.crafting.tools.option.stone_knife", "Stone Knife (2 Stones)"),
|
||||
("system.crafting.tools.option.snare", "Snare (1 Stick, 2 Vines)"),
|
||||
("system.crafting.tools.option.stone_axe", "Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]"),
|
||||
("system.crafting.tools.option.fishing_pole", "Fishing Pole (1 Stick, 2 Vines)"),
|
||||
("system.crafting.tools.option.rope", "Rope (3 Vines)"),
|
||||
("system.crafting.tools.option.quiver", "Quiver (2 Skins, 2 Vines)"),
|
||||
("system.crafting.tools.option.canoe", "Canoe (4 Logs, 11 Sticks, 11 Vines, 6 Skins, 2 Rope, 6 Reeds)"),
|
||||
("system.crafting.tools.option.reed_basket", "Reed Basket (3 Reeds)"),
|
||||
("system.crafting.tools.option.clay_pot", "Clay Pot (3 Clay)"),
|
||||
("system.crafting.materials.prompt", "Materials. {option}"),
|
||||
("system.crafting.materials.option.butcher_game", "Butcher Game [Requires Game, Knife, Fire nearby]"),
|
||||
("system.crafting.materials.option.smoke_fish", "Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby]"),
|
||||
("system.crafting.materials.option.arrows", "Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver]"),
|
||||
("system.crafting.materials.option.bowstring", "Bowstring (3 Sinew) [Requires Fire nearby]"),
|
||||
("system.crafting.materials.option.incense", "Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"),
|
||||
("system.crafting.clothing.prompt", "Clothing. {option}"),
|
||||
("system.crafting.clothing.option.skin_hat", "Skin Hat (1 Skin, 1 Vine)"),
|
||||
("system.crafting.clothing.option.skin_gloves", "Skin Gloves (1 Skin, 1 Vine)"),
|
||||
("system.crafting.clothing.option.skin_pants", "Skin Pants (6 Skins, 3 Vines)"),
|
||||
("system.crafting.clothing.option.skin_tunic", "Skin Tunic (4 Skins, 2 Vines)"),
|
||||
("system.crafting.clothing.option.moccasins", "Moccasins (2 Skins, 1 Vine)"),
|
||||
("system.crafting.clothing.option.skin_pouch", "Skin Pouch (2 Skins, 1 Vine)"),
|
||||
("system.crafting.clothing.option.backpack", "Backpack (11 Skins, 5 Vines, 4 Skin Pouches)"),
|
||||
("system.crafting.buildings.prompt", "Buildings. {option}"),
|
||||
("system.crafting.buildings.option.firepit", "Firepit (9 Stones)"),
|
||||
("system.crafting.buildings.option.fire", "Fire (2 Sticks, 1 Log) [Requires Firepit]"),
|
||||
("system.crafting.buildings.option.herb_garden", "Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]"),
|
||||
("system.crafting.buildings.option.storage_upgrade_1",
|
||||
"Upgrade Storage (6 Logs, 9 Stones, 8 Vines) [Base Only, 50 each]"),
|
||||
("system.crafting.buildings.option.storage_upgrade_2",
|
||||
"Upgrade Storage (12 Logs, 18 Stones, 16 Vines) [Base Only, 100 each]"),
|
||||
("system.crafting.buildings.option.pasture", "Pasture (8 Logs, 18 Ropes) [Base Only, Requires Storage Upgrade]"),
|
||||
("system.crafting.buildings.option.stable",
|
||||
"Stable (10 Logs, 15 Stones, 10 Vines) [Base Only, Requires Storage Upgrade]"),
|
||||
("system.crafting.buildings.option.altar", "Altar (9 Stones, 3 Sticks) [Base Only]"),
|
||||
("system.crafting.barricade.prompt", "Barricade. {option}"),
|
||||
("system.crafting.barricade.option.reinforce_sticks",
|
||||
"Reinforce with sticks ({cost} sticks, +{health} health)"),
|
||||
("system.crafting.barricade.option.reinforce_vines", "Reinforce with vines ({cost} vines, +{health} health)"),
|
||||
("system.crafting.barricade.option.reinforce_log", "Reinforce with log ({cost} log, +{health} health)"),
|
||||
("system.crafting.barricade.option.reinforce_stones",
|
||||
"Reinforce with stones ({cost} stones, +{health} health)"),
|
||||
("system.crafting.missing", "Missing: {requirements}"),
|
||||
("system.crafting.requirement.stone_knife", "Stone Knife"),
|
||||
("system.crafting.requirement.game", "Game"),
|
||||
("system.crafting.requirement.favor", "favor"),
|
||||
("system.crafting.requirement.resources", "resources"),
|
||||
]
|
||||
|
||||
lines: List[str] = []
|
||||
lines.append("; Draugnorak localization catalog")
|
||||
lines.append("; Copy this file to lang/<code>.ini and translate only the right-hand values.")
|
||||
lines.append("; Keep keys unchanged. Use placeholders like {arg1}, {count}, {language} exactly as written.")
|
||||
lines.append("")
|
||||
|
||||
lines.append("[meta]")
|
||||
for key, value in manual_entries[:3]:
|
||||
lines.append(f"{key.split('.', 1)[1]}={escape_for_ini(value)}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("[system]")
|
||||
for key, value in manual_entries[3:]:
|
||||
lines.append(f"{key.split('.', 1)[1]}={escape_for_ini(value)}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("[messages]")
|
||||
|
||||
for key in sorted(entries.keys()):
|
||||
value = entries[key]["value"]
|
||||
refs: List[str] = entries[key]["refs"] # type: ignore[assignment]
|
||||
for ref in refs[:3]:
|
||||
lines.append(f"; {ref}")
|
||||
lines.append(f"{key}={escape_for_ini(str(value))}")
|
||||
|
||||
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
entries: Dict[str, Dict[str, object]] = {}
|
||||
|
||||
for file_path in iter_nvgt_files():
|
||||
scan_file(file_path, entries)
|
||||
|
||||
add_item_registry_fragments(entries)
|
||||
add_seed_messages(entries)
|
||||
add_learn_sounds_label_seeds(entries)
|
||||
|
||||
en_path = ROOT / "lang" / "en.ini"
|
||||
template_path = ROOT / "lang" / "en.template.ini"
|
||||
|
||||
write_catalog(entries, en_path)
|
||||
write_catalog(entries, template_path)
|
||||
|
||||
print(f"Wrote {en_path.relative_to(ROOT)} with {len(entries)} generated entries")
|
||||
print(f"Wrote {template_path.relative_to(ROOT)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user