Browse Source

i18n module, initial import.

Bozhin Zafirov 2 months ago
parent
commit
6b9f6526d7
5 changed files with 198 additions and 2 deletions
  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:
 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.
 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
+}