taler-merchant-demos

Python-based Frontends for the Demonstration Web site
Log | Files | Refs | README | LICENSE

i18n.go (4308B)


      1 package web
      2 
      3 import (
      4 	"bufio"
      5 	"fmt"
      6 	"io/fs"
      7 	"regexp"
      8 	"strconv"
      9 	"strings"
     10 )
     11 
     12 type catalogs map[string]map[string]string
     13 
     14 var (
     15 	placeholderPattern    = regexp.MustCompile(`\{(?:[A-Za-z_][A-Za-z0-9_]*)?\}`)
     16 	translationTagPattern = regexp.MustCompile(`</?[A-Za-z][A-Za-z0-9]*\b`)
     17 )
     18 
     19 func loadCatalogs(supported map[string]bool) (catalogs, error) {
     20 	result := make(catalogs)
     21 	entries, err := fs.ReadDir(Assets, "assets/translations")
     22 	if err != nil {
     23 		return nil, fmt.Errorf("read translations: %w", err)
     24 	}
     25 	for _, entry := range entries {
     26 		if !entry.IsDir() {
     27 			continue
     28 		}
     29 		lang := entry.Name()
     30 		if !supported[lang] {
     31 			continue
     32 		}
     33 		data, err := Assets.ReadFile("assets/translations/" + lang + "/LC_MESSAGES/messages.po")
     34 		if err != nil {
     35 			continue
     36 		}
     37 		catalog := parsePO(string(data))
     38 		for message, translation := range catalog {
     39 			if !samePlaceholders(message, translation) {
     40 				return nil, fmt.Errorf("translation %s has different placeholders for %q", lang, message)
     41 			}
     42 			if !sameHTMLTags(message, translation) {
     43 				return nil, fmt.Errorf("translation %s has different HTML tags for %q", lang, message)
     44 			}
     45 		}
     46 		result[lang] = catalog
     47 	}
     48 	for lang := range supported {
     49 		if result[lang] == nil {
     50 			return nil, fmt.Errorf("translation catalog %s is missing", lang)
     51 		}
     52 	}
     53 	return result, nil
     54 }
     55 
     56 func parsePO(input string) map[string]string {
     57 	translations := make(map[string]string)
     58 	var id, value strings.Builder
     59 	field := ""
     60 	fuzzy := false
     61 	flush := func() {
     62 		if !fuzzy && id.Len() != 0 && value.Len() != 0 {
     63 			translations[id.String()] = value.String()
     64 		}
     65 		id.Reset()
     66 		value.Reset()
     67 		field = ""
     68 		fuzzy = false
     69 	}
     70 	scanner := bufio.NewScanner(strings.NewReader(input))
     71 	for scanner.Scan() {
     72 		line := strings.TrimSpace(scanner.Text())
     73 		switch {
     74 		case line == "":
     75 			flush()
     76 		case strings.HasPrefix(line, "#,") && strings.Contains(line, "fuzzy"):
     77 			fuzzy = true
     78 		case strings.HasPrefix(line, "msgid "):
     79 			if id.Len() != 0 || value.Len() != 0 {
     80 				flush()
     81 			}
     82 			field = "id"
     83 			id.WriteString(poString(strings.TrimSpace(strings.TrimPrefix(line, "msgid"))))
     84 		case strings.HasPrefix(line, "msgstr "):
     85 			field = "value"
     86 			value.WriteString(poString(strings.TrimSpace(strings.TrimPrefix(line, "msgstr"))))
     87 		case strings.HasPrefix(line, `"`):
     88 			if field == "id" {
     89 				id.WriteString(poString(line))
     90 			} else if field == "value" {
     91 				value.WriteString(poString(line))
     92 			}
     93 		}
     94 	}
     95 	flush()
     96 	return translations
     97 }
     98 
     99 func poString(value string) string {
    100 	decoded, err := strconv.Unquote(value)
    101 	if err != nil {
    102 		return ""
    103 	}
    104 	return decoded
    105 }
    106 
    107 func (c catalogs) translate(lang, message string) string {
    108 	if translated := c[lang][message]; translated != "" {
    109 		return translated
    110 	}
    111 	if base, _, ok := strings.Cut(lang, "_"); ok {
    112 		if translated := c[base][message]; translated != "" {
    113 			return translated
    114 		}
    115 	}
    116 	return message
    117 }
    118 
    119 func formatNamed(message string, values ...any) string {
    120 	replacements := make(map[string]string)
    121 	for i := 0; i+1 < len(values); i += 2 {
    122 		replacements["{"+fmt.Sprint(values[i])+"}"] = fmt.Sprint(values[i+1])
    123 	}
    124 	return placeholderPattern.ReplaceAllStringFunc(message, func(placeholder string) string {
    125 		if replacement, ok := replacements[placeholder]; ok {
    126 			return replacement
    127 		}
    128 		return placeholder
    129 	})
    130 }
    131 
    132 func samePlaceholders(message, translation string) bool {
    133 	count := func(input string) map[string]int {
    134 		result := make(map[string]int)
    135 		for _, placeholder := range placeholderPattern.FindAllString(input, -1) {
    136 			result[placeholder]++
    137 		}
    138 		return result
    139 	}
    140 	original := count(message)
    141 	translated := count(translation)
    142 	if len(original) != len(translated) {
    143 		return false
    144 	}
    145 	for placeholder, occurrences := range original {
    146 		if translated[placeholder] != occurrences {
    147 			return false
    148 		}
    149 	}
    150 	return true
    151 }
    152 
    153 func sameHTMLTags(message, translation string) bool {
    154 	count := func(input string) map[string]int {
    155 		result := make(map[string]int)
    156 		for _, tag := range translationTagPattern.FindAllString(input, -1) {
    157 			result[strings.ToLower(tag)]++
    158 		}
    159 		return result
    160 	}
    161 	original := count(message)
    162 	translated := count(translation)
    163 	if len(original) != len(translated) {
    164 		return false
    165 	}
    166 	for tag, occurrences := range original {
    167 		if translated[tag] != occurrences {
    168 			return false
    169 		}
    170 	}
    171 	return true
    172 }