159 lines
3.4 KiB
Go
159 lines
3.4 KiB
Go
// Package i18n — katalog bahasa (F-023). Bahasa Indonesia default,
|
|
// fallback otomatis ke English bila kunci belum ada di katalog aktif.
|
|
// Katalog di-embed ke binary (tetap single binary).
|
|
package i18n
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
//go:embed locales/*.yaml
|
|
var locales embed.FS
|
|
|
|
const (
|
|
DefaultLang = "id"
|
|
FallbackLang = "en"
|
|
)
|
|
|
|
var (
|
|
mu sync.RWMutex
|
|
active = DefaultLang
|
|
catalogs = map[string]map[string]string{}
|
|
)
|
|
|
|
func init() {
|
|
for _, lang := range []string{DefaultLang, FallbackLang} {
|
|
if c, err := load(lang); err == nil {
|
|
catalogs[lang] = c
|
|
}
|
|
}
|
|
}
|
|
|
|
// shortcut: bahasa hanya dari TIA_LANG + default id — akan dibaca dari
|
|
// config.yaml (ui.language) begitu internal/config masuk (Phase 1).
|
|
func load(lang string) (map[string]string, error) {
|
|
b, err := locales.ReadFile("locales/" + lang + ".yaml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var raw map[string]any
|
|
if err := yaml.Unmarshal(b, &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
flatten("", raw, out)
|
|
return out, nil
|
|
}
|
|
|
|
func flatten(prefix string, in map[string]any, out map[string]string) {
|
|
for k, v := range in {
|
|
key := k
|
|
if prefix != "" {
|
|
key = prefix + "." + k
|
|
}
|
|
switch vv := v.(type) {
|
|
case map[string]any:
|
|
flatten(key, vv, out)
|
|
case string:
|
|
out[key] = vv
|
|
default:
|
|
out[key] = fmt.Sprint(vv)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetLang mengganti bahasa aktif (dipakai wizard & layar Pengaturan).
|
|
func SetLang(lang string) {
|
|
mu.Lock()
|
|
active = lang
|
|
mu.Unlock()
|
|
}
|
|
|
|
func Lang() string {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
return active
|
|
}
|
|
|
|
// T — terjemahan; fallback: bahasa aktif → en → key itu sendiri.
|
|
func T(key string) string {
|
|
mu.RLock()
|
|
lang := active
|
|
mu.RUnlock()
|
|
if v, ok := catalogs[lang][key]; ok {
|
|
return v
|
|
}
|
|
if v, ok := catalogs[FallbackLang][key]; ok {
|
|
return v
|
|
}
|
|
return key
|
|
}
|
|
|
|
// Tf — T + fmt.Sprintf.
|
|
func Tf(key string, args ...any) string {
|
|
s := T(key)
|
|
if len(args) == 0 {
|
|
return s
|
|
}
|
|
return fmt.Sprintf(s, args...)
|
|
}
|
|
|
|
// TOr — T dengan fallback eksplisit (mis. nama state Docker apa adanya).
|
|
func TOr(key, fallback string) string {
|
|
mu.RLock()
|
|
lang := active
|
|
mu.RUnlock()
|
|
if v, ok := catalogs[lang][key]; ok {
|
|
return v
|
|
}
|
|
if v, ok := catalogs[FallbackLang][key]; ok {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// Keys — semua kunci pada katalog lang (untuk tes paritas katalog).
|
|
func Keys(lang string) []string {
|
|
out := make([]string, 0, len(catalogs[lang]))
|
|
for k := range catalogs[lang] {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// --- nama hari & bulan (locale data) ---
|
|
|
|
var weekdays = map[string][]string{
|
|
"id": {"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"},
|
|
"en": {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
|
}
|
|
|
|
var months = map[string][]string{
|
|
"id": {"Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"},
|
|
"en": {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"},
|
|
}
|
|
|
|
func WeekdayName(t time.Time) string { return pick(weekdays, int(t.Weekday())) }
|
|
func MonthName(t time.Time) string { return pick(months, int(t.Month())-1) }
|
|
|
|
func pick(m map[string][]string, i int) string {
|
|
mu.RLock()
|
|
lang := active
|
|
mu.RUnlock()
|
|
arr, ok := m[lang]
|
|
if !ok {
|
|
arr = m[FallbackLang]
|
|
}
|
|
if i < 0 || i >= len(arr) {
|
|
return ""
|
|
}
|
|
return arr[i]
|
|
}
|