website-htmx-rustelo-code/scripts/sh/content/sync-translations.sh
2026-07-10 03:44:13 +01:00

685 lines
24 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Content synchronization script for localized blog and recipe content
# Synchronizes content structure and metadata across languages while preserving translations
set -e
# Configuration
CONTENT_DIR="content"
SUPPORTED_LANGUAGES=("en" "es")
DEFAULT_LANGUAGE="en"
BACKUP_DIR="content_backup_$(date +%Y%m%d_%H%M%S)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
# Helper functions
log_error() {
echo -e "${RED}❌ ERROR: $1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ WARNING: $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_info() {
echo -e "${BLUE} $1${NC}"
}
log_step() {
echo -e "${CYAN}🔧 $1${NC}"
}
log_sync() {
echo -e "${PURPLE}🔄 $1${NC}"
}
# Display usage information
show_usage() {
echo -e "${BLUE}🔄 Content Synchronization Tool${NC}"
echo "=================================="
echo ""
echo "Usage: $0 [COMMAND] [OPTIONS]"
echo ""
echo "Commands:"
echo " sync-structure Synchronize content structure across languages"
echo " sync-metadata Synchronize metadata (keeping translations intact)"
echo " check-missing Check for missing translations"
echo " generate-missing Generate placeholder content for missing translations"
echo " backup Create backup of current content"
echo " restore Restore from backup"
echo " validate Validate content synchronization"
echo " help Show this help message"
echo ""
echo "Options:"
echo " --source-lang Source language for synchronization (default: en)"
echo " --target-lang Target language (default: all supported languages)"
echo " --content-type Content type to sync (blog/recipes/all, default: all)"
echo " --dry-run Show what would be done without making changes"
echo " --force Force overwrite existing files"
echo " --backup Create backup before changes"
echo ""
echo "Examples:"
echo " $0 sync-structure --source-lang en --target-lang es"
echo " $0 check-missing --content-type blog"
echo " $0 generate-missing --dry-run"
echo " $0 backup"
echo ""
}
# Parse command line arguments
parse_args() {
SOURCE_LANG="$DEFAULT_LANGUAGE"
TARGET_LANG="all"
CONTENT_TYPE="all"
DRY_RUN=false
FORCE=false
CREATE_BACKUP=false
while [[ $# -gt 0 ]]; do
case $1 in
--source-lang)
SOURCE_LANG="$2"
shift 2
;;
--target-lang)
TARGET_LANG="$2"
shift 2
;;
--content-type)
CONTENT_TYPE="$2"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
--force)
FORCE=true
shift
;;
--backup)
CREATE_BACKUP=true
shift
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
}
# Check dependencies
check_dependencies() {
if ! command -v jq >/dev/null 2>&1; then
log_error "jq is required but not installed. Please install jq to continue."
exit 1
fi
log_success "Dependencies check passed"
}
# Create backup
create_backup() {
log_step "Creating backup of content..."
if [[ -d "$CONTENT_DIR" ]]; then
cp -r "$CONTENT_DIR" "$BACKUP_DIR"
log_success "Backup created: $BACKUP_DIR"
else
log_warning "Content directory not found, no backup created"
fi
}
# Restore from backup
restore_backup() {
local backup_path="$1"
if [[ -z "$backup_path" ]]; then
# Find most recent backup
backup_path=$(ls -1d content_backup_* 2>/dev/null | sort | tail -1)
if [[ -z "$backup_path" ]]; then
log_error "No backup found"
exit 1
fi
fi
if [[ ! -d "$backup_path" ]]; then
log_error "Backup directory not found: $backup_path"
exit 1
fi
log_step "Restoring from backup: $backup_path"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would restore content from $backup_path"
return
fi
if [[ -d "$CONTENT_DIR" ]]; then
rm -rf "${CONTENT_DIR}.old" 2>/dev/null || true
mv "$CONTENT_DIR" "${CONTENT_DIR}.old"
fi
cp -r "$backup_path" "$CONTENT_DIR"
log_success "Content restored from backup"
}
# Get content indices for a language
get_content_indices() {
local lang="$1"
local content_type="$2"
case "$content_type" in
"blog")
echo "${CONTENT_DIR}/blog/${lang}/index.json"
;;
"recipes")
echo "${CONTENT_DIR}/recipes/${lang}/index.json"
;;
*)
echo "${CONTENT_DIR}/blog/${lang}/index.json ${CONTENT_DIR}/recipes/${lang}/index.json"
;;
esac
}
# Get target languages
get_target_languages() {
if [[ "$TARGET_LANG" == "all" ]]; then
echo "${SUPPORTED_LANGUAGES[@]}"
else
echo "$TARGET_LANG"
fi
}
# Check for missing content
check_missing_content() {
log_step "Checking for missing translations..."
local missing_count=0
local source_indices
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "blog" ]]; then
local source_blog="${CONTENT_DIR}/blog/${SOURCE_LANG}/index.json"
if [[ -f "$source_blog" ]]; then
log_info "Checking blog translations..."
local blog_ids=$(jq -r '.blog[].id' "$source_blog" 2>/dev/null || echo "")
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_blog="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ ! -f "$target_blog" ]]; then
log_warning "Missing blog index for language: $lang"
((missing_count++))
continue
fi
while IFS= read -r id; do
if [[ -n "$id" && "$id" != "null" ]]; then
local exists=$(jq -r ".blog[] | select(.id == \"$id\") | .id" "$target_blog" 2>/dev/null || echo "")
if [[ -z "$exists" ]]; then
log_warning "Missing blog post translation: '$id' in language '$lang'"
((missing_count++))
fi
fi
done <<< "$blog_ids"
done
fi
fi
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "recipes" ]]; then
local source_recipes="${CONTENT_DIR}/recipes/${SOURCE_LANG}/index.json"
if [[ -f "$source_recipes" ]]; then
log_info "Checking recipe translations..."
local recipe_ids=$(jq -r '.recipes[].id' "$source_recipes" 2>/dev/null || echo "")
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_recipes="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ ! -f "$target_recipes" ]]; then
log_warning "Missing recipe index for language: $lang"
((missing_count++))
continue
fi
while IFS= read -r id; do
if [[ -n "$id" && "$id" != "null" ]]; then
local exists=$(jq -r ".recipes[] | select(.id == \"$id\") | .id" "$target_recipes" 2>/dev/null || echo "")
if [[ -z "$exists" ]]; then
log_warning "Missing recipe translation: '$id' in language '$lang'"
((missing_count++))
fi
fi
done <<< "$recipe_ids"
done
fi
fi
if [[ $missing_count -eq 0 ]]; then
log_success "No missing translations found!"
else
log_warning "Found $missing_count missing translations"
fi
return $missing_count
}
# Synchronize content structure
sync_content_structure() {
log_step "Synchronizing content structure from $SOURCE_LANG..."
if [[ "$CREATE_BACKUP" == "true" ]]; then
create_backup
fi
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
log_sync "Synchronizing structure for language: $lang"
# Sync blog structure
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "blog" ]]; then
local source_blog="${CONTENT_DIR}/blog/${SOURCE_LANG}/index.json"
local target_blog_dir="${CONTENT_DIR}/blog/${lang}"
local target_blog="${target_blog_dir}/index.json"
if [[ -f "$source_blog" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would sync blog structure for $lang"
else
mkdir -p "$target_blog_dir"
if [[ ! -f "$target_blog" ]]; then
# Create new index with proper language
jq --arg lang "$lang" '.language = $lang | .blog = []' "$source_blog" > "$target_blog"
log_success "Created blog index for $lang"
else
log_info "Blog index already exists for $lang"
fi
fi
fi
fi
# Sync recipe structure
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "recipes" ]]; then
local source_recipes="${CONTENT_DIR}/recipes/${SOURCE_LANG}/index.json"
local target_recipes_dir="${CONTENT_DIR}/recipes/${lang}"
local target_recipes="${target_recipes_dir}/index.json"
if [[ -f "$source_recipes" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would sync recipe structure for $lang"
else
mkdir -p "$target_recipes_dir"
if [[ ! -f "$target_recipes" ]]; then
# Create new index with proper language
jq --arg lang "$lang" '.language = $lang | .recipes = []' "$source_recipes" > "$target_recipes"
log_success "Created recipe index for $lang"
else
log_info "Recipe index already exists for $lang"
fi
fi
fi
fi
done
log_success "Content structure synchronization completed"
}
# Generate missing translations as placeholders
generate_missing_translations() {
log_step "Generating placeholder content for missing translations..."
if [[ "$CREATE_BACKUP" == "true" ]]; then
create_backup
fi
# Generate missing blog posts
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "blog" ]]; then
local source_blog="${CONTENT_DIR}/blog/${SOURCE_LANG}/index.json"
if [[ -f "$source_blog" ]]; then
local blog=$(jq -c '.blog[]' "$source_blog")
while IFS= read -r post; do
local post_id=$(echo "$post" | jq -r '.id')
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_blog="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ -f "$target_blog" ]]; then
local exists=$(jq -r ".blog[] | select(.id == \"$post_id\") | .id" "$target_blog" 2>/dev/null || echo "")
if [[ -z "$exists" ]]; then
log_sync "Generating placeholder blog post: $post_id ($lang)"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would create placeholder for blog post '$post_id' in $lang"
else
# Create placeholder post with translated metadata
local placeholder=$(echo "$post" | jq --arg lang "$lang" '
.language = $lang |
.title = .title + " [NEEDS TRANSLATION]" |
.excerpt = .excerpt + " [This content needs to be translated to " + $lang + "]" |
.source_file = "content/blog/" + $lang + "/" + .id + ".md" |
.html_file = "/blog/" + $lang + "/" + .id + ".html" |
.published = false
')
# Add to target index
local temp_file="/tmp/blog_sync_${lang}.json"
jq --argjson new_post "$placeholder" '.blog += [$new_post]' "$target_blog" > "$temp_file"
mv "$temp_file" "$target_blog"
log_success "Created placeholder blog post: $post_id ($lang)"
fi
fi
fi
done
done <<< "$blog"
fi
fi
# Generate missing recipes
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "recipes" ]]; then
local source_recipes="${CONTENT_DIR}/recipes/${SOURCE_LANG}/index.json"
if [[ -f "$source_recipes" ]]; then
local recipes=$(jq -c '.recipes[]' "$source_recipes")
while IFS= read -r recipe; do
local recipe_id=$(echo "$recipe" | jq -r '.id')
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_recipes="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ -f "$target_recipes" ]]; then
local exists=$(jq -r ".recipes[] | select(.id == \"$recipe_id\") | .id" "$target_recipes" 2>/dev/null || echo "")
if [[ -z "$exists" ]]; then
log_sync "Generating placeholder recipe: $recipe_id ($lang)"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would create placeholder for recipe '$recipe_id' in $lang"
else
# Create placeholder recipe with translated metadata
local placeholder=$(echo "$recipe" | jq --arg lang "$lang" '
.language = $lang |
.title = .title + " [NEEDS TRANSLATION]" |
.excerpt = .excerpt + " [This content needs to be translated to " + $lang + "]" |
.source_file = "content/recipes/" + $lang + "/" + .id + ".md" |
.html_file = "/recipes/" + $lang + "/" + .id + ".html"
')
# Add to target index
local temp_file="/tmp/recipe_sync_${lang}.json"
jq --argjson new_recipe "$placeholder" '.recipes += [$new_recipe]' "$target_recipes" > "$temp_file"
mv "$temp_file" "$target_recipes"
log_success "Created placeholder recipe: $recipe_id ($lang)"
fi
fi
fi
done
done <<< "$recipes"
fi
fi
log_success "Placeholder generation completed"
}
# Synchronize metadata while preserving translations
sync_metadata() {
log_step "Synchronizing metadata from $SOURCE_LANG while preserving translations..."
if [[ "$CREATE_BACKUP" == "true" ]]; then
create_backup
fi
# Sync blog metadata
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "blog" ]]; then
local source_blog="${CONTENT_DIR}/blog/${SOURCE_LANG}/index.json"
if [[ -f "$source_blog" ]]; then
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_blog="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ -f "$target_blog" ]]; then
log_sync "Synchronizing blog metadata for: $lang"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would sync blog metadata for $lang"
else
# Preserve existing translations while updating metadata
local temp_file="/tmp/blog_meta_sync_${lang}.json"
jq --slurpfile source "$source_blog" --arg lang "$lang" '
.language = $lang |
.blog = [
.blog[] as $target |
($source[0].blog[] | select(.id == $target.id)) as $source_post |
if $source_post then
$target |
.author = $source_post.author |
.date = $source_post.date |
.read_time = $source_post.read_time |
.tags = $source_post.tags |
.featured = $source_post.featured |
.published = $source_post.published |
.source_file = ("content/blog/" + $lang + "/" + .id + ".md") |
.html_file = ("/blog/" + $lang + "/" + .id + ".html")
else
$target
end
]
' "$target_blog" > "$temp_file"
mv "$temp_file" "$target_blog"
log_success "Synchronized blog metadata for: $lang"
fi
fi
done
fi
fi
# Sync recipe metadata
if [[ "$CONTENT_TYPE" == "all" ]] || [[ "$CONTENT_TYPE" == "recipes" ]]; then
local source_recipes="${CONTENT_DIR}/recipes/${SOURCE_LANG}/index.json"
if [[ -f "$source_recipes" ]]; then
for lang in $(get_target_languages); do
if [[ "$lang" == "$SOURCE_LANG" ]]; then
continue
fi
local target_recipes="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ -f "$target_recipes" ]]; then
log_sync "Synchronizing recipe metadata for: $lang"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would sync recipe metadata for $lang"
else
# Preserve existing translations while updating metadata
local temp_file="/tmp/recipe_meta_sync_${lang}.json"
jq --slurpfile source "$source_recipes" --arg lang "$lang" '
.language = $lang |
.recipes = [
.recipes[] as $target |
($source[0].recipes[] | select(.id == $target.id)) as $source_recipe |
if $source_recipe then
$target |
.category = $source_recipe.category |
.difficulty = $source_recipe.difficulty |
.prep_time = $source_recipe.prep_time |
.tags = $source_recipe.tags |
.featured = $source_recipe.featured |
.source_file = ("content/recipes/" + $lang + "/" + .id + ".md") |
.html_file = ("/recipes/" + $lang + "/" + .id + ".html")
else
$target
end
]
' "$target_recipes" > "$temp_file"
mv "$temp_file" "$target_recipes"
log_success "Synchronized recipe metadata for: $lang"
fi
fi
done
fi
fi
log_success "Metadata synchronization completed"
}
# Validate synchronization
validate_sync() {
log_step "Validating content synchronization..."
local validation_errors=0
# Validate that all languages have the same structure
for lang in "${SUPPORTED_LANGUAGES[@]}"; do
log_info "Validating structure for language: $lang"
# Check blog structure
local blog_index="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ ! -f "$blog_index" ]]; then
log_warning "Missing blog index for language: $lang"
((validation_errors++))
else
# Validate JSON syntax
if ! jq empty "$blog_index" 2>/dev/null; then
log_error "Invalid JSON in blog index for language: $lang"
((validation_errors++))
fi
# Check language field
local file_lang=$(jq -r '.language' "$blog_index" 2>/dev/null || echo "")
if [[ "$file_lang" != "$lang" ]]; then
log_error "Language mismatch in blog index for $lang: expected '$lang', found '$file_lang'"
((validation_errors++))
fi
fi
# Check recipe structure
local recipe_index="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ ! -f "$recipe_index" ]]; then
log_warning "Missing recipe index for language: $lang"
((validation_errors++))
else
# Validate JSON syntax
if ! jq empty "$recipe_index" 2>/dev/null; then
log_error "Invalid JSON in recipe index for language: $lang"
((validation_errors++))
fi
# Check language field
local file_lang=$(jq -r '.language' "$recipe_index" 2>/dev/null || echo "")
if [[ "$file_lang" != "$lang" ]]; then
log_error "Language mismatch in recipe index for $lang: expected '$lang', found '$file_lang'"
((validation_errors++))
fi
fi
done
if [[ $validation_errors -eq 0 ]]; then
log_success "Content synchronization validation passed!"
else
log_error "Validation failed with $validation_errors errors"
return 1
fi
}
# Main function
main() {
local command="$1"
shift
echo -e "${BLUE}🔄 Content Synchronization Tool${NC}"
echo "=================================="
echo ""
check_dependencies
case "$command" in
"sync-structure")
parse_args "$@"
sync_content_structure
;;
"sync-metadata")
parse_args "$@"
sync_metadata
;;
"check-missing")
parse_args "$@"
check_missing_content
;;
"generate-missing")
parse_args "$@"
generate_missing_translations
;;
"backup")
create_backup
;;
"restore")
parse_args "$@"
restore_backup "$2"
;;
"validate")
validate_sync
;;
"help"|"")
show_usage
;;
*)
log_error "Unknown command: $command"
show_usage
exit 1
;;
esac
}
# Run main function
main "$@"