package i18n import ( "bytes" "encoding/json" "fmt" "io" "strconv" "strings" "golang.org/x/text/language" ) // Pack is a self-contained browser artifact for one selected language. Its // documents include the entire configured fallback chain and parameter schemas. type Pack struct { Version int `json:"version"` Locale string `json:"locale"` Config Config `json:"config"` Documents []Document `json:"documents"` RuleVersion string `json:"ruleVersion"` PluralRules map[string]map[string]string `json:"pluralRules"` License string `json:"license"` } func (b *Bundle) namespaces(names []string) error { if b == nil { return fmt.Errorf("i18n: bundle required") } if len(names) == 0 { return fmt.Errorf("i18n: export requires explicit namespaces") } known := map[string]bool{} for ref := range b.entries { known[ref.Namespace] = true } seen := map[string]bool{} for _, ns := range names { if !known[ns] || seen[ns] { return fmt.Errorf("i18n: export namespace is unknown or duplicated: %s", ns) } seen[ns] = true } return nil } func (b *Bundle) Export(w io.Writer, locale string, namespaces ...string) error { if w == nil { return fmt.Errorf("i18n: writer required") } if err := b.namespaces(namespaces); err != nil { return err } l, err := canonical(locale) if err != nil { return err } if _, ok := b.tags[l]; !ok { return fmt.Errorf("i18n: export locale must be enabled") } docs := []Document{} for _, candidate := range b.chain(l) { docs = append(docs, b.Documents(candidate, namespaces...)...) } // Source is always needed by clients even if absent from an explicit chain. rules := map[string]map[string]string{} for _, locale := range b.chain(l) { rules[locale] = rulesFor(language.MustParse(locale)).text } pack := Pack{Version: Version, Locale: l, Config: b.Config(), Documents: docs, RuleVersion: PluralRulesVersion, PluralRules: rules, License: unicodeLicense} var buffer bytes.Buffer enc := json.NewEncoder(&buffer) enc.SetIndent("", " ") if err := enc.Encode(pack); err != nil { return err } if buffer.Len() > 4<<20 { return fmt.Errorf("i18n: exported pack exceeds 4 MiB; split namespaces") } _, err = io.Copy(w, &buffer) return err } // TypeScript emits nested namespace/key types, so keys containing punctuation // never collide through concatenation or Go-specific identifier conversion. func (b *Bundle) TypeScript(w io.Writer, namespaces ...string) error { if w == nil { return fmt.Errorf("i18n: writer required") } if err := b.namespaces(namespaces); err != nil { return err } var out strings.Builder out.WriteString("// Generated by go-omni i18n types. Do not edit.\nexport interface MessageParams {\n") for _, doc := range b.Documents(b.config.SourceLocale, namespaces...) { fmt.Fprintf(&out, " %s: {\n", strconv.Quote(doc.Namespace)) for _, key := range sortedKeys(doc.Messages) { m := doc.Messages[key] if len(m.Params) == 0 { fmt.Fprintf(&out, " %s: Record;\n", strconv.Quote(key)) continue } fmt.Fprintf(&out, " %s: {", strconv.Quote(key)) for _, name := range sortedKeys(m.Params) { typ := "string" switch m.Params[name] { case Boolean: typ = "boolean" case Integer: typ = "number | bigint | `${bigint}`" case Number: typ = "number" } fmt.Fprintf(&out, " %s: %s;", strconv.Quote(name), typ) } out.WriteString(" };\n") } out.WriteString(" };\n") } out.WriteString("}\nexport type Namespace = keyof MessageParams;\nexport type Translate = >(namespace: N, key: K, params: Extract>) => string;\n") _, err := io.WriteString(w, out.String()) return err }