#!/bin/sh # Rescan the theme sources into po/templates/footstrap.pot and merge it into every # po//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 n=0 for po in po/*/*.po; do [ -e "$po" ] || continue n=$((n + 1)) # 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 # A tree with no po/*/*.po at all walks zero catalogues, sets rc=0 (nothing in the loop ever ran # to set it otherwise) and printed "every string translated" — true of the empty set the same way # it is true of vacuously anything, and indistinguishable in the output from a real, fully # translated build. n=0 is the one case the loop cannot tell apart from success on its own. [ "$n" -gt 0 ] || { echo "update-po: no po/*/*.po found — nothing was checked, not everything is translated" >&2 exit 1 } [ "$rc" = 0 ] && echo "i18n: .pot current, every string translated ($n catalogue(s))" 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