package i18n import ( "strconv" "strings" "time" ) // ── Dates and numbers per language (R-562, v0.308.0, `09` §3 decision 223) ──────────────────────── // // The ONE place the dashboard decides how a date, a time or a decimal number looks: // // Hungarian 2026. 10. 10. 15:04 1,5 GB (the Hungarian date order with its dots; decimal COMMA) // English 2026-10-10 15:04 1.5 GB (ISO-like; decimal point) // // Every function takes the time ALREADY in the zone it is shown in (the dashboard shows Budapest // local time; the callers convert). An unknown language formats as Hungarian, like every other // fallback in this package. Pinned by TestFormat_* in format_test.go. // FormatDateTime renders a date with hour and minute. func FormatDateTime(lang string, t time.Time) string { if Normalize(lang) == "en" { return t.Format("2006-01-02 15:04") } return t.Format("2006. 01. 02. 15:04") } // FormatDate renders a date alone. func FormatDate(lang string, t time.Time) string { if Normalize(lang) == "en" { return t.Format("2006-01-02") } return t.Format("2006. 01. 02.") } // FormatMonthDayTime renders a date in the current year: month, day, hour and minute. func FormatMonthDayTime(lang string, t time.Time) string { if Normalize(lang) == "en" { return t.Format("01-02 15:04") } return t.Format("01. 02. 15:04") } // FormatDecimal renders v with prec digits after the decimal separator of lang. func FormatDecimal(lang string, v float64, prec int) string { s := strconv.FormatFloat(v, 'f', prec, 64) if Normalize(lang) == "en" { return s } return strings.Replace(s, ".", ",", 1) } // LocalizeDecimals rewrites the decimal POINT of every number in an already formatted short string // ("1.5 GB", "Immich (95.0 GB)") into lang's separator. Only a point with a digit on BOTH sides is // touched, so a sentence's full stop, a version ("0.307.0" — see below) or an address is not a target // by accident: a dot preceded by a digit and followed by a digit is rewritten only when the number it // belongs to has exactly ONE dot. English returns s unchanged. // // For strings another package formatted with %.1f (appbackup.HumanizeBytes and its siblings) and // handed to a page as data. A new formatter should call FormatDecimal instead. func LocalizeDecimals(lang, s string) string { if Normalize(lang) == "en" || !strings.Contains(s, ".") { return s } b := []byte(s) isDigit := func(c byte) bool { return c >= '0' && c <= '9' } for i := 1; i < len(b)-1; i++ { if b[i] != '.' || !isDigit(b[i-1]) || !isDigit(b[i+1]) { continue } // The whole digit-and-dot run around this dot: more than one dot is a version or an address. start, end := i, i for start > 0 && (isDigit(b[start-1]) || b[start-1] == '.') { start-- } for end < len(b)-1 && (isDigit(b[end+1]) || b[end+1] == '.') { end++ } if strings.Count(string(b[start:end+1]), ".") != 1 { i = end continue } b[i] = ',' } return string(b) }