package gonexapp import ( "encoding/json" "fmt" "html/template" "io/fs" "maps" "strings" ) // Phrases is one ExApp's merged Phrase set. // English is the complete set. A module set is merged with Merge; // a repeated id uses the set added later. // The page script receives this set. Library chrome is not included. type Phrases struct { en map[string]string de map[string]string } // PhrasesFromFS reads i18n/en.json and i18n/de.json. // Each file is one JSON object of id → text. func PhrasesFromFS(fsys fs.FS) (Phrases, error) { en, err := fs.ReadFile(fsys, "i18n/en.json") if err != nil { return Phrases{}, fmt.Errorf("read i18n/en.json: %w", err) } de, err := fs.ReadFile(fsys, "i18n/de.json") if err != nil { return Phrases{}, fmt.Errorf("read i18n/de.json: %w", err) } return ParsePhrases(en, de) } // ParsePhrases reads an English object and a German object. func ParsePhrases(en, de []byte) (Phrases, error) { enMap, err := decodePhraseFile(en) if err != nil { return Phrases{}, fmt.Errorf("en: %w", err) } deMap, err := decodePhraseFile(de) if err != nil { return Phrases{}, fmt.Errorf("de: %w", err) } return Phrases{en: enMap, de: deMap}, nil } func decodePhraseFile(raw []byte) (map[string]string, error) { if len(strings.TrimSpace(string(raw))) == 0 { return map[string]string{}, nil } var m map[string]string if err := json.Unmarshal(raw, &m); err != nil { return nil, err } if m == nil { m = map[string]string{} } return m, nil } // Merge returns the sets combined in order. // A repeated id uses the text from the set added later. func Merge(sets ...Phrases) Phrases { out := Phrases{en: map[string]string{}, de: map[string]string{}} for _, set := range sets { maps.Copy(out.en, set.en) maps.Copy(out.de, set.de) } return out } func (p Phrases) has(code string) bool { switch code { case "en": return p.en != nil case "de": return p.de != nil default: return false } } func (p Phrases) texts(lang string) map[string]string { if lang == "de" { return p.de } return p.en } // Sentence is the finished text for lang. // A missing text for that language uses the English text. // An id missing from English is the id. // A hole with no value stays visible as {name}. // The result is plain text; the caller escapes it when writing HTML. func (p Phrases) Sentence(lang, id string, holes map[string]string) string { lang = matchLanguage(lang, p.has) if text, ok := phraseText(p.texts(lang), id); ok { return fillHoles(text, holes) } if lang != "en" { if text, ok := phraseText(p.en, id); ok { return fillHoles(text, holes) } } return id } func phraseText(m map[string]string, id string) (string, bool) { text, ok := m[id] if !ok || text == "" { return "", false } return text, true } // resolved is the map the page script sees: one text per id for lang, // with English filling gaps. Ids missing from English are omitted. func (p Phrases) resolved(lang string) map[string]string { lang = matchLanguage(lang, p.has) out := map[string]string{} for id, text := range p.en { if text == "" { continue } out[id] = text } if lang == "en" { return out } for id, text := range p.de { if text == "" { continue } if _, ok := phraseText(p.en, id); !ok { continue } out[id] = text } return out } // Script defines exappPhrase for the page, from this set only. func (p Phrases) Script(lang string) template.HTML { raw, err := json.Marshal(p.resolved(lang)) if err != nil { raw = []byte("{}") } return template.HTML("") } const phraseRuntimeJS = `function exappFill(text, holes) { if (!holes || text.indexOf("{") < 0) return text; var out = ""; for (var i = 0; i < text.length; i++) { if (text.charAt(i) !== "{") { out += text.charAt(i); continue; } var end = text.indexOf("}", i + 1); if (end < 0) { out += text.charAt(i); continue; } var name = text.slice(i + 1, end); if (Object.prototype.hasOwnProperty.call(holes, name)) { out += String(holes[name]); i = end; continue; } out += text.charAt(i); } return out; } function exappPhraseMap(map) { return function (id, holes) { var text = Object.prototype.hasOwnProperty.call(map, id) ? map[id] : String(id); return exappFill(text, holes); }; }` // FillScript replaces English source after load and refreshes Fold from the // visible item name. It does not parse Phrase text as HTML. // App navigation includes it. A page the ExApp renders itself places Script // in the head and FillScript at the end of the body. func FillScript() template.HTML { return phraseFillScript } // phraseFillScript replaces English source after load and refreshes Fold // from the visible item name. It does not parse Phrase text as HTML. const phraseFillScript = template.HTML(``)