Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions extras/bundle_translations/apply_translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Apply translations from CSV back to pyRevit bundle YAML files.

This script reads a CSV file containing translations (generated by
extract_translations.py) and applies them to the corresponding bundle.yaml
files, updating or creating multilingual field dictionaries.

Configuration:
TRANSLATION_CSV: Path to the CSV file containing translations
LANGUAGE_KEY: Translation language key to apply (e.g., 'chinese_s')
SOURCE_LANG: Source language key (default: 'en_us')
"""
import csv
from pathlib import Path
from ruamel.yaml import YAML # pip install ruamel.yaml

# -------- CONFIG --------
TRANSLATION_CSV = r"C:\temp\translations.csv" # same path as in other script
LANGUAGE_KEY = "chinese_s" # same as in other script
SOURCE_LANG = "en_us" # same as in other script
# ------------------------

yaml = YAML()


def build_lookup(csv_path):
"""Build dictionary: {yaml_file: {key_type: {"en_us":..., "translation":...}}}"""
lookup = {}

with open(csv_path, encoding="utf-8") as f:
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading the CSV file, newline="" parameter should be specified for the open() call according to Python's CSV module documentation. This ensures proper handling of newlines across different platforms. The current code may not handle newlines in field values correctly on Windows.

Suggested change
with open(csv_path, encoding="utf-8") as f:
with open(csv_path, encoding="utf-8", newline="") as f:

Copilot uses AI. Check for mistakes.
reader = csv.DictReader(f)

for row in reader:
file = row["yaml_file"]
key = row["key_type"]

en = row[SOURCE_LANG]
tr = row[LANGUAGE_KEY]

lookup.setdefault(file, {}).setdefault(key, {})
lookup[file][key] = {"en": en, "tr": tr}

return lookup


def insert_translation(data, key_type, trans, node_path=""):
"""Insert translation from CSV.
Rule: write ONLY if CSV provides a non-empty translation; always overwrite."""
if not isinstance(data, dict):
return

translation = trans["tr"].strip()

if key_type in data:
value = data[key_type]

# CASE A: scalar -> convert to dict
if isinstance(value, str):
print(f"[Scalar detected during import] {node_path}/{key_type} | Converting to multilingual")
new_dict = {SOURCE_LANG: value}

if translation:
new_dict[LANGUAGE_KEY] = translation
print(f"[Write] {node_path}/{key_type} | {LANGUAGE_KEY} = '{translation}'")
else:
print(f"[Skip] CSV translation empty for {node_path}/{key_type}")

data[key_type] = new_dict
return

# CASE B: dict
elif isinstance(value, dict):
# ensure en_us exists
if SOURCE_LANG not in value:
if len(value):
first_key = next(iter(value.keys()))
value[SOURCE_LANG] = value[first_key]
print(f"[Promotion] No {SOURCE_LANG} → using '{first_key}' value")

# write only if CSV translation provided
if translation:
value[LANGUAGE_KEY] = translation
print(f"[Write] {node_path}/{key_type} | Overwriting {LANGUAGE_KEY} = '{translation}'")
else:
print(f"[Skip] CSV translation empty for {node_path}/{key_type}")

# recurse deeper
for k, v in data.items():
child_path = f"{node_path}/{k}" if node_path else k
insert_translation(v, key_type, trans, child_path)


def main():
lookup = build_lookup(TRANSLATION_CSV)

for yaml_file, fields in lookup.items():
yaml_file = Path(yaml_file)

if not yaml_file.exists():
print(f"[Missing] {yaml_file}")
continue

try:
with open(yaml_file, "r", encoding="utf-8") as f:
data = yaml.load(f)

for key_type, trans in fields.items():
insert_translation(data, key_type, trans)

with open(yaml_file, "w", encoding="utf-8") as f:
yaml.dump(data, f)

print(f"[Processing] {yaml_file}")

except Exception as e:
print(f"[ERROR] writing {yaml_file}: {e}")


if __name__ == "__main__":
main()
102 changes: 102 additions & 0 deletions extras/bundle_translations/extract_translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Extract translation strings from pyRevit bundle YAML files.

This script scans bundle.yaml files for title and tooltip fields,
extracting English source text and any existing translations to a CSV file
for easier translation workflow.

Configuration:
BASE_DIR: Root directory containing bundle YAML files
OUTPUT_CSV: Path where the CSV file will be written
LANGUAGE_KEY: Translation language key (e.g., 'chinese_s')
SOURCE_LANG: Source language key (default: 'en_us')
"""
import os
from pathlib import Path
from ruamel.yaml import YAML # pip install ruamel.yaml
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ruamel.yaml package is not listed in the project's Pipfile. This dependency should be added to ensure consistent installation across environments. Note that the project currently uses pyyaml (line 14 in Pipfile), so consider either using the existing pyyaml package or adding ruamel.yaml to the dependencies.

Copilot uses AI. Check for mistakes.
import csv

# -------- CONFIG --------
BASE_DIR = r"C:\Program Files\pyRevit-Master\extensions\pyRevitTools.extension" # adjust for custom installation
OUTPUT_CSV = r"C:\temp\translations.csv" # same path as in other script
LANGUAGE_KEY = "chinese_s" # translation key to extract/merge
SOURCE_LANG = "en_us" # main source language
# ------------------------

yaml = YAML()


def find_yaml_files(base_dir):
for root, _, files in os.walk(base_dir):
for f in files:
if f.endswith(".yaml"):
yield Path(root) / f


def extract_field(path, field_name, value, results):
"""Extracts English + existing translated value from dict or scalar."""
# CASE 1: multilingual dict
if isinstance(value, dict):
# English (preferred)
en = value.get(SOURCE_LANG)

# fallback to first language if no en_us exists
if not en and len(value):
first_key = next(iter(value.keys()))
en = value[first_key]

# Existing translation to preserve
tr = value.get(LANGUAGE_KEY, "")

if en:
results.append([path, field_name, en, tr])
return

# CASE 2: scalar string
if isinstance(value, str):
print(
f"[Scalar detected] File: {path} | Field: {field_name} | Value: '{value}'"
)
Comment on lines +56 to +58
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This print statement for debug purposes appears to be logging information that would be better suited for a configurable logging mechanism. Consider using the logging module to allow users to control verbosity levels, especially for scalar detection which may not always be relevant to users.

Copilot uses AI. Check for mistakes.
results.append([path, field_name, value, ""])
return


def extract_values(data, path, results):
"""Recursively walk through structure and extract fields."""
if not isinstance(data, dict):
return

for field_name in ("title", "tooltip"):
if field_name in data:
extract_field(path, field_name, data[field_name], results)

# recurse into children
for k, v in data.items():
child_path = f"{path}/{k}"
extract_values(v, child_path, results)


def main():
results = []

for yaml_file in find_yaml_files(BASE_DIR):
try:
with open(yaml_file, "r", encoding="utf-8") as f:
data = yaml.load(f)

extract_values(data, str(yaml_file), results)

except Exception as e:
print(f"ERROR reading {yaml_file}: {e}")

# write CSV
with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["yaml_file", "key_type", SOURCE_LANG, LANGUAGE_KEY])
for row in results:
writer.writerow(row)

print(f"\nExtracted {len(results)} records to {OUTPUT_CSV}")


if __name__ == "__main__":
main()