#!/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 "$@"