Skip to content

Commit beacce4

Browse files
committed
Add copydeck notes and export/import scripts
1 parent 8e0cc54 commit beacce4

4 files changed

Lines changed: 158 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Ignore any exported CSV files (see scripts/)
2+
copydeck*.csv
3+
14
# Node / TypeScript
25
node_modules/
36
dist/

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ npm run build -- --watch
6565
npm test
6666
```
6767

68+
### Copydecks
69+
70+
Copydecks are JSON files that contain rules and templates for matching and explaining errors. They are stored in `copydecks/` and can be edited or added to.
71+
72+
New error explanations can (should) be generated by an LLM, for ease (TODO: add system instructions for this). The generated content *must* be reviewed and edited by an appropriately-qualified human (eg. learning managers) prior to release, to ensure accuracy and clarity.
73+
74+
Copydecks contain prompts and additional context for localisation.
75+
76+
For management of human-reviewed copydeck content, scripts (in `./scripts`) are provided to extract and update copydeck content in a Google Sheet (and re-import it after review).
77+
6878
## Building
6979

7080
Create a clean build for distribution:

scripts/export_copydeck_csv.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""Export copydeck error messages to CSV for Google Sheets review.
3+
4+
Usage:
5+
python scripts/export_copydeck_csv.py
6+
python scripts/export_copydeck_csv.py --copydeck copydecks/fr/copydeck.json --out review_fr.csv
7+
"""
8+
9+
import argparse
10+
import csv
11+
import json
12+
import sys
13+
from pathlib import Path
14+
15+
COLUMNS = ["error_type", "variant_index", "if_condition", "title", "summary", "why",
16+
"step_1", "step_2", "step_3", "step_4", "step_5"]
17+
18+
19+
def export(copydeck_path: Path, out_path: Path) -> None:
20+
with open(copydeck_path) as f:
21+
data = json.load(f)
22+
23+
rows = []
24+
for error_type, error_def in data["errors"].items():
25+
for i, variant in enumerate(error_def["variants"]):
26+
steps = variant.get("steps", [])
27+
if_condition = variant.get("if")
28+
row = {
29+
"error_type": error_type,
30+
"variant_index": i,
31+
"if_condition": json.dumps(if_condition) if if_condition else "",
32+
"title": variant.get("title", ""),
33+
"summary": variant.get("summary", ""),
34+
"why": variant.get("why", ""),
35+
}
36+
for j in range(1, 6):
37+
row[f"step_{j}"] = steps[j - 1] if j - 1 < len(steps) else ""
38+
rows.append(row)
39+
40+
with open(out_path, "w", newline="", encoding="utf-8") as f:
41+
writer = csv.DictWriter(f, fieldnames=COLUMNS)
42+
writer.writeheader()
43+
writer.writerows(rows)
44+
45+
print(f"Exported {len(rows)} variants to {out_path}")
46+
47+
48+
def main():
49+
parser = argparse.ArgumentParser(description="Export copydeck to CSV for review")
50+
parser.add_argument("--copydeck", default="copydecks/en/copydeck.json",
51+
help="Path to source copydeck JSON (default: copydecks/en/copydeck.json)")
52+
parser.add_argument("--out", default="copydeck_review.csv",
53+
help="Output CSV path (default: copydeck_review.csv)")
54+
args = parser.parse_args()
55+
56+
copydeck_path = Path(args.copydeck)
57+
if not copydeck_path.exists():
58+
print(f"Error: {copydeck_path} not found", file=sys.stderr)
59+
sys.exit(1)
60+
61+
export(copydeck_path, Path(args.out))
62+
63+
64+
if __name__ == "__main__":
65+
main()

scripts/import_copydeck_csv.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
"""Import revised copy from CSV back into a copydeck JSON.
3+
4+
Reads the reviewed CSV (produced by export_copydeck_csv.py), updates title/summary/why/steps
5+
for each variant, and writes the result to a new JSON file. The original if_condition and
6+
_placeholders metadata are preserved from the source copydeck.
7+
8+
Usage:
9+
python scripts/import_copydeck_csv.py
10+
python scripts/import_copydeck_csv.py --csv copydeck_review.csv --copydeck copydecks/en/copydeck.json --out copydecks/en/copydeck_revised.json
11+
"""
12+
13+
import argparse
14+
import csv
15+
import json
16+
import sys
17+
from pathlib import Path
18+
19+
20+
def import_csv(csv_path: Path, copydeck_path: Path, out_path: Path) -> None:
21+
with open(copydeck_path) as f:
22+
data = json.load(f)
23+
24+
with open(csv_path, newline="", encoding="utf-8") as f:
25+
rows = list(csv.DictReader(f))
26+
27+
updated = 0
28+
for row in rows:
29+
error_type = row["error_type"]
30+
variant_index = int(row["variant_index"])
31+
32+
if error_type not in data["errors"]:
33+
print(f"Warning: error type '{error_type}' not found in copydeck, skipping", file=sys.stderr)
34+
continue
35+
36+
variants = data["errors"][error_type]["variants"]
37+
if variant_index >= len(variants):
38+
print(f"Warning: variant index {variant_index} out of range for {error_type}, skipping", file=sys.stderr)
39+
continue
40+
41+
variant = variants[variant_index]
42+
variant["title"] = row["title"]
43+
variant["summary"] = row["summary"]
44+
variant["why"] = row["why"]
45+
46+
steps = [row[f"step_{j}"] for j in range(1, 6) if row.get(f"step_{j}", "").strip()]
47+
if steps:
48+
variant["steps"] = steps
49+
elif "steps" in variant:
50+
del variant["steps"]
51+
52+
updated += 1
53+
54+
with open(out_path, "w", encoding="utf-8") as f:
55+
json.dump(data, f, indent=2, ensure_ascii=False)
56+
f.write("\n")
57+
58+
print(f"Updated {updated} variants → {out_path}")
59+
60+
61+
def main():
62+
parser = argparse.ArgumentParser(description="Import reviewed CSV back into copydeck JSON")
63+
parser.add_argument("--csv", default="copydeck_review.csv",
64+
help="Input CSV path (default: copydeck_review.csv)")
65+
parser.add_argument("--copydeck", default="copydecks/en/copydeck.json",
66+
help="Source copydeck JSON to update (default: copydecks/en/copydeck.json)")
67+
parser.add_argument("--out", default="copydecks/en/copydeck.json",
68+
help="Output JSON path (default: overwrites source copydeck)")
69+
args = parser.parse_args()
70+
71+
for p in [Path(args.csv), Path(args.copydeck)]:
72+
if not p.exists():
73+
print(f"Error: {p} not found", file=sys.stderr)
74+
sys.exit(1)
75+
76+
import_csv(Path(args.csv), Path(args.copydeck), Path(args.out))
77+
78+
79+
if __name__ == "__main__":
80+
main()

0 commit comments

Comments
 (0)