From 650c7b1c5a3d0728187afe1bd174a315eba6978b Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 30 Jan 2026 17:24:24 +0000 Subject: [PATCH] refactor(i18n): consolidate types into interfaces.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all exported types to interfaces.go for consistent organisation. Rename interface.go → interfaces.go. Co-Authored-By: Claude Opus 4.5 --- pkg/i18n/compose.go | 59 ---- pkg/i18n/grammar.go | 276 ----------------- pkg/i18n/i18n.go | 9 - pkg/i18n/interface.go | 151 ---------- pkg/i18n/interfaces.go | 654 +++++++++++++++++++++++++++++++++++++++++ pkg/i18n/language.go | 109 ------- pkg/i18n/mode.go | 12 - pkg/i18n/numbers.go | 16 - pkg/i18n/service.go | 20 -- 9 files changed, 654 insertions(+), 652 deletions(-) delete mode 100644 pkg/i18n/interface.go create mode 100644 pkg/i18n/interfaces.go diff --git a/pkg/i18n/compose.go b/pkg/i18n/compose.go index 20fbb75..dfabbe0 100644 --- a/pkg/i18n/compose.go +++ b/pkg/i18n/compose.go @@ -5,22 +5,6 @@ import ( "fmt" ) -// Subject represents a typed subject with metadata for semantic translations. -// Use S() to create a Subject and chain methods for additional context. -// -// S("file", "config.yaml") -// S("repo", "core-php").Count(3) -// S("user", user).Gender("feminine") -// S("colleague", name).Formal() -type Subject struct { - Noun string // The noun type (e.g., "file", "repo", "user") - Value any // The actual value (e.g., filename, struct, etc.) - count int // Count for pluralization (default 1) - gender string // Grammatical gender for languages that need it - location string // Location context (e.g., "in workspace") - formality Formality // Formality level override (-1 = use service default) -} - // S creates a new Subject with the given noun and value. // The noun is used for grammar rules, the value for display. // @@ -161,48 +145,6 @@ func (s *Subject) IsInformal() bool { return s != nil && s.formality == FormalityInformal } -// IntentMeta defines the behaviour and characteristics of an intent. -type IntentMeta struct { - Type string // "action", "question", "info" - Verb string // Reference to verb key (e.g., "delete", "save") - Dangerous bool // If true, requires extra confirmation - Default string // Default response: "yes" or "no" - Supports []string // Extra options supported by this intent -} - -// Composed holds all output forms for an intent after template resolution. -// Each field is ready to display to the user. -type Composed struct { - Question string // Question form: "Delete config.yaml?" - Confirm string // Confirmation form: "Really delete config.yaml?" - Success string // Success message: "config.yaml deleted" - Failure string // Failure message: "Failed to delete config.yaml" - Meta IntentMeta // Intent metadata for UI decisions -} - -// Intent defines a semantic intent with templates for all output forms. -// Templates use Go text/template syntax with Subject data available. -type Intent struct { - Meta IntentMeta // Intent behaviour and characteristics - Question string // Template for question form - Confirm string // Template for confirmation form - Success string // Template for success message - Failure string // Template for failure message -} - -// templateData is passed to intent templates during execution. -type templateData struct { - Subject string // Display value of subject - Noun string // Noun type - Count int // Count for pluralization - Gender string // Grammatical gender - Location string // Location context - Formality Formality // Formality level - IsFormal bool // Convenience: formality == FormalityFormal - IsPlural bool // Convenience: count != 1 - Value any // Raw value (for complex templates) -} - // newTemplateData creates templateData from a Subject. func newTemplateData(s *Subject) templateData { if s == nil { @@ -220,4 +162,3 @@ func newTemplateData(s *Subject) templateData { Value: s.Value, } } - diff --git a/pkg/i18n/grammar.go b/pkg/i18n/grammar.go index 4b1a294..f58decb 100644 --- a/pkg/i18n/grammar.go +++ b/pkg/i18n/grammar.go @@ -3,48 +3,10 @@ package i18n import ( "strings" - "sync" "text/template" "unicode" ) -// GrammarData holds language-specific grammar forms loaded from JSON. -type GrammarData struct { - Verbs map[string]VerbForms // verb -> forms - Nouns map[string]NounForms // noun -> forms - Articles ArticleForms // article configuration - Words map[string]string // base word translations - Punct PunctuationRules // language-specific punctuation -} - -// PunctuationRules holds language-specific punctuation patterns. -// French uses " :" (space before colon), English uses ":" -type PunctuationRules struct { - LabelSuffix string // Suffix for labels (default ":") - ProgressSuffix string // Suffix for progress (default "...") -} - -// NounForms holds plural and gender information for a noun. -type NounForms struct { - One string // Singular form - Other string // Plural form - Gender string // Grammatical gender (masculine, feminine, neuter, common) -} - -// ArticleForms holds article configuration for a language. -type ArticleForms struct { - IndefiniteDefault string // Default indefinite article (e.g., "a") - IndefiniteVowel string // Indefinite article before vowel sounds (e.g., "an") - Definite string // Definite article (e.g., "the") - ByGender map[string]string // Gender-specific articles for gendered languages -} - -// grammarCache holds loaded grammar data per language. -var ( - grammarCache = make(map[string]*GrammarData) - grammarCacheMu sync.RWMutex -) - // getGrammarData returns the grammar data for the current language. // Returns nil if no grammar data is loaded for the language. func getGrammarData(lang string) *GrammarData { @@ -140,132 +102,6 @@ func currentLangForGrammar() string { return "en-GB" } -// VerbForms holds irregular verb conjugations. -type VerbForms struct { - Past string // Past tense (e.g., "deleted") - Gerund string // Present participle (e.g., "deleting") -} - -// irregularVerbs maps base verbs to their irregular forms. -var irregularVerbs = map[string]VerbForms{ - "be": {Past: "was", Gerund: "being"}, - "have": {Past: "had", Gerund: "having"}, - "do": {Past: "did", Gerund: "doing"}, - "go": {Past: "went", Gerund: "going"}, - "make": {Past: "made", Gerund: "making"}, - "get": {Past: "got", Gerund: "getting"}, - "run": {Past: "ran", Gerund: "running"}, - "set": {Past: "set", Gerund: "setting"}, - "put": {Past: "put", Gerund: "putting"}, - "cut": {Past: "cut", Gerund: "cutting"}, - "let": {Past: "let", Gerund: "letting"}, - "hit": {Past: "hit", Gerund: "hitting"}, - "shut": {Past: "shut", Gerund: "shutting"}, - "split": {Past: "split", Gerund: "splitting"}, - "spread": {Past: "spread", Gerund: "spreading"}, - "read": {Past: "read", Gerund: "reading"}, - "write": {Past: "wrote", Gerund: "writing"}, - "send": {Past: "sent", Gerund: "sending"}, - "build": {Past: "built", Gerund: "building"}, - "begin": {Past: "began", Gerund: "beginning"}, - "find": {Past: "found", Gerund: "finding"}, - "take": {Past: "took", Gerund: "taking"}, - "see": {Past: "saw", Gerund: "seeing"}, - "keep": {Past: "kept", Gerund: "keeping"}, - "hold": {Past: "held", Gerund: "holding"}, - "tell": {Past: "told", Gerund: "telling"}, - "bring": {Past: "brought", Gerund: "bringing"}, - "think": {Past: "thought", Gerund: "thinking"}, - "buy": {Past: "bought", Gerund: "buying"}, - "catch": {Past: "caught", Gerund: "catching"}, - "teach": {Past: "taught", Gerund: "teaching"}, - "throw": {Past: "threw", Gerund: "throwing"}, - "grow": {Past: "grew", Gerund: "growing"}, - "know": {Past: "knew", Gerund: "knowing"}, - "show": {Past: "showed", Gerund: "showing"}, - "draw": {Past: "drew", Gerund: "drawing"}, - "break": {Past: "broke", Gerund: "breaking"}, - "speak": {Past: "spoke", Gerund: "speaking"}, - "choose": {Past: "chose", Gerund: "choosing"}, - "forget": {Past: "forgot", Gerund: "forgetting"}, - "lose": {Past: "lost", Gerund: "losing"}, - "win": {Past: "won", Gerund: "winning"}, - "swim": {Past: "swam", Gerund: "swimming"}, - "drive": {Past: "drove", Gerund: "driving"}, - "rise": {Past: "rose", Gerund: "rising"}, - "shine": {Past: "shone", Gerund: "shining"}, - "sing": {Past: "sang", Gerund: "singing"}, - "ring": {Past: "rang", Gerund: "ringing"}, - "drink": {Past: "drank", Gerund: "drinking"}, - "sink": {Past: "sank", Gerund: "sinking"}, - "sit": {Past: "sat", Gerund: "sitting"}, - "stand": {Past: "stood", Gerund: "standing"}, - "hang": {Past: "hung", Gerund: "hanging"}, - "dig": {Past: "dug", Gerund: "digging"}, - "stick": {Past: "stuck", Gerund: "sticking"}, - "bite": {Past: "bit", Gerund: "biting"}, - "hide": {Past: "hid", Gerund: "hiding"}, - "feed": {Past: "fed", Gerund: "feeding"}, - "meet": {Past: "met", Gerund: "meeting"}, - "lead": {Past: "led", Gerund: "leading"}, - "sleep": {Past: "slept", Gerund: "sleeping"}, - "feel": {Past: "felt", Gerund: "feeling"}, - "leave": {Past: "left", Gerund: "leaving"}, - "mean": {Past: "meant", Gerund: "meaning"}, - "lend": {Past: "lent", Gerund: "lending"}, - "spend": {Past: "spent", Gerund: "spending"}, - "bend": {Past: "bent", Gerund: "bending"}, - "deal": {Past: "dealt", Gerund: "dealing"}, - "lay": {Past: "laid", Gerund: "laying"}, - "pay": {Past: "paid", Gerund: "paying"}, - "say": {Past: "said", Gerund: "saying"}, - "sell": {Past: "sold", Gerund: "selling"}, - "seek": {Past: "sought", Gerund: "seeking"}, - "fight": {Past: "fought", Gerund: "fighting"}, - "fly": {Past: "flew", Gerund: "flying"}, - "wear": {Past: "wore", Gerund: "wearing"}, - "tear": {Past: "tore", Gerund: "tearing"}, - "bear": {Past: "bore", Gerund: "bearing"}, - "swear": {Past: "swore", Gerund: "swearing"}, - "wake": {Past: "woke", Gerund: "waking"}, - "freeze": {Past: "froze", Gerund: "freezing"}, - "steal": {Past: "stole", Gerund: "stealing"}, - "overwrite": {Past: "overwritten", Gerund: "overwriting"}, - "reset": {Past: "reset", Gerund: "resetting"}, - "reboot": {Past: "rebooted", Gerund: "rebooting"}, - - // Multi-syllable verbs with stressed final syllables (double consonant) - "submit": {Past: "submitted", Gerund: "submitting"}, - "permit": {Past: "permitted", Gerund: "permitting"}, - "admit": {Past: "admitted", Gerund: "admitting"}, - "omit": {Past: "omitted", Gerund: "omitting"}, - "commit": {Past: "committed", Gerund: "committing"}, - "transmit": {Past: "transmitted", Gerund: "transmitting"}, - "prefer": {Past: "preferred", Gerund: "preferring"}, - "refer": {Past: "referred", Gerund: "referring"}, - "transfer": {Past: "transferred", Gerund: "transferring"}, - "defer": {Past: "deferred", Gerund: "deferring"}, - "confer": {Past: "conferred", Gerund: "conferring"}, - "infer": {Past: "inferred", Gerund: "inferring"}, - "occur": {Past: "occurred", Gerund: "occurring"}, - "recur": {Past: "recurred", Gerund: "recurring"}, - "incur": {Past: "incurred", Gerund: "incurring"}, - "deter": {Past: "deterred", Gerund: "deterring"}, - "control": {Past: "controlled", Gerund: "controlling"}, - "patrol": {Past: "patrolled", Gerund: "patrolling"}, - "compel": {Past: "compelled", Gerund: "compelling"}, - "expel": {Past: "expelled", Gerund: "expelling"}, - "propel": {Past: "propelled", Gerund: "propelling"}, - "repel": {Past: "repelled", Gerund: "repelling"}, - "rebel": {Past: "rebelled", Gerund: "rebelling"}, - "excel": {Past: "excelled", Gerund: "excelling"}, - "cancel": {Past: "cancelled", Gerund: "cancelling"}, // UK spelling - "travel": {Past: "travelled", Gerund: "travelling"}, // UK spelling - "label": {Past: "labelled", Gerund: "labelling"}, // UK spelling - "model": {Past: "modelled", Gerund: "modelling"}, // UK spelling - "level": {Past: "levelled", Gerund: "levelling"}, // UK spelling -} - // PastTense returns the past tense of a verb. // Checks JSON locale data first, then irregular verbs, then applies regular rules. // @@ -328,32 +164,6 @@ func applyRegularPastTense(verb string) string { return verb + "ed" } -// noDoubleConsonant contains multi-syllable verbs that don't double the final consonant. -// Note: UK English doubles -l (travelled, cancelled) - those are in irregularVerbs. -var noDoubleConsonant = map[string]bool{ - "open": true, - "listen": true, - "happen": true, - "enter": true, - "offer": true, - "suffer": true, - "differ": true, - "cover": true, - "deliver": true, - "develop": true, - "visit": true, - "limit": true, - "edit": true, - "credit": true, - "orbit": true, - "total": true, - "target": true, - "budget": true, - "market": true, - "benefit": true, - "focus": true, -} - // shouldDoubleConsonant checks if the final consonant should be doubled. // Applies to CVC (consonant-vowel-consonant) endings in single-syllable words // and stressed final syllables in multi-syllable words. @@ -444,60 +254,6 @@ func applyRegularGerund(verb string) string { return verb + "ing" } -// irregularNouns maps singular nouns to their irregular plural forms. -var irregularNouns = map[string]string{ - "child": "children", - "person": "people", - "man": "men", - "woman": "women", - "foot": "feet", - "tooth": "teeth", - "mouse": "mice", - "goose": "geese", - "ox": "oxen", - "index": "indices", - "appendix": "appendices", - "matrix": "matrices", - "vertex": "vertices", - "crisis": "crises", - "analysis": "analyses", - "diagnosis": "diagnoses", - "thesis": "theses", - "hypothesis": "hypotheses", - "parenthesis":"parentheses", - "datum": "data", - "medium": "media", - "bacterium": "bacteria", - "criterion": "criteria", - "phenomenon": "phenomena", - "curriculum": "curricula", - "alumnus": "alumni", - "cactus": "cacti", - "focus": "foci", - "fungus": "fungi", - "nucleus": "nuclei", - "radius": "radii", - "stimulus": "stimuli", - "syllabus": "syllabi", - "fish": "fish", - "sheep": "sheep", - "deer": "deer", - "species": "species", - "series": "series", - "aircraft": "aircraft", - "life": "lives", - "wife": "wives", - "knife": "knives", - "leaf": "leaves", - "half": "halves", - "self": "selves", - "shelf": "shelves", - "wolf": "wolves", - "calf": "calves", - "loaf": "loaves", - "thief": "thieves", -} - // Pluralize returns the plural form of a noun based on count. // If count is 1, returns the singular form unchanged. // @@ -592,38 +348,6 @@ func applyRegularPlural(noun string) string { return noun + "s" } -// vowelSounds contains words that start with consonants but have vowel sounds. -// These take "an" instead of "a". -var vowelSounds = map[string]bool{ - "hour": true, - "honest": true, - "honour": true, - "honor": true, - "heir": true, - "herb": true, // US pronunciation -} - -// consonantSounds contains words that start with vowels but have consonant sounds. -// These take "a" instead of "an". -var consonantSounds = map[string]bool{ - "user": true, // "yoo-zer" - "union": true, // "yoon-yon" - "unique": true, - "unit": true, - "universe": true, - "university": true, - "uniform": true, - "usage": true, - "usual": true, - "utility": true, - "utensil": true, - "one": true, // "wun" - "once": true, - "euro": true, // "yoo-ro" - "eulogy": true, - "euphemism": true, -} - // Article returns the appropriate indefinite article ("a" or "an") for a word. // // Article("file") // "a" diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index e8624bb..a1e00f3 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -25,15 +25,10 @@ package i18n import ( "bytes" - "embed" "strings" - "sync" "text/template" ) -//go:embed locales/*.json -var localeFS embed.FS - // --- Global convenience functions --- // T translates a message using the default service. @@ -63,10 +58,6 @@ func _(messageID string, args ...any) string { // --- Template helpers --- -// templateCache stores compiled templates for reuse. -// Key is the template string, value is the compiled template. -var templateCache sync.Map - // executeIntentTemplate executes an intent template with the given data. // Templates are cached for performance - repeated calls with the same template // string will reuse the compiled template. diff --git a/pkg/i18n/interface.go b/pkg/i18n/interface.go deleted file mode 100644 index e8361b4..0000000 --- a/pkg/i18n/interface.go +++ /dev/null @@ -1,151 +0,0 @@ -// Package i18n provides internationalization for the CLI. -package i18n - -// Translator defines the interface for translation services. -// Implement this interface to provide custom translation backends -// or mock implementations for testing. -// -// Example usage in tests: -// -// type mockTranslator struct { -// translations map[string]string -// } -// -// func (m *mockTranslator) T(key string, args ...any) string { -// if v, ok := m.translations[key]; ok { -// return v -// } -// return key -// } -// -// func TestSomething(t *testing.T) { -// mock := &mockTranslator{translations: map[string]string{ -// "cli.success": "Test Success", -// }} -// // Use mock in your tests -// } -type Translator interface { - // T translates a message by its ID. - // Optional template data can be passed for interpolation. - // - // svc.T("cli.success") - // svc.T("cli.count.items", map[string]any{"Count": 5}) - T(messageID string, args ...any) string - - // SetLanguage sets the language for translations. - // Returns an error if the language is not supported. - SetLanguage(lang string) error - - // Language returns the current language code. - Language() string - - // SetMode sets the translation mode for missing key handling. - SetMode(m Mode) - - // Mode returns the current translation mode. - Mode() Mode - - // SetDebug enables or disables debug mode. - SetDebug(enabled bool) - - // Debug returns whether debug mode is enabled. - Debug() bool - - // SetFormality sets the default formality level for translations. - SetFormality(f Formality) - - // Formality returns the current formality level. - Formality() Formality - - // Direction returns the text direction for the current language. - Direction() TextDirection - - // IsRTL returns true if the current language uses RTL text. - IsRTL() bool - - // PluralCategory returns the plural category for a count. - PluralCategory(n int) PluralCategory - - // AvailableLanguages returns the list of available language codes. - AvailableLanguages() []string -} - -// Ensure Service implements Translator at compile time. -var _ Translator = (*Service)(nil) - -// --- Function type interfaces --- - -// MissingKeyHandler receives missing key events for analysis. -// Used in ModeCollect to capture translation keys that need to be added. -// -// i18n.OnMissingKey(func(m i18n.MissingKey) { -// log.Printf("MISSING: %s at %s:%d", m.Key, m.CallerFile, m.CallerLine) -// }) -type MissingKeyHandler func(missing MissingKey) - -// MissingKey is dispatched when a translation key is not found in ModeCollect. -// Used by QA tools to collect and report missing translations. -type MissingKey struct { - Key string // The missing translation key - Args map[string]any // Arguments passed to the translation - CallerFile string // Source file where T() was called - CallerLine int // Line number where T() was called -} - -// MissingKeyAction is an alias for backwards compatibility. -// Deprecated: Use MissingKey instead. -type MissingKeyAction = MissingKey - -// PluralRule is a function that determines the plural category for a count. -// Each language has its own plural rule based on CLDR data. -// -// rule := i18n.GetPluralRule("ru") -// category := rule(5) // Returns PluralMany for Russian -type PluralRule func(n int) PluralCategory - -// Message represents a translation - either a simple string or plural forms. -// Supports full CLDR plural categories for languages with complex plural rules. -type Message struct { - Text string // Simple string value (non-plural) - Zero string // count == 0 (Arabic, Latvian, Welsh) - One string // count == 1 (most languages) - Two string // count == 2 (Arabic, Welsh) - Few string // Small numbers (Slavic: 2-4, Arabic: 3-10) - Many string // Larger numbers (Slavic: 5+, Arabic: 11-99) - Other string // Default/fallback form -} - -// ForCategory returns the appropriate text for a plural category. -// Falls back through the category hierarchy to find a non-empty string. -func (m Message) ForCategory(cat PluralCategory) string { - switch cat { - case PluralZero: - if m.Zero != "" { - return m.Zero - } - case PluralOne: - if m.One != "" { - return m.One - } - case PluralTwo: - if m.Two != "" { - return m.Two - } - case PluralFew: - if m.Few != "" { - return m.Few - } - case PluralMany: - if m.Many != "" { - return m.Many - } - } - // Fallback to Other, then One, then Text - if m.Other != "" { - return m.Other - } - if m.One != "" { - return m.One - } - return m.Text -} diff --git a/pkg/i18n/interfaces.go b/pkg/i18n/interfaces.go new file mode 100644 index 0000000..528fb9e --- /dev/null +++ b/pkg/i18n/interfaces.go @@ -0,0 +1,654 @@ +// Package i18n provides internationalization for the CLI. +package i18n + +import ( + "embed" + "sync" + + "golang.org/x/text/language" +) + +// Service provides internationalization and localization. +type Service struct { + messages map[string]map[string]Message // lang -> key -> message + currentLang string + fallbackLang string + availableLangs []language.Tag + mode Mode // Translation mode (Normal, Strict, Collect) + debug bool // Debug mode shows key prefixes + formality Formality // Default formality level for translations + mu sync.RWMutex +} + +// Default is the global i18n service instance. +var ( + defaultService *Service + defaultOnce sync.Once + defaultErr error +) + +// templateCache stores compiled templates for reuse. +// Key is the template string, value is the compiled template. +var templateCache sync.Map + +//go:embed locales/*.json +var localeFS embed.FS + +// Translator defines the interface for translation services. +// Implement this interface to provide custom translation backends +// or mock implementations for testing. +// +// Example usage in tests: +// +// type mockTranslator struct { +// translations map[string]string +// } +// +// func (m *mockTranslator) T(key string, args ...any) string { +// if v, ok := m.translations[key]; ok { +// return v +// } +// return key +// } +// +// func TestSomething(t *testing.T) { +// mock := &mockTranslator{translations: map[string]string{ +// "cli.success": "Test Success", +// }} +// // Use mock in your tests +// } +type Translator interface { + // T translates a message by its ID. + // Optional template data can be passed for interpolation. + // + // svc.T("cli.success") + // svc.T("cli.count.items", map[string]any{"Count": 5}) + T(messageID string, args ...any) string + + // SetLanguage sets the language for translations. + // Returns an error if the language is not supported. + SetLanguage(lang string) error + + // Language returns the current language code. + Language() string + + // SetMode sets the translation mode for missing key handling. + SetMode(m Mode) + + // Mode returns the current translation mode. + Mode() Mode + + // SetDebug enables or disables debug mode. + SetDebug(enabled bool) + + // Debug returns whether debug mode is enabled. + Debug() bool + + // SetFormality sets the default formality level for translations. + SetFormality(f Formality) + + // Formality returns the current formality level. + Formality() Formality + + // Direction returns the text direction for the current language. + Direction() TextDirection + + // IsRTL returns true if the current language uses RTL text. + IsRTL() bool + + // PluralCategory returns the plural category for a count. + PluralCategory(n int) PluralCategory + + // AvailableLanguages returns the list of available language codes. + AvailableLanguages() []string +} + +// Ensure Service implements Translator at compile time. +var _ Translator = (*Service)(nil) + +// NumberFormat defines locale-specific number formatting rules. +type NumberFormat struct { + ThousandsSep string // "," for en, "." for de + DecimalSep string // "." for en, "," for de + PercentFmt string // "%s%%" for en, "%s %%" for de (space before %) +} + +// Default number formats by language. +var numberFormats = map[string]NumberFormat{ + "en": {ThousandsSep: ",", DecimalSep: ".", PercentFmt: "%s%%"}, + "de": {ThousandsSep: ".", DecimalSep: ",", PercentFmt: "%s %%"}, + "fr": {ThousandsSep: " ", DecimalSep: ",", PercentFmt: "%s %%"}, + "es": {ThousandsSep: ".", DecimalSep: ",", PercentFmt: "%s%%"}, + "zh": {ThousandsSep: ",", DecimalSep: ".", PercentFmt: "%s%%"}, +} + +// Mode determines how the i18n service handles missing translation keys. +type Mode int + +const ( + // ModeNormal returns the key as-is when a translation is missing (production). + ModeNormal Mode = iota + // ModeStrict panics immediately when a translation is missing (dev/CI). + ModeStrict + // ModeCollect dispatches MissingKey actions and returns [key] (QA testing). + ModeCollect +) + +// Subject represents a typed subject with metadata for semantic translations. +// Use S() to create a Subject and chain methods for additional context. +// +// S("file", "config.yaml") +// S("repo", "core-php").Count(3) +// S("user", user).Gender("feminine") +// S("colleague", name).Formal() +type Subject struct { + Noun string // The noun type (e.g., "file", "repo", "user") + Value any // The actual value (e.g., filename, struct, etc.) + count int // Count for pluralization (default 1) + gender string // Grammatical gender for languages that need it + location string // Location context (e.g., "in workspace") + formality Formality // Formality level override (-1 = use service default) +} + +// IntentMeta defines the behaviour and characteristics of an intent. +type IntentMeta struct { + Type string // "action", "question", "info" + Verb string // Reference to verb key (e.g., "delete", "save") + Dangerous bool // If true, requires extra confirmation + Default string // Default response: "yes" or "no" + Supports []string // Extra options supported by this intent +} + +// Composed holds all output forms for an intent after template resolution. +// Each field is ready to display to the user. +type Composed struct { + Question string // Question form: "Delete config.yaml?" + Confirm string // Confirmation form: "Really delete config.yaml?" + Success string // Success message: "config.yaml deleted" + Failure string // Failure message: "Failed to delete config.yaml" + Meta IntentMeta // Intent metadata for UI decisions +} + +// Intent defines a semantic intent with templates for all output forms. +// Templates use Go text/template syntax with Subject data available. +type Intent struct { + Meta IntentMeta // Intent behaviour and characteristics + Question string // Template for question form + Confirm string // Template for confirmation form + Success string // Template for success message + Failure string // Template for failure message +} + +// templateData is passed to intent templates during execution. +type templateData struct { + Subject string // Display value of subject + Noun string // Noun type + Count int // Count for pluralization + Gender string // Grammatical gender + Location string // Location context + Formality Formality // Formality level + IsFormal bool // Convenience: formality == FormalityFormal + IsPlural bool // Convenience: count != 1 + Value any // Raw value (for complex templates) +} + +// GrammarData holds language-specific grammar forms loaded from JSON. +type GrammarData struct { + Verbs map[string]VerbForms // verb -> forms + Nouns map[string]NounForms // noun -> forms + Articles ArticleForms // article configuration + Words map[string]string // base word translations + Punct PunctuationRules // language-specific punctuation +} + +// PunctuationRules holds language-specific punctuation patterns. +// French uses " :" (space before colon), English uses ":" +type PunctuationRules struct { + LabelSuffix string // Suffix for labels (default ":") + ProgressSuffix string // Suffix for progress (default "...") +} + +// NounForms holds plural and gender information for a noun. +type NounForms struct { + One string // Singular form + Other string // Plural form + Gender string // Grammatical gender (masculine, feminine, neuter, common) +} + +// ArticleForms holds article configuration for a language. +type ArticleForms struct { + IndefiniteDefault string // Default indefinite article (e.g., "a") + IndefiniteVowel string // Indefinite article before vowel sounds (e.g., "an") + Definite string // Definite article (e.g., "the") + ByGender map[string]string // Gender-specific articles for gendered languages +} + +// grammarCache holds loaded grammar data per language. +var ( + grammarCache = make(map[string]*GrammarData) + grammarCacheMu sync.RWMutex +) + +// VerbForms holds irregular verb conjugations. +type VerbForms struct { + Past string // Past tense (e.g., "deleted") + Gerund string // Present participle (e.g., "deleting") +} + +// irregularVerbs maps base verbs to their irregular forms. +var irregularVerbs = map[string]VerbForms{ + "be": {Past: "was", Gerund: "being"}, + "have": {Past: "had", Gerund: "having"}, + "do": {Past: "did", Gerund: "doing"}, + "go": {Past: "went", Gerund: "going"}, + "make": {Past: "made", Gerund: "making"}, + "get": {Past: "got", Gerund: "getting"}, + "run": {Past: "ran", Gerund: "running"}, + "set": {Past: "set", Gerund: "setting"}, + "put": {Past: "put", Gerund: "putting"}, + "cut": {Past: "cut", Gerund: "cutting"}, + "let": {Past: "let", Gerund: "letting"}, + "hit": {Past: "hit", Gerund: "hitting"}, + "shut": {Past: "shut", Gerund: "shutting"}, + "split": {Past: "split", Gerund: "splitting"}, + "spread": {Past: "spread", Gerund: "spreading"}, + "read": {Past: "read", Gerund: "reading"}, + "write": {Past: "wrote", Gerund: "writing"}, + "send": {Past: "sent", Gerund: "sending"}, + "build": {Past: "built", Gerund: "building"}, + "begin": {Past: "began", Gerund: "beginning"}, + "find": {Past: "found", Gerund: "finding"}, + "take": {Past: "took", Gerund: "taking"}, + "see": {Past: "saw", Gerund: "seeing"}, + "keep": {Past: "kept", Gerund: "keeping"}, + "hold": {Past: "held", Gerund: "holding"}, + "tell": {Past: "told", Gerund: "telling"}, + "bring": {Past: "brought", Gerund: "bringing"}, + "think": {Past: "thought", Gerund: "thinking"}, + "buy": {Past: "bought", Gerund: "buying"}, + "catch": {Past: "caught", Gerund: "catching"}, + "teach": {Past: "taught", Gerund: "teaching"}, + "throw": {Past: "threw", Gerund: "throwing"}, + "grow": {Past: "grew", Gerund: "growing"}, + "know": {Past: "knew", Gerund: "knowing"}, + "show": {Past: "showed", Gerund: "showing"}, + "draw": {Past: "drew", Gerund: "drawing"}, + "break": {Past: "broke", Gerund: "breaking"}, + "speak": {Past: "spoke", Gerund: "speaking"}, + "choose": {Past: "chose", Gerund: "choosing"}, + "forget": {Past: "forgot", Gerund: "forgetting"}, + "lose": {Past: "lost", Gerund: "losing"}, + "win": {Past: "won", Gerund: "winning"}, + "swim": {Past: "swam", Gerund: "swimming"}, + "drive": {Past: "drove", Gerund: "driving"}, + "rise": {Past: "rose", Gerund: "rising"}, + "shine": {Past: "shone", Gerund: "shining"}, + "sing": {Past: "sang", Gerund: "singing"}, + "ring": {Past: "rang", Gerund: "ringing"}, + "drink": {Past: "drank", Gerund: "drinking"}, + "sink": {Past: "sank", Gerund: "sinking"}, + "sit": {Past: "sat", Gerund: "sitting"}, + "stand": {Past: "stood", Gerund: "standing"}, + "hang": {Past: "hung", Gerund: "hanging"}, + "dig": {Past: "dug", Gerund: "digging"}, + "stick": {Past: "stuck", Gerund: "sticking"}, + "bite": {Past: "bit", Gerund: "biting"}, + "hide": {Past: "hid", Gerund: "hiding"}, + "feed": {Past: "fed", Gerund: "feeding"}, + "meet": {Past: "met", Gerund: "meeting"}, + "lead": {Past: "led", Gerund: "leading"}, + "sleep": {Past: "slept", Gerund: "sleeping"}, + "feel": {Past: "felt", Gerund: "feeling"}, + "leave": {Past: "left", Gerund: "leaving"}, + "mean": {Past: "meant", Gerund: "meaning"}, + "lend": {Past: "lent", Gerund: "lending"}, + "spend": {Past: "spent", Gerund: "spending"}, + "bend": {Past: "bent", Gerund: "bending"}, + "deal": {Past: "dealt", Gerund: "dealing"}, + "lay": {Past: "laid", Gerund: "laying"}, + "pay": {Past: "paid", Gerund: "paying"}, + "say": {Past: "said", Gerund: "saying"}, + "sell": {Past: "sold", Gerund: "selling"}, + "seek": {Past: "sought", Gerund: "seeking"}, + "fight": {Past: "fought", Gerund: "fighting"}, + "fly": {Past: "flew", Gerund: "flying"}, + "wear": {Past: "wore", Gerund: "wearing"}, + "tear": {Past: "tore", Gerund: "tearing"}, + "bear": {Past: "bore", Gerund: "bearing"}, + "swear": {Past: "swore", Gerund: "swearing"}, + "wake": {Past: "woke", Gerund: "waking"}, + "freeze": {Past: "froze", Gerund: "freezing"}, + "steal": {Past: "stole", Gerund: "stealing"}, + "overwrite": {Past: "overwritten", Gerund: "overwriting"}, + "reset": {Past: "reset", Gerund: "resetting"}, + "reboot": {Past: "rebooted", Gerund: "rebooting"}, + + // Multi-syllable verbs with stressed final syllables (double consonant) + "submit": {Past: "submitted", Gerund: "submitting"}, + "permit": {Past: "permitted", Gerund: "permitting"}, + "admit": {Past: "admitted", Gerund: "admitting"}, + "omit": {Past: "omitted", Gerund: "omitting"}, + "commit": {Past: "committed", Gerund: "committing"}, + "transmit": {Past: "transmitted", Gerund: "transmitting"}, + "prefer": {Past: "preferred", Gerund: "preferring"}, + "refer": {Past: "referred", Gerund: "referring"}, + "transfer": {Past: "transferred", Gerund: "transferring"}, + "defer": {Past: "deferred", Gerund: "deferring"}, + "confer": {Past: "conferred", Gerund: "conferring"}, + "infer": {Past: "inferred", Gerund: "inferring"}, + "occur": {Past: "occurred", Gerund: "occurring"}, + "recur": {Past: "recurred", Gerund: "recurring"}, + "incur": {Past: "incurred", Gerund: "incurring"}, + "deter": {Past: "deterred", Gerund: "deterring"}, + "control": {Past: "controlled", Gerund: "controlling"}, + "patrol": {Past: "patrolled", Gerund: "patrolling"}, + "compel": {Past: "compelled", Gerund: "compelling"}, + "expel": {Past: "expelled", Gerund: "expelling"}, + "propel": {Past: "propelled", Gerund: "propelling"}, + "repel": {Past: "repelled", Gerund: "repelling"}, + "rebel": {Past: "rebelled", Gerund: "rebelling"}, + "excel": {Past: "excelled", Gerund: "excelling"}, + "cancel": {Past: "cancelled", Gerund: "cancelling"}, // UK spelling + "travel": {Past: "travelled", Gerund: "travelling"}, // UK spelling + "label": {Past: "labelled", Gerund: "labelling"}, // UK spelling + "model": {Past: "modelled", Gerund: "modelling"}, // UK spelling + "level": {Past: "levelled", Gerund: "levelling"}, // UK spelling +} + +// noDoubleConsonant contains multi-syllable verbs that don't double the final consonant. +// Note: UK English doubles -l (travelled, cancelled) - those are in irregularVerbs. +var noDoubleConsonant = map[string]bool{ + "open": true, + "listen": true, + "happen": true, + "enter": true, + "offer": true, + "suffer": true, + "differ": true, + "cover": true, + "deliver": true, + "develop": true, + "visit": true, + "limit": true, + "edit": true, + "credit": true, + "orbit": true, + "total": true, + "target": true, + "budget": true, + "market": true, + "benefit": true, + "focus": true, +} + +// irregularNouns maps singular nouns to their irregular plural forms. +var irregularNouns = map[string]string{ + "child": "children", + "person": "people", + "man": "men", + "woman": "women", + "foot": "feet", + "tooth": "teeth", + "mouse": "mice", + "goose": "geese", + "ox": "oxen", + "index": "indices", + "appendix": "appendices", + "matrix": "matrices", + "vertex": "vertices", + "crisis": "crises", + "analysis": "analyses", + "diagnosis": "diagnoses", + "thesis": "theses", + "hypothesis": "hypotheses", + "parenthesis": "parentheses", + "datum": "data", + "medium": "media", + "bacterium": "bacteria", + "criterion": "criteria", + "phenomenon": "phenomena", + "curriculum": "curricula", + "alumnus": "alumni", + "cactus": "cacti", + "focus": "foci", + "fungus": "fungi", + "nucleus": "nuclei", + "radius": "radii", + "stimulus": "stimuli", + "syllabus": "syllabi", + "fish": "fish", + "sheep": "sheep", + "deer": "deer", + "species": "species", + "series": "series", + "aircraft": "aircraft", + "life": "lives", + "wife": "wives", + "knife": "knives", + "leaf": "leaves", + "half": "halves", + "self": "selves", + "shelf": "shelves", + "wolf": "wolves", + "calf": "calves", + "loaf": "loaves", + "thief": "thieves", +} + +// vowelSounds contains words that start with consonants but have vowel sounds. +// These take "an" instead of "a". +var vowelSounds = map[string]bool{ + "hour": true, + "honest": true, + "honour": true, + "honor": true, + "heir": true, + "herb": true, // US pronunciation +} + +// consonantSounds contains words that start with vowels but have consonant sounds. +// These take "a" instead of "an". +var consonantSounds = map[string]bool{ + "user": true, // "yoo-zer" + "union": true, // "yoon-yon" + "unique": true, + "unit": true, + "universe": true, + "university": true, + "uniform": true, + "usage": true, + "usual": true, + "utility": true, + "utensil": true, + "one": true, // "wun" + "once": true, + "euro": true, // "yoo-ro" + "eulogy": true, + "euphemism": true, +} + +// --- Function type interfaces --- + +// MissingKeyHandler receives missing key events for analysis. +// Used in ModeCollect to capture translation keys that need to be added. +// +// i18n.OnMissingKey(func(m i18n.MissingKey) { +// log.Printf("MISSING: %s at %s:%d", m.Key, m.CallerFile, m.CallerLine) +// }) +type MissingKeyHandler func(missing MissingKey) + +// MissingKey is dispatched when a translation key is not found in ModeCollect. +// Used by QA tools to collect and report missing translations. +type MissingKey struct { + Key string // The missing translation key + Args map[string]any // Arguments passed to the translation + CallerFile string // Source file where T() was called + CallerLine int // Line number where T() was called +} + +// MissingKeyAction is an alias for backwards compatibility. +// Deprecated: Use MissingKey instead. +type MissingKeyAction = MissingKey + +// PluralRule is a function that determines the plural category for a count. +// Each language has its own plural rule based on CLDR data. +// +// rule := i18n.GetPluralRule("ru") +// category := rule(5) // Returns PluralMany for Russian +type PluralRule func(n int) PluralCategory + +// PluralCategory represents CLDR plural categories. +// Different languages use different subsets of these categories. +// +// Examples: +// - English: one, other +// - Russian: one, few, many, other +// - Arabic: zero, one, two, few, many, other +// - Welsh: zero, one, two, few, many, other +type PluralCategory int + +const ( + // PluralOther is the default/fallback category + PluralOther PluralCategory = iota + // PluralZero is used when count == 0 (Arabic, Latvian, etc.) + PluralZero + // PluralOne is used when count == 1 (most languages) + PluralOne + // PluralTwo is used when count == 2 (Arabic, Welsh, etc.) + PluralTwo + // PluralFew is used for small numbers (Slavic: 2-4, Arabic: 3-10, etc.) + PluralFew + // PluralMany is used for larger numbers (Slavic: 5+, Arabic: 11-99, etc.) + PluralMany +) + +// GrammaticalGender represents grammatical gender for nouns. +type GrammaticalGender int + +const ( + // GenderNeuter is used for neuter nouns (das in German, it in English) + GenderNeuter GrammaticalGender = iota + // GenderMasculine is used for masculine nouns (der in German, le in French) + GenderMasculine + // GenderFeminine is used for feminine nouns (die in German, la in French) + GenderFeminine + // GenderCommon is used in languages with common gender (Swedish, Dutch) + GenderCommon +) + +// rtlLanguages contains language codes that use right-to-left text direction. +var rtlLanguages = map[string]bool{ + "ar": true, // Arabic + "ar-SA": true, + "ar-EG": true, + "he": true, // Hebrew + "he-IL": true, + "fa": true, // Persian/Farsi + "fa-IR": true, + "ur": true, // Urdu + "ur-PK": true, + "yi": true, // Yiddish + "ps": true, // Pashto + "sd": true, // Sindhi + "ug": true, // Uyghur +} + +// pluralRules contains CLDR plural rules for supported languages. +var pluralRules = map[string]PluralRule{ + "en": pluralRuleEnglish, + "en-GB": pluralRuleEnglish, + "en-US": pluralRuleEnglish, + "de": pluralRuleGerman, + "de-DE": pluralRuleGerman, + "de-AT": pluralRuleGerman, + "de-CH": pluralRuleGerman, + "fr": pluralRuleFrench, + "fr-FR": pluralRuleFrench, + "fr-CA": pluralRuleFrench, + "es": pluralRuleSpanish, + "es-ES": pluralRuleSpanish, + "es-MX": pluralRuleSpanish, + "ru": pluralRuleRussian, + "ru-RU": pluralRuleRussian, + "pl": pluralRulePolish, + "pl-PL": pluralRulePolish, + "ar": pluralRuleArabic, + "ar-SA": pluralRuleArabic, + "zh": pluralRuleChinese, + "zh-CN": pluralRuleChinese, + "zh-TW": pluralRuleChinese, + "ja": pluralRuleJapanese, + "ja-JP": pluralRuleJapanese, + "ko": pluralRuleKorean, + "ko-KR": pluralRuleKorean, +} + +// Formality represents the level of formality in translations. +// Used for languages that distinguish formal/informal address (Sie/du, vous/tu). +type Formality int + +const ( + // FormalityNeutral uses context-appropriate formality (default) + FormalityNeutral Formality = iota + // FormalityInformal uses informal address (du, tu, you) + FormalityInformal + // FormalityFormal uses formal address (Sie, vous, usted) + FormalityFormal +) + +// TextDirection represents text directionality. +type TextDirection int + +const ( + // DirLTR is left-to-right text direction (English, German, etc.) + DirLTR TextDirection = iota + // DirRTL is right-to-left text direction (Arabic, Hebrew, etc.) + DirRTL +) + +// Message represents a translation - either a simple string or plural forms. +// Supports full CLDR plural categories for languages with complex plural rules. +type Message struct { + Text string // Simple string value (non-plural) + Zero string // count == 0 (Arabic, Latvian, Welsh) + One string // count == 1 (most languages) + Two string // count == 2 (Arabic, Welsh) + Few string // Small numbers (Slavic: 2-4, Arabic: 3-10) + Many string // Larger numbers (Slavic: 5+, Arabic: 11-99) + Other string // Default/fallback form +} + +// ForCategory returns the appropriate text for a plural category. +// Falls back through the category hierarchy to find a non-empty string. +func (m Message) ForCategory(cat PluralCategory) string { + switch cat { + case PluralZero: + if m.Zero != "" { + return m.Zero + } + case PluralOne: + if m.One != "" { + return m.One + } + case PluralTwo: + if m.Two != "" { + return m.Two + } + case PluralFew: + if m.Few != "" { + return m.Few + } + case PluralMany: + if m.Many != "" { + return m.Many + } + } + // Fallback to Other, then One, then Text + if m.Other != "" { + return m.Other + } + if m.One != "" { + return m.One + } + return m.Text +} diff --git a/pkg/i18n/language.go b/pkg/i18n/language.go index 3f316c6..d82f8b1 100644 --- a/pkg/i18n/language.go +++ b/pkg/i18n/language.go @@ -1,19 +1,6 @@ // Package i18n provides internationalization for the CLI. package i18n -// Formality represents the level of formality in translations. -// Used for languages that distinguish formal/informal address (Sie/du, vous/tu). -type Formality int - -const ( - // FormalityNeutral uses context-appropriate formality (default) - FormalityNeutral Formality = iota - // FormalityInformal uses informal address (du, tu, you) - FormalityInformal - // FormalityFormal uses formal address (Sie, vous, usted) - FormalityFormal -) - // String returns the string representation of a Formality level. func (f Formality) String() string { switch f { @@ -26,16 +13,6 @@ func (f Formality) String() string { } } -// TextDirection represents text directionality. -type TextDirection int - -const ( - // DirLTR is left-to-right text direction (English, German, etc.) - DirLTR TextDirection = iota - // DirRTL is right-to-left text direction (Arabic, Hebrew, etc.) - DirRTL -) - // String returns the string representation of a TextDirection. func (d TextDirection) String() string { if d == DirRTL { @@ -44,31 +21,6 @@ func (d TextDirection) String() string { return "ltr" } -// PluralCategory represents CLDR plural categories. -// Different languages use different subsets of these categories. -// -// Examples: -// - English: one, other -// - Russian: one, few, many, other -// - Arabic: zero, one, two, few, many, other -// - Welsh: zero, one, two, few, many, other -type PluralCategory int - -const ( - // PluralOther is the default/fallback category - PluralOther PluralCategory = iota - // PluralZero is used when count == 0 (Arabic, Latvian, etc.) - PluralZero - // PluralOne is used when count == 1 (most languages) - PluralOne - // PluralTwo is used when count == 2 (Arabic, Welsh, etc.) - PluralTwo - // PluralFew is used for small numbers (Slavic: 2-4, Arabic: 3-10, etc.) - PluralFew - // PluralMany is used for larger numbers (Slavic: 5+, Arabic: 11-99, etc.) - PluralMany -) - // String returns the string representation of a PluralCategory. func (p PluralCategory) String() string { switch p { @@ -87,20 +39,6 @@ func (p PluralCategory) String() string { } } -// GrammaticalGender represents grammatical gender for nouns. -type GrammaticalGender int - -const ( - // GenderNeuter is used for neuter nouns (das in German, it in English) - GenderNeuter GrammaticalGender = iota - // GenderMasculine is used for masculine nouns (der in German, le in French) - GenderMasculine - // GenderFeminine is used for feminine nouns (die in German, la in French) - GenderFeminine - // GenderCommon is used in languages with common gender (Swedish, Dutch) - GenderCommon -) - // String returns the string representation of a GrammaticalGender. func (g GrammaticalGender) String() string { switch g { @@ -115,23 +53,6 @@ func (g GrammaticalGender) String() string { } } -// rtlLanguages contains language codes that use right-to-left text direction. -var rtlLanguages = map[string]bool{ - "ar": true, // Arabic - "ar-SA": true, - "ar-EG": true, - "he": true, // Hebrew - "he-IL": true, - "fa": true, // Persian/Farsi - "fa-IR": true, - "ur": true, // Urdu - "ur-PK": true, - "yi": true, // Yiddish - "ps": true, // Pashto - "sd": true, // Sindhi - "ug": true, // Uyghur -} - // IsRTLLanguage returns true if the language code uses right-to-left text. func IsRTLLanguage(lang string) bool { // Check exact match first @@ -146,36 +67,6 @@ func IsRTLLanguage(lang string) bool { return false } -// pluralRules contains CLDR plural rules for supported languages. -var pluralRules = map[string]PluralRule{ - "en": pluralRuleEnglish, - "en-GB": pluralRuleEnglish, - "en-US": pluralRuleEnglish, - "de": pluralRuleGerman, - "de-DE": pluralRuleGerman, - "de-AT": pluralRuleGerman, - "de-CH": pluralRuleGerman, - "fr": pluralRuleFrench, - "fr-FR": pluralRuleFrench, - "fr-CA": pluralRuleFrench, - "es": pluralRuleSpanish, - "es-ES": pluralRuleSpanish, - "es-MX": pluralRuleSpanish, - "ru": pluralRuleRussian, - "ru-RU": pluralRuleRussian, - "pl": pluralRulePolish, - "pl-PL": pluralRulePolish, - "ar": pluralRuleArabic, - "ar-SA": pluralRuleArabic, - "zh": pluralRuleChinese, - "zh-CN": pluralRuleChinese, - "zh-TW": pluralRuleChinese, - "ja": pluralRuleJapanese, - "ja-JP": pluralRuleJapanese, - "ko": pluralRuleKorean, - "ko-KR": pluralRuleKorean, -} - // English: one (n=1), other func pluralRuleEnglish(n int) PluralCategory { if n == 1 { diff --git a/pkg/i18n/mode.go b/pkg/i18n/mode.go index db572f1..aeb1167 100644 --- a/pkg/i18n/mode.go +++ b/pkg/i18n/mode.go @@ -1,18 +1,6 @@ // Package i18n provides internationalization for the CLI. package i18n -// Mode determines how the i18n service handles missing translation keys. -type Mode int - -const ( - // ModeNormal returns the key as-is when a translation is missing (production). - ModeNormal Mode = iota - // ModeStrict panics immediately when a translation is missing (dev/CI). - ModeStrict - // ModeCollect dispatches MissingKey actions and returns [key] (QA testing). - ModeCollect -) - // String returns the string representation of the mode. func (m Mode) String() string { switch m { diff --git a/pkg/i18n/numbers.go b/pkg/i18n/numbers.go index 3259236..a35baf5 100644 --- a/pkg/i18n/numbers.go +++ b/pkg/i18n/numbers.go @@ -8,22 +8,6 @@ import ( "strings" ) -// NumberFormat defines locale-specific number formatting rules. -type NumberFormat struct { - ThousandsSep string // "," for en, "." for de - DecimalSep string // "." for en, "," for de - PercentFmt string // "%s%%" for en, "%s %%" for de (space before %) -} - -// Default number formats by language. -var numberFormats = map[string]NumberFormat{ - "en": {ThousandsSep: ",", DecimalSep: ".", PercentFmt: "%s%%"}, - "de": {ThousandsSep: ".", DecimalSep: ",", PercentFmt: "%s %%"}, - "fr": {ThousandsSep: " ", DecimalSep: ",", PercentFmt: "%s %%"}, - "es": {ThousandsSep: ".", DecimalSep: ",", PercentFmt: "%s%%"}, - "zh": {ThousandsSep: ",", DecimalSep: ".", PercentFmt: "%s%%"}, -} - // getNumberFormat returns the number format for the current language. func getNumberFormat() NumberFormat { lang := currentLangForGrammar() diff --git a/pkg/i18n/service.go b/pkg/i18n/service.go index c199df9..8470004 100644 --- a/pkg/i18n/service.go +++ b/pkg/i18n/service.go @@ -7,30 +7,10 @@ import ( "io/fs" "path/filepath" "strings" - "sync" "golang.org/x/text/language" ) -// Service provides internationalization and localization. -type Service struct { - messages map[string]map[string]Message // lang -> key -> message - currentLang string - fallbackLang string - availableLangs []language.Tag - mode Mode // Translation mode (Normal, Strict, Collect) - debug bool // Debug mode shows key prefixes - formality Formality // Default formality level for translations - mu sync.RWMutex -} - -// Default is the global i18n service instance. -var ( - defaultService *Service - defaultOnce sync.Once - defaultErr error -) - // New creates a new i18n service with embedded locales. func New() (*Service, error) { return NewWithFS(localeFS, "locales")