#!/usr/bin/env nu # sync-page-ftl.nu # Extracts data-key/data-en/data-es from a source index.html and writes FTL files. # # Usage: # nu scripts/sync/sync-page-ftl.nu --html --out-en --out-es # nu scripts/sync/sync-page-ftl.nu --html --out-en --out-es --dry-run # # Convention required in source HTML: # def main [ --html: string, # Absolute path to source index.html --out-en: string, # Output path for English FTL --out-es: string, # Output path for Spanish FTL --dry-run # Print changes without writing ] { if not ($html | path exists) { error make { msg: $"Source HTML not found: ($html)" } } let content = open $html # Normalize multiline tags: collapse newlines + surrounding whitespace to a single space. # This handles HTML where data-key, data-en, data-es are on separate lines within a tag. let flat = $content | str replace --all --regex '\n[ \t]+' ' ' # Strategy: regex with a separator that skips quoted attribute values containing > or <. # Covers all 6 orderings of data-key / data-en / data-es within a tag. # # q = (?:"[^"]*"|[^>"])*? — matches space between attrs, skipping complete "..." values # Built via concatenation to avoid Nu interpolation parsing ( as expression. let k = 'data-key="(?P[^"]+)"' let e = 'data-en="(?P[^"]*)"' let s = 'data-es="(?P[^"]*)"' let q = '(?:"[^"]*"|[^>"])*?' let pat_kes = ($k + $q + $e + $q + $s) let pat_kse = ($k + $q + $s + $q + $e) let pat_eks = ($e + $q + $k + $q + $s) let pat_esk = ($e + $q + $s + $q + $k) let pat_ske = ($s + $q + $k + $q + $e) let pat_sek = ($s + $q + $e + $q + $k) let all_entries = ( [ ($flat | parse --regex $pat_kes), ($flat | parse --regex $pat_kse), ($flat | parse --regex $pat_eks), ($flat | parse --regex $pat_esk), ($flat | parse --regex $pat_ske), ($flat | parse --regex $pat_sek), ] | flatten | uniq-by key | sort-by key ) if ($all_entries | length) == 0 { print $"⚠ No data-key elements found in ($html)" print " Ensure elements have: data-key=\"ftl-key\" data-en=\"...\" data-es=\"...\"" return } def decode-entities [val: string]: nothing -> string { $val | str replace --all "&" "&" | str replace --all "<" "<" | str replace --all ">" ">" | str replace --all """ "\"" | str replace --all "'" "'" | str replace --all "—" "—" | str replace --all "–" "–" | str replace --all " " " " | str replace --all "í" "í" | str replace --all "é" "é" | str replace --all "ó" "ó" | str replace --all "ú" "ú" | str replace --all "á" "á" | str replace --all "ñ" "ñ" | str replace --all "&#[0-9]+;" "" } # Parse existing FTL into a map of key→value (preserves manually-maintained keys) def parse-ftl [path: string]: nothing -> list> { if not ($path | path exists) { return [] } open $path | lines | where { |l| not ($l | str starts-with '#') and ($l | str contains ' = ') } | each { |l| let idx = $l | str index-of ' = ' let k = $l | str substring 0..<$idx let v = $l | str substring ($idx + 3).. { key: $k, val: $v } } } # Build HTML-extracted maps (key → decoded value) let html_en = $all_entries | each { |row| { key: $row.key, val: (decode-entities $row.en) } } let html_es = $all_entries | each { |row| { key: $row.key, val: (decode-entities $row.es) } } # Merge: start from existing FTL, update/add keys from HTML, preserve FTL-only keys def merge-ftl [existing: list>, from_html: list>]: nothing -> string { let html_map = $from_html | reduce -f {} { |row, acc| $acc | insert $row.key $row.val } let merged = $existing | each { |row| let updated = $html_map | get -o $row.key if $updated != null { { key: $row.key, val: $updated } } else { $row } } # Add new keys from HTML that weren't in existing FTL let existing_keys = $existing | get key let new_keys = $from_html | where { |row| not ($existing_keys | any { |k| $k == $row.key }) } ($merged | append $new_keys) | each { |row| $"($row.key) = ($row.val)" } | str join "\n" } let existing_en = parse-ftl $out_en let existing_es = parse-ftl $out_es let ftl_en = if ($existing_en | length) > 0 { merge-ftl $existing_en $html_en } else { $html_en | each { |row| $"($row.key) = ($row.val)" } | str join "\n" } let ftl_es = if ($existing_es | length) > 0 { merge-ftl $existing_es $html_es } else { $html_es | each { |row| $"($row.key) = ($row.val)" } | str join "\n" } print $" Found ($all_entries | length) translatable keys from HTML" if $dry_run { print "\n── EN (preview first 10) ──" print ($ftl_en | lines | first 10 | str join "\n") print "\n── ES (preview first 10) ──" print ($ftl_es | lines | first 10 | str join "\n") return } # Write EN let current_en = if ($out_en | path exists) { open $out_en } else { "" } if $ftl_en != $current_en { $ftl_en | save --force $out_en print $" ✓ ($out_en)" } else { print $" — ($out_en) unchanged" } # Write ES let current_es = if ($out_es | path exists) { open $out_es } else { "" } if $ftl_es != $current_es { $ftl_es | save --force $out_es print $" ✓ ($out_es)" } else { print $" — ($out_es) unchanged" } }