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

466 lines
18 KiB
Text
Raw Permalink Normal View History

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