diff --git a/cmd/php/php_deploy.go b/cmd/php/php_deploy.go index 32404d8c..3a764e40 100644 --- a/cmd/php/php_deploy.go +++ b/cmd/php/php_deploy.go @@ -338,7 +338,7 @@ func printDeploymentSummary(index int, status *phppkg.DeploymentStatus) { age := "" if !status.StartedAt.IsZero() { - age = formatTimeAgo(status.StartedAt) + age = i18n.TimeAgo(status.StartedAt) } fmt.Printf(" %s %s %s", @@ -362,29 +362,3 @@ func printDeploymentSummary(index int, status *phppkg.DeploymentStatus) { fmt.Println() } -func formatTimeAgo(t time.Time) string { - duration := time.Since(t) - - switch { - case duration < time.Minute: - return i18n.T("cli.time.just_now") - case duration < time.Hour: - mins := int(duration.Minutes()) - if mins == 1 { - return i18n.T("cli.time.minute_ago") - } - return i18n.T("cli.time.minutes_ago", map[string]interface{}{"Count": mins}) - case duration < 24*time.Hour: - hours := int(duration.Hours()) - if hours == 1 { - return i18n.T("cli.time.hour_ago") - } - return i18n.T("cli.time.hours_ago", map[string]interface{}{"Count": hours}) - default: - days := int(duration.Hours() / 24) - if days == 1 { - return i18n.T("cli.time.day_ago") - } - return i18n.T("cli.time.days_ago", map[string]interface{}{"Count": days}) - } -} diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index a35c13b9..13849cb1 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -750,6 +750,14 @@ func (s *Service) handleI18nNamespace(key string, args []any) string { 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) + } + } + return "" } diff --git a/pkg/i18n/time.go b/pkg/i18n/time.go new file mode 100644 index 00000000..6bececf4 --- /dev/null +++ b/pkg/i18n/time.go @@ -0,0 +1,55 @@ +// Package i18n provides internationalization for the CLI. +package i18n + +import ( + "fmt" + "time" +) + +// TimeAgo returns a localized relative time string. +// +// TimeAgo(time.Now().Add(-5 * time.Minute)) // "5 minutes ago" +// TimeAgo(time.Now().Add(-1 * time.Hour)) // "1 hour ago" +func TimeAgo(t time.Time) string { + duration := time.Since(t) + + switch { + case duration < time.Minute: + return T("time.just_now") + case duration < time.Hour: + mins := int(duration.Minutes()) + return FormatAgo(mins, "minute") + case duration < 24*time.Hour: + hours := int(duration.Hours()) + return FormatAgo(hours, "hour") + case duration < 7*24*time.Hour: + days := int(duration.Hours() / 24) + return FormatAgo(days, "day") + default: + weeks := int(duration.Hours() / (24 * 7)) + return FormatAgo(weeks, "week") + } +} + +// FormatAgo formats "N unit ago" with proper pluralization. +// Uses locale-specific patterns from time.ago.{unit}. +// +// FormatAgo(5, "minute") // "5 minutes ago" +// FormatAgo(1, "hour") // "1 hour ago" +func FormatAgo(count int, unit string) string { + svc := Default() + if svc == nil { + return fmt.Sprintf("%d %ss ago", count, unit) + } + + // Try locale-specific pattern: time.ago.{unit} + key := "time.ago." + unit + result := svc.T(key, map[string]any{"Count": count}) + + // If key was returned as-is (not found), compose fallback + if result == key { + return fmt.Sprintf("%d %s ago", count, Pluralize(unit, count)) + } + + return result +} diff --git a/pkg/i18n/time_test.go b/pkg/i18n/time_test.go new file mode 100644 index 00000000..ee2e0f24 --- /dev/null +++ b/pkg/i18n/time_test.go @@ -0,0 +1,85 @@ +package i18n + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFormatAgo(t *testing.T) { + svc, err := New() + require.NoError(t, err) + SetDefault(svc) + + tests := []struct { + name string + count int + unit string + expected string + }{ + {"1 second", 1, "second", "1 second ago"}, + {"5 seconds", 5, "second", "5 seconds ago"}, + {"1 minute", 1, "minute", "1 minute ago"}, + {"30 minutes", 30, "minute", "30 minutes ago"}, + {"1 hour", 1, "hour", "1 hour ago"}, + {"3 hours", 3, "hour", "3 hours ago"}, + {"1 day", 1, "day", "1 day ago"}, + {"7 days", 7, "day", "7 days ago"}, + {"1 week", 1, "week", "1 week ago"}, + {"2 weeks", 2, "week", "2 weeks ago"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatAgo(tt.count, tt.unit) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTimeAgo(t *testing.T) { + svc, err := New() + require.NoError(t, err) + SetDefault(svc) + + tests := []struct { + name string + ago time.Duration + expected string + }{ + {"just now", 30 * time.Second, "just now"}, + {"1 minute", 1 * time.Minute, "1 minute ago"}, + {"5 minutes", 5 * time.Minute, "5 minutes ago"}, + {"1 hour", 1 * time.Hour, "1 hour ago"}, + {"3 hours", 3 * time.Hour, "3 hours ago"}, + {"1 day", 24 * time.Hour, "1 day ago"}, + {"3 days", 3 * 24 * time.Hour, "3 days ago"}, + {"1 week", 7 * 24 * time.Hour, "1 week ago"}, + {"2 weeks", 14 * 24 * time.Hour, "2 weeks ago"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TimeAgo(time.Now().Add(-tt.ago)) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestI18nAgoNamespace(t *testing.T) { + svc, err := New() + require.NoError(t, err) + SetDefault(svc) + + t.Run("i18n.ago pattern", func(t *testing.T) { + result := T("i18n.ago", 5, "minute") + assert.Equal(t, "5 minutes ago", result) + }) + + t.Run("i18n.ago singular", func(t *testing.T) { + result := T("i18n.ago", 1, "hour") + assert.Equal(t, "1 hour ago", result) + }) +}