#!/usr/bin/env nu # AuroraFrame i18n Framework - Hierarchical Locale System # # Supports global + page-specific locales with intelligent merging, # build-time validation, and AI-powered translation assistance. # Global i18n cache for performance mut $i18n_cache = {} mut $locale_bundles = {} # Locale resolution priority: page-specific > global > fallback export def resolve_locale_chain [ requested_locale: string page_name?: string site_config: record ] -> list { let available_locales = $site_config.i18n.available_locales let fallback_chain = $site_config.i18n.fallback_chain mut locale_chain = [] # 1. Try requested locale if ($requested_locale in ($available_locales | each { |l| $l.code })) { $locale_chain = ($locale_chain | append { code: $requested_locale source: "requested" priority: 1 }) } # 2. Find fallback for requested locale let requested_config = ($available_locales | where code == $requested_locale | first) if ($requested_config | is-not-empty) and ($requested_config.fallback? | is-not-empty) { let fallback = $requested_config.fallback if $fallback != $requested_locale { # Prevent infinite loops $locale_chain = ($locale_chain | append { code: $fallback source: "locale_fallback" priority: 2 }) } } # 3. Add global fallback chain for fallback in $fallback_chain { if not ($fallback in ($locale_chain | each { |l| $l.code })) { $locale_chain = ($locale_chain | append { code: $fallback source: "global_fallback" priority: 3 }) } } $locale_chain } # Load and merge locales for a specific page and locale export def load_merged_locales [ locale_code: string page_name?: string site_config: record page_config?: record ] -> record { let cache_key = $"($locale_code):($page_name | default 'global')" # Return cached if available if $cache_key in $locale_bundles { return ($locale_bundles | get $cache_key) } mut merged_bundle = {} # 1. Load global locales let global_dir = $site_config.i18n.global_locales_dir let global_files = try { ls $"($global_dir)/($locale_code)/*.ftl" | get name } catch { [] } for file in $global_files { let namespace = ($file | path basename | str replace ".ftl" "") let messages = (fluent parse $file) $merged_bundle = ($merged_bundle | insert $namespace $messages) } # 2. Load page-specific locales if page is specified if ($page_name | is-not-empty) and ($page_config | is-not-empty) { let page_i18n = ($page_config.i18n? | default {}) if ($page_i18n.enabled? | default false) and ($page_i18n.extends_global? | default true) { let pages_dir = $site_config.i18n.page_locales_dir let page_files = try { ls $"($pages_dir)/($page_name)/($locale_code)/*.ftl" | get name } catch { [] } for file in $page_files { let namespace = ($file | path basename | str replace ".ftl" "") let page_messages = (fluent parse $file) # Merge or override based on configuration if ($page_i18n.override_global? | default false) { # Page messages can override global ones if $namespace in $merged_bundle { let existing = ($merged_bundle | get $namespace) let combined = ($existing | merge $page_messages) $merged_bundle = ($merged_bundle | upsert $namespace $combined) } else { $merged_bundle = ($merged_bundle | insert $namespace $page_messages) } } else { # Global messages take precedence if not ($namespace in $merged_bundle) { $merged_bundle = ($merged_bundle | insert $namespace $page_messages) } } } } } # Cache the merged bundle $locale_bundles = ($locale_bundles | insert $cache_key $merged_bundle) $merged_bundle } # Smart locale detection with fallback chain export def detect_locale [ site_config: record detection_methods?: list ] -> string { let methods = $detection_methods | default $site_config.i18n.locale_detection let available_codes = ($site_config.i18n.available_locales | each { |l| $l.code }) for method in $methods { let detected = match $method { "url" => (detect_locale_from_url) "cookie" => (detect_locale_from_cookie) "header" => (detect_locale_from_header) "default" => $site_config.i18n.default_locale _ => null } if ($detected | is-not-empty) and ($detected in $available_codes) { return $detected } } # Ultimate fallback $site_config.i18n.default_locale } # Get localized message with fallback chain export def get_message [ message_id: string locale_code: string args?: record page_name?: string site_config: record page_config?: record ] -> string { let locale_chain = (resolve_locale_chain $locale_code $page_name $site_config) for locale_info in $locale_chain { let bundle = (load_merged_locales $locale_info.code $page_name $site_config $page_config) # Try to find message in all namespaces for namespace in ($bundle | columns) { let namespace_messages = ($bundle | get $namespace) if ($message_id in ($namespace_messages.messages | columns)) { let message = ($namespace_messages.messages | get $message_id) if ($args | is-not-empty) { return (fluent localize $message_id --bundle $namespace_messages --args $args) } else { return (fluent localize $message_id --bundle $namespace_messages) } } } } # Fallback to message ID if not found $"[[($message_id)]]" } # Build-time locale validation export def validate_i18n_build [ site_config: record pages: list ] -> record { mut validation_results = { valid: true warnings: [] errors: [] stats: {} } let available_locales = ($site_config.i18n.available_locales | each { |l| $l.code }) print "🌐 Validating i18n configuration..." # 1. Validate site configuration let site_validation = (validate_site_i18n_config $site_config) if not $site_validation.valid { $validation_results.valid = false $validation_results.errors = ($validation_results.errors | append $site_validation.errors) } # 2. Validate each locale mut locale_stats = {} for locale_code in $available_locales { print $" Checking locale: ($locale_code)" # Check global messages let global_bundle = (load_merged_locales $locale_code) let global_message_count = ($global_bundle | values | each { |ns| $ns.messages | columns | length } | reduce { |acc, x| $acc + $x }) # Check page-specific messages mut page_message_counts = {} for page in $pages { if ($page.config.i18n?.enabled? | default false) { let page_bundle = (load_merged_locales $locale_code $page.name $site_config $page.config) let page_message_count = ($page_bundle | values | each { |ns| $ns.messages | columns | length } | reduce { |acc, x| $acc + $x }) $page_message_counts = ($page_message_counts | insert $page.name $page_message_count) } } $locale_stats = ($locale_stats | insert $locale_code { global_messages: $global_message_count page_messages: $page_message_counts total_messages: ($global_message_count + ($page_message_counts | values | reduce { |acc, x| $acc + $x })) }) } # 3. Find missing translations let reference_locale = $site_config.i18n.default_locale let reference_stats = ($locale_stats | get $reference_locale) for locale_code in $available_locales { if $locale_code != $reference_locale { let current_stats = ($locale_stats | get $locale_code) if $current_stats.global_messages < $reference_stats.global_messages { let missing = ($reference_stats.global_messages - $current_stats.global_messages) $validation_results.warnings = ($validation_results.warnings | append $"($locale_code): Missing ($missing) global messages") } } } $validation_results.stats = $locale_stats if ($validation_results.errors | length) == 0 and ($validation_results.warnings | length) == 0 { print "✅ All i18n validations passed" } else { print $"⚠️ Found ($validation_results.warnings | length) warnings and ($validation_results.errors | length) errors" } $validation_results } # Generate build-optimized locale bundles export def build_locale_bundles [ site_config: record pages: list output_dir: string ] { print "🔨 Building optimized locale bundles..." let available_locales = ($site_config.i18n.available_locales | each { |l| $l.code }) let bundle_strategy = $site_config.i18n.bundle_splitting mkdir $"($output_dir)/locales" match $bundle_strategy { "global" => { # Single bundle per locale with all messages for locale_code in $available_locales { print $" Building global bundle: ($locale_code)" mut combined_bundle = {} # Add global messages let global_bundle = (load_merged_locales $locale_code) $combined_bundle = ($combined_bundle | merge $global_bundle) # Add all page messages for page in $pages { if ($page.config.i18n?.enabled? | default false) { let page_bundle = (load_merged_locales $locale_code $page.name $site_config $page.config) $combined_bundle = ($combined_bundle | merge $page_bundle) } } $combined_bundle | to json | save $"($output_dir)/locales/($locale_code).json" } } "per-page" => { # Separate bundle for each page + global for locale_code in $available_locales { print $" Building per-page bundles: ($locale_code)" # Global bundle let global_bundle = (load_merged_locales $locale_code) $global_bundle | to json | save $"($output_dir)/locales/($locale_code)-global.json" # Page bundles for page in $pages { if ($page.config.i18n?.enabled? | default false) { let page_bundle = (load_merged_locales $locale_code $page.name $site_config $page.config) $page_bundle | to json | save $"($output_dir)/locales/($locale_code)-($page.name).json" } } } } "hybrid" => { # Global + page-specific overrides for locale_code in $available_locales { print $" Building hybrid bundles: ($locale_code)" # Global bundle (shared across all pages) let global_bundle = (load_merged_locales $locale_code) $global_bundle | to json | save $"($output_dir)/locales/($locale_code)-global.json" # Page-specific additions only for page in $pages { if ($page.config.i18n?.enabled? | default false) { let page_locales_dir = $site_config.i18n.page_locales_dir let page_files = try { ls $"($page_locales_dir)/($page.name)/($locale_code)/*.ftl" | get name } catch { [] } if ($page_files | length) > 0 { mut page_only_bundle = {} for file in $page_files { let namespace = ($file | path basename | str replace ".ftl" "") let messages = (fluent parse $file) $page_only_bundle = ($page_only_bundle | insert $namespace $messages) } $page_only_bundle | to json | save $"($output_dir)/locales/($locale_code)-($page.name).json" } } } } } } print "✅ Locale bundles built successfully" } # Helper functions def detect_locale_from_url [] { # Extract locale from URL path let url_path = ($env.REQUEST_URI? | default "" | url parse | get path) let path_parts = ($url_path | str trim --char "/" | split row "/") if ($path_parts | length) > 0 { let potential_locale = ($path_parts | first) # Basic locale pattern matching if ($potential_locale | str length) == 5 and ($potential_locale | str contains "-") { return $potential_locale } } null } def detect_locale_from_cookie [] { $env.COOKIES?.locale? | default null } def detect_locale_from_header [] { let accept_lang = ($env.ACCEPT_LANGUAGE? | default "") if ($accept_lang | is-empty) { return null } # Parse Accept-Language header (simplified) let languages = ($accept_lang | split row "," | each { |lang| $lang | str trim | split row ";" | first | str trim }) # Return first supported language for lang in $languages { # Convert language codes to our format let normalized = match $lang { "en" => "en-US" "es" => "es-ES" "fr" => "fr-FR" _ => $lang } return $normalized } null } def validate_site_i18n_config [site_config: record] -> record { mut result = { valid: true errors: [] warnings: [] } let i18n_config = $site_config.i18n let available_codes = ($i18n_config.available_locales | each { |l| $l.code }) # Check default locale exists if not ($i18n_config.default_locale in $available_codes) { $result.valid = false $result.errors = ($result.errors | append $"Default locale '($i18n_config.default_locale)' not in available locales") } # Check fallback chain for fallback in $i18n_config.fallback_chain { if not ($fallback in $available_codes) { $result.valid = false $result.errors = ($result.errors | append $"Fallback locale '($fallback)' not in available locales") } } $result } # AI-powered commands (integrated with MCP) export def "ai i18n generate" [ source_locale: string target_locale: string --page: string --namespace: string ] { print $"🤖 Generating ($target_locale) translations from ($source_locale)" # Implementation will call MCP server for translation # This is a placeholder for the AI integration print " AI translation integration coming soon..." }