feat(i18n): add N() helper for numeric formatting

N(format, value) wraps T("i18n.numeric.{format}", value).

Supported formats:
- N("number", 1234567)  → "1,234,567"
- N("decimal", 1234.5)  → "1,234.5"
- N("percent", 0.85)    → "85%"
- N("bytes", 1536000)   → "1.5 MB"
- N("ordinal", 1)       → "1st"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 17:33:18 +00:00
parent 7b9f23dd9e
commit 8f977ed635
2 changed files with 51 additions and 28 deletions

View file

@ -56,6 +56,17 @@ func _(messageID string, args ...any) string {
return messageID
}
// N formats a number using the i18n.numeric.* namespace.
// Wrapper for T("i18n.numeric.{format}", value).
//
// N("number", 1234567) // T("i18n.numeric.number", 1234567)
// N("percent", 0.85) // T("i18n.numeric.percent", 0.85)
// N("bytes", 1536000) // T("i18n.numeric.bytes", 1536000)
// N("ordinal", 1) // T("i18n.numeric.ordinal", 1)
func N(format string, value any) string {
return T("i18n.numeric."+format, value)
}
// --- Template helpers ---
// executeIntentTemplate executes an intent template with the given data.

View file

@ -314,36 +314,48 @@ func (s *Service) handleI18nNamespace(key string, args []any) string {
return ActionFailed(verb, "")
}
// i18n.number → FormatNumber(n)
if key == "i18n.number" && len(args) > 0 {
return FormatNumber(toInt64(args[0]))
// i18n.numeric.* namespace (for N() helper)
if strings.HasPrefix(key, "i18n.numeric.") && len(args) > 0 {
format := strings.TrimPrefix(key, "i18n.numeric.")
switch format {
case "number", "int":
return FormatNumber(toInt64(args[0]))
case "decimal", "float":
return FormatDecimal(toFloat64(args[0]))
case "percent", "pct":
return FormatPercent(toFloat64(args[0]))
case "bytes", "size":
return FormatBytes(toInt64(args[0]))
case "ordinal", "ord":
return FormatOrdinal(toInt(args[0]))
case "ago":
if len(args) >= 2 {
if unit, ok := args[1].(string); ok {
return FormatAgo(toInt(args[0]), unit)
}
}
}
}
// i18n.decimal → FormatDecimal(f)
if key == "i18n.decimal" && len(args) > 0 {
return FormatDecimal(toFloat64(args[0]))
}
// i18n.percent → FormatPercent(f)
if key == "i18n.percent" && len(args) > 0 {
return FormatPercent(toFloat64(args[0]))
}
// i18n.bytes → FormatBytes(n)
if key == "i18n.bytes" && len(args) > 0 {
return FormatBytes(toInt64(args[0]))
}
// i18n.ordinal → FormatOrdinal(n)
if key == "i18n.ordinal" && len(args) > 0 {
return FormatOrdinal(toInt(args[0]))
}
// i18n.ago → FormatAgo(count, unit)
if key == "i18n.ago" && len(args) >= 2 {
count := toInt(args[0])
if unit, ok := args[1].(string); ok {
return FormatAgo(count, unit)
// Legacy i18n.{format} shortcuts (kept for compatibility)
if len(args) > 0 {
switch key {
case "i18n.number":
return FormatNumber(toInt64(args[0]))
case "i18n.decimal":
return FormatDecimal(toFloat64(args[0]))
case "i18n.percent":
return FormatPercent(toFloat64(args[0]))
case "i18n.bytes":
return FormatBytes(toInt64(args[0]))
case "i18n.ordinal":
return FormatOrdinal(toInt(args[0]))
case "i18n.ago":
if len(args) >= 2 {
if unit, ok := args[1].(string); ok {
return FormatAgo(toInt(args[0]), unit)
}
}
}
}