How to Implement Multi-Language Time Formatting with fabrica-util's xtime

Use xtime.SetLocale() to load JSON locale files dynamically, then call formatting helpers like FormatDateTime() or FormatWithLanguage() to generate locale-aware strings using template placeholders such as {%y}, {%M}, and {%d}.

The xtime package in the go-pantheon/fabrica-util repository provides a thread-safe, zero-dependency solution for locale-aware time formatting in Go. By abstracting language-specific month names, weekday names, and format templates into JSON configuration files, it enables efficient multi-language time formatting without hardcoding translation logic.

Understanding the xtime Architecture

At its core, xtime stores the active locale in an atomic.Value variable named currentLocale declared at the top of xtime/locale.go. This design choice eliminates lock contention when multiple goroutines read the locale simultaneously.

Each locale is represented by a *Locale struct loaded from JSON files located in xtime/lang/. The package ships with built-in support for English (en), Chinese Simplified (zh-CN), Chinese Traditional (zh-TW), Japanese (ja), and Korean (ko). Each JSON file defines month names, weekday names, duration strings, and format templates using a customizable placeholder syntax.

Loading and Switching Locales

To change the active language, call xtime.SetLocale(Language), which is implemented in xtime/locale.go (lines 62-70). This function performs a thread-safe lookup in an in-memory map, and if the locale isn't loaded yet, it invokes LoadLocale (lines 86-124).

The LoadLocale function uses sanitizeAndBuildPath (lines 26-46) to validate the language code against a regex pattern (^[a-z]{2}(-[A-Z]{2})?$) and construct a safe file path using filepath.Clean. This prevents path traversal attacks while loading the appropriate JSON file from the xtime/lang/ directory.

All formatting functions internally call GetCurrentLocale() (lines 52-60), which retrieves the *Locale instance stored in the atomic.Value without locking.

Formatting Functions and Template Syntax

The high-level API resides in xtime/format.go and provides several convenience methods:

  • Format(t time.Time) – Returns RFC-style layout with timezone
  • FormatDateTime(t time.Time) – Uses the locale's datetime template
  • FormatDate(t time.Time) – Uses the locale's date template
  • FormatTime(t time.Time) – Uses the locale's time template
  • FormatWithLanguage(t time.Time, lang, tmpl string) – Formats using an explicit language without changing the global locale

These functions rely on Locale.FormatTemplate (lines 30-48), which processes placeholder tokens:

Placeholder Meaning
{%y} Year
{%M} Full month name
{%d} Day of month
{%w} Full weekday name
{%h} Hour (24-hour)
{%m} Minute
{%s} Second

For duration and relative time formatting, use FormatDuration() and FormatRelative(), which automatically pluralize based on the locale's rules (e.g., "1 hour" vs "%d hours").

Adding Custom Language Support

Extending xtime to support additional languages requires only a JSON file. Create a new file in xtime/lang/ named with the language code (e.g., fr.json for French) following this schema:

{
  "months": "January|February|March|April|May|June|July|August|September|October|November|December",
  "months_short": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
  "weeks": "Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
  "weeks_short": "Sun|Mon|Tue|Wed|Thu|Fri|Sat",
  "constellations": "Aries|Taurus|Gemini|Cancer|Leo|Virgo|Libra|Scorpio|Sagittarius|Capricorn|Aquarius|Pisces",
  "format": {
    "date": "{%w}, {%M} {%d}, {%y}",
    "datetime": "{%w}, {%M} {%d}, {%y} {%h}:{%m}:{%s}",
    "time": "{%h}:{%m}:{%s}"
  },
  "year": "1 year|%d years",
  "month": "1 month|%d months",
  "week": "1 week|%d weeks",
  "day": "1 day|%d days",
  "hour": "1 hour|%d hours",
  "minute": "1 minute|%d minutes",
  "second": "1 second|%d seconds",
  "now": "just now",
  "ago": "%s ago",
  "from_now": "%s from now"
}

The validLanguageCodes regex (^[a-z]{2}(-[A-Z]{2})?$) ensures only safe language identifiers are accepted. After adding the file, load it with:

if err := xtime.SetLocale("fr"); err != nil {
    log.Fatal(err)
}

Thread Safety and Performance Characteristics

The xtime package is optimized for high-concurrency scenarios:

  • Lazy loading: SetLocale reads JSON files only once per language, storing results in locales map[string]*Locale. Subsequent calls retrieve from memory.
  • Lock-free reads: atomic.Value stores currentLocale, allowing goroutines to read the active locale without mutex contention.
  • Safe path construction: sanitizeAndBuildPath uses filepath.Clean and regex validation to prevent directory traversal when loading locale files.
  • Fallback protection: initDefaultLocale registers English as the default during package initialization, ensuring operations succeed even if locale loading fails.

Practical Implementation Examples

Initialize and Switch Locales

package main

import (
	"fmt"
	"time"

	"github.com/go-pantheon/fabrica-util/xtime"
)

func main() {
	// Initialize with English (optional)
	_ = xtime.InitSimple("en")

	// Switch to Chinese (Simplified)
	if err := xtime.SetLocale(xtime.LanguageZhCN); err != nil {
		panic(err)
	}

	now := time.Now()
	fmt.Println("DateTime (zh-CN):", xtime.FormatDateTime(now))
	fmt.Println("Relative (zh-CN):", xtime.FormatRelative(now.Add(-2*time.Hour)))
}

Format with Explicit Language

Use FormatWithLanguage when you need a one-off format without changing the global locale:

t := time.Date(2023, 12, 25, 15, 30, 45, 0, time.UTC)

// Use Japanese locale only for this call
fmt.Println(xtime.FormatWithLanguage(t, xtime.LanguageJa,
	"{%w}, {%M} {%d}, {%y} {%h}:{%m}:{%s}"))
// Output: 月曜日, 12月 25, 2023 15:30:45

Custom Template Formatting

Create custom layouts using the placeholder syntax:

// Custom layout: "25-Dec-2023 15:30"
tmpl := "{%d}-{%M}-{%y} {%h}:{%m}"
fmt.Println(xtime.FormatLocalized(time.Now(), tmpl))

Concurrent Usage

The atomic storage allows safe concurrent access:

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
	wg.Add(1)
	go func(id int) {
		defer wg.Done()
		fmt.Printf("goroutine %d: %s\n", id, xtime.Format(time.Now()))
	}(i)
}
wg.Wait()

Summary

  • xtime.SetLocale() loads JSON locale files safely using path sanitization and regex validation (^[a-z]{2}(-[A-Z]{2})?$).
  • GetCurrentLocale() retrieves the active locale from an atomic.Value without locking, ensuring thread-safe performance.
  • Template placeholders ({%y}, {%M}, {%d}, etc.) enable flexible formatting defined in JSON configuration files located in xtime/lang/.
  • FormatWithLanguage() allows temporary locale switching without modifying global state.
  • Lazy loading ensures JSON files are read only once, with English serving as a built-in fallback.

Frequently Asked Questions

How does xtime ensure thread safety when switching locales?

xtime stores the current locale in an atomic.Value variable named currentLocale at the package level in xtime/locale.go. When you call SetLocale(), it updates this value atomically. All formatting functions call GetCurrentLocale(), which reads from this atomic.Value without acquiring locks, making it safe for high-concurrency applications.

Can I use multiple languages simultaneously in the same application?

Yes. While xtime maintains a global default locale via SetLocale(), you can use FormatWithLanguage(t time.Time, lang, tmpl string) to format specific timestamps using any loaded language without changing the global state. This function looks up the specified language in the locale map and applies the template using that language's configuration.

What is the performance impact of adding many language files?

Negligible. xtime implements lazy loading: LoadLocale reads and parses a JSON file only when first requested via SetLocale(). The resulting *Locale struct is cached in an in-memory map (locales map[string]*Locale). Subsequent formatting calls use the cached struct, so file I/O occurs only once per language per program lifecycle.

How do I validate that my custom language JSON file will be accepted?

Ensure your filename matches the regex ^[a-z]{2}(-[A-Z]{2})?$ (e.g., fr.json, pt-BR.json). The file must reside in xtime/lang/ and contain the required fields: months, weeks, format (with date, datetime, and time sub-fields), and duration strings (year, month, day, etc.). The sanitizeAndBuildPath function in xtime/locale.go validates these constraints before attempting to open the file.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →