skilldrop-cli 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/catalogue.json +18 -2
- package/model-routing.json +16 -0
- package/package.json +1 -1
- package/packs/event-planner/pack.json +61 -0
- package/packs/event-planner/skills/run-of-show/SKILL.md +141 -0
- package/packs/event-planner/skills/run-of-show/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/run-of-show/evals/evals.json +103 -0
- package/packs/event-planner/skills/run-of-show/evals/files/1/agenda.md +16 -0
- package/packs/event-planner/skills/run-of-show/evals/files/2/schedule.csv +8 -0
- package/packs/event-planner/skills/run-of-show/evals/files/3/notes.txt +3 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/sessions_export.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/venue_and_crew.txt +27 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/runsheet_v4.csv +28 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/venue_and_suppliers.txt +22 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/acts_from_booker.csv +26 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/venue_rooms_and_crew.txt +14 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/runsheet_draft_v4.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/venue_and_fixed_times.txt +24 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.md +77 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.svg +58 -0
- package/packs/event-planner/skills/run-of-show/manifest.json +44 -0
- package/packs/event-planner/skills/run-of-show/scripts/show_check.py +295 -0
- package/packs/event-planner/skills/run-of-show/templates/show.json +15 -0
- package/packs/event-planner/skills/seating-plan/SKILL.md +152 -0
- package/packs/event-planner/skills/seating-plan/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/seating-plan/evals/evals.json +134 -0
- package/packs/event-planner/skills/seating-plan/evals/files/1/guests.csv +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/2/table-plan.txt +8 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/brief.txt +4 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/guests.csv +49 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/rsvp_export.csv +127 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/seating_notes.txt +21 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/organiser_notes.txt +10 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/registrations.csv +93 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/table_plan_v3.csv +94 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/bridgets_notes.txt +30 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/ticket_export.csv +117 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/venue_floorplan_notes.txt +14 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/keep_apart_CONFIDENTIAL.txt +9 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/prom_seating_v3.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/ticket_list.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/venue_info.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/registrations_export.csv +147 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/room_layout.md +22 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/seating_requests.txt +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/guest_list.csv +84 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/notes.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/table_plan.csv +85 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.md +61 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.svg +39 -0
- package/packs/event-planner/skills/seating-plan/manifest.json +44 -0
- package/packs/event-planner/skills/seating-plan/scripts/seating_check.py +324 -0
- package/packs/event-planner/skills/seating-plan/templates/seating.json +28 -0
- package/packs/experience-design/pack.json +5 -0
- package/packs/experience-design/skills/locale-check/SKILL.md +139 -0
- package/packs/experience-design/skills/locale-check/evals/eval_queries.json +38 -0
- package/packs/experience-design/skills/locale-check/evals/evals.json +104 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/budget.json +1 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/ar.json +16 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/de.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/en.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/pl.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-es/strings.xml +11 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-fr/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ja.po +20 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/messages.pot +23 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ru.po +22 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/budgets.json +15 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/ar.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/de.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/en.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/fr.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/pl.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values/strings.xml +77 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-es/strings.xml +63 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-ja/strings.xml +61 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/cs.po +605 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/marlin.pot +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/pt_BR.po +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/ru.po +608 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.stringsdict +70 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.strings +157 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.stringsdict +86 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.stringsdict +68 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.stringsdict +62 -0
- package/packs/experience-design/skills/locale-check/examples/settings-release.md +107 -0
- package/packs/experience-design/skills/locale-check/manifest.json +45 -0
- package/packs/experience-design/skills/locale-check/scripts/locale_check.py +1105 -0
- package/packs/operations-manager/pack.json +60 -0
- package/packs/operations-manager/skills/shift-roster/SKILL.md +166 -0
- package/packs/operations-manager/skills/shift-roster/evals/eval_queries.json +38 -0
- package/packs/operations-manager/skills/shift-roster/evals/evals.json +135 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/shifts.csv +3 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/staff.csv +8 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/rota.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/staff.csv +6 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/3/team.md +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/availability_export_wk47.csv +23 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/rota_notes.txt +20 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/rota_draft_16-29nov.csv +107 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/staff_list.csv +17 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/availability_notes.txt +24 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/peoplehub_staff_export.csv +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/rota_template.csv +25 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/leave_and_availability.txt +18 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/rota_wc_30nov.csv +47 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/staff_list.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/availability_form_responses.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/cover_requirements.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/staff_export.csv +29 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/leave_and_restrictions.txt +16 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/rota_export_07-13Dec.csv +85 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/staff_list.csv +27 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.md +72 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.svg +123 -0
- package/packs/operations-manager/skills/shift-roster/manifest.json +34 -0
- package/packs/operations-manager/skills/shift-roster/scripts/roster_check.py +633 -0
- package/packs/operations-manager/skills/shift-roster/templates/roster.json +22 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check a table seating plan against its rules and draw the room. Stdlib only.
|
|
3
|
+
|
|
4
|
+
Reads a seating.json: the tables (capacity, shape, position in metres, accessible or not),
|
|
5
|
+
the guests (name, party or household, needs), the rules (groups that sit together, pairs
|
|
6
|
+
kept apart, fixed seats) and the assignments (table -> guests). It fails a plan where
|
|
7
|
+
- a guest is unseated, or seated twice
|
|
8
|
+
- a table holds more guests than its capacity
|
|
9
|
+
- a together group is split across tables
|
|
10
|
+
- an apart pair shares a table
|
|
11
|
+
- a wheelchair user sits at a table not marked accessible
|
|
12
|
+
- a guest with a fixed seat is not at that table
|
|
13
|
+
and warns where
|
|
14
|
+
- a household or party that would fit at one table is split
|
|
15
|
+
- a table is under half full, or empty
|
|
16
|
+
- a guest who came alone sits at a table where everyone else is one other party
|
|
17
|
+
- a guest who needs to be near an exit is further than --exit-distance from one (when
|
|
18
|
+
the room's exits are given)
|
|
19
|
+
Then it lists every table with its guests and dietary needs, and with --svg draws the room:
|
|
20
|
+
tables at their positions, labelled with their fill, names around each, failures in red.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 seating_check.py seating.json [--svg room.svg] [--json] [--exit-distance 8]
|
|
24
|
+
Exit codes: 0 no failures, 1 a failure, 2 bad input.
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BadInput(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _num(v, what):
|
|
37
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
|
|
38
|
+
raise BadInput(f"{what} must be a number, got {v!r}")
|
|
39
|
+
return float(v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _point(v, what):
|
|
43
|
+
if not isinstance(v, (list, tuple)) or len(v) != 2:
|
|
44
|
+
raise BadInput(f"{what} must be [x, y] in metres")
|
|
45
|
+
return (_num(v[0], what + " x"), _num(v[1], what + " y"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load(data):
|
|
49
|
+
"""Validate the raw JSON and return a normalised plan. Raises BadInput."""
|
|
50
|
+
if not isinstance(data, dict):
|
|
51
|
+
raise BadInput("the file must hold a JSON object")
|
|
52
|
+
tables, guests = {}, {}
|
|
53
|
+
for i, t in enumerate(data.get("tables") or []):
|
|
54
|
+
if not isinstance(t, dict) or not t.get("id"):
|
|
55
|
+
raise BadInput(f"table {i + 1} needs an id")
|
|
56
|
+
tid = str(t["id"])
|
|
57
|
+
if tid in tables:
|
|
58
|
+
raise BadInput(f"two tables are called {tid!r}")
|
|
59
|
+
cap = _num(t.get("capacity", 10), f"table {tid} capacity")
|
|
60
|
+
if cap < 1 or cap != int(cap):
|
|
61
|
+
raise BadInput(f"table {tid} capacity must be a whole number of seats")
|
|
62
|
+
shape = str(t.get("shape", "round"))
|
|
63
|
+
if shape not in ("round", "rect"):
|
|
64
|
+
raise BadInput(f"table {tid} shape must be round or rect")
|
|
65
|
+
tables[tid] = {"id": tid, "capacity": int(cap), "shape": shape,
|
|
66
|
+
"at": _point(t["at"], f"table {tid} position") if "at" in t else None,
|
|
67
|
+
"accessible": bool(t.get("accessible", False)), "label": str(t.get("label", ""))}
|
|
68
|
+
if not tables:
|
|
69
|
+
raise BadInput("no tables")
|
|
70
|
+
for i, g in enumerate(data.get("guests") or []):
|
|
71
|
+
if not isinstance(g, dict) or not g.get("id"):
|
|
72
|
+
raise BadInput(f"guest {i + 1} needs an id")
|
|
73
|
+
gid = str(g["id"])
|
|
74
|
+
if gid in guests:
|
|
75
|
+
raise BadInput(f"two guests have the id {gid!r}")
|
|
76
|
+
needs = g.get("needs") or []
|
|
77
|
+
if isinstance(needs, str):
|
|
78
|
+
needs = [needs]
|
|
79
|
+
guests[gid] = {"id": gid, "name": str(g.get("name") or gid), "party": str(g.get("party") or ""),
|
|
80
|
+
"needs": [str(n).lower() for n in needs], "dietary": str(g.get("dietary") or "")}
|
|
81
|
+
if not guests:
|
|
82
|
+
raise BadInput("no guests")
|
|
83
|
+
|
|
84
|
+
def known(gid, where):
|
|
85
|
+
if str(gid) not in guests:
|
|
86
|
+
raise BadInput(f"{where} names a guest {gid!r} who is not on the guest list")
|
|
87
|
+
return str(gid)
|
|
88
|
+
|
|
89
|
+
together = []
|
|
90
|
+
for i, grp in enumerate(data.get("together") or []):
|
|
91
|
+
if not isinstance(grp, list) or len(grp) < 2:
|
|
92
|
+
raise BadInput(f"together group {i + 1} must list two or more guests")
|
|
93
|
+
together.append([known(x, f"together group {i + 1}") for x in grp])
|
|
94
|
+
apart = []
|
|
95
|
+
for i, pair in enumerate(data.get("apart") or []):
|
|
96
|
+
if not isinstance(pair, list) or len(pair) != 2:
|
|
97
|
+
raise BadInput(f"apart pair {i + 1} must list exactly two guests")
|
|
98
|
+
apart.append([known(x, f"apart pair {i + 1}") for x in pair])
|
|
99
|
+
fixed = {}
|
|
100
|
+
if not isinstance(data.get("fixed") or {}, dict):
|
|
101
|
+
raise BadInput("fixed must map a guest id to a table id")
|
|
102
|
+
for gid, tid in (data.get("fixed") or {}).items():
|
|
103
|
+
if str(tid) not in tables:
|
|
104
|
+
raise BadInput(f"the fixed seat for {gid} names a table {tid!r} that does not exist")
|
|
105
|
+
fixed[known(gid, "fixed")] = str(tid)
|
|
106
|
+
|
|
107
|
+
raw = data.get("assignments") or {}
|
|
108
|
+
if not isinstance(raw, dict):
|
|
109
|
+
raise BadInput("assignments must map each table id to a list of guest ids")
|
|
110
|
+
seats = {tid: [] for tid in tables}
|
|
111
|
+
for tid, gl in raw.items():
|
|
112
|
+
if str(tid) not in tables:
|
|
113
|
+
raise BadInput(f"assignments name a table {tid!r} that does not exist")
|
|
114
|
+
if not isinstance(gl, list):
|
|
115
|
+
raise BadInput(f"assignments for table {tid} must be a list of guest ids")
|
|
116
|
+
seats[str(tid)] += [known(g, f"table {tid}") for g in gl]
|
|
117
|
+
exits = [_point(e, f"exit {i + 1}") for i, e in enumerate(data.get("exits") or [])]
|
|
118
|
+
room = data.get("room") or {}
|
|
119
|
+
if not isinstance(room, dict):
|
|
120
|
+
raise BadInput("room must be {\"width\": metres, \"depth\": metres}")
|
|
121
|
+
return {"tables": tables, "guests": guests, "together": together, "apart": apart, "fixed": fixed,
|
|
122
|
+
"seats": seats, "exits": exits,
|
|
123
|
+
"room": (_num(room["width"], "room width"), _num(room["depth"], "room depth"))
|
|
124
|
+
if room.get("width") and room.get("depth") else None}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def check(plan, exit_distance=8.0):
|
|
128
|
+
tables, guests, seats = plan["tables"], plan["guests"], plan["seats"]
|
|
129
|
+
fails, warns, bad_tables = [], [], set()
|
|
130
|
+
where = {}
|
|
131
|
+
for tid, gl in seats.items():
|
|
132
|
+
for g in gl:
|
|
133
|
+
where.setdefault(g, []).append(tid)
|
|
134
|
+
name = lambda g: guests[g]["name"]
|
|
135
|
+
for g in guests:
|
|
136
|
+
at = where.get(g, [])
|
|
137
|
+
if not at:
|
|
138
|
+
fails.append(f"{name(g)} has no seat")
|
|
139
|
+
elif len(at) > 1:
|
|
140
|
+
fails.append(f"{name(g)} is seated more than once ({', '.join(at)})")
|
|
141
|
+
bad_tables.update(at)
|
|
142
|
+
table_of = {g: at[0] for g, at in where.items() if len(at) == 1}
|
|
143
|
+
for tid, gl in seats.items():
|
|
144
|
+
t = tables[tid]
|
|
145
|
+
if len(gl) > t["capacity"]:
|
|
146
|
+
fails.append(f"table {tid} has {len(gl)} guests for {t['capacity']} seats")
|
|
147
|
+
bad_tables.add(tid)
|
|
148
|
+
for grp in plan["together"]:
|
|
149
|
+
at = {table_of.get(g) for g in grp if g in table_of}
|
|
150
|
+
if len(at) > 1:
|
|
151
|
+
fails.append("must sit together but are split: " + ", ".join(f"{name(g)} ({table_of.get(g, 'unseated')})" for g in grp))
|
|
152
|
+
bad_tables.update(t for t in at if t)
|
|
153
|
+
for a, b in plan["apart"]:
|
|
154
|
+
if a in table_of and table_of.get(a) == table_of.get(b):
|
|
155
|
+
fails.append(f"{name(a)} and {name(b)} must be kept apart but both sit at table {table_of[a]}")
|
|
156
|
+
bad_tables.add(table_of[a])
|
|
157
|
+
for g, tid in table_of.items():
|
|
158
|
+
if "wheelchair" in guests[g]["needs"] and not tables[tid]["accessible"]:
|
|
159
|
+
fails.append(f"{name(g)} uses a wheelchair but table {tid} is not marked accessible")
|
|
160
|
+
bad_tables.add(tid)
|
|
161
|
+
for g, tid in plan["fixed"].items():
|
|
162
|
+
if table_of.get(g) != tid:
|
|
163
|
+
fails.append(f"{name(g)} has a fixed seat at table {tid} but sits at {table_of.get(g, 'no table')}")
|
|
164
|
+
bad_tables.add(tid)
|
|
165
|
+
|
|
166
|
+
biggest = max(t["capacity"] for t in tables.values())
|
|
167
|
+
parties = {}
|
|
168
|
+
for g, info in guests.items():
|
|
169
|
+
if info["party"]:
|
|
170
|
+
parties.setdefault(info["party"], []).append(g)
|
|
171
|
+
for party, members in sorted(parties.items()):
|
|
172
|
+
at = sorted({table_of[g] for g in members if g in table_of})
|
|
173
|
+
if len(at) > 1 and len(members) <= biggest:
|
|
174
|
+
warns.append(f"the {party} party ({len(members)}) is split across tables {', '.join(at)}")
|
|
175
|
+
for tid, gl in seats.items():
|
|
176
|
+
cap = tables[tid]["capacity"]
|
|
177
|
+
if not gl:
|
|
178
|
+
warns.append(f"table {tid} is empty")
|
|
179
|
+
elif len(gl) < cap / 2:
|
|
180
|
+
warns.append(f"table {tid} is under half full ({len(gl)} of {cap})")
|
|
181
|
+
in_group = {g for grp in plan["together"] for g in grp}
|
|
182
|
+
for g, tid in table_of.items():
|
|
183
|
+
p = guests[g]["party"]
|
|
184
|
+
alone = (not p or len(parties.get(p, [])) == 1) and g not in in_group
|
|
185
|
+
others = [o for o in seats[tid] if o != g]
|
|
186
|
+
other_parties = {guests[o]["party"] for o in others}
|
|
187
|
+
if alone and len(others) >= 2 and len(other_parties) == 1 and "" not in other_parties:
|
|
188
|
+
warns.append(f"{name(g)} came alone and is the only guest at table {tid} not in the {other_parties.pop()} party")
|
|
189
|
+
if plan["exits"]:
|
|
190
|
+
for g, tid in table_of.items():
|
|
191
|
+
t = tables[tid]
|
|
192
|
+
if "near exit" in guests[g]["needs"] and t["at"]:
|
|
193
|
+
d = min(math.dist(t["at"], e) for e in plan["exits"])
|
|
194
|
+
if d > exit_distance:
|
|
195
|
+
warns.append(f"{name(g)} needs to be near an exit but table {tid} is {d:.1f} m from the nearest")
|
|
196
|
+
return {"fails": fails, "warns": warns, "bad_tables": sorted(bad_tables), "table_of": table_of,
|
|
197
|
+
"seated": len(table_of), "guests": len(guests),
|
|
198
|
+
"seats": sum(t["capacity"] for t in tables.values())}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def table_rows(plan, r):
|
|
202
|
+
rows = []
|
|
203
|
+
for tid, t in plan["tables"].items():
|
|
204
|
+
gl = plan["seats"][tid]
|
|
205
|
+
diet = {}
|
|
206
|
+
for g in gl:
|
|
207
|
+
d = plan["guests"][g]["dietary"]
|
|
208
|
+
if d:
|
|
209
|
+
diet[d] = diet.get(d, 0) + 1
|
|
210
|
+
rows.append({"table": tid, "shape": t["shape"], "capacity": t["capacity"], "accessible": t["accessible"],
|
|
211
|
+
"seated": len(gl), "guests": [plan["guests"][g]["name"] for g in gl], "dietary": diet,
|
|
212
|
+
"ok": tid not in r["bad_tables"]})
|
|
213
|
+
return rows
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def report(plan, r):
|
|
217
|
+
out = [f"{r['seated']} of {r['guests']} guests seated, {len(plan['tables'])} tables, {r['seats']} seats."]
|
|
218
|
+
for row in table_rows(plan, r):
|
|
219
|
+
flags = ("accessible, " if row["accessible"] else "") + f"{row['seated']}/{row['capacity']}"
|
|
220
|
+
diet = ("; " + ", ".join(f"{n} {d}" for d, n in sorted(row["dietary"].items()))) if row["dietary"] else ""
|
|
221
|
+
out.append(f" {row['table']} ({row['shape']}, {flags}{diet}): {', '.join(row['guests']) or '-'}")
|
|
222
|
+
out += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
|
|
223
|
+
out.append("PASS" if not r["fails"] else f"{len(r['fails'])} failure(s)")
|
|
224
|
+
return "\n".join(out) + "\n"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def esc(t):
|
|
228
|
+
return str(t).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def layout(plan):
|
|
232
|
+
"""Positions for every table: given ones, or a grid for tables without one."""
|
|
233
|
+
pos, n = {}, 0
|
|
234
|
+
for tid, t in plan["tables"].items():
|
|
235
|
+
if t["at"]:
|
|
236
|
+
pos[tid] = t["at"]
|
|
237
|
+
cols = max(1, math.ceil(math.sqrt(len(plan["tables"]))))
|
|
238
|
+
for tid, t in plan["tables"].items():
|
|
239
|
+
if tid not in pos:
|
|
240
|
+
pos[tid] = (2.5 + 4.0 * (n % cols), 2.5 + 4.0 * (n // cols))
|
|
241
|
+
n += 1
|
|
242
|
+
return pos
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def svg(plan, r, scale=40):
|
|
246
|
+
pos = layout(plan)
|
|
247
|
+
radius = lambda t: 0.45 + 0.09 * t["capacity"] # metres, so a 10-seat round is about 1.35 m
|
|
248
|
+
xs = [p[0] for p in pos.values()] + [e[0] for e in plan["exits"]]
|
|
249
|
+
ys = [p[1] for p in pos.values()] + [e[1] for e in plan["exits"]]
|
|
250
|
+
w, d = plan["room"] or (max(xs) + 3, max(ys) + 3)
|
|
251
|
+
W, H = w * scale + 80, d * scale + 110
|
|
252
|
+
P = lambda x, y: (40 + x * scale, 60 + y * scale)
|
|
253
|
+
out = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W:.0f} {H:.0f}" width="{W:.0f}" height="{H:.0f}" '
|
|
254
|
+
'font-family="Helvetica, Arial, sans-serif">',
|
|
255
|
+
f'<rect width="{W:.0f}" height="{H:.0f}" fill="#ffffff"/>',
|
|
256
|
+
f'<text x="40" y="30" font-size="16" font-weight="bold">Seating plan: {r["seated"]} of {r["guests"]} guests, '
|
|
257
|
+
f'{len(plan["tables"])} tables</text>',
|
|
258
|
+
f'<rect x="40" y="60" width="{w * scale:.0f}" height="{d * scale:.0f}" fill="#fafafa" stroke="#444" stroke-width="2"/>']
|
|
259
|
+
for i, e in enumerate(plan["exits"]):
|
|
260
|
+
x, y = P(*e)
|
|
261
|
+
out.append(f'<rect x="{x - 14:.0f}" y="{y - 6:.0f}" width="28" height="12" fill="#2e7d32"/>'
|
|
262
|
+
f'<text x="{x:.0f}" y="{y + 22:.0f}" font-size="10" text-anchor="middle" fill="#2e7d32">EXIT</text>')
|
|
263
|
+
for tid, t in plan["tables"].items():
|
|
264
|
+
x, y = P(*pos[tid])
|
|
265
|
+
rad = radius(t) * scale
|
|
266
|
+
bad = tid in r["bad_tables"]
|
|
267
|
+
stroke, fill = ("#c62828", "#ffebee") if bad else ("#455a64", "#eceff1")
|
|
268
|
+
if t["shape"] == "round":
|
|
269
|
+
out.append(f'<circle cx="{x:.0f}" cy="{y:.0f}" r="{rad:.0f}" fill="{fill}" stroke="{stroke}" stroke-width="{3 if bad else 1.5}"/>')
|
|
270
|
+
else:
|
|
271
|
+
out.append(f'<rect x="{x - rad * 1.4:.0f}" y="{y - rad * 0.6:.0f}" width="{rad * 2.8:.0f}" height="{rad * 1.2:.0f}" '
|
|
272
|
+
f'fill="{fill}" stroke="{stroke}" stroke-width="{3 if bad else 1.5}"/>')
|
|
273
|
+
gl = plan["seats"][tid]
|
|
274
|
+
mark = " ✗" if bad else ""
|
|
275
|
+
acc = " · accessible" if t["accessible"] else ""
|
|
276
|
+
out.append(f'<text x="{x:.0f}" y="{y - 2:.0f}" font-size="13" font-weight="bold" text-anchor="middle" '
|
|
277
|
+
f'fill="{stroke}">{esc(tid)}{mark}</text>'
|
|
278
|
+
f'<text x="{x:.0f}" y="{y + 13:.0f}" font-size="10" text-anchor="middle" fill="#555">{len(gl)}/{t["capacity"]}{acc}</text>')
|
|
279
|
+
rx, ry = (rad + 8, rad + 8) if t["shape"] == "round" else (rad * 1.4 + 10, rad * 0.6 + 10)
|
|
280
|
+
for k, g in enumerate(gl):
|
|
281
|
+
a = -math.pi / 2 + 2 * math.pi * k / max(len(gl), 1)
|
|
282
|
+
gx, gy = x + rx * math.cos(a), y + ry * math.sin(a)
|
|
283
|
+
anchor = "start" if math.cos(a) > 0.2 else ("end" if math.cos(a) < -0.2 else "middle")
|
|
284
|
+
out.append(f'<text x="{gx:.0f}" y="{gy + 3:.0f}" font-size="9" text-anchor="{anchor}" fill="#222">'
|
|
285
|
+
f'{esc(plan["guests"][g]["name"])}</text>')
|
|
286
|
+
ly = 60 + d * scale + 28
|
|
287
|
+
status = "PASS: every rule met" if not r["fails"] else f"{len(r['fails'])} rule(s) broken: tables marked ✗ in red"
|
|
288
|
+
out.append(f'<text x="40" y="{ly:.0f}" font-size="12" fill="{"#2e7d32" if not r["fails"] else "#c62828"}">{esc(status)}</text>')
|
|
289
|
+
out.append(f'<text x="40" y="{ly + 18:.0f}" font-size="10" fill="#666">Positions in metres, '
|
|
290
|
+
f'1 m = {scale} px · names run clockwise from the top of each table</text>')
|
|
291
|
+
out.append("</svg>")
|
|
292
|
+
return "\n".join(out) + "\n"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main(argv=None):
|
|
296
|
+
ap = argparse.ArgumentParser(description="Check a seating plan against its rules and draw the room.")
|
|
297
|
+
ap.add_argument("seating")
|
|
298
|
+
ap.add_argument("--svg", help="write the room plan here")
|
|
299
|
+
ap.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
300
|
+
ap.add_argument("--exit-distance", type=float, default=8.0,
|
|
301
|
+
help="metres from an exit that counts as near one (default 8)")
|
|
302
|
+
args = ap.parse_args(argv)
|
|
303
|
+
try:
|
|
304
|
+
with open(args.seating, encoding="utf-8") as fh:
|
|
305
|
+
plan = load(json.load(fh))
|
|
306
|
+
except (OSError, ValueError) as e: # BadInput and JSONDecodeError are ValueErrors
|
|
307
|
+
print(f"FAIL cannot read {args.seating}: {e}")
|
|
308
|
+
return 2
|
|
309
|
+
r = check(plan, args.exit_distance)
|
|
310
|
+
if args.json:
|
|
311
|
+
print(json.dumps({"pass": not r["fails"], "fails": r["fails"], "warns": r["warns"],
|
|
312
|
+
"seated": r["seated"], "guests": r["guests"], "tables": table_rows(plan, r)}, indent=1))
|
|
313
|
+
else:
|
|
314
|
+
print(report(plan, r), end="")
|
|
315
|
+
if args.svg:
|
|
316
|
+
with open(args.svg, "w", encoding="utf-8") as fh:
|
|
317
|
+
fh.write(svg(plan, r))
|
|
318
|
+
if not args.json:
|
|
319
|
+
print(f"\nWrote {args.svg}")
|
|
320
|
+
return 1 if r["fails"] else 0
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
sys.exit(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"room": {"width": 14, "depth": 10},
|
|
3
|
+
"exits": [[0, 5], [14, 9]],
|
|
4
|
+
"tables": [
|
|
5
|
+
{"id": "Head", "capacity": 8, "shape": "rect", "at": [7, 1.5], "accessible": true},
|
|
6
|
+
{"id": "T1", "capacity": 10, "shape": "round", "at": [3, 5], "accessible": true},
|
|
7
|
+
{"id": "T2", "capacity": 10, "shape": "round", "at": [7, 5.5]},
|
|
8
|
+
{"id": "T3", "capacity": 10, "shape": "round", "at": [11, 5]}
|
|
9
|
+
],
|
|
10
|
+
"guests": [
|
|
11
|
+
{"id": "g1", "name": "Mira Osei", "party": "Couple"},
|
|
12
|
+
{"id": "g2", "name": "Tom Hale", "party": "Couple"},
|
|
13
|
+
{"id": "g3", "name": "Ruth Osei", "party": "Osei", "needs": ["wheelchair"], "dietary": "vegetarian"},
|
|
14
|
+
{"id": "g4", "name": "Kwame Osei", "party": "Osei"},
|
|
15
|
+
{"id": "g5", "name": "Dana Hale", "party": "Hale", "needs": ["near exit"]},
|
|
16
|
+
{"id": "g6", "name": "Pete Hale", "party": "Hale", "dietary": "gluten-free"},
|
|
17
|
+
{"id": "g7", "name": "Lena Brook", "party": ""}
|
|
18
|
+
],
|
|
19
|
+
"together": [["g3", "g4"]],
|
|
20
|
+
"apart": [["g4", "g6"]],
|
|
21
|
+
"fixed": {"g1": "Head", "g2": "Head"},
|
|
22
|
+
"assignments": {
|
|
23
|
+
"Head": ["g1", "g2"],
|
|
24
|
+
"T1": ["g3", "g4", "g7"],
|
|
25
|
+
"T2": [],
|
|
26
|
+
"T3": ["g5", "g6"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"you_get": "A blueprint table and Mermaid diagram with fail points, waits, pains traced to causes, and 2–3 ranked fixes.",
|
|
58
58
|
"decide": "Which fix becomes committed work."
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"skill": "locale-check",
|
|
62
|
+
"you_do": "Hand over the source strings and the translated files before release.",
|
|
63
|
+
"you_get": "Whether the release is safe to ship, and each failure by locale with the corrected string or a note for the translator."
|
|
64
|
+
},
|
|
60
65
|
{
|
|
61
66
|
"skill": "doc-critique",
|
|
62
67
|
"you_do": "Run it on any of the specs before you share them.",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: locale-check
|
|
3
|
+
description: Check a product's translations against the source strings before they ship — every key present, every placeholder ({name}, %s, %1$d, ICU plural and select) carried over intact, the plural forms each language needs, markup tags balanced, and strings over their character budget — with a script that reads JSON, ARB, gettext .po, Android strings.xml and iOS .strings, and lists each problem with its key and locale. Use when someone asks to "check the translations", "QA these locale files", "will the German fit", "why does the French build crash on this string", or hands over locale files before a release.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# locale-check
|
|
7
|
+
|
|
8
|
+
Decides whether a set of translated string files is safe to ship, and says exactly what to fix
|
|
9
|
+
in each one. The defects that matter are mechanical and easy to miss by reading: a `{count}`
|
|
10
|
+
dropped from one German string, Polish shipped with two plural forms where it needs four, an
|
|
11
|
+
Arabic file missing `zero`, two printf arguments swapped in French, an apostrophe that stops an
|
|
12
|
+
Android build. A read-through of 500 strings misses them; a check of every string doesn't. This
|
|
13
|
+
skill does not write the source strings (that's `ux-writing`) or the page content around them
|
|
14
|
+
(that's `content-design`), and it does not translate.
|
|
15
|
+
|
|
16
|
+
## How to respond
|
|
17
|
+
|
|
18
|
+
1. **Run the check on the source and every target before reading anything.** The source is
|
|
19
|
+
the language the product is written in, often `en`; the targets are the translations.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/locale_check.py" en.json de.json fr.json pl.json --json
|
|
23
|
+
# Other IDEs (from the skill folder)
|
|
24
|
+
python3 scripts/locale_check.py values/strings.xml values-ar/strings.xml values-fr/strings.xml
|
|
25
|
+
python3 scripts/locale_check.py messages.pot fr.po --budget budget.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It reads JSON (i18next `key_one` / `key_other` plurals included), ARB, `.po` against a
|
|
29
|
+
`.pot` or source `.po`, Android `strings.xml`, iOS `.strings` and `.stringsdict`. The locale
|
|
30
|
+
comes from the path (`de.json`, `values-fr/`, `fr.lproj`); when a path doesn't say, pass
|
|
31
|
+
`--locale` once per target, in order. A file in another format (XLIFF, YAML, a
|
|
32
|
+
spreadsheet) is read by hand: list its keys, placeholders and plural forms, and apply the
|
|
33
|
+
rules below to every row. Exit 0 means no failures, 1 failures, 2 a file it couldn't read.
|
|
34
|
+
|
|
35
|
+
2. **Read the source string beside every finding, and beside every reordered placeholder.**
|
|
36
|
+
The check matches tokens, not meaning, so a clean result is not the answer.
|
|
37
|
+
- **A reorder is valid syntax and can still be wrong.** For every target whose numbered
|
|
38
|
+
placeholders (`%2$s … %1$s`, `{1} … {0}`) appear in a different order from the source,
|
|
39
|
+
read both sentences and confirm each argument still fills the same role. ✅ *"de %2$s a
|
|
40
|
+
%1$s" for "from %1$s to %2$s" sends the parcel the wrong way: a failure, with the fix
|
|
41
|
+
"de %1$s a %2$s".* A reorder that only follows the target's word order is fine.
|
|
42
|
+
- **Read in full the strings on any screen the person names** (checkout, payment, sign-in),
|
|
43
|
+
whatever the check says.
|
|
44
|
+
A placeholder that moved is fine only once its role is confirmed; one that vanished is not. A select keyword is code, so
|
|
45
|
+
`male` translated as `masculin` breaks the branch. A plural form that exists but says the
|
|
46
|
+
wrong number is still wrong. Confirm each finding against the source before reporting it.
|
|
47
|
+
|
|
48
|
+
3. **Rank by what breaks first.**
|
|
49
|
+
1. **The build or the app crashes:** a file that doesn't parse, an ICU message that
|
|
50
|
+
doesn't parse, an Android apostrophe without a backslash, printf arguments whose types
|
|
51
|
+
changed or swapped, a missing key the code reads.
|
|
52
|
+
2. **The screen shows the wrong thing:** a dropped placeholder, plural forms the language
|
|
53
|
+
needs but doesn't have, markup that doesn't balance.
|
|
54
|
+
3. **The screen overflows:** strings over their budget.
|
|
55
|
+
4. **Style:** warnings such as possibly untranslated, punctuation dropped, a stale key.
|
|
56
|
+
A warning is reported as a warning. It never sits in the failures table.
|
|
57
|
+
|
|
58
|
+
4. **Give the corrected string when the fix is mechanical, and only then.**
|
|
59
|
+
- **Restore a dropped placeholder** where the sentence's grammar puts it, and say so:
|
|
60
|
+
✅ *`"Hallo, {name}!"` — restored `{name}` after the greeting, as in the source.*
|
|
61
|
+
- **Number swapped printf arguments** so the order can stay as the translator wrote it:
|
|
62
|
+
✅ *`"Vous avez %2$d messages, %1$s"`.*
|
|
63
|
+
- **Add a missing plural form** by repeating the language's pattern only when the
|
|
64
|
+
form is regular and you are sure of it (Polish `few` for 2–4: *"2 pliki"*). Otherwise
|
|
65
|
+
mark it for a translator with the numbers it covers: *"pl `few` (2–4, 22–24, …):
|
|
66
|
+
needs a translator"*.
|
|
67
|
+
- **Escape** an Android apostrophe as `\'`.
|
|
68
|
+
- **Never write new translated text silently.** An untranslated string, a wrong meaning or a
|
|
69
|
+
missing key goes to a translator with the source string, the locale and the slot. When you
|
|
70
|
+
do suggest wording for a missing key, label it *suggested, for a translator to confirm*.
|
|
71
|
+
|
|
72
|
+
5. **Change no file unless asked.** Offer a patched copy of each target with the mechanical
|
|
73
|
+
fixes applied, written beside the original (`de.fixed.json`), and list what it changed.
|
|
74
|
+
|
|
75
|
+
6. **Measure every length the person gives you.** A limit stated in the request ("buttons
|
|
76
|
+
max 24 characters", "the German must fit a 120 px tab") is a budget: pass it with
|
|
77
|
+
`--budget`, or count the characters of every string it covers, and report each one over
|
|
78
|
+
with its length and the limit. Never say a string fits without counting it. Only when no
|
|
79
|
+
limit is given anywhere, say in one line that lengths weren't checked, and name the slots
|
|
80
|
+
most at risk (buttons, tabs, toasts).
|
|
81
|
+
A format read by hand, and a locale whose plural rules aren't known, are named too.
|
|
82
|
+
|
|
83
|
+
7. **Reply in this order.** First line: whether the release is safe to ship, as a plain yes
|
|
84
|
+
or no, with the count of failures by locale, counted from the table below it so the two
|
|
85
|
+
agree. Then the failures, in one table per locale
|
|
86
|
+
or one table with a locale column: key, what breaks, source, target, and the corrected
|
|
87
|
+
string or *needs a translator*. Then the warnings, shorter. Then what wasn't checked. Write
|
|
88
|
+
for the person shipping the release: no tool names, no script output pasted, no paths
|
|
89
|
+
inside this skill's folder. Asked which string caused a crash, name every string that would
|
|
90
|
+
cause it, and pick one only when the evidence (a log, a stack trace, a screen) says which.
|
|
91
|
+
|
|
92
|
+
**Non-interactive runs:** a limit in the request is a budget (step 6); with none anywhere,
|
|
93
|
+
there is no length check, and the reply says so. A missing
|
|
94
|
+
locale code is inferred from the strings' script and language, tagged `[assumption]`, and
|
|
95
|
+
listed first. Emit `BLOCKED: need the source strings and at least one translation` only when
|
|
96
|
+
there is no source file or no target at all; with one of several targets unreadable, check the
|
|
97
|
+
rest and report that one as a failure.
|
|
98
|
+
|
|
99
|
+
## Useful references in this skill
|
|
100
|
+
|
|
101
|
+
- [`scripts/locale_check.py`](scripts/locale_check.py) — every check in this skill, with CLDR plural categories for about 60 languages and gettext `nplurals` (stdlib only)
|
|
102
|
+
- [`examples/settings-release.md`](examples/settings-release.md) — worked example: a settings screen in German, Russian and Arabic, with translated select keywords, missing Russian plurals and a toggle over budget
|
|
103
|
+
|
|
104
|
+
## Quality bar
|
|
105
|
+
|
|
106
|
+
- **Every target is checked, every finding read against its source string.** A count with no check behind it is a guess.
|
|
107
|
+
- **Crashes first, then wrong screens, then overflow, then style.** The reader fixes in that order.
|
|
108
|
+
- **A mechanical fix comes with the corrected string;** a wording fix goes to a translator.
|
|
109
|
+
- **Plural forms are judged by the target language's rules,** not the source's: Polish needs four, Arabic six, Japanese one.
|
|
110
|
+
- **Nothing is translated or edited unasked,** and nothing is cut: a dubious string is flagged, not removed.
|
|
111
|
+
- **Every reordered placeholder is read for meaning,** not passed because the syntax is valid.
|
|
112
|
+
- **Every length limit the person states is measured.** "It fits" always has a count behind it.
|
|
113
|
+
- **What wasn't checked is said in one line.**
|
|
114
|
+
|
|
115
|
+
## When to use this skill
|
|
116
|
+
|
|
117
|
+
- ✅ "Check these translations before the release" with locale files
|
|
118
|
+
- ✅ "Why does the French build crash on this string?"
|
|
119
|
+
- ✅ "Will the German fit on the buttons?" with a budget or the widths
|
|
120
|
+
- ✅ "QA the strings the agency sent back"
|
|
121
|
+
- ✅ A pull request that adds or updates locale files
|
|
122
|
+
|
|
123
|
+
## When NOT to use this skill
|
|
124
|
+
|
|
125
|
+
- ❌ Writing the source strings, button labels or error messages: that's `ux-writing`
|
|
126
|
+
- ❌ Writing or restructuring the page content itself: that's `content-design`
|
|
127
|
+
- ❌ "Translate this into French": that's translation, and this skill only checks it
|
|
128
|
+
|
|
129
|
+
## Anti-patterns to avoid
|
|
130
|
+
|
|
131
|
+
- ❌ **Reading the files instead of checking them.** A dropped `{count}` in string 312 of 500 is invisible to a skim.
|
|
132
|
+
- ❌ **Judging plurals by the source.** English has two forms; shipping Russian with two is the bug.
|
|
133
|
+
- ❌ **"Fixing" a translation by rewriting it.** Restore the placeholder; leave the wording to the translator.
|
|
134
|
+
- ❌ **Calling a missing key a warning.** The code reads it; the user sees the raw key or the source text.
|
|
135
|
+
- ❌ **Calling `masculin` a good translation of `male`.** Select keywords are code, never text.
|
|
136
|
+
- ❌ **"Lengths look fine" with no budget.** Say they weren't checked.
|
|
137
|
+
- ❌ **Trusting a clean check on a reordered sentence.** `%2$s … %1$s` passes the syntax and can still swap the sender and the receiver.
|
|
138
|
+
- ❌ **Naming one string as the crash on a guess.** Name every candidate until the evidence picks one.
|
|
139
|
+
- ❌ **Pasting the raw report.** Lead with the ship decision and the fixes, by locale.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "check these translation files before we release",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "QA the locale JSON the agency sent back for German and Polish",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "why does the French Android build crash on this strings.xml",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "will the German strings fit on the buttons? here are the files and the limits",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "are the plural forms right in our ru.po and ar.json?",
|
|
20
|
+
"should_trigger": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "translate these button labels into French",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "write the error messages for our checkout form",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"query": "rewrite our help page in plain English",
|
|
32
|
+
"should_trigger": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"query": "make the wording consistent across the settings screens",
|
|
36
|
+
"should_trigger": false
|
|
37
|
+
}
|
|
38
|
+
]
|