Files
op-packages/luci-theme-footstrap/update-po.sh
T
github-actions[bot] 410d762592
Merge-upstream / merge (push) Canceled after 0s
🎄 Sync 2026-08-27 02:13:09
2026-08-27 02:13:09 +08:00

112 lines
5.0 KiB
Bash
Executable File

#!/bin/sh
# Rescan the theme sources into po/templates/footstrap.pot and merge it into every
# po/<lang>/footstrap.po. Run after adding or changing ANY _('…') string.
#
# ./update-po.sh rescan, merge, report what is still untranslated
# ./update-po.sh --check change nothing; fail if the .pot is stale or a string is
# untranslated. The CI gate.
#
# A missing translation cannot fail loudly — an uncompiled _() falls through to its English msgid
# and nothing reports it — so --check is a gate rather than a suggestion.
#
# The directory is `po/`, which is what LUCI_LANGUAGES globs and what Weblate translates. Naming it
# `i18n/` stops luci.mk emitting a per-language package, which mattered while a self-updater
# resolved the theme by name and took head -1 (issue #6); that updater is retired and owfeed builds
# one artifact per format either way, so the rename only cost the catalogue its visibility to the
# translation platform.
#
# Nothing here runs on the buildbot — luci.mk calls po2lmo itself. This needs perl and gettext,
# which the OpenWrt build does not.
#
# The scanner is LuCI's OWN build/i18n-scan.pl rather than a grep: it lexes a .ut, rewriting the
# template into JS before xgettext, and covers the rpcd acl.d/*.json title. A grep would miss the
# ACL string and choke on any apostrophe.
set -eu
cd "$(dirname "$0")"
# Pinned to a commit and checksummed: a perl script fetched over the network and then EXECUTED, and
# the gate deciding whether the catalogue is complete. Off a moving branch, the gate is whatever
# upstream pushed last. luci-upstream.pin is the single source of the commit and both checksums.
. ./luci-upstream.pin
SCANNER_URL="https://raw.githubusercontent.com/openwrt/luci/${LUCI_PIN}/build/i18n-scan.pl"
SCANNER_SHA256="$I18N_SCAN_SHA256"
POT='po/templates/footstrap.pot'
CHECK=0
[ "${1:-}" = '--check' ] && CHECK=1
for tool in perl xgettext msgmerge msgfmt; do
command -v "$tool" >/dev/null || { echo "update-po: $tool not found (install perl + gettext)" >&2; exit 1; }
done
# Prefer a scanner from a local LuCI checkout ($LUCI_SRC) so the gate is not hostage to the
# network; fall back to fetching it. jsmin.c is pinned the same way in CI.
#
# ONE trap, covering every temp and every exit. It used to be installed inside the fetch branch
# only — so the LUCI_SRC branch had none at all — and the three mktemps below were cleaned by hand
# at each success path. With `set -eu`, any failure in between (perl choking on a template, i.e.
# exactly the stale-.pot session this script exists for) leaked them. `fetched` is what the trap
# removes, so the LUCI_SRC path never deletes the user's own checkout.
fetched=''; fresh=''; old_ids=''; new_ids=''
# shellcheck disable=SC2064 # expand nothing now: the names are assigned as the script proceeds
trap 'rm -f "$fetched" "$fresh" "$old_ids" "$new_ids"' EXIT INT TERM
scanner=''
if [ -n "${LUCI_SRC:-}" ] && [ -f "$LUCI_SRC/build/i18n-scan.pl" ]; then
scanner="$LUCI_SRC/build/i18n-scan.pl"
else
scanner="$(mktemp)"; fetched="$scanner"
curl -sfL --proto '=https' --proto-redir '=https' "$SCANNER_URL" -o "$scanner" || {
echo "update-po: cannot fetch $SCANNER_URL — set LUCI_SRC to a luci checkout" >&2
exit 1
}
# the pin says WHICH script; this says it is that script and nothing else
echo "$SCANNER_SHA256 $scanner" | sha256sum -c - >/dev/null || {
echo "update-po: i18n-scan.pl checksum mismatch — refusing to run it" >&2
exit 1
}
fi
mkdir -p po/templates
fresh="$(mktemp)"
# htdocs = the theme JS, ucode = the templates, root = the rpcd ACL title
perl "$scanner" htdocs ucode root > "$fresh"
if [ "$CHECK" = 1 ]; then
# Compare msgid AND msgctxt (line-number comments churn on every edit and say nothing).
# The CONTEXT is part of the key — po2lmo hashes "ctxt\1msgid" — so a .pot carrying the
# same msgids with the context dropped describes a different catalogue, which comparing
# msgid alone waved through.
old_ids="$(mktemp)"; new_ids="$(mktemp)"
grep '^msgid\|^msgctxt' "$POT" | sort > "$old_ids"
grep '^msgid\|^msgctxt' "$fresh" | sort > "$new_ids"
if ! cmp -s "$old_ids" "$new_ids"; then
echo "update-po: $POT is STALE — a string was added or removed without rerunning ./update-po.sh" >&2
diff "$old_ids" "$new_ids" | grep '^[<>]' >&2 || true
exit 1
fi
rc=0
for po in po/*/*.po; do
[ -e "$po" ] || continue
# an empty msgstr means the string renders in English for that language
missing="$(msgfmt --statistics -o /dev/null "$po" 2>&1 | grep -o '[0-9]* untranslated' || true)"
if [ -n "$missing" ]; then
echo "update-po: $po has $missing message(s) — they will silently render in English" >&2
rc=1
fi
msgfmt --check -o /dev/null "$po" || rc=1
done
[ "$rc" = 0 ] && echo "i18n: .pot current, every string translated"
exit "$rc"
fi
mv "$fresh" "$POT"
echo "scanned -> $POT ($(grep -c '^msgid' "$POT") strings)"
for po in po/*/*.po; do
[ -e "$po" ] || continue
msgmerge --quiet --update --backup=none "$po" "$POT"
echo "merged -> $po: $(msgfmt --statistics -o /dev/null "$po" 2>&1 | tr '\n' ' ')"
done