website-htmx-rustelo-code/scripts/sh/content/sync-translations.sh

686 lines
24 KiB
Bash
Raw Permalink Normal View History

2026-07-10 03:44:13 +01:00
#!/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 "$@"