verify-locales.sh 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env bash
  2. # verify-locales.sh — completeness gate for locale FTL files.
  3. #
  4. # Asserts two invariants for every non-en locale:
  5. # 1. COVERAGE — every key in en/main.ftl exists in the locale.
  6. # 2. TRANSLATION — no key's value is byte-identical to the en value
  7. # (catches untranslated English left in a non-en file).
  8. #
  9. # Ignores: lines that are comments (#), blank, or Fluent selectors
  10. # (contain `->` / `[one]` / `*[other]` etc. — those are plural branches,
  11. # not standalone values, and may legitimately match across locales).
  12. #
  13. # Usage: bash scripts/verify-locales.sh [locales_dir]
  14. # Exit 0 = all locales complete; 1 = gaps found.
  15. set -euo pipefail
  16. LOCALES="${1:-locales}"
  17. EN="$LOCALES/en/main.ftl"
  18. FAIL=0
  19. # Loanwords: technical terms identical across all locales (en/ru/es/it/fr/de).
  20. # A value whose only prose words are loanwords is legitimately the same
  21. # in every language and must not be flagged as untranslated.
  22. 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'
  23. is_loanword() { echo "$LOANWORDS" | grep -qwi "$1"; }
  24. # Extract top-level key names (lines starting with `identifier =`).
  25. keys_of() { grep -oE '^[a-z0-9-]+ = ' "$1" | sed 's/ = $//'; }
  26. # Extract the value portion of a given key (first match, trailing whitespace
  27. # trimmed). Returns empty if the key is absent.
  28. value_of() {
  29. local file="$1" key="$2"
  30. awk -v k="^$key = " '$0 ~ k { sub(/^[^=]+= /, ""); print; exit }' "$file" \
  31. | sed 's/[[:space:]]*$//'
  32. }
  33. en_keys=$(keys_of "$EN" | sort -u)
  34. if [ -z "$en_keys" ]; then
  35. echo "FAIL: no keys found in $EN"
  36. exit 1
  37. fi
  38. for loc in ru es it fr de; do
  39. f="$LOCALES/$loc/main.ftl"
  40. [ -f "$f" ] || { echo "FAIL: missing $f"; FAIL=1; continue; }
  41. loc_keys=$(keys_of "$f" | sort -u)
  42. # 1. Coverage: every en key must be present.
  43. missing=$(comm -23 <(echo "$en_keys") <(echo "$loc_keys"))
  44. if [ -n "$missing" ]; then
  45. cnt=$(echo "$missing" | wc -l | tr -d ' ')
  46. echo "FAIL: $loc missing $cnt key(s):"
  47. echo "$missing" | sed 's/^/ /'
  48. FAIL=1
  49. fi
  50. # 2. Translation: no present key's value may equal the en value,
  51. # UNLESS the en value is pure placeholders/symbols (no translatable
  52. # prose). A value is "translatable" if, after stripping { $x }
  53. # placeholders and non-alphanumerics, it contains at least one
  54. # alphabetic word of length >= 3.
  55. untranslated=""
  56. while IFS= read -r key; do
  57. [ -z "$key" ] && continue
  58. en_val=$(value_of "$EN" "$key")
  59. loc_val=$(value_of "$f" "$key")
  60. [ -z "$loc_val" ] && continue
  61. [ -z "$en_val" ] && continue
  62. # Skip untranslatable values (pure placeholders/symbols/identifiers).
  63. # Strip { $x } placeholders, then non-alpha. Words containing '_'
  64. # are config/code identifiers (host_key, etc.) — not translatable prose.
  65. prose=$(echo "$en_val" | sed -E 's/\{ \$[a-z_]+ \}//g; s/[^[:alpha:]_]/ /g; s/[a-z]+_[a-z_]+//g')
  66. translatable=0
  67. for w in $prose; do
  68. if [ ${#w} -ge 3 ] && ! is_loanword "$w"; then translatable=1; break; fi
  69. done
  70. [ "$translatable" -eq 0 ] && continue
  71. if [ "$en_val" = "$loc_val" ]; then
  72. untranslated="$untranslated$key"$'\n'
  73. fi
  74. done < <(echo "$en_keys")
  75. untranslated=$(echo "$untranslated" | sed '/^$/d')
  76. if [ -n "$untranslated" ]; then
  77. cnt=$(echo "$untranslated" | wc -l | tr -d ' ')
  78. echo "FAIL: $loc has $cnt untranslated key(s) (English value identical to en):"
  79. echo "$untranslated" | sed 's/^/ /'
  80. FAIL=1
  81. fi
  82. done
  83. if [ "$FAIL" -eq 0 ]; then
  84. echo "OK: all non-en locales complete (coverage + translation)."
  85. exit 0
  86. fi
  87. exit 1