Move all exported types to interfaces.go for consistent organisation. Rename interface.go → interfaces.go. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
223 lines
4.9 KiB
Go
223 lines
4.9 KiB
Go
// Package i18n provides internationalization for the CLI.
|
|
package i18n
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// getNumberFormat returns the number format for the current language.
|
|
func getNumberFormat() NumberFormat {
|
|
lang := currentLangForGrammar()
|
|
// Extract base language (en-GB → en)
|
|
if idx := strings.IndexAny(lang, "-_"); idx > 0 {
|
|
lang = lang[:idx]
|
|
}
|
|
if fmt, ok := numberFormats[lang]; ok {
|
|
return fmt
|
|
}
|
|
return numberFormats["en"] // fallback
|
|
}
|
|
|
|
// FormatNumber formats an integer with locale-specific thousands separators.
|
|
//
|
|
// FormatNumber(1234567) // "1,234,567" (en) or "1.234.567" (de)
|
|
func FormatNumber(n int64) string {
|
|
nf := getNumberFormat()
|
|
return formatIntWithSep(n, nf.ThousandsSep)
|
|
}
|
|
|
|
// FormatDecimal formats a float with locale-specific separators.
|
|
// Uses up to 2 decimal places, trimming trailing zeros.
|
|
//
|
|
// FormatDecimal(1234.5) // "1,234.5" (en) or "1.234,5" (de)
|
|
// FormatDecimal(1234.00) // "1,234" (en) or "1.234" (de)
|
|
func FormatDecimal(f float64) string {
|
|
return FormatDecimalN(f, 2)
|
|
}
|
|
|
|
// FormatDecimalN formats a float with N decimal places.
|
|
//
|
|
// FormatDecimalN(1234.5678, 3) // "1,234.568" (en)
|
|
func FormatDecimalN(f float64, decimals int) string {
|
|
nf := getNumberFormat()
|
|
|
|
// Split into integer and fractional parts
|
|
intPart := int64(f)
|
|
fracPart := math.Abs(f - float64(intPart))
|
|
|
|
// Format integer part with thousands separator
|
|
intStr := formatIntWithSep(intPart, nf.ThousandsSep)
|
|
|
|
// Format fractional part
|
|
if decimals <= 0 || fracPart == 0 {
|
|
return intStr
|
|
}
|
|
|
|
// Round and format fractional part
|
|
multiplier := math.Pow(10, float64(decimals))
|
|
fracInt := int64(math.Round(fracPart * multiplier))
|
|
|
|
if fracInt == 0 {
|
|
return intStr
|
|
}
|
|
|
|
// Format with leading zeros, then trim trailing zeros
|
|
fracStr := fmt.Sprintf("%0*d", decimals, fracInt)
|
|
fracStr = strings.TrimRight(fracStr, "0")
|
|
|
|
return intStr + nf.DecimalSep + fracStr
|
|
}
|
|
|
|
// FormatPercent formats a decimal as a percentage.
|
|
//
|
|
// FormatPercent(0.85) // "85%" (en) or "85 %" (de)
|
|
// FormatPercent(0.333) // "33.3%" (en)
|
|
// FormatPercent(1.5) // "150%" (en)
|
|
func FormatPercent(f float64) string {
|
|
nf := getNumberFormat()
|
|
pct := f * 100
|
|
|
|
// Format the number part
|
|
var numStr string
|
|
if pct == float64(int64(pct)) {
|
|
numStr = strconv.FormatInt(int64(pct), 10)
|
|
} else {
|
|
numStr = FormatDecimalN(pct, 1)
|
|
}
|
|
|
|
return fmt.Sprintf(nf.PercentFmt, numStr)
|
|
}
|
|
|
|
// FormatBytes formats bytes as human-readable size.
|
|
//
|
|
// FormatBytes(1536) // "1.5 KB"
|
|
// FormatBytes(1536000) // "1.5 MB"
|
|
// FormatBytes(1536000000) // "1.4 GB"
|
|
func FormatBytes(bytes int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
GB = MB * 1024
|
|
TB = GB * 1024
|
|
)
|
|
|
|
nf := getNumberFormat()
|
|
|
|
var value float64
|
|
var unit string
|
|
|
|
switch {
|
|
case bytes >= TB:
|
|
value = float64(bytes) / TB
|
|
unit = "TB"
|
|
case bytes >= GB:
|
|
value = float64(bytes) / GB
|
|
unit = "GB"
|
|
case bytes >= MB:
|
|
value = float64(bytes) / MB
|
|
unit = "MB"
|
|
case bytes >= KB:
|
|
value = float64(bytes) / KB
|
|
unit = "KB"
|
|
default:
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
|
|
// Format with 1 decimal place, trim .0
|
|
intPart := int64(value)
|
|
fracPart := value - float64(intPart)
|
|
|
|
if fracPart < 0.05 {
|
|
return fmt.Sprintf("%d %s", intPart, unit)
|
|
}
|
|
|
|
fracDigit := int(math.Round(fracPart * 10))
|
|
if fracDigit == 10 {
|
|
return fmt.Sprintf("%d %s", intPart+1, unit)
|
|
}
|
|
|
|
return fmt.Sprintf("%d%s%d %s", intPart, nf.DecimalSep, fracDigit, unit)
|
|
}
|
|
|
|
// FormatOrdinal formats a number as an ordinal.
|
|
//
|
|
// FormatOrdinal(1) // "1st" (en) or "1." (de)
|
|
// FormatOrdinal(2) // "2nd" (en) or "2." (de)
|
|
// FormatOrdinal(3) // "3rd" (en) or "3." (de)
|
|
// FormatOrdinal(11) // "11th" (en) or "11." (de)
|
|
func FormatOrdinal(n int) string {
|
|
lang := currentLangForGrammar()
|
|
// Extract base language
|
|
if idx := strings.IndexAny(lang, "-_"); idx > 0 {
|
|
lang = lang[:idx]
|
|
}
|
|
|
|
// Most languages just use number + period
|
|
switch lang {
|
|
case "en":
|
|
return formatEnglishOrdinal(n)
|
|
default:
|
|
return fmt.Sprintf("%d.", n)
|
|
}
|
|
}
|
|
|
|
// formatEnglishOrdinal returns English ordinal suffix.
|
|
func formatEnglishOrdinal(n int) string {
|
|
abs := n
|
|
if abs < 0 {
|
|
abs = -abs
|
|
}
|
|
|
|
// Special cases for 11, 12, 13
|
|
if abs%100 >= 11 && abs%100 <= 13 {
|
|
return fmt.Sprintf("%dth", n)
|
|
}
|
|
|
|
switch abs % 10 {
|
|
case 1:
|
|
return fmt.Sprintf("%dst", n)
|
|
case 2:
|
|
return fmt.Sprintf("%dnd", n)
|
|
case 3:
|
|
return fmt.Sprintf("%drd", n)
|
|
default:
|
|
return fmt.Sprintf("%dth", n)
|
|
}
|
|
}
|
|
|
|
// formatIntWithSep formats an integer with thousands separator.
|
|
func formatIntWithSep(n int64, sep string) string {
|
|
if sep == "" {
|
|
return strconv.FormatInt(n, 10)
|
|
}
|
|
|
|
negative := n < 0
|
|
if negative {
|
|
n = -n
|
|
}
|
|
|
|
str := strconv.FormatInt(n, 10)
|
|
if len(str) <= 3 {
|
|
if negative {
|
|
return "-" + str
|
|
}
|
|
return str
|
|
}
|
|
|
|
// Insert separators from right to left
|
|
var result strings.Builder
|
|
for i, c := range str {
|
|
if i > 0 && (len(str)-i)%3 == 0 {
|
|
result.WriteString(sep)
|
|
}
|
|
result.WriteRune(c)
|
|
}
|
|
|
|
if negative {
|
|
return "-" + result.String()
|
|
}
|
|
return result.String()
|
|
}
|