test: add translation validation system with CI integration (#4306)

* feat: add translation validation script and update JSON files

Introduced a new script `validate-translations.sh` to validate the structure
of JSON translation files against an English reference. This script checks
for missing and extra translation keys, ensuring consistency across language
files. Additionally, several JSON files were updated to include new keys
and improve existing translations, enhancing the overall localization
efforts for the application.

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: enhance translation validation script

Updated the translation validation script to improve its functionality and usability. The script now validates JSON translation files against a reference English file, checking for JSON syntax, structural integrity, and reporting missing or extra keys. It also integrates with GitHub Actions for CI/CD, providing annotations for errors and warnings. Additionally, the usage instructions have been clarified, and verbose output options have been added for better debugging.

Signed-off-by: Deluan <deluan@navidrome.org>

* revert translations

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: Hungarian translation JSON structure

Signed-off-by: Deluan <deluan@navidrome.org>

* chore: update testall target in Makefile

Modified the 'testall' target in the Makefile to include 'test-i18n' in the test sequence. This change ensures that internationalization tests are run alongside other tests, improving the overall testing process and ensuring that translation-related issues are caught early in the development cycle.

Signed-off-by: Deluan <deluan@navidrome.org>

* run validation with verbose output

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2025-07-03 09:59:39 -04:00
committed by GitHub
parent 9b3d3d15a1
commit c583ff57a3
4 changed files with 268 additions and 21 deletions

View File

@@ -157,6 +157,8 @@ jobs:
exit 1
fi
done
- run: ./.github/workflows/validate-translations.sh -v
check-push-enabled:
name: Check Docker configuration

238
.github/workflows/validate-translations.sh vendored Executable file
View File

@@ -0,0 +1,238 @@
#!/bin/bash
# validate-translations.sh
#
# This script validates the structure of JSON translation files by comparing them
# against the reference English translation file (ui/src/i18n/en.json).
#
# The script performs the following validations:
# 1. JSON syntax validation using jq
# 2. Structural validation - ensures all keys from English file are present
# 3. Reports missing keys (translation incomplete)
# 4. Reports extra keys (keys not in English reference, possibly deprecated)
# 5. Emits GitHub Actions annotations for CI/CD integration
#
# Usage:
# ./validate-translations.sh
#
# Environment Variables:
# EN_FILE - Path to reference English file (default: ui/src/i18n/en.json)
# TRANSLATION_DIR - Directory containing translation files (default: resources/i18n)
#
# Exit codes:
# 0 - All translations are valid
# 1 - One or more translations have structural issues
#
# GitHub Actions Integration:
# The script outputs GitHub Actions annotations using ::error and ::warning
# format that will be displayed in PR checks and workflow summaries.
# Script to validate JSON translation files structure against en.json
set -e
# Path to the reference English translation file
EN_FILE="${EN_FILE:-ui/src/i18n/en.json}"
TRANSLATION_DIR="${TRANSLATION_DIR:-resources/i18n}"
VERBOSE=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
echo "Usage: $0 [options]"
echo ""
echo "Validates JSON translation files structure against English reference file."
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --verbose Show detailed output (default: only show errors)"
echo ""
echo "Environment Variables:"
echo " EN_FILE Path to reference English file (default: ui/src/i18n/en.json)"
echo " TRANSLATION_DIR Directory with translation files (default: resources/i18n)"
echo ""
echo "Examples:"
echo " $0 # Validate all translation files (quiet mode)"
echo " $0 -v # Validate with detailed output"
echo " EN_FILE=custom/en.json $0 # Use custom reference file"
echo " TRANSLATION_DIR=custom/i18n $0 # Use custom translations directory"
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help for usage information" >&2
exit 1
;;
esac
done
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
if [[ "$VERBOSE" == "true" ]]; then
echo "Validating translation files structure against ${EN_FILE}..."
fi
# Check if English reference file exists
if [[ ! -f "$EN_FILE" ]]; then
echo "::error::Reference file $EN_FILE not found"
exit 1
fi
# Function to extract all JSON keys from a file, creating a flat list of dot-separated paths
extract_keys() {
local file="$1"
jq -r 'paths(scalars) as $p | $p | join(".")' "$file" 2>/dev/null | sort
}
# Function to extract all non-empty string keys (to identify structural issues)
extract_structure_keys() {
local file="$1"
# Get only keys where values are not empty strings
jq -r 'paths(scalars) as $p | select(getpath($p) != "") | $p | join(".")' "$file" 2>/dev/null | sort
}
# Function to validate a single translation file
validate_translation() {
local translation_file="$1"
local filename=$(basename "$translation_file")
local has_errors=false
local verbose=${2:-false}
if [[ "$verbose" == "true" ]]; then
echo "Validating $filename..."
fi
# First validate JSON syntax
if ! jq empty "$translation_file" 2>/dev/null; then
echo "::error file=$translation_file::Invalid JSON syntax"
echo -e "${RED}$filename has invalid JSON syntax${NC}"
return 1
fi
# Extract all keys from both files (for statistics)
local en_keys_file=$(mktemp)
local translation_keys_file=$(mktemp)
extract_keys "$EN_FILE" > "$en_keys_file"
extract_keys "$translation_file" > "$translation_keys_file"
# Extract only non-empty structure keys (to validate structural issues)
local en_structure_file=$(mktemp)
local translation_structure_file=$(mktemp)
extract_structure_keys "$EN_FILE" > "$en_structure_file"
extract_structure_keys "$translation_file" > "$translation_structure_file"
# Find structural issues: keys in translation not in English (misplaced)
local extra_keys=$(comm -13 "$en_keys_file" "$translation_keys_file")
# Find missing keys (for statistics only)
local missing_keys=$(comm -23 "$en_keys_file" "$translation_keys_file")
# Count keys for statistics
local total_en_keys=$(wc -l < "$en_keys_file")
local total_translation_keys=$(wc -l < "$translation_keys_file")
local missing_count=0
local extra_count=0
if [[ -n "$missing_keys" ]]; then
missing_count=$(echo "$missing_keys" | grep -c '^' || echo 0)
fi
if [[ -n "$extra_keys" ]]; then
extra_count=$(echo "$extra_keys" | grep -c '^' || echo 0)
has_errors=true
fi
# Report extra/misplaced keys (these are structural issues)
if [[ -n "$extra_keys" ]]; then
if [[ "$verbose" == "true" ]]; then
echo -e "${YELLOW}Misplaced keys in $filename ($extra_count):${NC}"
fi
while IFS= read -r key; do
# Try to find the line number
line=$(grep -n "\"$(echo "$key" | sed 's/.*\.//')" "$translation_file" | head -1 | cut -d: -f1)
line=${line:-1} # Default to line 1 if not found
echo "::error file=$translation_file,line=$line::Misplaced key: $key"
if [[ "$verbose" == "true" ]]; then
echo " + $key (line ~$line)"
fi
done <<< "$extra_keys"
fi
# Clean up temp files
rm -f "$en_keys_file" "$translation_keys_file" "$en_structure_file" "$translation_structure_file"
# Print statistics
if [[ "$verbose" == "true" ]]; then
echo " Keys: $total_translation_keys/$total_en_keys (Missing: $missing_count, Extra/Misplaced: $extra_count)"
if [[ "$has_errors" == "true" ]]; then
echo -e "${RED}$filename has structural issues${NC}"
else
echo -e "${GREEN}$filename structure is valid${NC}"
fi
elif [[ "$has_errors" == "true" ]]; then
echo -e "${RED}$filename has structural issues (Extra/Misplaced: $extra_count)${NC}"
fi
return $([[ "$has_errors" == "true" ]] && echo 1 || echo 0)
}
# Main validation loop
validation_failed=false
total_files=0
failed_files=0
valid_files=0
for translation_file in "$TRANSLATION_DIR"/*.json; do
if [[ -f "$translation_file" ]]; then
total_files=$((total_files + 1))
if ! validate_translation "$translation_file" "$VERBOSE"; then
validation_failed=true
failed_files=$((failed_files + 1))
else
valid_files=$((valid_files + 1))
fi
if [[ "$VERBOSE" == "true" ]]; then
echo "" # Add spacing between files
fi
fi
done
# Summary
if [[ "$VERBOSE" == "true" ]]; then
echo "========================================="
echo "Translation Validation Summary:"
echo " Total files: $total_files"
echo " Valid files: $valid_files"
echo " Files with structural issues: $failed_files"
echo "========================================="
fi
if [[ "$validation_failed" == "true" ]]; then
if [[ "$VERBOSE" == "true" ]]; then
echo -e "${RED}Translation validation failed - $failed_files file(s) have structural issues${NC}"
else
echo -e "${RED}Translation validation failed - $failed_files/$total_files file(s) have structural issues${NC}"
fi
exit 1
elif [[ "$VERBOSE" == "true" ]]; then
echo -e "${GREEN}All translation files are structurally valid${NC}"
fi
exit 0
# Contains AI-generated edits.

View File

@@ -41,14 +41,21 @@ test: ##@Development Run Go tests
go test -tags netgo $(PKG)
.PHONY: test
testrace: ##@Development Run Go tests with race detector
go test -tags netgo -race -shuffle=on ./...
.PHONY: test
testall: testrace ##@Development Run Go and JS tests
@(cd ./ui && npm run test)
testall: test-race test-i18n test-js ##@Development Run Go and JS tests
.PHONY: testall
test-race: ##@Development Run Go tests with race detector
go test -tags netgo -race -shuffle=on ./...
.PHONY: test-race
test-js: ##@Development Run JS tests
@(cd ./ui && npm run test)
.PHONY: test-js
test-i18n: ##@Development Validate all translations files
./.github/workflows/validate-translations.sh
.PHONY: test-i18n
install-golangci-lint: ##@Development Install golangci-lint if not present
@PATH=$$PATH:./bin which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s v2.1.6)
.PHONY: install-golangci-lint

View File

@@ -511,23 +511,23 @@
"disabled": "Kikapcsolva",
"waiting": "Várakozás"
}
},
"tabs": {
"about": "Rólunk",
"config": "Konfiguráció"
},
"config": {
"configName": "Beállítás neve",
"environmentVariable": "Környezeti változó",
"currentValue": "Jelenlegi érték",
"configurationFile": "Konfigurációs fájl",
"exportToml": "Konfiguráció exportálása (TOML)",
"exportSuccess": "Konfiguráció kiexportálva a vágólapra, TOML formában",
"exportFailed": "Nem sikerült kimásolni a konfigurációt",
"devFlagsHeader": "Fejlesztői beállítások (változások/eltávolítás jogát fenntartjuk)",
"devFlagsComment": "Ezek kísérleti beállítások, és a jövőbeli verziókban eltávolíthatók"
}
},
"tabs": {
"about": "Rólunk",
"config": "Konfiguráció"
},
"config": {
"configName": "Beállítás neve",
"environmentVariable": "Környezeti változó",
"currentValue": "Jelenlegi érték",
"configurationFile": "Konfigurációs fájl",
"exportToml": "Konfiguráció exportálása (TOML)",
"exportSuccess": "Konfiguráció kiexportálva a vágólapra, TOML formában",
"exportFailed": "Nem sikerült kimásolni a konfigurációt",
"devFlagsHeader": "Fejlesztői beállítások (változások/eltávolítás jogát fenntartjuk)",
"devFlagsComment": "Ezek kísérleti beállítások, és a jövőbeli verziókban eltávolíthatók"
},
"activity": {
"title": "Aktivitás",
"totalScanned": "Összes beolvasott mappa:",