A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:
A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.
English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.
Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.
Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.
Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.
All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.
scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.
One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.
close #1856
170 lines
4.7 KiB
Bash
Executable File
170 lines
4.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# Git sets GIT_DIR (and friends) for hook invocations. Inside `flutter pub
|
|
# run`, that leaks into Flutter's own SDK-version probe (`git describe` from
|
|
# Flutter's checkout) and makes Flutter misreport its version as
|
|
# `1.35.1-0.0.pre-1`, which then fails dependency resolution. Strip those
|
|
# vars so the script behaves the same when invoked from a hook as it does
|
|
# from a plain shell.
|
|
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_PREFIX
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
if [ -t 1 ]; then
|
|
BOLD=$'\e[1m'; RED=$'\e[31m'; GRN=$'\e[32m'; DIM=$'\e[2m'; RST=$'\e[0m'
|
|
else
|
|
BOLD=""; RED=""; GRN=""; DIM=""; RST=""
|
|
fi
|
|
section() { printf "\n%s==> %s%s\n" "$BOLD" "$1" "$RST"; }
|
|
ok() { printf " %sPASS%s %s\n" "$GRN" "$RST" "$1"; }
|
|
fail() { printf " %sFAIL%s %s\n" "$RED" "$RST" "$1"; }
|
|
skip() { printf " %sSKIP%s %s\n" "$DIM" "$RST" "$1"; }
|
|
|
|
if ! command -v flutter >/dev/null 2>&1 || ! command -v dart >/dev/null 2>&1; then
|
|
fail "flutter/dart not in PATH"
|
|
echo " Install Flutter: https://docs.flutter.dev/get-started/install"
|
|
echo " Bypass temporarily: SKIP_HOOKS=1 git commit ..."
|
|
exit 1
|
|
fi
|
|
|
|
have_dart_code_linter() {
|
|
[ -f "$ROOT/.dart_tool/package_config.json" ] && \
|
|
grep -q '"name": *"dart_code_linter"' "$ROOT/.dart_tool/package_config.json" 2>/dev/null
|
|
}
|
|
|
|
FAILED=0
|
|
|
|
# 1. dart format (mirrors ci.yml "Verify formatting")
|
|
section "dart format"
|
|
files=()
|
|
while IFS= read -r -d '' f; do files+=("$f"); done < <(
|
|
find lib $([ -d test ] && echo test) \
|
|
-name "*.dart" ! -name "*.g.dart" ! -name "*.freezed.dart" \
|
|
-type f -print0 2>/dev/null
|
|
)
|
|
if [ ${#files[@]} -eq 0 ]; then
|
|
skip "no dart files"
|
|
else
|
|
out="$(mktemp)"
|
|
if dart format --output=none --set-exit-if-changed "${files[@]}" >"$out" 2>&1; then
|
|
ok "${#files[@]} file(s) correctly formatted"
|
|
else
|
|
fail "formatting issues"
|
|
sed 's/^/ /' "$out"
|
|
FAILED=1
|
|
fi
|
|
rm -f "$out"
|
|
fi
|
|
|
|
# 2. Codegen freshness
|
|
section "codegen freshness"
|
|
out="$(mktemp)"
|
|
if scripts/codegen.sh --check >"$out" 2>&1; then
|
|
ok "generated files are current"
|
|
else
|
|
fail "generated files are stale"
|
|
sed 's/^/ /' "$out"
|
|
FAILED=1
|
|
fi
|
|
rm -f "$out"
|
|
|
|
# 3. Translation hygiene
|
|
section "translation hygiene"
|
|
if python3 scripts/clean_translations.py --check --strict; then
|
|
ok "locale files normalized and no unused keys found"
|
|
else
|
|
fail "translation files need cleanup or contain unused keys"
|
|
FAILED=1
|
|
fi
|
|
|
|
# 4. Hardcoded user-facing strings
|
|
section "hardcoded UI strings"
|
|
if python3 scripts/check_hardcoded_strings.py; then
|
|
ok "user-facing strings use the translation layer"
|
|
else
|
|
fail "hardcoded user-facing English strings found"
|
|
FAILED=1
|
|
fi
|
|
|
|
# 5. Workflow and script regression guards
|
|
section "workflow and script guards"
|
|
if bash scripts/ci_guard_checks.sh; then
|
|
ok "workflow and script guards passed"
|
|
else
|
|
fail "workflow or script guard failed"
|
|
FAILED=1
|
|
fi
|
|
|
|
# 6. Icon consistency
|
|
section "icon consistency"
|
|
if dart run scripts/check_icon_consistency.dart; then
|
|
ok "production icons use AppIcon and rounded Symbols"
|
|
else
|
|
fail "icon consistency violations found"
|
|
FAILED=1
|
|
fi
|
|
|
|
# 7. Native formatting
|
|
section "native format"
|
|
out="$(mktemp)"
|
|
if scripts/format_native.sh --check >"$out" 2>&1; then
|
|
ok "native files correctly formatted"
|
|
else
|
|
fail "native formatting check failed"
|
|
sed 's/^/ /' "$out"
|
|
FAILED=1
|
|
fi
|
|
rm -f "$out"
|
|
|
|
# 8. Dart analyzer (mirrors ci.yml "Analyze code")
|
|
section "Dart analyzer"
|
|
if dart run scripts/check_analyzer.dart; then
|
|
ok "no unapproved diagnostics"
|
|
else
|
|
fail "analyzer errors, warnings, unexpected infos, or tool failure"
|
|
FAILED=1
|
|
fi
|
|
|
|
# 9. Unused code (mirrors ci.yml "Check for unused code")
|
|
section "dart_code_linter: unused code"
|
|
if ! have_dart_code_linter; then
|
|
skip "dart_code_linter unresolved — run 'flutter pub get'"
|
|
else
|
|
out="$(mktemp)"
|
|
flutter pub run dart_code_linter:metrics check-unused-code lib >"$out" 2>&1 || true
|
|
if grep -qi "no unused code found" "$out"; then
|
|
ok "none"
|
|
else
|
|
fail "unused code detected:"
|
|
sed 's/^/ /' "$out"
|
|
FAILED=1
|
|
fi
|
|
rm -f "$out"
|
|
fi
|
|
|
|
# 10. Unused files (mirrors ci.yml "Check for unused files")
|
|
section "dart_code_linter: unused files"
|
|
if ! have_dart_code_linter; then
|
|
skip "dart_code_linter unresolved — run 'flutter pub get'"
|
|
else
|
|
out="$(mktemp)"
|
|
flutter pub run dart_code_linter:metrics check-unused-files lib >"$out" 2>&1 || true
|
|
if grep -qi "no unused files found" "$out"; then
|
|
ok "none"
|
|
else
|
|
fail "unused files detected:"
|
|
sed 's/^/ /' "$out"
|
|
FAILED=1
|
|
fi
|
|
rm -f "$out"
|
|
fi
|
|
|
|
if [ "$FAILED" -ne 0 ]; then
|
|
printf "\n%sOne or more checks failed.%s Bypass with SKIP_HOOKS=1 (or --no-verify).\n" "$RED" "$RST"
|
|
exit 1
|
|
fi
|
|
printf "\n%sAll checks passed.%s\n" "$GRN" "$RST"
|