Loading...
Validates translation files for missing keys and ensures consistency across different language files
{
"hookConfig": {
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/i18n-translation-validator.sh",
"matchers": [
"write",
"edit"
]
}
}
},
"scriptContent": "#!/usr/bin/env bash\n\n# Read the tool input from stdin\nINPUT=$(cat)\nTOOL_NAME=$(echo \"$INPUT\" | jq -r '.tool_name')\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // .tool_input.path // \"\"')\n\nif [ -z \"$FILE_PATH\" ]; then\n exit 0\nfi\n\n# Check if this is a translation/localization file\nif [[ \"$FILE_PATH\" == *locales/*.json ]] || [[ \"$FILE_PATH\" == *i18n/*.json ]] || [[ \"$FILE_PATH\" == *lang/*.json ]] || [[ \"$FILE_PATH\" == *translations/*.json ]] || [[ \"$FILE_PATH\" == *.po ]] || [[ \"$FILE_PATH\" == *messages/*.properties ]]; then\n echo \"π i18n Translation Validation for: $(basename \"$FILE_PATH\")\" >&2\n \n # Initialize validation counters\n ERRORS=0\n WARNINGS=0\n MISSING_KEYS=0\n ORPHANED_KEYS=0\n TOTAL_KEYS=0\n \n # Function to report validation results\n report_validation() {\n local level=\"$1\"\n local message=\"$2\"\n \n case \"$level\" in\n \"ERROR\")\n echo \"β ERROR: $message\" >&2\n ERRORS=$((ERRORS + 1))\n ;;\n \"WARNING\")\n echo \"β οΈ WARNING: $message\" >&2\n WARNINGS=$((WARNINGS + 1))\n ;;\n \"MISSING\")\n echo \"π MISSING: $message\" >&2\n MISSING_KEYS=$((MISSING_KEYS + 1))\n ;;\n \"ORPHANED\")\n echo \"π·οΈ ORPHANED: $message\" >&2\n ORPHANED_KEYS=$((ORPHANED_KEYS + 1))\n ;;\n \"PASS\")\n echo \"β
PASS: $message\" >&2\n ;;\n \"INFO\")\n echo \"βΉοΈ INFO: $message\" >&2\n ;;\n esac\n }\n \n # Check if file exists and is readable\n if [ ! -f \"$FILE_PATH\" ]; then\n report_validation \"ERROR\" \"Translation file not found: $FILE_PATH\"\n exit 1\n fi\n \n if [ ! -r \"$FILE_PATH\" ]; then\n report_validation \"ERROR\" \"Translation file is not readable: $FILE_PATH\"\n exit 1\n fi\n \n # Determine file format\n FILE_EXT=\"${FILE_PATH##*.}\"\n LOCALE_DIR=\"$(dirname \"$FILE_PATH\")\"\n FILE_NAME=\"$(basename \"$FILE_PATH\")\"\n LOCALE_CODE=\"${FILE_NAME%.*}\"\n \n echo \"π Translation file: $FILE_NAME (format: $FILE_EXT, locale: $LOCALE_CODE)\" >&2\n \n # 1. File Format Validation\n echo \"π Validating file format...\" >&2\n \n case \"$FILE_EXT\" in\n \"json\")\n if jq empty \"$FILE_PATH\" 2>/dev/null; then\n report_validation \"PASS\" \"Valid JSON syntax\"\n TOTAL_KEYS=$(jq -r 'keys | length' \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n else\n report_validation \"ERROR\" \"Invalid JSON syntax - file cannot be parsed\"\n exit 1\n fi\n ;;\n \"po\")\n if command -v msgfmt &> /dev/null; then\n if msgfmt --check \"$FILE_PATH\" -o /dev/null 2>/dev/null; then\n report_validation \"PASS\" \"Valid PO file format\"\n else\n report_validation \"ERROR\" \"Invalid PO file format\"\n fi\n else\n report_validation \"WARNING\" \"msgfmt not available - limited PO validation\"\n fi\n ;;\n \"properties\")\n # Basic properties file validation\n if grep -q '=' \"$FILE_PATH\" 2>/dev/null; then\n report_validation \"PASS\" \"Properties file format detected\"\n else\n report_validation \"WARNING\" \"No key=value pairs found in properties file\"\n fi\n ;;\n *)\n report_validation \"WARNING\" \"Unknown translation file format: $FILE_EXT\"\n ;;\n esac\n \n # 2. Find Base Translation File (for comparison)\n echo \"π Locating base translation file...\" >&2\n \n BASE_FILE=\"\"\n BASE_CANDIDATES=(\"en.json\" \"en-US.json\" \"en_US.json\" \"base.json\" \"default.json\")\n \n for candidate in \"${BASE_CANDIDATES[@]}\"; do\n if [ -f \"$LOCALE_DIR/$candidate\" ] && [ \"$LOCALE_DIR/$candidate\" != \"$FILE_PATH\" ]; then\n BASE_FILE=\"$LOCALE_DIR/$candidate\"\n echo \" π Base file found: $candidate\" >&2\n break\n fi\n done\n \n if [ -z \"$BASE_FILE\" ]; then\n # Look for any .json file in the directory as base\n FIRST_JSON=$(find \"$LOCALE_DIR\" -name '*.json' -not -path \"$FILE_PATH\" | head -1)\n if [ -n \"$FIRST_JSON\" ]; then\n BASE_FILE=\"$FIRST_JSON\"\n echo \" π Using first available JSON as base: $(basename \"$BASE_FILE\")\" >&2\n else\n echo \" β οΈ No base translation file found - running standalone validation\" >&2\n fi\n fi\n \n # 3. Key Structure Validation (JSON files)\n if [ \"$FILE_EXT\" = \"json\" ]; then\n echo \"π Analyzing translation keys...\" >&2\n \n # Check for nested vs flat structure\n NESTED_COUNT=$(jq '[.. | objects | keys] | flatten | length' \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n if [ \"$NESTED_COUNT\" -gt \"$TOTAL_KEYS\" ]; then\n echo \" π Nested key structure detected ($NESTED_COUNT total keys)\" >&2\n else\n echo \" π Flat key structure ($TOTAL_KEYS keys)\" >&2\n fi\n \n # Check for empty values\n EMPTY_VALUES=$(jq '[.. | select(type == \"string\" and length == 0)] | length' \"$FILE_PATH\" 2>/dev/null || echo \"0\")\n if [ \"$EMPTY_VALUES\" -gt 0 ]; then\n report_validation \"WARNING\" \"$EMPTY_VALUES empty translation values found\"\n fi\n \n # Check for untranslated strings (same as key)\n UNTRANSLATED=0\n if command -v jq &> /dev/null; then\n UNTRANSLATED=$(jq -r 'to_entries[] | select(.key == .value) | .key' \"$FILE_PATH\" 2>/dev/null | wc -l | xargs || echo \"0\")\n if [ \"$UNTRANSLATED\" -gt 0 ]; then\n report_validation \"WARNING\" \"$UNTRANSLATED potentially untranslated strings (key equals value)\"\n fi\n fi\n fi\n \n # 4. Compare with Base File (if available)\n if [ -n \"$BASE_FILE\" ] && [ -f \"$BASE_FILE\" ]; then\n echo \"π Comparing with base translation file...\" >&2\n \n if [ \"$FILE_EXT\" = \"json\" ]; then\n # Extract all keys from both files\n BASE_KEYS_FILE=\"/tmp/base_keys_$$\"\n CURRENT_KEYS_FILE=\"/tmp/current_keys_$$\"\n \n # Get all nested keys (dot notation)\n jq -r 'paths(scalars) as $p | $p | join(\".\")' \"$BASE_FILE\" 2>/dev/null | sort > \"$BASE_KEYS_FILE\"\n jq -r 'paths(scalars) as $p | $p | join(\".\")' \"$FILE_PATH\" 2>/dev/null | sort > \"$CURRENT_KEYS_FILE\"\n \n # Find missing keys (in base but not in current)\n MISSING_KEYS_LIST=\"/tmp/missing_keys_$$\"\n comm -23 \"$BASE_KEYS_FILE\" \"$CURRENT_KEYS_FILE\" > \"$MISSING_KEYS_LIST\"\n MISSING_COUNT=$(wc -l < \"$MISSING_KEYS_LIST\" | xargs)\n \n if [ \"$MISSING_COUNT\" -gt 0 ]; then\n report_validation \"MISSING\" \"$MISSING_COUNT translation keys missing from base\"\n echo \" Missing keys:\" >&2\n head -10 \"$MISSING_KEYS_LIST\" | while read key; do\n echo \" - $key\" >&2\n done\n [ \"$MISSING_COUNT\" -gt 10 ] && echo \" ... and $((MISSING_COUNT - 10)) more\" >&2\n else\n report_validation \"PASS\" \"All base translation keys are present\"\n fi\n \n # Find orphaned keys (in current but not in base)\n ORPHANED_KEYS_LIST=\"/tmp/orphaned_keys_$$\"\n comm -13 \"$BASE_KEYS_FILE\" \"$CURRENT_KEYS_FILE\" > \"$ORPHANED_KEYS_LIST\"\n ORPHANED_COUNT=$(wc -l < \"$ORPHANED_KEYS_LIST\" | xargs)\n \n if [ \"$ORPHANED_COUNT\" -gt 0 ]; then\n report_validation \"ORPHANED\" \"$ORPHANED_COUNT keys not found in base (potential orphans)\"\n echo \" Orphaned keys:\" >&2\n head -5 \"$ORPHANED_KEYS_LIST\" | while read key; do\n echo \" - $key\" >&2\n done\n [ \"$ORPHANED_COUNT\" -gt 5 ] && echo \" ... and $((ORPHANED_COUNT - 5)) more\" >&2\n else\n report_validation \"PASS\" \"No orphaned keys detected\"\n fi\n \n # Calculate completeness percentage\n BASE_KEY_COUNT=$(wc -l < \"$BASE_KEYS_FILE\" | xargs)\n if [ \"$BASE_KEY_COUNT\" -gt 0 ]; then\n TRANSLATED_COUNT=$((BASE_KEY_COUNT - MISSING_COUNT))\n COMPLETENESS=$((TRANSLATED_COUNT * 100 / BASE_KEY_COUNT))\n echo \" π Translation completeness: $COMPLETENESS% ($TRANSLATED_COUNT/$BASE_KEY_COUNT)\" >&2\n \n if [ \"$COMPLETENESS\" -eq 100 ]; then\n report_validation \"PASS\" \"Translation is 100% complete\"\n elif [ \"$COMPLETENESS\" -ge 90 ]; then\n report_validation \"WARNING\" \"Translation is $COMPLETENESS% complete (good but not perfect)\"\n elif [ \"$COMPLETENESS\" -ge 70 ]; then\n report_validation \"WARNING\" \"Translation is $COMPLETENESS% complete (needs attention)\"\n else\n report_validation \"ERROR\" \"Translation is only $COMPLETENESS% complete (significant gaps)\"\n fi\n fi\n \n # Cleanup temp files\n rm -f \"$BASE_KEYS_FILE\" \"$CURRENT_KEYS_FILE\" \"$MISSING_KEYS_LIST\" \"$ORPHANED_KEYS_LIST\"\n fi\n fi\n \n # 5. Variable Placeholder Validation\n echo \"π€ Checking variable placeholders...\" >&2\n \n if [ \"$FILE_EXT\" = \"json\" ]; then\n # Check for common placeholder patterns\n PLACEHOLDER_PATTERNS=(\n '{{[^}]+}}' # Handlebars: {{variable}}\n '{[^}]+}' # Simple: {variable}\n '%[a-zA-Z_]+%' # Percent: %variable%\n '\\$\\{[^}]+\\}' # Dollar: ${variable}\n '%[sd]' # Printf style: %s, %d\n )\n \n PLACEHOLDER_COUNT=0\n for pattern in \"${PLACEHOLDER_PATTERNS[@]}\"; do\n COUNT=$(grep -oE \"$pattern\" \"$FILE_PATH\" 2>/dev/null | wc -l | xargs || echo \"0\")\n PLACEHOLDER_COUNT=$((PLACEHOLDER_COUNT + COUNT))\n done\n \n if [ \"$PLACEHOLDER_COUNT\" -gt 0 ]; then\n echo \" π $PLACEHOLDER_COUNT variable placeholders found\" >&2\n \n # Check for unmatched placeholders if base file exists\n if [ -n \"$BASE_FILE\" ]; then\n # This is a simplified check - in practice, you'd want more sophisticated matching\n echo \" π Cross-referencing placeholders with base file...\" >&2\n fi\n else\n echo \" βΉοΈ No variable placeholders detected\" >&2\n fi\n fi\n \n # 6. Locale-Specific Validation\n echo \"π Locale-specific validation...\" >&2\n \n case \"$LOCALE_CODE\" in\n \"ar\"*|\"he\"*|\"fa\"*)\n echo \" π RTL language detected - ensure proper text direction handling\" >&2\n ;;\n \"zh\"*|\"ja\"*|\"ko\"*)\n echo \" π΄ CJK language detected - ensure proper character encoding\" >&2\n ;;\n \"en\"*)\n echo \" πΊπΈ English locale - checking for common issues\" >&2\n ;;\n *)\n echo \" π Locale: $LOCALE_CODE\" >&2\n ;;\n esac\n \n # Check for potential encoding issues (non-ASCII characters)\n if [ \"$FILE_EXT\" = \"json\" ]; then\n NON_ASCII_COUNT=$(grep -P '[^\\x00-\\x7F]' \"$FILE_PATH\" 2>/dev/null | wc -l | xargs || echo \"0\")\n if [ \"$NON_ASCII_COUNT\" -gt 0 ]; then\n echo \" π€ $NON_ASCII_COUNT lines contain non-ASCII characters (normal for international content)\" >&2\n fi\n fi\n \n # 7. Multi-file Consistency Check\n echo \"π Checking consistency across locale files...\" >&2\n \n LOCALE_FILES=()\n while IFS= read -r -d '' file; do\n LOCALE_FILES+=(\"$file\")\n done < <(find \"$LOCALE_DIR\" -name \"*.$FILE_EXT\" -print0 2>/dev/null)\n \n LOCALE_COUNT=${#LOCALE_FILES[@]}\n if [ \"$LOCALE_COUNT\" -gt 1 ]; then\n echo \" π Found $LOCALE_COUNT locale files in directory\" >&2\n \n # Check if all files have similar key counts (within 20% difference)\n if [ \"$FILE_EXT\" = \"json\" ] && [ \"$TOTAL_KEYS\" -gt 0 ]; then\n INCONSISTENT_FILES=0\n for locale_file in \"${LOCALE_FILES[@]}\"; do\n if [ \"$locale_file\" != \"$FILE_PATH\" ]; then\n OTHER_KEY_COUNT=$(jq -r 'keys | length' \"$locale_file\" 2>/dev/null || echo \"0\")\n DIFF_PERCENT=$((abs(TOTAL_KEYS - OTHER_KEY_COUNT) * 100 / TOTAL_KEYS))\n \n if [ \"$DIFF_PERCENT\" -gt 20 ]; then\n INCONSISTENT_FILES=$((INCONSISTENT_FILES + 1))\n fi\n fi\n done\n \n if [ \"$INCONSISTENT_FILES\" -gt 0 ]; then\n report_validation \"WARNING\" \"$INCONSISTENT_FILES locale files have significantly different key counts\"\n else\n report_validation \"PASS\" \"All locale files have consistent key counts\"\n fi\n fi\n else\n echo \" βΉοΈ Single locale file found\" >&2\n fi\n \n # 8. Generate Validation Summary\n echo \"\" >&2\n echo \"π i18n Translation Validation Summary:\" >&2\n echo \"=====================================\" >&2\n echo \" π File: $FILE_NAME\" >&2\n echo \" π Locale: $LOCALE_CODE\" >&2\n echo \" π Format: $FILE_EXT\" >&2\n [ \"$TOTAL_KEYS\" -gt 0 ] && echo \" π Total Keys: $TOTAL_KEYS\" >&2\n echo \" β Errors: $ERRORS\" >&2\n echo \" β οΈ Warnings: $WARNINGS\" >&2\n echo \" π Missing Keys: $MISSING_KEYS\" >&2\n echo \" π·οΈ Orphaned Keys: $ORPHANED_KEYS\" >&2\n \n if [ \"$ERRORS\" -eq 0 ] && [ \"$MISSING_KEYS\" -eq 0 ]; then\n if [ \"$WARNINGS\" -eq 0 ] && [ \"$ORPHANED_KEYS\" -eq 0 ]; then\n echo \" π Status: EXCELLENT - Translation file is complete and consistent\" >&2\n else\n echo \" β
Status: GOOD - Translation is functional with minor issues\" >&2\n fi\n elif [ \"$ERRORS\" -eq 0 ]; then\n echo \" β οΈ Status: INCOMPLETE - Missing translations need attention\" >&2\n else\n echo \" β Status: ERRORS - Critical issues must be fixed\" >&2\n fi\n \n echo \"\" >&2\n echo \"π‘ i18n Translation Best Practices:\" >&2\n echo \" β’ Keep translation keys consistent across all locales\" >&2\n echo \" β’ Use meaningful, hierarchical key names\" >&2\n echo \" β’ Validate placeholder variables across languages\" >&2\n echo \" β’ Consider cultural context in translations\" >&2\n echo \" β’ Test with longer/shorter text in different languages\" >&2\n echo \" β’ Use proper character encoding (UTF-8)\" >&2\n \n # Exit with error if there are critical issues\n if [ \"$ERRORS\" -gt 0 ]; then\n echo \"β οΈ Translation validation completed with errors\" >&2\n exit 1\n fi\n \nelse\n # Not a translation file, exit silently\n exit 0\nfi\n\nexit 0"
}.claude/hooks/~/.claude/hooks/{
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/i18n-translation-validator.sh",
"matchers": [
"write",
"edit"
]
}
}
}#!/usr/bin/env bash
# Read the tool input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Check if this is a translation/localization file
if [[ "$FILE_PATH" == *locales/*.json ]] || [[ "$FILE_PATH" == *i18n/*.json ]] || [[ "$FILE_PATH" == *lang/*.json ]] || [[ "$FILE_PATH" == *translations/*.json ]] || [[ "$FILE_PATH" == *.po ]] || [[ "$FILE_PATH" == *messages/*.properties ]]; then
echo "π i18n Translation Validation for: $(basename "$FILE_PATH")" >&2
# Initialize validation counters
ERRORS=0
WARNINGS=0
MISSING_KEYS=0
ORPHANED_KEYS=0
TOTAL_KEYS=0
# Function to report validation results
report_validation() {
local level="$1"
local message="$2"
case "$level" in
"ERROR")
echo "β ERROR: $message" >&2
ERRORS=$((ERRORS + 1))
;;
"WARNING")
echo "β οΈ WARNING: $message" >&2
WARNINGS=$((WARNINGS + 1))
;;
"MISSING")
echo "π MISSING: $message" >&2
MISSING_KEYS=$((MISSING_KEYS + 1))
;;
"ORPHANED")
echo "π·οΈ ORPHANED: $message" >&2
ORPHANED_KEYS=$((ORPHANED_KEYS + 1))
;;
"PASS")
echo "β
PASS: $message" >&2
;;
"INFO")
echo "βΉοΈ INFO: $message" >&2
;;
esac
}
# Check if file exists and is readable
if [ ! -f "$FILE_PATH" ]; then
report_validation "ERROR" "Translation file not found: $FILE_PATH"
exit 1
fi
if [ ! -r "$FILE_PATH" ]; then
report_validation "ERROR" "Translation file is not readable: $FILE_PATH"
exit 1
fi
# Determine file format
FILE_EXT="${FILE_PATH##*.}"
LOCALE_DIR="$(dirname "$FILE_PATH")"
FILE_NAME="$(basename "$FILE_PATH")"
LOCALE_CODE="${FILE_NAME%.*}"
echo "π Translation file: $FILE_NAME (format: $FILE_EXT, locale: $LOCALE_CODE)" >&2
# 1. File Format Validation
echo "π Validating file format..." >&2
case "$FILE_EXT" in
"json")
if jq empty "$FILE_PATH" 2>/dev/null; then
report_validation "PASS" "Valid JSON syntax"
TOTAL_KEYS=$(jq -r 'keys | length' "$FILE_PATH" 2>/dev/null || echo "0")
else
report_validation "ERROR" "Invalid JSON syntax - file cannot be parsed"
exit 1
fi
;;
"po")
if command -v msgfmt &> /dev/null; then
if msgfmt --check "$FILE_PATH" -o /dev/null 2>/dev/null; then
report_validation "PASS" "Valid PO file format"
else
report_validation "ERROR" "Invalid PO file format"
fi
else
report_validation "WARNING" "msgfmt not available - limited PO validation"
fi
;;
"properties")
# Basic properties file validation
if grep -q '=' "$FILE_PATH" 2>/dev/null; then
report_validation "PASS" "Properties file format detected"
else
report_validation "WARNING" "No key=value pairs found in properties file"
fi
;;
*)
report_validation "WARNING" "Unknown translation file format: $FILE_EXT"
;;
esac
# 2. Find Base Translation File (for comparison)
echo "π Locating base translation file..." >&2
BASE_FILE=""
BASE_CANDIDATES=("en.json" "en-US.json" "en_US.json" "base.json" "default.json")
for candidate in "${BASE_CANDIDATES[@]}"; do
if [ -f "$LOCALE_DIR/$candidate" ] && [ "$LOCALE_DIR/$candidate" != "$FILE_PATH" ]; then
BASE_FILE="$LOCALE_DIR/$candidate"
echo " π Base file found: $candidate" >&2
break
fi
done
if [ -z "$BASE_FILE" ]; then
# Look for any .json file in the directory as base
FIRST_JSON=$(find "$LOCALE_DIR" -name '*.json' -not -path "$FILE_PATH" | head -1)
if [ -n "$FIRST_JSON" ]; then
BASE_FILE="$FIRST_JSON"
echo " π Using first available JSON as base: $(basename "$BASE_FILE")" >&2
else
echo " β οΈ No base translation file found - running standalone validation" >&2
fi
fi
# 3. Key Structure Validation (JSON files)
if [ "$FILE_EXT" = "json" ]; then
echo "π Analyzing translation keys..." >&2
# Check for nested vs flat structure
NESTED_COUNT=$(jq '[.. | objects | keys] | flatten | length' "$FILE_PATH" 2>/dev/null || echo "0")
if [ "$NESTED_COUNT" -gt "$TOTAL_KEYS" ]; then
echo " π Nested key structure detected ($NESTED_COUNT total keys)" >&2
else
echo " π Flat key structure ($TOTAL_KEYS keys)" >&2
fi
# Check for empty values
EMPTY_VALUES=$(jq '[.. | select(type == "string" and length == 0)] | length' "$FILE_PATH" 2>/dev/null || echo "0")
if [ "$EMPTY_VALUES" -gt 0 ]; then
report_validation "WARNING" "$EMPTY_VALUES empty translation values found"
fi
# Check for untranslated strings (same as key)
UNTRANSLATED=0
if command -v jq &> /dev/null; then
UNTRANSLATED=$(jq -r 'to_entries[] | select(.key == .value) | .key' "$FILE_PATH" 2>/dev/null | wc -l | xargs || echo "0")
if [ "$UNTRANSLATED" -gt 0 ]; then
report_validation "WARNING" "$UNTRANSLATED potentially untranslated strings (key equals value)"
fi
fi
fi
# 4. Compare with Base File (if available)
if [ -n "$BASE_FILE" ] && [ -f "$BASE_FILE" ]; then
echo "π Comparing with base translation file..." >&2
if [ "$FILE_EXT" = "json" ]; then
# Extract all keys from both files
BASE_KEYS_FILE="/tmp/base_keys_$$"
CURRENT_KEYS_FILE="/tmp/current_keys_$$"
# Get all nested keys (dot notation)
jq -r 'paths(scalars) as $p | $p | join(".")' "$BASE_FILE" 2>/dev/null | sort > "$BASE_KEYS_FILE"
jq -r 'paths(scalars) as $p | $p | join(".")' "$FILE_PATH" 2>/dev/null | sort > "$CURRENT_KEYS_FILE"
# Find missing keys (in base but not in current)
MISSING_KEYS_LIST="/tmp/missing_keys_$$"
comm -23 "$BASE_KEYS_FILE" "$CURRENT_KEYS_FILE" > "$MISSING_KEYS_LIST"
MISSING_COUNT=$(wc -l < "$MISSING_KEYS_LIST" | xargs)
if [ "$MISSING_COUNT" -gt 0 ]; then
report_validation "MISSING" "$MISSING_COUNT translation keys missing from base"
echo " Missing keys:" >&2
head -10 "$MISSING_KEYS_LIST" | while read key; do
echo " - $key" >&2
done
[ "$MISSING_COUNT" -gt 10 ] && echo " ... and $((MISSING_COUNT - 10)) more" >&2
else
report_validation "PASS" "All base translation keys are present"
fi
# Find orphaned keys (in current but not in base)
ORPHANED_KEYS_LIST="/tmp/orphaned_keys_$$"
comm -13 "$BASE_KEYS_FILE" "$CURRENT_KEYS_FILE" > "$ORPHANED_KEYS_LIST"
ORPHANED_COUNT=$(wc -l < "$ORPHANED_KEYS_LIST" | xargs)
if [ "$ORPHANED_COUNT" -gt 0 ]; then
report_validation "ORPHANED" "$ORPHANED_COUNT keys not found in base (potential orphans)"
echo " Orphaned keys:" >&2
head -5 "$ORPHANED_KEYS_LIST" | while read key; do
echo " - $key" >&2
done
[ "$ORPHANED_COUNT" -gt 5 ] && echo " ... and $((ORPHANED_COUNT - 5)) more" >&2
else
report_validation "PASS" "No orphaned keys detected"
fi
# Calculate completeness percentage
BASE_KEY_COUNT=$(wc -l < "$BASE_KEYS_FILE" | xargs)
if [ "$BASE_KEY_COUNT" -gt 0 ]; then
TRANSLATED_COUNT=$((BASE_KEY_COUNT - MISSING_COUNT))
COMPLETENESS=$((TRANSLATED_COUNT * 100 / BASE_KEY_COUNT))
echo " π Translation completeness: $COMPLETENESS% ($TRANSLATED_COUNT/$BASE_KEY_COUNT)" >&2
if [ "$COMPLETENESS" -eq 100 ]; then
report_validation "PASS" "Translation is 100% complete"
elif [ "$COMPLETENESS" -ge 90 ]; then
report_validation "WARNING" "Translation is $COMPLETENESS% complete (good but not perfect)"
elif [ "$COMPLETENESS" -ge 70 ]; then
report_validation "WARNING" "Translation is $COMPLETENESS% complete (needs attention)"
else
report_validation "ERROR" "Translation is only $COMPLETENESS% complete (significant gaps)"
fi
fi
# Cleanup temp files
rm -f "$BASE_KEYS_FILE" "$CURRENT_KEYS_FILE" "$MISSING_KEYS_LIST" "$ORPHANED_KEYS_LIST"
fi
fi
# 5. Variable Placeholder Validation
echo "π€ Checking variable placeholders..." >&2
if [ "$FILE_EXT" = "json" ]; then
# Check for common placeholder patterns
PLACEHOLDER_PATTERNS=(
'{{[^}]+}}' # Handlebars: {{variable}}
'{[^}]+}' # Simple: {variable}
'%[a-zA-Z_]+%' # Percent: %variable%
'\$\{[^}]+\}' # Dollar: ${variable}
'%[sd]' # Printf style: %s, %d
)
PLACEHOLDER_COUNT=0
for pattern in "${PLACEHOLDER_PATTERNS[@]}"; do
COUNT=$(grep -oE "$pattern" "$FILE_PATH" 2>/dev/null | wc -l | xargs || echo "0")
PLACEHOLDER_COUNT=$((PLACEHOLDER_COUNT + COUNT))
done
if [ "$PLACEHOLDER_COUNT" -gt 0 ]; then
echo " π $PLACEHOLDER_COUNT variable placeholders found" >&2
# Check for unmatched placeholders if base file exists
if [ -n "$BASE_FILE" ]; then
# This is a simplified check - in practice, you'd want more sophisticated matching
echo " π Cross-referencing placeholders with base file..." >&2
fi
else
echo " βΉοΈ No variable placeholders detected" >&2
fi
fi
# 6. Locale-Specific Validation
echo "π Locale-specific validation..." >&2
case "$LOCALE_CODE" in
"ar"*|"he"*|"fa"*)
echo " π RTL language detected - ensure proper text direction handling" >&2
;;
"zh"*|"ja"*|"ko"*)
echo " π΄ CJK language detected - ensure proper character encoding" >&2
;;
"en"*)
echo " πΊπΈ English locale - checking for common issues" >&2
;;
*)
echo " π Locale: $LOCALE_CODE" >&2
;;
esac
# Check for potential encoding issues (non-ASCII characters)
if [ "$FILE_EXT" = "json" ]; then
NON_ASCII_COUNT=$(grep -P '[^\x00-\x7F]' "$FILE_PATH" 2>/dev/null | wc -l | xargs || echo "0")
if [ "$NON_ASCII_COUNT" -gt 0 ]; then
echo " π€ $NON_ASCII_COUNT lines contain non-ASCII characters (normal for international content)" >&2
fi
fi
# 7. Multi-file Consistency Check
echo "π Checking consistency across locale files..." >&2
LOCALE_FILES=()
while IFS= read -r -d '' file; do
LOCALE_FILES+=("$file")
done < <(find "$LOCALE_DIR" -name "*.$FILE_EXT" -print0 2>/dev/null)
LOCALE_COUNT=${#LOCALE_FILES[@]}
if [ "$LOCALE_COUNT" -gt 1 ]; then
echo " π Found $LOCALE_COUNT locale files in directory" >&2
# Check if all files have similar key counts (within 20% difference)
if [ "$FILE_EXT" = "json" ] && [ "$TOTAL_KEYS" -gt 0 ]; then
INCONSISTENT_FILES=0
for locale_file in "${LOCALE_FILES[@]}"; do
if [ "$locale_file" != "$FILE_PATH" ]; then
OTHER_KEY_COUNT=$(jq -r 'keys | length' "$locale_file" 2>/dev/null || echo "0")
DIFF_PERCENT=$((abs(TOTAL_KEYS - OTHER_KEY_COUNT) * 100 / TOTAL_KEYS))
if [ "$DIFF_PERCENT" -gt 20 ]; then
INCONSISTENT_FILES=$((INCONSISTENT_FILES + 1))
fi
fi
done
if [ "$INCONSISTENT_FILES" -gt 0 ]; then
report_validation "WARNING" "$INCONSISTENT_FILES locale files have significantly different key counts"
else
report_validation "PASS" "All locale files have consistent key counts"
fi
fi
else
echo " βΉοΈ Single locale file found" >&2
fi
# 8. Generate Validation Summary
echo "" >&2
echo "π i18n Translation Validation Summary:" >&2
echo "=====================================" >&2
echo " π File: $FILE_NAME" >&2
echo " π Locale: $LOCALE_CODE" >&2
echo " π Format: $FILE_EXT" >&2
[ "$TOTAL_KEYS" -gt 0 ] && echo " π Total Keys: $TOTAL_KEYS" >&2
echo " β Errors: $ERRORS" >&2
echo " β οΈ Warnings: $WARNINGS" >&2
echo " π Missing Keys: $MISSING_KEYS" >&2
echo " π·οΈ Orphaned Keys: $ORPHANED_KEYS" >&2
if [ "$ERRORS" -eq 0 ] && [ "$MISSING_KEYS" -eq 0 ]; then
if [ "$WARNINGS" -eq 0 ] && [ "$ORPHANED_KEYS" -eq 0 ]; then
echo " π Status: EXCELLENT - Translation file is complete and consistent" >&2
else
echo " β
Status: GOOD - Translation is functional with minor issues" >&2
fi
elif [ "$ERRORS" -eq 0 ]; then
echo " β οΈ Status: INCOMPLETE - Missing translations need attention" >&2
else
echo " β Status: ERRORS - Critical issues must be fixed" >&2
fi
echo "" >&2
echo "π‘ i18n Translation Best Practices:" >&2
echo " β’ Keep translation keys consistent across all locales" >&2
echo " β’ Use meaningful, hierarchical key names" >&2
echo " β’ Validate placeholder variables across languages" >&2
echo " β’ Consider cultural context in translations" >&2
echo " β’ Test with longer/shorter text in different languages" >&2
echo " β’ Use proper character encoding (UTF-8)" >&2
# Exit with error if there are critical issues
if [ "$ERRORS" -gt 0 ]; then
echo "β οΈ Translation validation completed with errors" >&2
exit 1
fi
else
# Not a translation file, exit silently
exit 0
fi
exit 0Hook validates non-i18n JSON files in directories with similar names
Strengthen path detection: [[ "$FILE_PATH" =~ /(locales|i18n|lang|translations)/ ]] || exit 0. Check for translation-specific keys like locale/language markers before running full validation.
Missing keys detection fails when base file uses nested structure
Use jq to flatten nested keys: jq -r 'paths(scalars) as $p | $p | join(".")' to get dot notation paths. Compare flattened key lists instead of top-level keys only for accurate missing key detection.
Validation shows false positives for orphaned keys in locale-specific translations
Some translations legitimately have locale-specific keys (e.g., currency formats, date patterns). Add whitelist patterns or check key prefixes like 'locale.' to skip cultural adaptation keys from orphan detection.
Completeness percentage incorrectly calculated for multi-level nested JSON
Script uses top-level key count but should count leaf nodes: jq '[paths(scalars)] | length'. Ensure both base and current files are counted at same nesting level for accurate percentage.
Hook crashes with 'command not found: abs' on DIFF_PERCENT calculation
Bash doesn't have abs() function. Replace with: DIFF_PERCENT=$(( (TOTAL_KEYS - OTHER_KEY_COUNT) < 0 ? (OTHER_KEY_COUNT - TOTAL_KEYS) : (TOTAL_KEYS - OTHER_KEY_COUNT) )) or use bc: echo "scale=0; sqrt(($A-$B)^2)" | bc.
Loading reviews...
Join our community of Claude power users. No spam, unsubscribe anytime.
Automated accessibility testing and compliance checking for web applications following WCAG guidelines
Automatically generates or updates API documentation when endpoint files are modified
Automatically formats code files after Claude writes or edits them using Prettier, Black, or other formatters