// 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() }