@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.1
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/CHANGELOG.md +12 -0
- package/docs/facts.json +1 -1
- package/manifest.json +63 -62
- package/package.json +2 -2
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +21 -21
- package/pipeline/skills/shared/README.md +64 -64
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
- package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
- package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
- package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
- package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
- package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
|
@@ -11,6 +11,7 @@ import html
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
|
+
import shlex
|
|
14
15
|
import shutil
|
|
15
16
|
import struct
|
|
16
17
|
import subprocess
|
|
@@ -22,7 +23,8 @@ import zipfile
|
|
|
22
23
|
from collections import namedtuple
|
|
23
24
|
from xml.sax.saxutils import escape, quoteattr
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
from _shared import LANGS, slug, xml_text
|
|
27
|
+
|
|
26
28
|
WARN = "\u26a0\ufe0f"
|
|
27
29
|
DASH = "-"
|
|
28
30
|
VERDICTS = ("reuse", "review", "new")
|
|
@@ -120,11 +122,6 @@ def look(row):
|
|
|
120
122
|
return LOOKS.get(row.get("verdict"), UNKNOWN_LOOK)
|
|
121
123
|
|
|
122
124
|
|
|
123
|
-
def slug(text):
|
|
124
|
-
value = re.sub(r"[\W_]+", "-", str(text or "").strip().lower()).strip("-")
|
|
125
|
-
return value or "screen"
|
|
126
|
-
|
|
127
|
-
|
|
128
125
|
def norm(value):
|
|
129
126
|
text = unicodedata.normalize("NFC", str(value or "")).strip()
|
|
130
127
|
text = re.sub(r"\s+", " ", text)
|
|
@@ -189,8 +186,12 @@ def load_keyshots(mapping, mapping_dir, out_dir):
|
|
|
189
186
|
return shots
|
|
190
187
|
|
|
191
188
|
|
|
189
|
+
def xml_escape(value):
|
|
190
|
+
return escape(xml_text(value))
|
|
191
|
+
|
|
192
|
+
|
|
192
193
|
def node(name, attrs=(), *children):
|
|
193
|
-
opening = name + "".join(f" {k}={quoteattr(
|
|
194
|
+
opening = name + "".join(f" {k}={quoteattr(xml_text(v))}" for k, v in attrs)
|
|
194
195
|
if not children:
|
|
195
196
|
return f"<{opening}/>"
|
|
196
197
|
return f"<{opening}>{''.join(children)}</{name}>"
|
|
@@ -291,12 +292,12 @@ class Doc:
|
|
|
291
292
|
return storage_macro(
|
|
292
293
|
"status",
|
|
293
294
|
node("ac:parameter", [("ac:name", "colour")], look(row).status),
|
|
294
|
-
node("ac:parameter", [("ac:name", "title")],
|
|
295
|
+
node("ac:parameter", [("ac:name", "title")], xml_escape(self.verdict(row))),
|
|
295
296
|
)
|
|
296
297
|
|
|
297
298
|
def xml(self):
|
|
298
299
|
t, m = self.t, self.m
|
|
299
|
-
e =
|
|
300
|
+
e = xml_escape
|
|
300
301
|
|
|
301
302
|
def heading(label):
|
|
302
303
|
return f"<h2>{e(t[label])}</h2>"
|
|
@@ -504,7 +505,7 @@ def para(text, bold=False, size=None, rtl=False):
|
|
|
504
505
|
if rtl:
|
|
505
506
|
styling.append(w("rtl"))
|
|
506
507
|
run = [w("rPr", *styling)] if styling else []
|
|
507
|
-
run.append(node("w:t", [("xml:space", "preserve")],
|
|
508
|
+
run.append(node("w:t", [("xml:space", "preserve")], xml_escape(text)))
|
|
508
509
|
lead = [w("pPr", w("bidi"))] if rtl else []
|
|
509
510
|
return w("p", *lead, w("r", *run))
|
|
510
511
|
|
|
@@ -663,17 +664,21 @@ def chrome_candidates():
|
|
|
663
664
|
|
|
664
665
|
def pdf_libreoffice(docx_path, out_dir):
|
|
665
666
|
binaries = [shutil.which("soffice"), shutil.which("libreoffice"), mac_app("LibreOffice", "soffice")]
|
|
666
|
-
|
|
667
|
+
name = os.path.splitext(os.path.basename(docx_path))[0] + ".pdf"
|
|
667
668
|
for binary in binaries:
|
|
668
669
|
if not (binary and os.path.isfile(binary)):
|
|
669
670
|
continue
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
671
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
672
|
+
try:
|
|
673
|
+
subprocess.run([binary, "--headless", "--convert-to", "pdf", "--outdir", scratch, docx_path],
|
|
674
|
+
capture_output=True, timeout=180)
|
|
675
|
+
except (OSError, subprocess.SubprocessError):
|
|
676
|
+
continue
|
|
677
|
+
rendered = os.path.join(scratch, name)
|
|
678
|
+
if os.path.isfile(rendered) and os.path.getsize(rendered) > 0:
|
|
679
|
+
target = os.path.join(out_dir, name)
|
|
680
|
+
shutil.copyfile(rendered, target)
|
|
681
|
+
return target
|
|
677
682
|
return None
|
|
678
683
|
|
|
679
684
|
|
|
@@ -698,7 +703,7 @@ def pdf_chrome(preview_path, pdf_path):
|
|
|
698
703
|
with tempfile.TemporaryDirectory() as profile:
|
|
699
704
|
if os.path.exists(pdf_path):
|
|
700
705
|
os.remove(pdf_path)
|
|
701
|
-
argv = [chrome, headless, "--disable-gpu", f"--user-data-dir={profile}", "--no-pdf-header-footer",
|
|
706
|
+
argv = [chrome, headless, "--no-sandbox", "--disable-gpu", f"--user-data-dir={profile}", "--no-pdf-header-footer",
|
|
702
707
|
f"--print-to-pdf={pdf_path}", page]
|
|
703
708
|
try:
|
|
704
709
|
proc = subprocess.Popen(argv, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
|
@@ -721,17 +726,31 @@ def pdf_wkhtml(preview_path, pdf_path):
|
|
|
721
726
|
return pdf_path if os.path.isfile(pdf_path) and os.path.getsize(pdf_path) > 0 else None
|
|
722
727
|
|
|
723
728
|
|
|
724
|
-
def
|
|
729
|
+
def upload_path(name, mapping_dir, out_dir):
|
|
730
|
+
if os.path.isabs(name):
|
|
731
|
+
return name
|
|
732
|
+
for folder in (mapping_dir, out_dir):
|
|
733
|
+
if os.path.exists(os.path.join(folder, name)):
|
|
734
|
+
return os.path.join(folder, name)
|
|
735
|
+
return os.path.join(out_dir, name)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def print_upload(name, mapping, screen, mapping_dir, out_dir):
|
|
725
739
|
prefix = os.environ.get("LOCALIZATION_PAGE_PREFIX", "Localization")
|
|
726
|
-
|
|
727
|
-
flags
|
|
728
|
-
flags
|
|
740
|
+
quote = shlex.quote
|
|
741
|
+
flags = [f"--xml {quote(os.path.join(out_dir, name + '.confluence.xml'))}", f"--screen {quote(str(screen))}"]
|
|
742
|
+
flags += [
|
|
743
|
+
f"--{field} {quote(upload_path(str(mapping[field]), mapping_dir, out_dir))}"
|
|
744
|
+
for field in ("screenshot", "overlay", "keyshots") if mapping.get(field)
|
|
745
|
+
]
|
|
746
|
+
flags.append(f"--attach {quote(os.path.join(out_dir, name + '.localization.xlsx'))}")
|
|
729
747
|
print(" \\\n ".join(["python3 scripts/publish-confluence.py"] + flags))
|
|
730
748
|
print()
|
|
749
|
+
title = json.dumps(f"{prefix} - {screen}", ensure_ascii=False).replace("'", "'\\''")
|
|
731
750
|
print("# The same call by hand, for Server/DC with a Bearer PAT read from the keychain (argv never sees the token):")
|
|
732
|
-
print("# printf 'header = \"Authorization: Bearer %s\"\\n' \"$(security find-generic-password -s CONFLUENCE_API_TOKEN -w)\" > auth.curlrc
|
|
751
|
+
print("# (umask 077 && rm -f auth.curlrc && printf 'header = \"Authorization: Bearer %s\"\\n' \"$(security find-generic-password -s CONFLUENCE_API_TOKEN -w)\" > auth.curlrc)")
|
|
733
752
|
print("# curl -sS -K auth.curlrc -X POST -H 'Content-Type: application/json' \"$CONFLUENCE_BASE_URL/rest/api/content\" \\")
|
|
734
|
-
print(f"# --data '{{\"type\":\"page\",\"title\"
|
|
753
|
+
print(f"# --data '{{\"type\":\"page\",\"title\":{title},\"space\":{{\"key\":\"'$CONFLUENCE_SPACE'\"}},"
|
|
735
754
|
"\"ancestors\":[{\"id\":\"'$CONFLUENCE_PARENT'\"}],\"body\":{\"storage\":{\"value\":\"...\",\"representation\":\"storage\"}}}'")
|
|
736
755
|
print("# rm auth.curlrc")
|
|
737
756
|
|
|
@@ -797,7 +816,7 @@ def main():
|
|
|
797
816
|
make_pdf(docx_path, out_dir, stem, keep_docx)
|
|
798
817
|
|
|
799
818
|
if opts.print_upload:
|
|
800
|
-
print_upload(stem, mapping, mapping.get("screen"))
|
|
819
|
+
print_upload(stem, mapping, mapping.get("screen"), mapping_dir, out_dir)
|
|
801
820
|
|
|
802
821
|
|
|
803
822
|
if __name__ == "__main__":
|
|
@@ -15,6 +15,8 @@ import sys
|
|
|
15
15
|
import zipfile
|
|
16
16
|
from xml.sax.saxutils import escape
|
|
17
17
|
|
|
18
|
+
from _shared import clean_key, key_is_dynamic, slug, xml_text
|
|
19
|
+
|
|
18
20
|
COLUMNS = [
|
|
19
21
|
"Channel", "Property Group", "Property Module", "Key",
|
|
20
22
|
"EN Value", "TR Value", "AR Value", "Anotation EN", "Anotation TR",
|
|
@@ -37,11 +39,6 @@ DEFAULT_TAXONOMY = {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
def slug(text):
|
|
41
|
-
value = re.sub(r"[\W_]+", "-", str(text or "").strip().lower()).strip("-")
|
|
42
|
-
return value or "screen"
|
|
43
|
-
|
|
44
|
-
|
|
45
42
|
def read_json(source):
|
|
46
43
|
with open(source, encoding="utf-8") as stream:
|
|
47
44
|
return json.load(stream)
|
|
@@ -62,12 +59,6 @@ def load_taxonomy(path):
|
|
|
62
59
|
return taxonomy
|
|
63
60
|
|
|
64
61
|
|
|
65
|
-
def clean_key(new_key):
|
|
66
|
-
text = str(new_key or "")
|
|
67
|
-
cut = text.find("(owned by")
|
|
68
|
-
return (text[:cut] if cut >= 0 else text).strip()
|
|
69
|
-
|
|
70
|
-
|
|
71
62
|
def bucket(key, taxonomy):
|
|
72
63
|
lowered = key.lower()
|
|
73
64
|
if "error" in lowered:
|
|
@@ -88,9 +79,12 @@ def bucket(key, taxonomy):
|
|
|
88
79
|
|
|
89
80
|
|
|
90
81
|
def table_rows(mapping, channel, taxonomy):
|
|
91
|
-
out = []
|
|
92
|
-
for row in mapping.get("rows") or []:
|
|
82
|
+
out, skipped = [], []
|
|
83
|
+
for number, row in enumerate(mapping.get("rows") or [], 1):
|
|
93
84
|
key = clean_key(row.get("newKey"))
|
|
85
|
+
if not key or key_is_dynamic(key):
|
|
86
|
+
skipped.append((number, key))
|
|
87
|
+
continue
|
|
94
88
|
group, module = bucket(key, taxonomy)
|
|
95
89
|
if row.get("propertyGroup"):
|
|
96
90
|
group = row["propertyGroup"]
|
|
@@ -103,7 +97,7 @@ def table_rows(mapping, channel, taxonomy):
|
|
|
103
97
|
new.get("en") or "", new.get("tr") or "", new.get("ar") or "",
|
|
104
98
|
cms.get("en") or "", cms.get("tr") or "",
|
|
105
99
|
])
|
|
106
|
-
return out
|
|
100
|
+
return out, skipped
|
|
107
101
|
|
|
108
102
|
|
|
109
103
|
def column_letter(index):
|
|
@@ -137,7 +131,7 @@ def part(root, attrs, *children):
|
|
|
137
131
|
|
|
138
132
|
|
|
139
133
|
def cell(ref, value, style):
|
|
140
|
-
text = escape(
|
|
134
|
+
text = escape(xml_text(value)).replace("\n", " ")
|
|
141
135
|
attrs = [("r", ref), ("t", "inlineStr")]
|
|
142
136
|
if style:
|
|
143
137
|
attrs.append(("s", style))
|
|
@@ -276,7 +270,7 @@ def main():
|
|
|
276
270
|
if not isinstance(mapping, dict) or "rows" not in mapping:
|
|
277
271
|
cli.error("mapping must be a JSON object with rows")
|
|
278
272
|
|
|
279
|
-
rows = table_rows(mapping, opts.channel, load_taxonomy(opts.taxonomy))
|
|
273
|
+
rows, skipped = table_rows(mapping, opts.channel, load_taxonomy(opts.taxonomy))
|
|
280
274
|
stem = opts.slug or slug(mapping.get("screen"))
|
|
281
275
|
os.makedirs(opts.target_dir, exist_ok=True)
|
|
282
276
|
|
|
@@ -294,6 +288,10 @@ def main():
|
|
|
294
288
|
summary = {"slug": stem, "rows": len(rows), "columns": COLUMNS, "channel": opts.channel, "written": produced}
|
|
295
289
|
json.dump(summary, sys.stdout, indent=2, ensure_ascii=False)
|
|
296
290
|
sys.stdout.write("\n")
|
|
291
|
+
if skipped:
|
|
292
|
+
print(f"# {len(skipped)} row(s) left out of the sheet: no literal key (empty, dynamic or <placeholder>)", file=sys.stderr)
|
|
293
|
+
for number, key in skipped:
|
|
294
|
+
print(f"# row {number}: {key or '(empty)'}", file=sys.stderr)
|
|
297
295
|
|
|
298
296
|
|
|
299
297
|
if __name__ == "__main__":
|
|
@@ -24,8 +24,12 @@ TR_RE = re.compile("^(?:tr|tr[kc]e|t[\u00fcu]rk[\u00e7c]e)\\s*" + SEP + "\\s*(.+
|
|
|
24
24
|
EN_RE = re.compile("^(?:en|eng|ing|[i\u0130]ngilizce)\\s*" + SEP + "\\s*(.+)$", re.IGNORECASE)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
URL_NODE_ID = re.compile(r"^\d+-\d+$")
|
|
28
|
+
|
|
29
|
+
|
|
27
30
|
def canon_id(node_id):
|
|
28
|
-
|
|
31
|
+
text = "" if node_id is None else str(node_id).strip()
|
|
32
|
+
return text.replace("-", ":") if URL_NODE_ID.match(text) else text
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
def split_lang(text):
|
|
@@ -68,7 +72,7 @@ def pick_label(labels):
|
|
|
68
72
|
def record(node_id, name, node_type, design_text, labels):
|
|
69
73
|
raw, parsed = pick_label(labels)
|
|
70
74
|
return {
|
|
71
|
-
"nodeId": canon_id(node_id),
|
|
75
|
+
"nodeId": canon_id(node_id) or None,
|
|
72
76
|
"name": name,
|
|
73
77
|
"type": node_type,
|
|
74
78
|
"designText": design_text,
|
|
@@ -246,10 +250,13 @@ def main():
|
|
|
246
250
|
|
|
247
251
|
source, found, files, audited = pick_source(opts, cli, file_key, nodes)
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
unique, seen = [], set()
|
|
250
254
|
for item in found:
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
if item["nodeId"]:
|
|
256
|
+
if item["nodeId"] in seen:
|
|
257
|
+
continue
|
|
258
|
+
seen.add(item["nodeId"])
|
|
259
|
+
unique.append(item)
|
|
253
260
|
|
|
254
261
|
def having(lang):
|
|
255
262
|
return sum(1 for entry in unique if entry[lang])
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py
CHANGED
|
@@ -16,10 +16,10 @@ import subprocess
|
|
|
16
16
|
import sys
|
|
17
17
|
import tempfile
|
|
18
18
|
import urllib.parse
|
|
19
|
-
from xml.sax.saxutils import quoteattr
|
|
19
|
+
from xml.sax.saxutils import escape, quoteattr
|
|
20
20
|
|
|
21
21
|
EMBED_EXTENSIONS = {".xlsx", ".xls", ".csv", ".docx", ".pdf"}
|
|
22
|
-
EMBED_HEADING = "CMS
|
|
22
|
+
EMBED_HEADING = "CMS Import File"
|
|
23
23
|
CONTENT = "/rest/api/content"
|
|
24
24
|
|
|
25
25
|
|
|
@@ -82,8 +82,8 @@ def view_file_macro(path):
|
|
|
82
82
|
return f'<p><ac:structured-macro ac:name="view-file">{parameter}</ac:structured-macro></p>'
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def embed_block(files):
|
|
86
|
-
return f"<h2>{
|
|
85
|
+
def embed_block(files, heading):
|
|
86
|
+
return f"<h2>{escape(heading)}</h2>" + "".join(map(view_file_macro, files))
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def keyshot_files(manifest_path):
|
|
@@ -113,6 +113,7 @@ def build_cli():
|
|
|
113
113
|
cli.add_argument("--parent", default=os.environ.get("CONFLUENCE_PARENT"), help="id of the parent page")
|
|
114
114
|
cli.add_argument("--token", help="Bearer PAT; CONFLUENCE_API_TOKEN or the keychain is safer since other processes can read argv")
|
|
115
115
|
cli.add_argument("--keychain-name", default="CONFLUENCE_API_TOKEN", help="keychain service that stores the PAT")
|
|
116
|
+
cli.add_argument("--embed-heading", default=EMBED_HEADING, help="heading above the embedded spreadsheet / document previews")
|
|
116
117
|
cli.add_argument("--dry-run", action="store_true", help="report the planned actions without touching Confluence")
|
|
117
118
|
return cli
|
|
118
119
|
|
|
@@ -174,7 +175,7 @@ def main():
|
|
|
174
175
|
body = stream.read()
|
|
175
176
|
embedded = [p for p in opts.attach if os.path.splitext(p)[1].lower() in EMBED_EXTENSIONS]
|
|
176
177
|
if embedded:
|
|
177
|
-
body += embed_block(embedded)
|
|
178
|
+
body += embed_block(embedded, opts.embed_heading)
|
|
178
179
|
|
|
179
180
|
if opts.dry_run:
|
|
180
181
|
plan = {
|
|
@@ -25,6 +25,8 @@ import urllib.error
|
|
|
25
25
|
import urllib.parse
|
|
26
26
|
import urllib.request
|
|
27
27
|
|
|
28
|
+
from _shared import slug
|
|
29
|
+
|
|
28
30
|
API = "https://api.figma.com/v1"
|
|
29
31
|
SEP = "[:\\-\u2013]"
|
|
30
32
|
TR_RE = re.compile("^(?:tr|tr[kc]e|t[\u00fcu]rk[\u00e7c]e)\\s*" + SEP + "\\s*(.+)$", re.IGNORECASE)
|
|
@@ -40,9 +42,10 @@ RIGHT_MARGIN = 40
|
|
|
40
42
|
PINK = "#EC1E91"
|
|
41
43
|
GRAY = "#9aa0a6"
|
|
42
44
|
BACKGROUND = "#1e1e1e"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
LABELS = {
|
|
46
|
+
"en": {"cms": "CMS copy", "pending": "Awaiting copy", "unmatched": "(unmatched)"},
|
|
47
|
+
"tr": {"cms": "\u0130\u00e7erik (CMS)", "pending": "\u00d6neri bekleniyor", "unmatched": "(e\u015fle\u015fmedi)"},
|
|
48
|
+
}
|
|
46
49
|
|
|
47
50
|
PAGE_OPEN = "<!doctype html><html><head>"
|
|
48
51
|
META_UTF8 = '<meta charset="utf-8">'
|
|
@@ -60,13 +63,12 @@ PATH_CHROMES = (
|
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
return str(node_id or "").strip().replace("-", ":")
|
|
66
|
+
URL_NODE_ID = re.compile(r"^\d+-\d+$")
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
return
|
|
69
|
+
def canon_id(node_id):
|
|
70
|
+
text = str(node_id or "").strip()
|
|
71
|
+
return text.replace("-", ":") if URL_NODE_ID.match(text) else text
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def split_lang(text):
|
|
@@ -248,7 +250,7 @@ def page_from_rest(file_key, node_id, scale, token, ca_file):
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
|
|
251
|
-
def build_cards(page, rows):
|
|
253
|
+
def build_cards(page, rows, labels):
|
|
252
254
|
index = build_index(rows)
|
|
253
255
|
cards = []
|
|
254
256
|
taken = set()
|
|
@@ -273,7 +275,7 @@ def build_cards(page, rows):
|
|
|
273
275
|
box = {axis: float(node.get(axis, 0)) for axis in ("x", "y", "w", "h")}
|
|
274
276
|
cards.append({
|
|
275
277
|
"num": number,
|
|
276
|
-
"key": new_key or
|
|
278
|
+
"key": new_key or labels["unmatched"],
|
|
277
279
|
"tr": tr,
|
|
278
280
|
"en": en,
|
|
279
281
|
"text": node.get("characters") or "",
|
|
@@ -302,9 +304,9 @@ def chip(kind, colour, content):
|
|
|
302
304
|
return f'<span class="{kind}" style="background:{colour}">{content}</span>'
|
|
303
305
|
|
|
304
306
|
|
|
305
|
-
def card_html(card, left):
|
|
307
|
+
def card_html(card, left, labels):
|
|
306
308
|
colour = PINK if card["annotated"] else GRAY
|
|
307
|
-
pill =
|
|
309
|
+
pill = labels["cms"] if card["annotated"] else labels["pending"]
|
|
308
310
|
if card["annotated"]:
|
|
309
311
|
body = "".join(
|
|
310
312
|
f'<div class="line"><b>{label}</b> {html.escape(card[lang] or "-")}</div>'
|
|
@@ -336,7 +338,7 @@ def connector(card, lane, lane_count, start_x, target_x, from_left):
|
|
|
336
338
|
)
|
|
337
339
|
|
|
338
340
|
|
|
339
|
-
def build_html(page, cards):
|
|
341
|
+
def build_html(page, cards, labels):
|
|
340
342
|
fw, fh = page["frame"]["w"], page["frame"]["h"]
|
|
341
343
|
sides = {"left": [], "right": []}
|
|
342
344
|
for card in cards:
|
|
@@ -350,7 +352,7 @@ def build_html(page, cards):
|
|
|
350
352
|
for card_x, group in ((COLUMN_X, sides["left"]), (right_x, sides["right"])):
|
|
351
353
|
from_left = card_x == COLUMN_X
|
|
352
354
|
for lane, card in enumerate(group):
|
|
353
|
-
boxes.append(card_html(card, card_x))
|
|
355
|
+
boxes.append(card_html(card, card_x, labels))
|
|
354
356
|
if from_left:
|
|
355
357
|
wire = connector(card, lane, len(group), COLUMN_X + CARD_W, screen_x + card["x"], True)
|
|
356
358
|
else:
|
|
@@ -430,12 +432,26 @@ def chrome_screenshot(chrome, html_text, out_png, width, height, scale):
|
|
|
430
432
|
raise RuntimeError((log.read() or "no output").strip()[-300:])
|
|
431
433
|
|
|
432
434
|
|
|
435
|
+
def is_number(value):
|
|
436
|
+
try:
|
|
437
|
+
float(value)
|
|
438
|
+
except (TypeError, ValueError):
|
|
439
|
+
return False
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
|
|
433
443
|
def load_spec_pages(path):
|
|
434
444
|
with open(path, encoding="utf-8") as stream:
|
|
435
445
|
spec = json.load(stream)
|
|
436
446
|
folder = os.path.dirname(os.path.abspath(path))
|
|
437
447
|
pages = spec.get("pages") or []
|
|
438
|
-
for page in pages:
|
|
448
|
+
for number, page in enumerate(pages, 1):
|
|
449
|
+
frame = page.get("frame") if isinstance(page, dict) else None
|
|
450
|
+
if not (isinstance(frame, dict) and all(is_number(frame.get(side)) for side in ("w", "h"))):
|
|
451
|
+
sys.exit(f"{path}: page {number} needs \"frame\": {{\"w\": <width>, \"h\": <height>}} in px")
|
|
452
|
+
for side in ("w", "h"):
|
|
453
|
+
if not isinstance(frame[side], (int, float)):
|
|
454
|
+
frame[side] = float(frame[side])
|
|
439
455
|
page["image"] = image_source(page.get("image"), folder)
|
|
440
456
|
return spec, pages
|
|
441
457
|
|
|
@@ -445,7 +461,7 @@ def split_nodes(flag, mapping):
|
|
|
445
461
|
return given or list(mapping.get("figmaNodes") or [])
|
|
446
462
|
|
|
447
463
|
|
|
448
|
-
def
|
|
464
|
+
def build_cli():
|
|
449
465
|
cli = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
450
466
|
cli.add_argument("--mapping", required=True, help="the reuse-map JSON")
|
|
451
467
|
cli.add_argument("--spec", help="geometry from MCP or measured by hand: {fileKey, pages:[{nodeId, image, frame, nodes}]}")
|
|
@@ -456,7 +472,14 @@ def main():
|
|
|
456
472
|
cli.add_argument("--ca-file", help="PEM roots to trust when a proxy intercepts TLS")
|
|
457
473
|
cli.add_argument("--out", dest="target_dir", default=".", help="folder that receives the generated files")
|
|
458
474
|
cli.add_argument("--slug", help="stem for the file names; derived from the screen name when omitted")
|
|
475
|
+
cli.add_argument("--ui-lang", choices=sorted(LABELS), default="en", help="language of the card labels")
|
|
476
|
+
return cli
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def main():
|
|
480
|
+
cli = build_cli()
|
|
459
481
|
opts = cli.parse_args()
|
|
482
|
+
labels = LABELS[opts.ui_lang]
|
|
460
483
|
|
|
461
484
|
with open(opts.mapping, encoding="utf-8") as stream:
|
|
462
485
|
mapping = json.load(stream)
|
|
@@ -467,6 +490,9 @@ def main():
|
|
|
467
490
|
if not pages:
|
|
468
491
|
sys.exit("--spec lists no pages")
|
|
469
492
|
page = pages[0]
|
|
493
|
+
if len(pages) > 1:
|
|
494
|
+
print(f"# warning: {len(pages) - 1} extra page(s) in --spec ignored; the overlay draws the first page only "
|
|
495
|
+
"(render-key-shots.py reads every page)", file=sys.stderr)
|
|
470
496
|
file_key = spec.get("fileKey") or mapping.get("figmaFileKey")
|
|
471
497
|
else:
|
|
472
498
|
file_key = opts.file or mapping.get("figmaFileKey")
|
|
@@ -479,8 +505,8 @@ def main():
|
|
|
479
505
|
"Without one, measure the boxes on the screenshot and hand them over with --spec.")
|
|
480
506
|
page = page_from_rest(file_key, canon_id(nodes[0]), opts.scale, token, opts.ca_file)
|
|
481
507
|
|
|
482
|
-
cards = build_cards(page, mapping.get("rows") or [])
|
|
483
|
-
document, width, height = build_html(page, cards)
|
|
508
|
+
cards = build_cards(page, mapping.get("rows") or [], labels)
|
|
509
|
+
document, width, height = build_html(page, cards, labels)
|
|
484
510
|
os.makedirs(opts.target_dir, exist_ok=True)
|
|
485
511
|
|
|
486
512
|
def output(suffix):
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py
CHANGED
|
@@ -13,7 +13,8 @@ import plistlib
|
|
|
13
13
|
import sys
|
|
14
14
|
import urllib.request
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
from _shared import LANGS as ALL_LANGS
|
|
17
|
+
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def split_csv(value):
|
|
@@ -149,6 +150,8 @@ def main():
|
|
|
149
150
|
|
|
150
151
|
keys = split_csv(args.keys)
|
|
151
152
|
langs = parse_langs(args.langs)
|
|
153
|
+
if not (args.snapshot_root or args.plist_root or args.endpoint):
|
|
154
|
+
sys.exit("no source given: pass --snapshot-root, --plist-root, or --live with --endpoint")
|
|
152
155
|
|
|
153
156
|
if args.snapshot_root and not args.live:
|
|
154
157
|
result = from_snapshot(args.snapshot_root, keys, langs, args.prefix)
|
|
@@ -165,7 +168,7 @@ def main():
|
|
|
165
168
|
elif args.plist_root:
|
|
166
169
|
result = from_plist(args.plist_root, keys, langs, args.prefix)
|
|
167
170
|
else:
|
|
168
|
-
|
|
171
|
+
sys.exit("no --snapshot-root or --plist-root to fall back on")
|
|
169
172
|
|
|
170
173
|
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
|
|
171
174
|
sys.stdout.write("\n")
|
package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py
CHANGED
|
@@ -11,7 +11,8 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
import sys
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
from _shared import LANGS as ALL_LANGS, normalize_key
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
DOMAIN_DIRS = {
|
|
17
18
|
"localization": ("Suggested", "LocalizationStrings"),
|
|
@@ -25,10 +26,6 @@ def parse_langs(value):
|
|
|
25
26
|
return [part.strip() for part in value.split(",") if part.strip()]
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def normalize_key(key):
|
|
29
|
-
return ".".join(seg[:1].upper() + seg[1:] for seg in key.split("."))
|
|
30
|
-
|
|
31
|
-
|
|
32
29
|
def read_keys(opts):
|
|
33
30
|
raw = [piece.strip() for piece in opts.keys.split(",")] if opts.keys else []
|
|
34
31
|
if opts.keys_file:
|
|
@@ -85,12 +82,14 @@ def read_suggested(path, langs):
|
|
|
85
82
|
|
|
86
83
|
|
|
87
84
|
def from_catalog(catalog, key, langs):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
for candidate in dict.fromkeys((key, normalize_key(key))):
|
|
86
|
+
entry = catalog.get(candidate)
|
|
87
|
+
if not isinstance(entry, dict):
|
|
88
|
+
continue
|
|
89
|
+
values = {lang: entry.get(lang) for lang in langs}
|
|
90
|
+
if any(value is not None for value in values.values()):
|
|
91
|
+
return values
|
|
92
|
+
return None
|
|
94
93
|
|
|
95
94
|
|
|
96
95
|
def main():
|
|
@@ -13,22 +13,17 @@ import os
|
|
|
13
13
|
import re
|
|
14
14
|
import sys
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
from _shared import IDENT, WEB_DOLLAR_T, WEB_T, WORD, is_interpolated, lsk_keys, source_files
|
|
17
|
+
|
|
17
18
|
ANDROID = {".kt", ".kts", ".xml"}
|
|
18
19
|
WEB = {".ts", ".tsx", ".js", ".jsx", ".vue"}
|
|
19
|
-
SKIP_PARTS = ("/.git", "/Generated/")
|
|
20
20
|
|
|
21
21
|
ERROR_WORDS = ("toast", "snackbar", "alert", "warning", "failure", "exception", "showerror", "errormessage", "error", "validat")
|
|
22
22
|
ERROR_CONTEXT = re.compile("|".join(ERROR_WORDS), re.IGNORECASE)
|
|
23
23
|
|
|
24
|
-
WORD = "A-Za-z0-9_"
|
|
25
|
-
IDENT = rf"[A-Za-z_][{WORD}]*"
|
|
26
|
-
LSK = re.compile(rf"LocalizationStringKey((?:\.{IDENT}){{2,}})")
|
|
27
24
|
LITERAL_LOCALIZED = re.compile(r"\"([^\"\\\n]+)\"\.localized\b")
|
|
28
25
|
R_STRING = re.compile(rf"\bR\.string\.([{WORD}]+)")
|
|
29
26
|
XML_STRING = re.compile(rf"@string/([{WORD}.]+)")
|
|
30
|
-
WEB_T = re.compile(r"(?<![\w$])t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
31
|
-
WEB_DOLLAR_T = re.compile(r"\$t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
32
27
|
WEB_T_DYNAMIC = re.compile(r"(?<![\w$])t\(\s*[`$A-Za-z_]")
|
|
33
28
|
WEB_DOLLAR_T_DYNAMIC = re.compile(r"\$t\(\s*[`$A-Za-z_]")
|
|
34
29
|
LSK_DYNAMIC = re.compile(r"LocalizationStringKey\(")
|
|
@@ -38,13 +33,6 @@ INTERPOLATED_LOCALIZED = re.compile(r"\"([^\"\n]*(?:\\\(|\$\{|\$[A-Za-z_])[^\"\n
|
|
|
38
33
|
RECEIVER_LOCALIZED = re.compile(rf"(?<![\w.\"])({IDENT}(?:\.{IDENT})*)\.localized\b")
|
|
39
34
|
|
|
40
35
|
|
|
41
|
-
def strip_accessor(chain):
|
|
42
|
-
segments = chain.strip(".").split(".")
|
|
43
|
-
while segments and segments[-1].startswith("localized"):
|
|
44
|
-
segments.pop()
|
|
45
|
-
return ".".join(segments)
|
|
46
|
-
|
|
47
|
-
|
|
48
36
|
def platform_of(path):
|
|
49
37
|
ext = os.path.splitext(path)[1]
|
|
50
38
|
if ext in ANDROID:
|
|
@@ -54,20 +42,6 @@ def platform_of(path):
|
|
|
54
42
|
return "ios"
|
|
55
43
|
|
|
56
44
|
|
|
57
|
-
def source_files(root):
|
|
58
|
-
if os.path.isfile(root):
|
|
59
|
-
yield root
|
|
60
|
-
return
|
|
61
|
-
for current, dirs, files in os.walk(root):
|
|
62
|
-
probe = current.replace(os.sep, "/") + "/"
|
|
63
|
-
if any(part in probe for part in SKIP_PARTS):
|
|
64
|
-
dirs[:] = []
|
|
65
|
-
continue
|
|
66
|
-
for name in sorted(files):
|
|
67
|
-
if os.path.splitext(name)[1] in EXTENSIONS:
|
|
68
|
-
yield os.path.join(current, name)
|
|
69
|
-
|
|
70
|
-
|
|
71
45
|
def hits_in_line(line, ext):
|
|
72
46
|
static, dynamic = [], []
|
|
73
47
|
if ext == ".xml":
|
|
@@ -81,11 +55,8 @@ def hits_in_line(line, ext):
|
|
|
81
55
|
if WEB_DOLLAR_T_DYNAMIC.search(line):
|
|
82
56
|
dynamic.append("(dynamic: $t(...))")
|
|
83
57
|
return static, dynamic
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if key.count(".") >= 1:
|
|
87
|
-
static.append(key)
|
|
88
|
-
static.extend(m.group(1) for m in LITERAL_LOCALIZED.finditer(line))
|
|
58
|
+
static.extend(lsk_keys(line))
|
|
59
|
+
static.extend(m.group(1) for m in LITERAL_LOCALIZED.finditer(line) if not is_interpolated(m.group(1)))
|
|
89
60
|
static.extend(m.group(1) for m in R_STRING.finditer(line))
|
|
90
61
|
if LSK_DYNAMIC.search(line):
|
|
91
62
|
dynamic.append("(dynamic: LocalizationStringKey(...))")
|