#!/usr/bin/env bash # verify-locales.sh — completeness gate for locale FTL files. # # Asserts two invariants for every non-en locale: # 1. COVERAGE — every key in en/main.ftl exists in the locale. # 2. TRANSLATION — no key's value is byte-identical to the en value # (catches untranslated English left in a non-en file). # # Ignores: lines that are comments (#), blank, or Fluent selectors # (contain `->` / `[one]` / `*[other]` etc. — those are plural branches, # not standalone values, and may legitimately match across locales). # # Usage: bash scripts/verify-locales.sh [locales_dir] # Exit 0 = all locales complete; 1 = gaps found. set -euo pipefail LOCALES="${1:-locales}" EN="$LOCALES/en/main.ftl" FAIL=0 # Loanwords: technical terms identical across all locales (en/ru/es/it/fr/de). # A value whose only prose words are loanwords is legitimately the same # in every language and must not be flagged as untranslated. LOANWORDS='cluster offline online node nodes image container backend api ssh ha tmux ray mp doctor recipe exec mod mods hook hooks rank dry sync digest repair log logs session pty alias aliases error' is_loanword() { echo "$LOANWORDS" | grep -qwi "$1"; } # Extract top-level key names (lines starting with `identifier =`). keys_of() { grep -oE '^[a-z0-9-]+ = ' "$1" | sed 's/ = $//'; } # Extract the value portion of a given key (full multi-line value, # trailing whitespace trimmed). Captures selector blocks ({ $count -> ... }). # Returns empty if the key is absent. value_of() { local file="$1" key="$2" awk -v k="^$key = " ' $0 ~ k { in_val=1; sub(/^[^=]+= /, ""); print; next } in_val && /^[a-z0-9-]+ = / { exit } in_val && /^#/ { exit } in_val && /^$/ { exit } in_val { print } ' "$file" | sed 's/[[:space:]]*$//' | sed '/^$/d' } en_keys=$(keys_of "$EN" | sort -u) if [ -z "$en_keys" ]; then echo "FAIL: no keys found in $EN" exit 1 fi for loc in ru es it fr de; do f="$LOCALES/$loc/main.ftl" [ -f "$f" ] || { echo "FAIL: missing $f"; FAIL=1; continue; } loc_keys=$(keys_of "$f" | sort -u) # 1. Coverage: every en key must be present. missing=$(comm -23 <(echo "$en_keys") <(echo "$loc_keys")) if [ -n "$missing" ]; then cnt=$(echo "$missing" | wc -l | tr -d ' ') echo "FAIL: $loc missing $cnt key(s):" echo "$missing" | sed 's/^/ /' FAIL=1 fi # 2. Translation: no present key's value may equal the en value, # UNLESS the en value is pure placeholders/symbols (no translatable # prose). A value is "translatable" if, after stripping { $x } # placeholders and non-alphanumerics, it contains at least one # alphabetic word of length >= 3. untranslated="" while IFS= read -r key; do [ -z "$key" ] && continue en_val=$(value_of "$EN" "$key") loc_val=$(value_of "$f" "$key") [ -z "$loc_val" ] && continue [ -z "$en_val" ] && continue # Skip untranslatable values (pure placeholders/symbols/identifiers). # Strip { $x } placeholders, then non-alpha. Words containing '_' # are config/code identifiers (host_key, etc.) — not translatable prose. prose=$(echo "$en_val" | sed -E 's/\{ \$[a-z_]+ \}//g; s/[^[:alpha:]_]/ /g; s/[a-z]+_[a-z_]+//g') translatable=0 for w in $prose; do if [ ${#w} -ge 3 ] && ! is_loanword "$w"; then translatable=1; break; fi done [ "$translatable" -eq 0 ] && continue if [ "$en_val" = "$loc_val" ]; then untranslated="$untranslated$key"$'\n' fi done < <(echo "$en_keys") untranslated=$(echo "$untranslated" | sed '/^$/d') if [ -n "$untranslated" ]; then cnt=$(echo "$untranslated" | wc -l | tr -d ' ') echo "FAIL: $loc has $cnt untranslated key(s) (English value identical to en):" echo "$untranslated" | sed 's/^/ /' FAIL=1 fi done if [ "$FAIL" -eq 0 ]; then echo "OK: all non-en locales complete (coverage + translation)." exit 0 fi exit 1