🏡 index : quine-global/quintodrome.git

#!/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