#!/usr/bin/env nu # Translation Synchronization Script # Nushell version of sync-translations.sh # Synchronizes translation keys and manages localization files def main [command?: string, ...args] { let config = { content_dir: ($env.SITE_CONTENT_PATH? | default "site/content"), languages: ["en", "es"], locales_dir: ($env.SITE_I18N_PATH? | default "site/i18n") } print $"(ansi blue)🌍 Translation Synchronization Tool(ansi reset)" if ($command | is-empty) or $command in ["-h", "--help", "help"] { show_sync_usage exit 0 } match $command { "extract-keys" | "extract" => { cmd_extract_keys $config $args } "sync-keys" | "sync" => { cmd_sync_keys $config $args } "validate-translations" | "validate" => { cmd_validate_translations $config $args } "generate-missing" | "missing" => { cmd_generate_missing_translations $config $args } "show-stats" | "stats" => { cmd_show_translation_stats $config } "create-template" | "template" => { cmd_create_translation_template $config $args } _ => { print $"(ansi red)❌ Unknown command: ($command)(ansi reset)" show_sync_usage exit 1 } } } # Show usage information def show_sync_usage [] { print "Usage: nu sync-translations.nu [COMMAND] [OPTIONS]" print "" print "Commands:" print " extract-keys Extract translation keys from content files" print " sync-keys Synchronize translation keys across languages" print " validate-translations Validate translation completeness" print " generate-missing Generate missing translation entries" print " show-stats Show translation statistics" print " create-template Create translation template for new language" print " help Show this help message" print "" print "Options:" print " --lang LANGUAGE Target specific language [default: all]" print " --source LANG Source language for template [default: en]" print " --output DIR Output directory [default: content/locales]" print "" print "Examples:" print " nu sync-translations.nu extract-keys" print " nu sync-translations.nu sync-keys --lang es" print " nu sync-translations.nu validate-translations" print " nu sync-translations.nu create-template --lang fr --source en" } # Extract translation keys from content files def cmd_extract_keys [config, args] { print $"(ansi blue)🔧 Extracting translation keys from content...(ansi reset)" let content_types = get_content_types $config.content_dir mut all_keys = {} for content_type in $content_types { print $"(ansi yellow) Processing ($content_type) content...(ansi reset)" for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { continue } let md_files = (ls $"($content_dir)/**/*.md" | where type == file) for file_info in $md_files { let md_file = $file_info.name let file_keys = extract_translation_keys_from_file $md_file for key in $file_keys { if $key not-in ($all_keys | columns) { $all_keys = ($all_keys | upsert $key { source: $"($content_type)/($language)/($md_file | path basename)", languages: [$language] }) } else { let current = ($all_keys | get $key) let updated_languages = ($current.languages | append $language | uniq) $all_keys = ($all_keys | upsert $key ($current | upsert languages $updated_languages)) } } } } } let keys_count = ($all_keys | columns | length) print $"(ansi green)✅ Extracted ($keys_count) unique translation keys(ansi reset)" # Save extracted keys mkdir $config.locales_dir let keys_file = $"($config.locales_dir)/extracted-keys.json" $all_keys | to json | save $keys_file print $"(ansi green)💾 Keys saved to: ($keys_file)(ansi reset)" # Show key statistics print "" print $"(ansi blue)📊 Key Statistics:(ansi reset)" for key in ($all_keys | columns | first 10) { let key_info = ($all_keys | get $key) print $" ($key): found in ($key_info.languages | length) language(s)" } if $keys_count > 10 { print $" ... and ($keys_count - 10) more keys" } } # Extract translation keys from a single file def extract_translation_keys_from_file [file_path: string] { try { let content = (open $file_path) # Look for common translation key patterns # This is a simplified version - can be enhanced with more sophisticated patterns mut keys = [] # Pattern 1: {{t "key"}} or {{t 'key'}} let t_pattern_matches = ($content | str find-replace --all --regex '\{\{t\s+["\']([^"\']+)["\'][^}]*\}\}' '$1' | split row '\n' | where {|line| $line != $content}) $keys = ($keys | append $t_pattern_matches) # Pattern 2: i18n("key") or i18n('key') let i18n_pattern_matches = ($content | str find-replace --all --regex 'i18n\(["\']([^"\']+)["\']\)' '$1' | split row '\n' | where {|line| $line != $content}) $keys = ($keys | append $i18n_pattern_matches) # Pattern 3: translate("key") or translate('key') let translate_pattern_matches = ($content | str find-replace --all --regex 'translate\(["\']([^"\']+)["\']\)' '$1' | split row '\n' | where {|line| $line != $content}) $keys = ($keys | append $translate_pattern_matches) $keys | uniq } catch { [] } } # Synchronize translation keys across languages def cmd_sync_keys [config, args] { print $"(ansi blue)🔧 Synchronizing translation keys across languages...(ansi reset)" # Parse target language from args let target_lang = (parse_lang_arg $args | default "all") let languages = if $target_lang == "all" { $config.languages } else { [$target_lang] } # Load existing translation files mut translation_files = {} for language in $config.languages { let fluent_file = $"($config.content_dir)/($language).ftl" let locale_file = $"($config.locales_dir)/($language).json" # Try to load existing translations mut translations = {} if ($fluent_file | path exists) { $translations = (load_fluent_translations $fluent_file) } else if ($locale_file | path exists) { $translations = (open $locale_file | from json) } $translation_files = ($translation_files | upsert $language $translations) } # Load extracted keys let keys_file = $"($config.locales_dir)/extracted-keys.json" if not ($keys_file | path exists) { print $"(ansi red)❌ No extracted keys found. Run 'extract-keys' first.(ansi reset)" exit 1 } let extracted_keys = (open $keys_file | from json) # Sync keys across languages for language in $languages { print $"(ansi yellow) Syncing keys for ($language)...(ansi reset)" let current_translations = ($translation_files | get $language) mut updated_translations = $current_translations # Add missing keys mut added_keys = 0 for key in ($extracted_keys | columns) { if $key not-in ($current_translations | columns) { # Add placeholder translation $updated_translations = ($updated_translations | upsert $key $"[TODO: Translate '($key)' to ($language)]") $added_keys = $added_keys + 1 } } if $added_keys > 0 { # Save updated translations let output_file = $"($config.locales_dir)/($language).json" $updated_translations | to json | save $output_file print $"(ansi green) ✅ Added ($added_keys) new keys to ($language)(ansi reset)" } else { print $"(ansi green) ✅ ($language) translations are up to date(ansi reset)" } } print $"(ansi green)🎉 Key synchronization completed!(ansi reset)" } # Validate translation completeness def cmd_validate_translations [config, args] { print $"(ansi blue)🔧 Validating translation completeness...(ansi reset)" mut total_issues = 0 mut translation_stats = {} for language in $config.languages { let fluent_file = $"($config.content_dir)/($language).ftl" let locale_file = $"($config.locales_dir)/($language).json" mut translations = {} mut file_type = "" if ($fluent_file | path exists) { $translations = (load_fluent_translations $fluent_file) $file_type = "ftl" } else if ($locale_file | path exists) { $translations = (open $locale_file | from json) $file_type = "json" } else { print $"(ansi red)❌ No translation file found for ($language)(ansi reset)" $total_issues = $total_issues + 1 continue } # Count translations and find issues let total_keys = ($translations | columns | length) let todo_translations = ($translations | values | where {|val| ($val | str contains "TODO") or ($val | str contains "TRANSLATE")}) let empty_translations = ($translations | values | where {|val| ($val | str trim | is-empty)}) let incomplete_count = ($todo_translations | length) + ($empty_translations | length) let complete_count = $total_keys - $incomplete_count $translation_stats = ($translation_stats | upsert $language { total: $total_keys, complete: $complete_count, incomplete: $incomplete_count, file_type: $file_type, completion_rate: (if $total_keys > 0 { ($complete_count * 100 / $total_keys) } else { 0 }) }) if $incomplete_count > 0 { print $"(ansi yellow)⚠️ ($language): ($incomplete_count) incomplete translations out of ($total_keys)(ansi reset)" $total_issues = $total_issues + $incomplete_count } else { print $"(ansi green)✅ ($language): All ($total_keys) translations complete(ansi reset)" } } # Show summary print "" print $"(ansi blue)📊 Translation Completeness Summary(ansi reset)" for language in $config.languages { if $language in ($translation_stats | columns) { let stats = ($translation_stats | get $language) let rate = ($stats.completion_rate | into string) print $" ($language): ($stats.complete)/($stats.total) complete (($rate)%) - ($stats.file_type) format" } } if $total_issues == 0 { print $"(ansi green)🎉 All translations are complete!(ansi reset)" } else { print $"(ansi yellow)⚠️ ($total_issues) translation issues found(ansi reset)" } } # Generate missing translation entries def cmd_generate_missing_translations [config, args] { print $"(ansi blue)🔧 Generating missing translation entries...(ansi reset)" # This would integrate with translation APIs or create templates # For now, create structured templates for manual translation let target_lang = (parse_lang_arg $args | default "all") let languages = if $target_lang == "all" { $config.languages } else { [$target_lang] } for language in $languages { let locale_file = $"($config.locales_dir)/($language).json" if ($locale_file | path exists) { let translations = (open $locale_file | from json) let incomplete_keys = ($translations | transpose key value | where {|row| ($row.value | str contains "TODO") or ($row.value | str trim | is-empty)}) if ($incomplete_keys | length) > 0 { let template_file = $"($config.locales_dir)/($language)-template.md" create_translation_template $incomplete_keys $language $template_file print $"(ansi green)✅ Created translation template: ($template_file)(ansi reset)" } else { print $"(ansi green)✅ ($language): No missing translations(ansi reset)" } } } } # Show translation statistics def cmd_show_translation_stats [config] { print $"(ansi blue)📊 Translation Statistics(ansi reset)" print $"(ansi blue)Languages: ($config.languages | str join ', ')(ansi reset)" print $"(ansi blue)Locales directory: ($config.locales_dir)(ansi reset)" print "" for language in $config.languages { let fluent_file = $"($config.content_dir)/($language).ftl" let locale_file = $"($config.locales_dir)/($language).json" print $"(ansi cyan)📝 ($language | str upcase):(ansi reset)" if ($fluent_file | path exists) { let file_size = (du $fluent_file | get apparent | first) let line_count = (open $fluent_file | lines | length) print $" Fluent file: ($fluent_file) (($line_count) lines, ($file_size))" } if ($locale_file | path exists) { let translations = (open $locale_file | from json) let total_keys = ($translations | columns | length) let incomplete = ($translations | values | where {|val| ($val | str contains "TODO") or ($val | str trim | is-empty)} | length) let complete = $total_keys - $incomplete print $" JSON file: ($locale_file) (($total_keys) keys)" print $" Complete: ($complete), Incomplete: ($incomplete)" } if not ($fluent_file | path exists) and not ($locale_file | path exists) { print $" ❌ No translation files found" } print "" } } # Create translation template for new language def cmd_create_translation_template [config, args] { let target_lang = (parse_lang_arg $args | default "") let source_lang = (parse_source_arg $args | default "en") if ($target_lang | is-empty) { print $"(ansi red)❌ Target language required. Use --lang LANGUAGE(ansi reset)" exit 1 } print $"(ansi blue)🔧 Creating translation template for ($target_lang) based on ($source_lang)...(ansi reset)" let source_file = $"($config.locales_dir)/($source_lang).json" if not ($source_file | path exists) { print $"(ansi red)❌ Source translation file not found: ($source_file)(ansi reset)" exit 1 } let source_translations = (open $source_file | from json) let template_translations = ($source_translations | transpose key value | each {|row| {key: $row.key, value: $"[TODO: Translate '($row.value)' to ($target_lang)]"} } | reduce --fold {} {|row, acc| $acc | upsert $row.key $row.value}) let target_file = $"($config.locales_dir)/($target_lang).json" $template_translations | to json | save $target_file print $"(ansi green)✅ Created translation template: ($target_file)(ansi reset)" print $"(ansi green)📝 ($source_translations | columns | length) keys ready for translation(ansi reset)" } # Get content types from directory structure def get_content_types [content_dir: string] { if not ($content_dir | path exists) { return [] } ls $content_dir | where type == dir | where name !~ "(locales|themes|menus|footer|tmp|routes)" | get name | path basename } # Load translations from Fluent (.ftl) file def load_fluent_translations [file_path: string] { try { let content = (open $file_path | lines) mut translations = {} for line in $content { if ($line | str trim | str starts-with "#") or ($line | str trim | is-empty) { continue } if $line =~ '^([^=]+)=(.*)$' { let matches = ($line | parse --regex '^([^=]+)=(.*)$') if ($matches | length) > 0 { let key = ($matches | first | get capture0 | str trim) let value = ($matches | first | get capture1 | str trim) $translations = ($translations | upsert $key $value) } } } $translations } catch { {} } } # Create translation template markdown file def create_translation_template [incomplete_keys, language: string, template_file: string] { mut content = $"# Translation Template for ($language | str upcase)\n\n" $content = $content + $"This file contains keys that need translation to ($language).\n" $content = $content + $"Please translate the values and update the JSON file.\n\n" for row in $incomplete_keys { $content = $content + $"## ($row.key)\n" $content = $content + $"Current: ($row.value)\n" $content = $content + $"Translation: _[Please provide ($language) translation here]_\n\n" } $content | save $template_file } # Parse language argument from args def parse_lang_arg [args] { mut i = 0 while $i < ($args | length) { let arg = ($args | get $i) if $arg == "--lang" { $i = $i + 1 if $i < ($args | length) { return ($args | get $i) } } $i = $i + 1 } null } # Parse source language argument from args def parse_source_arg [args] { mut i = 0 while $i < ($args | length) { let arg = ($args | get $i) if $arg == "--source" { $i = $i + 1 if $i < ($args | length) { return ($args | get $i) } } $i = $i + 1 } null }