mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
feat: generate changelog translator credits from Crowdin
Adds tools/translators/translators.sh, which pulls a Crowdin "Top Members" report for a release window (between two tags/dates) and prints the changelog "## Translations" block grouped by language. Crowdin contributors are joined against docs/changelog/translators.json, a Crowdin-username/id -> npub mapping kept alongside the changelogs. Contributors with no mapping are listed under UNMAPPED so they can be credited by hand and backfilled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWQWVdWLAwBUBX2gJ55y6b
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Crowdin username (or numeric user id) -> Nostr npub.",
|
||||
"Used by tools/translators/translators.sh to turn a Crowdin 'Top Members'",
|
||||
"report for a release window into the changelog '## Translations' section.",
|
||||
"Keys are matched case-insensitively against the Crowdin username; a numeric",
|
||||
"key is matched against the Crowdin user id. Add a row whenever a new",
|
||||
"translator gives you their npub. Contributors with no entry here are listed",
|
||||
"by the script under 'UNMAPPED' so you can credit them by hand and backfill.",
|
||||
"Example shape (replace with real Crowdin usernames):",
|
||||
" \"vitorpamplona\": \"npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z\""
|
||||
],
|
||||
"mappings": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Translator credits for the changelog
|
||||
|
||||
Generates the `## Translations` block for a release by asking Crowdin **who
|
||||
translated between two releases** and joining them against the npub mapping kept
|
||||
in the changelog folder.
|
||||
|
||||
## Pieces
|
||||
|
||||
- **`docs/changelog/translators.json`** — the Crowdin-user → npub mapping (lives
|
||||
next to the changelogs). Keyed by Crowdin username (case-insensitive) or numeric
|
||||
user id. Add a row whenever a translator gives you their npub.
|
||||
- **`tools/translators/translators.sh`** — pulls a Crowdin *Top Members* report
|
||||
for a date window, joins it against the mapping, and prints the credit block
|
||||
grouped by language. Anyone Crowdin reports who isn't in the mapping is listed
|
||||
under `UNMAPPED` so you can credit them by hand and backfill the JSON.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
export CROWDIN_PROJECT_ID=... # same env vars crowdin.yml already uses
|
||||
export CROWDIN_PERSONAL_TOKEN=... # token needs the "reports" scope
|
||||
|
||||
# Between the previous tag and now:
|
||||
tools/translators/translators.sh --from v1.12.00
|
||||
|
||||
# Between two tags:
|
||||
tools/translators/translators.sh --from v1.11.00 --to v1.12.00
|
||||
|
||||
# Discover Crowdin usernames to add to translators.json:
|
||||
tools/translators/translators.sh --from v1.12.00 --raw
|
||||
```
|
||||
|
||||
`--from` / `--to` accept either a `YYYY-MM-DD` date or a git tag/ref (resolved to
|
||||
its commit date). `--to` defaults to now.
|
||||
|
||||
Requires `bash`, `curl`, `jq`, `git`.
|
||||
|
||||
## How the window maps to "between two versions"
|
||||
|
||||
A release is a git tag, so the contribution window is the commit date of the
|
||||
previous tag → the commit date of the new tag. The Crowdin Top Members report
|
||||
takes that `dateFrom`/`dateTo` and returns every member who translated/approved
|
||||
in it, per language.
|
||||
|
||||
> The script talks to the live `api.crowdin.com` REST API. The JSON field paths
|
||||
> for the downloaded report follow Crowdin's `top-members` schema; if Crowdin
|
||||
> changes it, adjust the `jq` block at the bottom of `translators.sh`.
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the changelog "## Translations" section for a release window.
|
||||
#
|
||||
# Pulls a Crowdin "Top Members" report for a date range (the gap between two
|
||||
# releases), joins each Crowdin contributor against docs/changelog/translators.json
|
||||
# (Crowdin username/id -> npub), and prints a ready-to-paste credit block grouped
|
||||
# by language. Contributors with no npub mapping are listed under UNMAPPED so you
|
||||
# can credit them by hand and backfill translators.json.
|
||||
#
|
||||
# Usage:
|
||||
# tools/translators/translators.sh --from <date|tag> --to <date|tag>
|
||||
#
|
||||
# --from / --to A date (YYYY-MM-DD) or a git tag/ref. Tags are resolved to
|
||||
# their commit date. --to defaults to now if omitted.
|
||||
# --mapping PATH Override mapping file (default docs/changelog/translators.json).
|
||||
# --raw Also dump the raw per-member report rows (for debugging /
|
||||
# discovering Crowdin usernames to add to the mapping).
|
||||
#
|
||||
# Environment (same names crowdin.yml already uses):
|
||||
# CROWDIN_PROJECT_ID Crowdin numeric project id.
|
||||
# CROWDIN_PERSONAL_TOKEN Crowdin personal access token (needs report scope).
|
||||
#
|
||||
# Requires: bash, curl, jq, git.
|
||||
#
|
||||
# NOTE: This talks to the live Crowdin REST API (api.crowdin.com). The JSON field
|
||||
# paths for the downloaded "top-members" report are documented at
|
||||
# https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post and
|
||||
# can be adjusted in the jq block below if Crowdin changes the schema.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
API="https://api.crowdin.com/api/v2"
|
||||
MAPPING="$REPO_ROOT/docs/changelog/translators.json"
|
||||
FROM=""
|
||||
TO=""
|
||||
RAW=0
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--from) FROM="${2:?--from needs a value}"; shift 2 ;;
|
||||
--to) TO="${2:?--to needs a value}"; shift 2 ;;
|
||||
--mapping) MAPPING="${2:?--mapping needs a value}"; shift 2 ;;
|
||||
--raw) RAW=1; shift ;;
|
||||
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
|
||||
*) die "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v jq >/dev/null || die "jq not found"
|
||||
command -v curl >/dev/null || die "curl not found"
|
||||
[ -n "$FROM" ] || die "--from is required"
|
||||
[ -n "${CROWDIN_PROJECT_ID:-}" ] || die "CROWDIN_PROJECT_ID is not set"
|
||||
[ -n "${CROWDIN_PERSONAL_TOKEN:-}" ] || die "CROWDIN_PERSONAL_TOKEN is not set"
|
||||
[ -f "$MAPPING" ] || die "mapping file not found: $MAPPING"
|
||||
|
||||
# Resolve a date (YYYY-MM-DD) or a git ref to an ISO-8601 timestamp.
|
||||
resolve_ts() {
|
||||
local v="$1"
|
||||
if [[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||
echo "${v}T00:00:00+00:00"
|
||||
elif git -C "$REPO_ROOT" rev-parse -q --verify "$v" >/dev/null 2>&1; then
|
||||
git -C "$REPO_ROOT" log -1 --format=%cI "$v"
|
||||
else
|
||||
die "--from/--to value '$v' is neither a YYYY-MM-DD date nor a known git ref"
|
||||
fi
|
||||
}
|
||||
|
||||
DATE_FROM="$(resolve_ts "$FROM")"
|
||||
DATE_TO="$( [ -n "$TO" ] && resolve_ts "$TO" || date -u +%Y-%m-%dT%H:%M:%S+00:00 )"
|
||||
echo "# Crowdin contributors from $DATE_FROM to $DATE_TO" >&2
|
||||
|
||||
auth=(-H "Authorization: Bearer ${CROWDIN_PERSONAL_TOKEN}" -H "Content-Type: application/json")
|
||||
base="${API}/projects/${CROWDIN_PROJECT_ID}/reports"
|
||||
|
||||
# 1) Kick off a top-members report for the window.
|
||||
gen_body="$(jq -n --arg from "$DATE_FROM" --arg to "$DATE_TO" '{
|
||||
name: "top-members",
|
||||
schema: { unit: "words", format: "json", dateFrom: $from, dateTo: $to }
|
||||
}')"
|
||||
report_id="$(curl -fsS "${auth[@]}" -X POST "$base" -d "$gen_body" | jq -r '.data.identifier')"
|
||||
[ -n "$report_id" ] && [ "$report_id" != "null" ] || die "Crowdin did not return a report identifier"
|
||||
|
||||
# 2) Poll until the report is finished.
|
||||
for _ in $(seq 1 60); do
|
||||
status="$(curl -fsS "${auth[@]}" "${base}/${report_id}" | jq -r '.data.status')"
|
||||
case "$status" in
|
||||
finished) break ;;
|
||||
failed) die "Crowdin report generation failed" ;;
|
||||
*) sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
[ "$status" = "finished" ] || die "report did not finish in time (last status: $status)"
|
||||
|
||||
# 3) Download the report JSON.
|
||||
dl_url="$(curl -fsS "${auth[@]}" "${base}/${report_id}/download" | jq -r '.data.url')"
|
||||
[ -n "$dl_url" ] && [ "$dl_url" != "null" ] || die "Crowdin did not return a download url"
|
||||
report="$(curl -fsS "$dl_url")"
|
||||
|
||||
if [ "$RAW" = "1" ]; then
|
||||
echo "$report" | jq '(.data // .)' >&2
|
||||
fi
|
||||
|
||||
# 4) Join report members against the npub mapping, grouped by language.
|
||||
# Mapping keys are lower-cased; we match by username (lower) or numeric id.
|
||||
echo "$report" | jq -r --slurpfile m "$MAPPING" '
|
||||
($m[0].mappings // {}) as $map
|
||||
| ( $map | with_entries(.key |= ascii_downcase) ) as $byname
|
||||
| (.data // .) as $members
|
||||
| reduce $members[] as $mem ({langs:{}, unmapped:[]};
|
||||
($mem.user // {}) as $u
|
||||
| ( ($u.username // "") | ascii_downcase ) as $uname
|
||||
| ( $byname[$uname] // $map[($u.id|tostring)] ) as $npub
|
||||
| if $npub == null then
|
||||
.unmapped += [ ($u.fullName // $u.username // ("id " + ($u.id|tostring))) ]
|
||||
else
|
||||
reduce ( ($mem.languages // []) | if length>0 then . else [{name:"(unknown language)"}] end | .[] ) as $l (.;
|
||||
.langs[$l.name] = ((.langs[$l.name] // []) + ["@\($npub)"] | unique))
|
||||
end
|
||||
)
|
||||
| "## Translations\n"
|
||||
+ ( [ .langs | to_entries[] | "- \(.key) by " + (.value | sort | join(" and ")) ] | sort | join("\n") )
|
||||
+ ( if (.unmapped|length)>0
|
||||
then "\n\n# UNMAPPED (no npub in translators.json — credit by hand, then add them):\n"
|
||||
+ ( [ .unmapped | unique[] | "# - " + . ] | join("\n") )
|
||||
else "" end )
|
||||
'
|
||||
Reference in New Issue
Block a user