provisioning-outreach/presentations/rust-laspalmas-250926/auroraframe/framework/i18n.nu

444 lines
No EOL
15 KiB
Text

#!/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<record> {
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>
] -> 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>
] -> 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<record>
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..."
}