浏览代码

i18n module, initial import.

Bozhin Zafirov 2 月之前
父节点
当前提交
6b9f6526d7
共有 5 个文件被更改,包括 198 次插入2 次删除
  1. 1 1
      LICENSE
  2. 74 1
      README.md
  3. 73 0
      detect/http.go
  4. 3 0
      go.mod
  5. 47 0
      translator.go

+ 1 - 1
LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) <year> <owner> All rights reserved.
+Copyright (c) 2026 Bozhin Zafirov All rights reserved.
 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

+ 74 - 1
README.md

@@ -1,2 +1,75 @@
-# i18n
+# i18n - translations module for Go
 
+i18n is a set of functions to help with project translations and language detection.
+
+## Usage
+
+First get the module:
+
+    go get go.deck17.com/i18n
+
+
+Import the the module in the final project:
+
+    import (
+        "go.deck17.com/i18n"
+        "go.deck17.com/i18n/detect"
+    )
+
+
+## Translate text
+
+i18n exports a single constructor that accepts a map[string]map[string]string with language and translations maps. As
+a result it returns two funcs, each accepting language code as a string, key to translate and optional arguments for
+string substitution (as formatted by fmt.Sprintf). The returned functions are defined as:
+
+    func(string, string, args ...any) string
+    func(string, string, args ...any) error
+
+### Initialize translator
+
+This is an example how to initialize the translator:
+
+    var translations = map[string]map[string]string{
+        "en": {
+            "hello": "Hello World!",
+        },
+        "de": {
+            "hello": "Hallo, Welt!",
+        },
+    }
+
+    // in this case "en" is the default language
+    var T, Terr = i18n.NewTranslator(translations, "en")
+
+
+### Use translator in the code
+
+Translate text inside Go code:
+
+    fmt.Printf("Translated text (en): %s\n", T("en", "hello"))
+    fmt.Printf("Translated text (de): %s\n", T("de", "hello"))
+
+### Translated error messages
+
+Similarly, Terr can be used to generate localized error messages instead of strings:
+
+    func do_stuff() error {
+        return Terr("en", "hello")
+    }
+
+
+## Detecting language in http server applications
+
+The i18n/detect module contains a function that can be used to detect currently selected web language. It uses query
+string and cookies to do so and automatically sends / updates cookie when language is changed (with query parameter).
+
+    // the first language in the list is used as a default language
+    var detectLanguage = detect.NewHttpDetector(
+        []string{"en", "de"}, // list of languages
+        "/",                  // cookie path
+        31536000,             // max age in seconds
+    }
+
+The result function detectLanguage accepts http.ResponseWriter and \*http.Request arguments and returns string with
+language code.

+ 73 - 0
detect/http.go

@@ -0,0 +1,73 @@
+package detect
+
+import (
+	"net/http"
+	"strings"
+)
+
+/* NewHttpDetector initializes new language detector */
+func NewHttpDetector(languages []string, path string, maxAge int) func(http.ResponseWriter, *http.Request) string {
+	const cookieName = "lang"
+	if len(languages) == 0 {
+		panic("empty languages list")
+	}
+	/* initialize state */
+	defaultLang := strings.ToLower(languages[0])
+	langSet := make(map[string]struct{}, len(languages))
+	for _, lang := range languages {
+		langSet[strings.ToLower(lang)] = struct{}{}
+	}
+
+	/* detectLanguage returns a language string */
+	return func(w http.ResponseWriter, r *http.Request) string {
+		/* check for query language override */
+		if lang := strings.ToLower(r.URL.Query().Get(cookieName)); lang != "" {
+			if _, ok := langSet[lang]; ok {
+				/* language provided by query string, enforce it and send cookie */
+				http.SetCookie(w, &http.Cookie{
+					Name:     cookieName,
+					Value:    lang,
+					Path:     path,
+					HttpOnly: true,
+					SameSite: http.SameSiteLaxMode,
+					Secure:   r.TLS != nil,
+					MaxAge:   maxAge,
+				})
+				return lang
+			}
+		}
+		/* check for cookie preferences */
+		cookie, err := r.Cookie(cookieName)
+		if err == nil {
+			cookieLang := strings.ToLower(cookie.Value)
+			if _, ok := langSet[cookieLang]; ok {
+				return cookieLang
+			}
+		}
+		/* fall back to browser preferences */
+		acceptLanguage := strings.ToLower(r.Header.Get("Accept-Language"))
+		parts := strings.Split(acceptLanguage, ",")
+		for _, part := range parts {
+			part = strings.TrimSpace(part)
+			if part == "" {
+				continue
+			}
+			/* cut off ";q=..." */
+			if idx := strings.IndexByte(part, ';'); idx >= 0 {
+				part = part[:idx]
+			}
+			/* try exact match first */
+			if _, ok := langSet[part]; ok {
+				return part
+			}
+			/* try base language */
+			if i := strings.IndexByte(part, '-'); i >= 0 {
+				base := part[:i]
+				if _, ok := langSet[base]; ok {
+					return base
+				}
+			}
+		}
+		return defaultLang
+	}
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module go.deck17.com/i18n
+
+go 1.24.4

+ 47 - 0
translator.go

@@ -0,0 +1,47 @@
+package i18n
+
+import (
+	"errors"
+	"fmt"
+)
+
+/* NewTranslator creates closure data map and returns translation functions */
+func NewTranslator(data map[string]map[string]string, defaultLang string) (func(string, string, ...any) string, func(string, string, ...any) error) {
+	/* initialize map and copy data */
+	translationMap := make(map[string]map[string]string, len(data))
+	for lang, translation := range data {
+		translationMap[lang] = make(map[string]string, len(translation))
+		for key, value := range translation {
+			translationMap[lang][key] = value
+		}
+	}
+	/* T translates and returns localized string */
+	T := func(lang string, key string, args ...any) string {
+		/* attempt direct translation */
+		if l, ok := translationMap[lang]; ok {
+			if tr, ok := l[key]; ok {
+				if len(args) == 0 {
+					return tr
+				}
+				return fmt.Sprintf(tr, args...)
+			}
+		}
+		/* fall back to default language */
+		if l, ok := translationMap[defaultLang]; ok {
+			if tr, ok := l[key]; ok {
+				if len(args) == 0 {
+					return tr
+				}
+				return fmt.Sprintf(tr, args...)
+			}
+		}
+		/* no localized text, return key */
+		return key
+	}
+	/* Terr translates localized string and returns it into error */
+	Terr := func(lang string, key string, args ...any) error {
+		return errors.New(T(lang, key, args...))
+	}
+	/* return functions */
+	return T, Terr
+}