fd28d70bff
Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged. R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
87 lines
3.0 KiB
Go
87 lines
3.0 KiB
Go
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)
|
|
}
|