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

> Effortlessly implement multi-language time formatting with fabrica-util's xtime. Load locales dynamically and format dates with ease using SetLocale and FormatDateTime functions.

- Repository: [Pantheon/fabrica-util](https://github.com/go-pantheon/fabrica-util)
- Tags: how-to-guide
- Published: 2026-03-02

---

**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`](https://github.com/go-pantheon/fabrica-util/blob/main/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`](https://github.com/go-pantheon/fabrica-util/blob/main/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`](https://github.com/go-pantheon/fabrica-util/blob/main/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`](https://github.com/go-pantheon/fabrica-util/blob/main/fr.json) for French) following this schema:

```json
{
  "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:

```go
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

```go
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:

```go
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:

```go
// 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:

```go
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`](https://github.com/go-pantheon/fabrica-util/blob/main/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`](https://github.com/go-pantheon/fabrica-util/blob/main/fr.json), [`pt-BR.json`](https://github.com/go-pantheon/fabrica-util/blob/main/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`](https://github.com/go-pantheon/fabrica-util/blob/main/xtime/locale.go) validates these constraints before attempting to open the file.