361 lines
9.2 KiB
Bash
361 lines
9.2 KiB
Bash
|
|
#!/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<String> = env::args().collect();
|
||
|
|
if args.len() < 2 {
|
||
|
|
eprintln!("Usage: validate_ftl <file.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
|