website-htmx-rustelo-code/scripts/sync/sync-page-ftl.nu

157 lines
6.1 KiB
Text
Raw Normal View History

2026-07-10 03:44:13 +01:00
#!/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 <path> --out-en <path> --out-es <path>
# nu scripts/sync/sync-page-ftl.nu --html <path> --out-en <path> --out-es <path> --dry-run
#
# Convention required in source HTML:
# <element data-key="ftl-key-name" data-en="English text" data-es="Texto español">
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<key>[^"]+)"'
let e = 'data-en="(?P<en>[^"]*)"'
let s = 'data-es="(?P<es>[^"]*)"'
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 "&amp;" "&"
| str replace --all "&lt;" "<"
| str replace --all "&gt;" ">"
| str replace --all "&quot;" "\""
| str replace --all "&#39;" "'"
| str replace --all "&mdash;" "—"
| str replace --all "&ndash;" ""
| str replace --all "&nbsp;" " "
| str replace --all "&iacute;" "í"
| str replace --all "&eacute;" "é"
| str replace --all "&oacute;" "ó"
| str replace --all "&uacute;" "ú"
| str replace --all "&aacute;" "á"
| str replace --all "&ntilde;" "ñ"
| 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<record<key: string, val: string>> {
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<record<key: string, val: string>>, from_html: list<record<key: string, val: string>>]: 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"
}
}