Files
go-omni/i18n/export.go

124 lines
3.8 KiB
Go

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<string, never>;\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 = <N extends Namespace, K extends Extract<keyof MessageParams[N], string>>(namespace: N, key: K, params: Extract<MessageParams[N][K], Record<string, unknown>>) => string;\n")
_, err := io.WriteString(w, out.String())
return err
}