#!/usr/bin/env bash # Fluent Validation Script using native工具和Rust # Validates Fluent (.ftl) files for syntax errors and structural issues set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # No Color # Configuration CONTENT_DIR="content" VERBOSE=${VERBOSE:-false} STRICT=${STRICT:-false} REPORT_FILE="" # Error tracking ERRORS=0 WARNINGS=0 TOTAL_FILES=0 print_usage() { cat << EOF Usage: $0 [OPTIONS] [DIRECTORY] Validate Fluent (.ftl) localization files for syntax and structure errors. OPTIONS: -f, --file FILE Validate specific FTL file -v, --verbose Enable verbose output -s, --strict Treat warnings as errors -r, --report FILE Generate JSON report file -h, --help Show this help message EXAMPLES: $0 # Validate all FTL files in content/ $0 content/en # Validate English FTL files $0 -f content/en/auth.ftl # Validate specific file $0 -v -r report.json # Verbose with JSON report ENVIRONMENT VARIABLES: VERBOSE=true Enable verbose output STRICT=true Treat warnings as errors EOF } log() { echo -e "${CYAN}[VALIDATE]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 ((ERRORS++)) } log_warning() { echo -e "${YELLOW}[WARN]${NC} $1" ((WARNINGS++)) } log_success() { echo -e "${GREEN}[OK]${NC} $1" } # Check if required tools are available check_dependencies() { if ! command -v cargo &> /dev/null; then log_error "Cargo not found. Please install Rust toolchain." exit 1 fi } # Install fluent-syntax tool if needed install_fluent_tool() { if ! command -v fluent-syntax &> /dev/null; then log "Creating simple fluent parser..." # Create a simple validate tool if fluent-cli not available cat > /tmp/validate_ftl.rs << 'EOF' use std::fs; use std::env; use std::process; fn validate_ftl_file(file_path: &str) -> Result<(), String> { let content = fs::read_to_string(file_path) .map_err(|e| format!("Failed to read file: {}", e))?; // Basic FTL validation: check for unbalanced braces let open_braces = content.chars().filter(|&c| c == '{').count(); let close_braces = content.chars().filter(|&c| c == '}').count(); if open_braces != close_braces { return Err(format!("Unbalanced braces: {} open, {} close", open_braces, close_braces)); } // Basic syntax validation for (line_num, line) in content.lines().enumerate() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } // Check for message format if line.contains('=') && !line.starts_with('[') { let parts: Vec<&str> = line.splitn(2, '=').collect(); if parts.len() != 2 { return Err(format!("Line {}: Invalid message format", line_num + 1)); } let key = parts[0].trim(); if key.is_empty() { return Err(format!("Line {}: Empty message key", line_num + 1)); } // Check for valid key characters if !key.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') { return Err(format!("Line {}: Invalid key characters: {}", line_num + 1, key)); } } } Ok(()) } fn main() { let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("Usage: validate_ftl "); process::exit(1); } match validate_ftl_file(&args[1]) { Ok(()) => { println!("✓ {}: Valid", args[1]); process::exit(0); } Err(e) => { eprintln!("✗ {}: {}", args[1], e); process::exit(1); } } } EOF rustc /tmp/validate_ftl.rs -o /tmp/validate_ftl export PATH="/tmp:$PATH" fi } # Validate a single FTL file validate_ftl_file() { local file="$1" local errors=0 if [[ ! -f "$file" ]]; then log_error "File not found: $file" return 1 fi ((TOTAL_FILES++)) log "Validating: $file" # Check UTF-8 encoding if ! iconv -f UTF-8 -t UTF-8 "$file" >/dev/null 2>&1; then log_error "File is not valid UTF-8: $file" ((errors++)) fi # Check for BOM (Byte Order Mark) if head -c 3 "$file" | grep -q '\xEF\xBB\xBF'; then log_warning "File contains UTF-8 BOM: $file" fi # Check for mixed line endings if file "$file" | grep -q "CRLF"; then log_warning "Mixed line endings (CRLF detected): $file" fi # Use basic validation tool if command -v fluent-syntax &> /dev/null; then if fluent-syntax validate "$file" > /dev/null 2>&1; then log_success "$file: Valid syntax" else log_error "$file: Syntax error" if [[ "$VERBOSE" == "true" ]]; then fluent-syntax validate "$file" fi ((errors++)) fi else # Use simple validation if /tmp/validate_ftl "$file" 2>/dev/null; then log_success "$file: Valid" else log_error "$file: Validation failed" ((errors++)) fi fi # Custom checks for common issues local content content=$(cat "$file") # Check for unbalanced braces local open_braces local close_braces open_braces=$(grep -o '{' <<< "$content" | wc -l) close_braces=$(grep -o '}' <<< "$content" | wc -l) if [[ $open_braces -ne $close_braces ]]; then log_error "$file: Unbalanced braces ($open_braces open, $close_braces close)" ((errors++)) fi # Check for empty messages while IFS= read -r line; do if [[ "$line" =~ ^[a-zA-Z0-9_-]+[[:space:]]*=[[:space:]]*$ ]]; then log_warning "$file: Empty message value: $line" fi done <<< "$content" # Check for duplicate keys local keys keys=$(grep -E '^[a-zA-Z0-9\-_]+[[:space:]]*=' "$file" | cut -d'=' -f1 | sed 's/[[:space:]]*$//') local duplicate_keys duplicate_keys=$(echo "$keys" | sort | uniq -d) if [[ -n "$duplicate_keys" ]]; then log_error "$file: Duplicate keys found: $duplicate_keys" ((errors++)) fi return $errors } # Validate all FTL files in directory validate_directory() { local dir="$1" local files_found=0 if [[ ! -d "$dir" ]]; then log_error "Directory not found: $dir" return 1 fi log "Searching for FTL files in: $dir" # Find all .ftl files recursively while IFS= read -r -d '' file; do validate_ftl_file "$file" ((files_found++)) done < <(find "$dir" -name "*.ftl" -type f -print0) if [[ $files_found -eq 0 ]]; then log_warning "No FTL files found in: $dir" fi } # Generate JSON report generate_report() { local output_file="$1" cat > "$output_file" << EOF { "validation": { "total_files": $TOTAL_FILES, "errors": $ERRORS, "warnings": $WARNINGS, "success": $(($TOTAL_FILES - $ERRORS)), "timestamp": "$(date -Iseconds)", "content_dir": "$CONTENT_DIR" } } EOF log "Report saved to: $output_file" } # Main validation function main() { local target_dir="$CONTENT_DIR" local target_file="" # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -f|--file) target_file="$2" shift 2 ;; -v|--verbose) VERBOSE=true shift ;; -s|--strict) STRICT=true shift ;; -r|--report) REPORT_FILE="$2" shift 2 ;; -h|--help) print_usage exit 0 ;; *) target_dir="$1" shift ;; esac done # Override with environment variables [[ -n "${VERBOSE:-}" ]] && VERBOSE=true [[ -n "${STRICT:-}" ]] && STRICT=true log "Starting Fluent validation" check_dependencies install_fluent_tool if [[ -n "$target_file" ]]; then validate_ftl_file "$target_file" else validate_directory "$target_dir" fi # Generate report if requested if [[ -n "$REPORT_FILE" ]]; then generate_report "$REPORT_FILE" fi # Final summary echo echo "=== VALIDATION SUMMARY ===" echo "Total files processed: $TOTAL_FILES" echo -e "Errors: ${RED}$ERRORS${NC}" echo -e "Warnings: ${YELLOW}$WARNINGS${NC}" if [[ $ERRORS -gt 0 ]]; then echo -e "${RED}✗ Validation failed with errors${NC}" exit 1 elif [[ $WARNINGS -gt 0 ]] && [[ "$STRICT" == "true" ]]; then echo -e "${YELLOW}⚠ Validation failed with warnings (strict mode)${NC}" exit 1 elif [[ $WARNINGS -gt 0 ]]; then echo -e "${YELLOW}⚠ Validation passed with warnings${NC}" exit 0 else echo -e "${GREEN}✓ Validation passed successfully${NC}" exit 0 fi } # Run main function if script is executed directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi