i18n: per-player EN/PT via Adventure GlobalTranslator (#1)

Foundation + commands module of the i18n spec.

- I18n registry/loader + Lang.tr facade + reloadI18n
- PT source-of-truth bundle + EN translation
- CanalhandiaCommand player-facing strings migrated; admin-tuning/help/enum-labels deferred
- I18nTest: parity + per-locale render + pt_BR fallback; 316/316 green

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit was merged in pull request #1.
This commit is contained in:
2026-08-12 15:53:27 +00:00
parent c1a6b9730f
commit dafd96a4b6
63 changed files with 9075 additions and 177 deletions
Executable
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env bash
# Pre-flight harness for a Canalhandia deploy.
#
# Every check below is READ-ONLY. It never restarts the server, never writes to
# the plugins directory, and never touches the world. Run it after staging a
# jar and before asking Crafty to restart: a red line here is a crash-on-boot
# you get to fix while the server is still up.
#
# Usage: ./preflight.sh [path/to/new.jar]
# (defaults to target/Canalhandia-1.0.0.jar)
set -uo pipefail
JAR="${1:-target/Canalhandia-1.0.0.jar}"
NS=minecraft
SERVER_ID=6e39a8b2-300b-42d6-8139-f397c23e461b
PLUGINS="/crafty/servers/${SERVER_ID}/plugins"
K="microk8s kubectl -n ${NS}"
fail=0
pass() { printf ' \033[32mOK\033[0m %s\n' "$1"; }
warn() { printf ' \033[33mWARN\033[0m %s\n' "$1"; }
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail + 1)); }
head() { printf '\n\033[1m%s\033[0m\n' "$1"; }
CRAFTY=$($K get pods -l app=crafty-controller -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if [ -z "$CRAFTY" ]; then
CRAFTY=$($K get pods --no-headers 2>/dev/null | awk '/^crafty-controller/ {print $1; exit}')
fi
head "1. Local build"
if [ ! -f "$JAR" ]; then
bad "jar not found: $JAR"
else
pass "jar present: $JAR ($(stat -c%s "$JAR") bytes)"
# A jar that does not open is a jar the server will refuse at boot.
if unzip -t "$JAR" >/dev/null 2>&1; then
pass "jar archive is intact"
else
bad "jar archive is corrupt (unzip -t failed)"
fi
# plugin.yml is the only file Bukkit truly requires; without it the plugin is
# skipped silently and nothing in the deploy takes effect.
if unzip -p "$JAR" plugin.yml >/dev/null 2>&1; then
pass "plugin.yml present in jar"
else
bad "plugin.yml MISSING from jar — the plugin would not load at all"
fi
# Kept in step with Canalhandia.onEnable's register() list. A command that is
# registered in code but absent here logs a warning at boot and then silently
# does nothing in game, which is a hard failure to diagnose from inside.
CMDS="canalhandia curiosidade adivinha enquete ranking reagir reacoes \
palpite votar legal wow top f ia iap errado nota save \
recado recados mortes conquistas"
missing=""
for cmd in $CMDS; do
unzip -p "$JAR" plugin.yml 2>/dev/null | grep -qE "^ ${cmd}:" || missing="$missing $cmd"
done
if [ -z "$missing" ]; then
pass "all $(echo $CMDS | wc -w) commands declared in plugin.yml"
else
bad "commands missing from plugin.yml:$missing"
fi
# Permissions the new features gate on. An undeclared Bukkit permission falls
# back to op-only, which would silently stop normal players writing notes.
for perm in canalhandia.nota canalhandia.nota.publica canalhandia.recado; do
if unzip -p "$JAR" plugin.yml 2>/dev/null | grep -q " ${perm}:"; then
pass "permission ${perm} declared"
else
bad "permission ${perm} MISSING — would default to op-only"
fi
done
# BlueMap's API is compile-only (provided scope): BlueMap ships those classes
# itself, and a second copy inside this jar would shadow them and break the
# real plugin. This check is the guard against someone dropping the scope.
if unzip -l "$JAR" 2>/dev/null | grep -q "bluecolored"; then
bad "BlueMap classes are BUNDLED in the jar — the dependency must stay 'provided'"
else
pass "BlueMap API not bundled (provided scope intact)"
fi
# config.yml ships defaults; a jar without it means saveDefaultConfig() writes
# nothing and every setting silently falls back to the hardcoded default.
if unzip -p "$JAR" config.yml >/dev/null 2>&1; then
pass "config.yml present in jar"
else
bad "config.yml MISSING from jar"
fi
fi
head "2. Tests"
if [ -d target/surefire-reports ]; then
tests=$(grep -ho 'tests="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null |
grep -o '[0-9]*' | paste -sd+ | bc)
bads=$(grep -ho 'failures="[0-9]*"\|errors="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null |
grep -o '[0-9]*' | paste -sd+ | bc)
if [ "${bads:-1}" = "0" ]; then
pass "${tests} tests, 0 failures/errors"
else
bad "${bads} test failures/errors — run mvn test"
fi
else
warn "no surefire reports; run the test suite before deploying"
fi
head "3. Cluster"
if [ -z "$CRAFTY" ]; then
bad "crafty-controller pod not found in namespace ${NS}"
else
pass "crafty pod: $CRAFTY"
if $K exec "$CRAFTY" -- test -d "$PLUGINS" 2>/dev/null; then
pass "plugins directory reachable"
else
bad "cannot reach $PLUGINS"
fi
fi
head "4. Staged jar on the server"
if [ -n "$CRAFTY" ] && [ -f "$JAR" ]; then
local_sum=$(sha256sum "$JAR" | cut -c1-8)
remote_sum=$($K exec "$CRAFTY" -- sha256sum "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null | cut -c1-8)
if [ -z "$remote_sum" ]; then
bad "no Canalhandia jar staged on the server"
elif [ "$local_sum" = "$remote_sum" ]; then
pass "staged jar matches the local build (sha $local_sum)"
else
bad "staged jar is sha $remote_sum, local build is $local_sum — copy it again"
fi
# Ownership: the JVM runs as uid 1000 / gid 0. A root-owned jar is a jar the
# server cannot read, and the failure looks like "plugin just did not load".
owner=$($K exec "$CRAFTY" -- stat -c '%u:%g' "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null)
if [ "$owner" = "1000:0" ]; then
pass "jar ownership 1000:0"
else
bad "jar ownership is '$owner', expected 1000:0 — chown it"
fi
# A rollback target must exist before, not after, something goes wrong.
if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia-1.0.0.jar.bak-* >/dev/null 2>&1"; then
pass "rollback jar(s) present"
else
bad "no .bak jar to roll back to"
fi
fi
head "5. Live config"
if [ -n "$CRAFTY" ]; then
cfg="$PLUGINS/Canalhandia/config.yml"
if $K exec "$CRAFTY" -- test -f "$cfg" 2>/dev/null; then
pass "config.yml present on the server"
# Parsed HERE rather than in the pod: the crafty image has no python, and a
# config.yml that does not parse is a plugin that disables itself on boot.
tmp=$(mktemp)
if $K exec "$CRAFTY" -- cat "$cfg" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
# Pick a parser. "no parser available" must NOT be reported as "invalid":
# a harness that cries wolf is a harness people learn to ignore.
if python3 -c "import yaml" 2>/dev/null; then
yaml_check() { python3 -c "import yaml,sys;yaml.safe_load(open(sys.argv[1]))" "$1"; }
elif command -v docker >/dev/null 2>&1; then
yaml_check() { docker run --rm -v "$1":/c.yml:ro python:3.12-slim \
sh -c "pip install -q pyyaml >/dev/null 2>&1 && python3 -c \
'import yaml;yaml.safe_load(open(\"/c.yml\"))'"; }
else
yaml_check() { return 2; }
fi
yaml_check "$tmp" 2>/dev/null
case $? in
0) pass "config.yml parses as valid YAML" ;;
2) warn "no YAML parser available (pip install pyyaml) — not checked" ;;
*) bad "config.yml on the server is NOT valid YAML — the plugin would fail to load" ;;
esac
# The keys this deploy depends on. A missing key is not fatal (Settings
# has defaults) but it means the merge did not happen as intended.
for key in "modulos:" "zoacao:" "personalidade:" "contexto-chat:" "estado-servidor:"; do
if grep -q "$key" "$tmp"; then
pass "config has ${key%:}"
else
warn "config has no '${key%:}' — will fall back to the built-in default"
fi
done
else
warn "could not read config.yml out of the pod"
fi
rm -f "$tmp"
if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia/config.yml.bak-* >/dev/null 2>&1"; then
pass "config backup(s) present"
else
bad "no config.yml.bak-* to roll back to"
fi
else
bad "config.yml missing on the server"
fi
fi
head "6. Server health right now"
if [ -n "$CRAFTY" ]; then
logf="/crafty/servers/${SERVER_ID}/logs/latest.log"
# tr -dc keeps digits only: grep -c prints nothing on no-match under some
# shells, and the stray newline made the numeric test below explode.
online=$($K exec "$CRAFTY" -- sh -c "grep -c 'joined the game' $logf" 2>/dev/null | tr -dc '0-9')
pass "log readable (${online:-0} join lines this session)"
errs=$($K exec "$CRAFTY" -- sh -c "tail -500 $logf | grep -ci 'ERROR\]'" 2>/dev/null | tr -dc '0-9')
if [ "${errs:-0}" -gt 0 ]; then
warn "${errs} ERROR lines in the last 500 — read them before restarting"
else
pass "no ERROR lines in the last 500"
fi
fi
printf '\n'
if [ "$fail" -eq 0 ]; then
printf '\033[32mPRE-FLIGHT CLEAN — safe to restart.\033[0m\n'
exit 0
fi
printf '\033[31m%d CHECK(S) FAILED — do NOT restart yet.\033[0m\n' "$fail"
exit 1