feat(quando-zbr): implement i18n infrastructure for EN/DE languages

Add internationalization support with translation data for English and
German languages. This provides the foundation for localized formatting
of dates and durations.

Changes:
- Add i18n.go with translation maps for month names, weekday names,
  and duration units (both EN and DE)
- Implement helper methods on Lang type: MonthName, MonthNameShort,
  WeekdayName, WeekdayNameShort, DurationUnit
- Add automatic fallback to English for unknown languages
- Create comprehensive test suite in i18n_test.go (97.8% coverage)
- Update Lang type documentation with future expansion notes
- Add example tests demonstrating i18n usage

Blocks: quando-10t, quando-5ol, quando-95w
This commit is contained in:
Oliver Jakoubek 2026-02-11 19:27:16 +01:00
commit 436d8dd411
5 changed files with 568 additions and 4 deletions

View file

@ -4,7 +4,7 @@
{"id":"quando-4bh","title":"Unit type and constants","description":"Define Unit type for time unit constants used in arithmetic operations.\n\n**Technical Details:**\n```go\ntype Unit int\n\nconst (\n Seconds Unit = iota\n Minutes\n Hours\n Days\n Weeks\n Months\n Quarters\n Years\n)\n```\n\n**Design:**\n- Type-safe constants (compile-time safety)\n- Use iota for clear ordering\n- Optional internal ParseUnit(string) for external inputs\n\n## Acceptance Criteria\n- [ ] Unit type defined as int\n- [ ] All 8 unit constants defined (Seconds through Years)\n- [ ] Units use iota for ordering\n- [ ] Godoc comments for Unit type and constants\n- [ ] Unit tests verifying constants","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:37.246514285+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:34:46.001417204+01:00","closed_at":"2026-02-11T16:34:46.001417204+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-4bh","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.349519431+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":4,"issue_id":"quando-4bh","author":"Oliver Jakoubek","text":"Plan: 1) Create unit.go with Unit type as int, 2) Define all 8 unit constants (Seconds, Minutes, Hours, Days, Weeks, Months, Quarters, Years) using iota, 3) Add String() method for better debugging, 4) Add comprehensive godoc comments, 5) Write unit tests verifying constants and ordering, 6) Add example tests","created_at":"2026-02-11T15:33:41Z"}]}
{"id":"quando-5ib","title":"Date inspection methods","description":"Implement date inspection methods for querying metadata about a date.\n\n**Individual Methods:**\n```go\nfunc (d Date) WeekNumber() int\nfunc (d Date) Quarter() int\nfunc (d Date) DayOfYear() int\nfunc (d Date) IsWeekend() bool\nfunc (d Date) IsLeapYear() bool\n```\n\n**Aggregated Method:**\n```go\ntype DateInfo struct {\n WeekNumber int\n Quarter int\n DayOfYear int\n IsWeekend bool\n IsLeapYear bool\n Unix int64\n}\n\nfunc (d Date) Info() DateInfo\n```\n\n**Specifications:**\n- **WeekNumber**: ISO 8601 (Monday first, Week 1 = first week containing Thursday)\n- **Quarter**: 1-4 (Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec)\n- **DayOfYear**: 1-366 (considering leap years)\n- **IsWeekend**: Saturday or Sunday (not configurable in Phase 1)\n- **IsLeapYear**: Divisible by 4, except centuries, except divisible by 400\n\n## Acceptance Criteria\n- [ ] WeekNumber() returns ISO 8601 week number\n- [ ] Quarter() returns 1-4\n- [ ] DayOfYear() returns 1-366\n- [ ] IsWeekend() returns true for Sat/Sun\n- [ ] IsLeapYear() returns true for leap years\n- [ ] Info() returns struct with all fields\n- [ ] ISO 8601 week compliance (Week 1 contains Thursday)\n- [ ] Leap year rules correct (4, 100, 400 rule)\n- [ ] Unit tests for all methods\n- [ ] Edge cases: year boundaries, week 53, leap years\n- [ ] Benchmarks meet \u003c1µs target\n- [ ] Godoc for each method with conventions documented","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:06.966079923+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:06.966079923+01:00","dependencies":[{"issue_id":"quando-5ib","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:13.310800388+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-5ol","title":"Format presets and constants","description":"Implement preset format constants and Format() method.\n\n**API:**\n```go\ntype Format int\n\nconst (\n ISO Format = iota // \"2026-02-09\"\n EU // \"09.02.2026\"\n US // \"02/09/2026\"\n Long // \"February 9, 2026\" (language-dependent)\n RFC2822 // \"Mon, 09 Feb 2026 00:00:00 +0000\"\n)\n\nfunc (d Date) Format(format Format) string\n```\n\n**Language Dependency:**\n- ISO, EU, US, RFC2822: Always language-independent\n- Long: Uses Lang setting\n - EN: \"February 9, 2026\"\n - DE: \"9. Februar 2026\"\n\n**Implementation:**\n- Map Format constants to Go layout strings\n- Use Lang() setting for Long format\n- Delegate to time.Format() internally\n\n## Acceptance Criteria\n- [ ] Format type and constants defined\n- [ ] Format() method implemented\n- [ ] ISO format outputs \"YYYY-MM-DD\"\n- [ ] EU format outputs \"DD.MM.YYYY\"\n- [ ] US format outputs \"MM/DD/YYYY\"\n- [ ] RFC2822 format correct\n- [ ] Long format respects Lang setting (EN and DE)\n- [ ] Non-Long formats ignore Lang setting\n- [ ] Unit tests for all formats\n- [ ] Unit tests for Long with both EN and DE\n- [ ] Benchmark meets \u003c5µs without i18n, \u003c10µs with i18n\n- [ ] Godoc for each format constant\n- [ ] Example tests showing each format","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:53.217816331+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:53.217816331+01:00","dependencies":[{"issue_id":"quando-5ol","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:12.164278602+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-5ol","depends_on_id":"quando-zbr","type":"blocks","created_at":"2026-02-11T16:23:12.20045666+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-6c3","title":"CI/CD pipeline setup","description":"Set up GitHub Actions CI/CD pipeline for automated testing and quality checks.\n\n**CI Workflow:**\n- Run on push and pull requests\n- Test on multiple Go versions (1.22, 1.23, latest)\n- Test on multiple platforms (Linux, macOS, Windows)\n- Run go fmt check\n- Run go vet\n- Run tests with coverage\n- Run benchmarks (informational)\n- Optional: golangci-lint\n\n**Coverage Reporting:**\n- Generate coverage report\n- Fail if coverage \u003c95% for core calculation functions\n- Optional: Upload to codecov.io or similar\n\n**Performance Monitoring:**\n- Run benchmarks on each commit\n- Report benchmark results (informational, no fail)\n\n## Acceptance Criteria\n- [ ] .github/workflows/ci.yml created\n- [ ] Tests run on push and PR\n- [ ] Multi-version Go support (1.22+)\n- [ ] Multi-platform support (Linux, macOS, Windows)\n- [ ] go fmt check passes\n- [ ] go vet check passes\n- [ ] Tests run with coverage report\n- [ ] Coverage requirement enforced (95%+)\n- [ ] Benchmarks run (informational)\n- [ ] CI badge in README (if applicable)","status":"open","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:51.928117055+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:51.928117055+01:00","dependencies":[{"issue_id":"quando-6c3","depends_on_id":"quando-r1o","type":"blocks","created_at":"2026-02-11T16:23:16.259739409+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-6c3","title":"CI/CD pipeline setup","description":"Set up GitHub Actions CI/CD pipeline for automated testing and quality checks.\n\n**CI Workflow:**\n- Run on push and pull requests\n- Test on multiple Go versions (1.22, 1.23, latest)\n- Test on multiple platforms (Linux, macOS, Windows)\n- Run go fmt check\n- Run go vet\n- Run tests with coverage\n- Run benchmarks (informational)\n- Optional: golangci-lint\n\n**Coverage Reporting:**\n- Generate coverage report\n- Fail if coverage \u003c95% for core calculation functions\n- Optional: Upload to codecov.io or similar\n\n**Performance Monitoring:**\n- Run benchmarks on each commit\n- Report benchmark results (informational, no fail)\n\n## Acceptance Criteria\n- [ ] .github/workflows/ci.yml created\n- [ ] Tests run on push and PR\n- [ ] Multi-version Go support (1.22+)\n- [ ] Multi-platform support (Linux, macOS, Windows)\n- [ ] go fmt check passes\n- [ ] go vet check passes\n- [ ] Tests run with coverage report\n- [ ] Coverage requirement enforced (95%+)\n- [ ] Benchmarks run (informational)\n- [ ] CI badge in README (if applicable)","status":"tombstone","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:51.928117055+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T19:21:16.246958875+01:00","dependencies":[{"issue_id":"quando-6c3","depends_on_id":"quando-r1o","type":"blocks","created_at":"2026-02-11T16:23:16.259739409+01:00","created_by":"Oliver Jakoubek"}],"deleted_at":"2026-02-11T19:21:16.246958875+01:00","deleted_by":"daemon","delete_reason":"delete","original_type":"task"}
{"id":"quando-6x3","title":"Documentation and examples","description":"Complete documentation including README, godoc, and example code.\n\n**README.md Contents:**\n- Project overview and vision\n- Installation instructions (go get)\n- Quick start examples\n- Core features showcase\n- Performance characteristics\n- Comparison to time.Time\n- Contributing guidelines\n- License\n\n**Godoc Requirements:**\n- Every exported type/function documented\n- Godoc comments start with type/function name\n- Complex behaviors explained (month-end overflow, DST, etc.)\n- Edge cases noted where applicable\n\n**Example Tests:**\n- Example_basicArithmetic\n- Example_monthEndOverflow\n- Example_snapOperations\n- Example_diffCalculation\n- Example_humanFormat\n- Example_parsing\n- Example_formatting\n- Example_timezoneConversion\n- Example_chainedOperations\n\n**Code Examples in README:**\n- Complex chaining\n- Month-end handling\n- Human-readable diffs\n- Timezone-aware operations\n- Comparison with stdlib\n\n## Acceptance Criteria\n- [ ] README.md complete with all sections\n- [ ] Installation instructions clear\n- [ ] At least 5 code examples in README\n- [ ] All exported items have godoc comments\n- [ ] Godoc comments follow conventions (name first)\n- [ ] At least 8 example tests\n- [ ] Examples demonstrate key features\n- [ ] Edge cases documented in relevant godoc\n- [ ] Comparison table with time.Time\n- [ ] Contributing guidelines (if open source)","status":"open","priority":3,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:45.693695262+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:22:45.693695262+01:00","dependencies":[{"issue_id":"quando-6x3","depends_on_id":"quando-r1o","type":"blocks","created_at":"2026-02-11T16:23:16.222906911+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-7m5","title":"MustParse convenience function","description":"Implement MustParse() convenience function that panics on error (for tests/initialization).\n\n**API:**\n```go\nfunc MustParse(s string) Date\n```\n\n**Behavior:**\n- Calls Parse() internally\n- Returns Date on success\n- Panics on error (with clear panic message)\n\n**Use Cases:**\n- Test fixtures\n- Static initialization\n- Configuration files where values are known-good\n\n**Documentation:**\n- MUST clearly document that this function panics\n- MUST recommend using Parse() in production code\n- MUST show test usage examples\n\n## Acceptance Criteria\n- [ ] MustParse() implemented\n- [ ] Returns Date on successful parse\n- [ ] Panics with clear message on error\n- [ ] Godoc clearly warns about panic behavior\n- [ ] Godoc recommends Parse() for production\n- [ ] Example test showing test fixture usage\n- [ ] Unit tests verifying panic on invalid input","status":"open","priority":3,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:46.007442996+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:46.007442996+01:00","dependencies":[{"issue_id":"quando-7m5","depends_on_id":"quando-gr5","type":"blocks","created_at":"2026-02-11T16:23:11.340600075+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-91w","title":"Project setup and structure","description":"Set up initial project structure, module, and tooling.\n\n**Repository Structure:**\n```\nquando/\n├── quando.go # Package-level functions\n├── date.go # Date type and core methods\n├── arithmetic.go # Add, Sub\n├── snap.go # StartOf, EndOf, Next, Prev\n├── diff.go # Duration type, Diff\n├── inspect.go # WeekNumber, Quarter, etc.\n├── format.go # Formatting\n├── parse.go # Parsing\n├── clock.go # Clock abstraction\n├── i18n.go # Internationalization\n├── errors.go # Error types\n├── internal/calc/ # Internal helpers\n├── *_test.go # Unit tests\n├── example_test.go # Godoc examples\n├── bench_test.go # Benchmarks\n├── go.mod\n├── go.sum\n├── README.md\n├── LICENSE # MIT\n└── .github/workflows/ci.yml\n```\n\n**Go Module:**\n- Module path: code.beautifulmachines.dev/quando\n- Go version: 1.22+\n- Zero dependencies (stdlib only)\n\n**Tooling:**\n- go fmt\n- go vet\n- golangci-lint (optional)\n\n## Acceptance Criteria\n- [ ] go.mod initialized with correct module path\n- [ ] Go 1.22+ specified in go.mod\n- [ ] Directory structure created\n- [ ] README.md with project overview\n- [ ] LICENSE file (MIT)\n- [ ] .gitignore for Go projects\n- [ ] Basic CI/CD workflow (if applicable)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:22:30.054241058+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:28:17.730717812+01:00","closed_at":"2026-02-11T16:28:17.730717812+01:00","close_reason":"Closed","comments":[{"id":1,"issue_id":"quando-91w","author":"Oliver Jakoubek","text":"Plan: 1) Initialize go.mod with module path code.beautifulmachines.dev/quando and Go 1.22+, 2) Create internal/calc/ directory structure, 3) Write comprehensive README.md, 4) Add MIT LICENSE, 5) Populate .gitignore for Go, 6) Create GitHub Actions CI workflow for testing and linting","created_at":"2026-02-11T15:26:35Z"}]}
@ -19,4 +19,4 @@
{"id":"quando-tn3","title":"Relative date parsing","description":"Implement ParseRelative for parsing relative date expressions.\n\n**API:**\n```go\nfunc ParseRelative(s string) (Date, error)\n```\n\n**Supported Expressions (Phase 1):**\n- \"today\" → Today 00:00:00\n- \"tomorrow\" → Tomorrow 00:00:00\n- \"yesterday\" → Yesterday 00:00:00\n- \"+2 days\" → Today + 2 days\n- \"-1 week\" → Today - 1 week\n- \"+3 months\" → Today + 3 months\n\n**Format:**\n- Relative offsets: [+|-]\u003cnumber\u003e \u003cunit\u003e\n- Units: day(s), week(s), month(s), quarter(s), year(s)\n- Singular and plural forms supported\n\n**Out of Scope (Phase 1):**\n- Complex expressions (\"next monday\", \"start of month\")\n- These are nice-to-have for later versions\n\n## Acceptance Criteria\n- [ ] ParseRelative() implemented\n- [ ] \"today\" returns today 00:00:00\n- [ ] \"tomorrow\" returns tomorrow 00:00:00\n- [ ] \"yesterday\" returns yesterday 00:00:00\n- [ ] \"+N \u003cunit\u003e\" adds N units to today\n- [ ] \"-N \u003cunit\u003e\" subtracts N units from today\n- [ ] Singular and plural units both work\n- [ ] All Phase 1 units supported (days, weeks, months, quarters, years)\n- [ ] Invalid expressions return clear errors\n- [ ] Never panics\n- [ ] Unit tests for all supported expressions\n- [ ] Unit tests for invalid inputs\n- [ ] Benchmark meets \u003c20µs target\n- [ ] Godoc with supported expressions listed\n- [ ] Note about future complex expressions","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:40.790156181+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:40.790156181+01:00","dependencies":[{"issue_id":"quando-tn3","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.247985003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-tn3","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.278394998+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-tn3","depends_on_id":"quando-b4r","type":"blocks","created_at":"2026-02-11T16:23:11.309549914+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-vih","title":"Clock abstraction for testability","description":"Implement Clock interface to enable dependency injection and deterministic testing.\n\n**Technical Details:**\n```go\ntype Clock interface {\n Now() Date\n From(t time.Time) Date\n}\n```\n\n**Implementations:**\n- DefaultClock: Uses time.Now()\n- FixedClock: Returns fixed time for tests\n\n**API:**\n- `NewClock()` - returns DefaultClock\n- `NewFixedClock(time.Time)` - returns FixedClock for tests\n\n## Acceptance Criteria\n- [ ] Clock interface defined\n- [ ] DefaultClock implementation using time.Now()\n- [ ] FixedClock implementation with fixed time\n- [ ] NewClock() factory function\n- [ ] NewFixedClock(time.Time) factory function\n- [ ] Unit tests demonstrating deterministic test patterns\n- [ ] Godoc comments\n- [ ] Example test showing test usage pattern","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:20:33.357927572+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:33:26.936900547+01:00","closed_at":"2026-02-11T16:33:26.936900547+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-vih","depends_on_id":"quando-91w","type":"blocks","created_at":"2026-02-11T16:23:05.308383809+01:00","created_by":"Oliver Jakoubek"}],"comments":[{"id":3,"issue_id":"quando-vih","author":"Oliver Jakoubek","text":"Plan: 1) Create clock.go with Clock interface (Now, From methods), 2) Implement DefaultClock using time.Now(), 3) Implement FixedClock with fixed time for deterministic tests, 4) Add factory functions NewClock() and NewFixedClock(time.Time), 5) Write comprehensive unit tests demonstrating deterministic test patterns, 6) Add example tests showing test usage, 7) Ensure all exports have godoc comments","created_at":"2026-02-11T15:31:45Z"}]}
{"id":"quando-wny","title":"Explicit parsing with layout","description":"Implement explicit parsing using Go's standard layout format.\n\n**API:**\n```go\nfunc ParseWithLayout(s, layout string) (Date, error)\n```\n\n**Purpose:**\nHandle ambiguous or custom formats by providing explicit layout\n\n**Examples:**\n```go\nParseWithLayout(\"01/02/2026\", \"02/01/2006\") // EU format\nParseWithLayout(\"01/02/2026\", \"01/02/2006\") // US format\nParseWithLayout(\"9. Februar 2026\", \"2. January 2006\") // German custom\n```\n\n**Implementation:**\n- Delegate to time.Parse() with layout\n- Wrap result in quando.Date\n- Return clear errors for invalid inputs\n\n## Acceptance Criteria\n- [ ] ParseWithLayout() implemented\n- [ ] Uses Go's standard layout format (reference date)\n- [ ] Wraps time.Parse() correctly\n- [ ] Returns Date on success\n- [ ] Returns error on parse failure\n- [ ] Never panics\n- [ ] Unit tests for various layouts\n- [ ] Unit tests for invalid inputs\n- [ ] Benchmark meets \u003c10µs target\n- [ ] Godoc with Go layout format explanation\n- [ ] Example tests showing EU vs US disambiguation","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:33.246073999+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:33.246073999+01:00","dependencies":[{"issue_id":"quando-wny","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:11.182490834+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"quando-wny","depends_on_id":"quando-36t","type":"blocks","created_at":"2026-02-11T16:23:11.216169487+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-zbr","title":"Language constants and i18n infrastructure","description":"Define language constants and i18n infrastructure for multi-language support.\n\n**API:**\n```go\ntype Lang string\n\nconst (\n LangEN Lang = \"en\" // English (Default)\n LangDE Lang = \"de\" // Deutsch\n)\n\nfunc (d Date) Lang(lang Lang) Date // Fluent API\n```\n\n**Phase 1 Languages:**\n- EN (English) - default\n- DE (Deutsch) - must-have\n\n**i18n Applies To:**\n- Format(Long) - \"February 9, 2026\" vs \"9. Februar 2026\"\n- Custom layouts with month/weekday names\n- Human() duration format\n\n**i18n Does NOT Apply To:**\n- ISO, EU, US, RFC2822 formats (always language-independent)\n- Numeric outputs (WeekNumber, Quarter, etc.)\n\n## Acceptance Criteria\n- [ ] Lang type defined as string\n- [ ] LangEN and LangDE constants defined\n- [ ] Lang() method on Date for fluent API\n- [ ] Month names in EN and DE\n- [ ] Weekday names in EN and DE\n- [ ] Time unit names for Human() in EN and DE\n- [ ] Unit tests for language switching\n- [ ] Godoc comments\n- [ ] Documentation noting future language expansion (21 more)","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:19.72536676+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T16:21:19.72536676+01:00","dependencies":[{"issue_id":"quando-zbr","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:09.056424359+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"quando-zbr","title":"Language constants and i18n infrastructure","description":"Define language constants and i18n infrastructure for multi-language support.\n\n**API:**\n```go\ntype Lang string\n\nconst (\n LangEN Lang = \"en\" // English (Default)\n LangDE Lang = \"de\" // Deutsch\n)\n\nfunc (d Date) Lang(lang Lang) Date // Fluent API\n```\n\n**Phase 1 Languages:**\n- EN (English) - default\n- DE (Deutsch) - must-have\n\n**i18n Applies To:**\n- Format(Long) - \"February 9, 2026\" vs \"9. Februar 2026\"\n- Custom layouts with month/weekday names\n- Human() duration format\n\n**i18n Does NOT Apply To:**\n- ISO, EU, US, RFC2822 formats (always language-independent)\n- Numeric outputs (WeekNumber, Quarter, etc.)\n\n## Acceptance Criteria\n- [ ] Lang type defined as string\n- [ ] LangEN and LangDE constants defined\n- [ ] Lang() method on Date for fluent API\n- [ ] Month names in EN and DE\n- [ ] Weekday names in EN and DE\n- [ ] Time unit names for Human() in EN and DE\n- [ ] Unit tests for language switching\n- [ ] Godoc comments\n- [ ] Documentation noting future language expansion (21 more)","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-02-11T16:21:19.72536676+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-02-11T19:27:04.692864456+01:00","closed_at":"2026-02-11T19:27:04.692864456+01:00","close_reason":"Closed","dependencies":[{"issue_id":"quando-zbr","depends_on_id":"quando-j2s","type":"blocks","created_at":"2026-02-11T16:23:09.056424359+01:00","created_by":"Oliver Jakoubek"}]}

18
date.go
View file

@ -5,8 +5,22 @@ import (
"time"
)
// Lang represents a language for i18n formatting.
// This is a placeholder - full implementation in i18n.go.
// Lang represents a language for internationalization (i18n) in formatting.
//
// Phase 1 supports English (EN) and German (DE). Future phases will expand to
// include 21 additional languages: FR, ES, IT, PT, NL, PL, RU, JA, ZH, KO, AR,
// HI, TR, SV, NO, DA, FI, CS, HU, RO, UK, EL.
//
// Language affects:
// - Format(Long): month and weekday names
// - FormatLayout: custom layouts with month/weekday names
// - Duration.Human(): time unit names
//
// Language does NOT affect:
// - ISO, EU, US, RFC2822 formats (always language-independent)
// - Numeric outputs (WeekNumber, Quarter, DayOfYear)
//
// See i18n.go for translation data and helper methods.
type Lang string
const (

View file

@ -458,3 +458,37 @@ func ExampleDate_In_error() {
// Output: Invalid timezone name
}
// ExampleLang_MonthName demonstrates localized month names
func ExampleLang_MonthName() {
fmt.Println(quando.EN.MonthName(time.February))
fmt.Println(quando.DE.MonthName(time.February))
// Output:
// February
// Februar
}
// ExampleLang_WeekdayName demonstrates localized weekday names
func ExampleLang_WeekdayName() {
fmt.Println(quando.EN.WeekdayName(time.Monday))
fmt.Println(quando.DE.WeekdayName(time.Monday))
// Output:
// Monday
// Montag
}
// ExampleLang_DurationUnit demonstrates localized duration units
func ExampleLang_DurationUnit() {
// Singular
fmt.Println(quando.EN.DurationUnit("month", false))
fmt.Println(quando.DE.DurationUnit("month", false))
// Plural
fmt.Println(quando.EN.DurationUnit("month", true))
fmt.Println(quando.DE.DurationUnit("month", true))
// Output:
// month
// Monat
// months
// Monate
}

138
i18n.go Normal file
View file

@ -0,0 +1,138 @@
package quando
import "time"
// i18n.go - Internationalization support for quando
//
// This file contains translations for month names, weekday names, and
// duration units used in formatting operations.
//
// Phase 1 Languages:
// - EN (English) - Default
// - DE (Deutsch/German)
//
// Future expansion will add 21 more languages including:
// FR, ES, IT, PT, NL, PL, RU, JA, ZH, KO, AR, HI, TR, SV, NO, DA, FI, CS, HU, RO, UK, EL
//
// i18n applies to:
// - Format(Long): "February 9, 2026" vs "9. Februar 2026"
// - FormatLayout with month/weekday names
// - Duration.Human(): "10 months, 16 days" vs "10 Monate, 16 Tage"
//
// i18n does NOT apply to:
// - ISO, EU, US, RFC2822 formats (always language-independent)
// - Numeric outputs (WeekNumber, Quarter, DayOfYear)
// monthNames contains full month name translations.
var monthNames = map[Lang][12]string{
EN: {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
},
DE: {
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
},
}
// monthNamesShort contains short (3-letter) month name translations.
var monthNamesShort = map[Lang][12]string{
EN: {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
DE: {"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"},
}
// weekdayNames contains full weekday name translations.
// Index: Sunday = 0, Monday = 1, ..., Saturday = 6
var weekdayNames = map[Lang][7]string{
EN: {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
DE: {"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"},
}
// weekdayNamesShort contains short (3-letter) weekday name translations.
// Index: Sunday = 0, Monday = 1, ..., Saturday = 6
var weekdayNamesShort = map[Lang][7]string{
EN: {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"},
DE: {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"},
}
// durationUnits contains duration unit translations for Human() formatting.
// Each unit has singular and plural forms: [0] = singular, [1] = plural
var durationUnits = map[Lang]map[string][2]string{
EN: {
"year": {"year", "years"},
"month": {"month", "months"},
"week": {"week", "weeks"},
"day": {"day", "days"},
"hour": {"hour", "hours"},
"minute": {"minute", "minutes"},
"second": {"second", "seconds"},
},
DE: {
"year": {"Jahr", "Jahre"},
"month": {"Monat", "Monate"},
"week": {"Woche", "Wochen"},
"day": {"Tag", "Tage"},
"hour": {"Stunde", "Stunden"},
"minute": {"Minute", "Minuten"},
"second": {"Sekunde", "Sekunden"},
},
}
// MonthName returns the localized month name for the given language.
// Returns English name if language not found.
func (l Lang) MonthName(month time.Month) string {
if names, ok := monthNames[l]; ok {
return names[month-1]
}
// Fallback to English
return monthNames[EN][month-1]
}
// MonthNameShort returns the short (3-letter) localized month name.
// Returns English abbreviation if language not found.
func (l Lang) MonthNameShort(month time.Month) string {
if names, ok := monthNamesShort[l]; ok {
return names[month-1]
}
return monthNamesShort[EN][month-1]
}
// WeekdayName returns the localized weekday name for the given language.
// Returns English name if language not found.
func (l Lang) WeekdayName(weekday time.Weekday) string {
if names, ok := weekdayNames[l]; ok {
return names[weekday]
}
return weekdayNames[EN][weekday]
}
// WeekdayNameShort returns the short (3-letter) localized weekday name.
// Returns English abbreviation if language not found.
func (l Lang) WeekdayNameShort(weekday time.Weekday) string {
if names, ok := weekdayNamesShort[l]; ok {
return names[weekday]
}
return weekdayNamesShort[EN][weekday]
}
// DurationUnit returns the localized duration unit name (singular or plural).
// The plural parameter determines which form to use.
// Returns English name if language not found.
func (l Lang) DurationUnit(unit string, plural bool) string {
if units, ok := durationUnits[l]; ok {
if forms, ok := units[unit]; ok {
if plural {
return forms[1]
}
return forms[0]
}
}
// Fallback to English
if forms, ok := durationUnits[EN][unit]; ok {
if plural {
return forms[1]
}
return forms[0]
}
return unit
}

378
i18n_test.go Normal file
View file

@ -0,0 +1,378 @@
package quando
import (
"testing"
"time"
)
func TestMonthName(t *testing.T) {
tests := []struct {
lang Lang
month time.Month
expected string
}{
// English months
{EN, time.January, "January"},
{EN, time.February, "February"},
{EN, time.March, "March"},
{EN, time.April, "April"},
{EN, time.May, "May"},
{EN, time.June, "June"},
{EN, time.July, "July"},
{EN, time.August, "August"},
{EN, time.September, "September"},
{EN, time.October, "October"},
{EN, time.November, "November"},
{EN, time.December, "December"},
// German months
{DE, time.January, "Januar"},
{DE, time.February, "Februar"},
{DE, time.March, "März"},
{DE, time.April, "April"},
{DE, time.May, "Mai"},
{DE, time.June, "Juni"},
{DE, time.July, "Juli"},
{DE, time.August, "August"},
{DE, time.September, "September"},
{DE, time.October, "Oktober"},
{DE, time.November, "November"},
{DE, time.December, "Dezember"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.lang.MonthName(tt.month)
if result != tt.expected {
t.Errorf("MonthName(%v, %v) = %v, want %v", tt.lang, tt.month, result, tt.expected)
}
})
}
}
func TestMonthNameShort(t *testing.T) {
tests := []struct {
lang Lang
month time.Month
expected string
}{
// English short months
{EN, time.January, "Jan"},
{EN, time.February, "Feb"},
{EN, time.March, "Mar"},
{EN, time.April, "Apr"},
{EN, time.May, "May"},
{EN, time.June, "Jun"},
{EN, time.July, "Jul"},
{EN, time.August, "Aug"},
{EN, time.September, "Sep"},
{EN, time.October, "Oct"},
{EN, time.November, "Nov"},
{EN, time.December, "Dec"},
// German short months
{DE, time.January, "Jan"},
{DE, time.February, "Feb"},
{DE, time.March, "Mär"},
{DE, time.April, "Apr"},
{DE, time.May, "Mai"},
{DE, time.June, "Jun"},
{DE, time.July, "Jul"},
{DE, time.August, "Aug"},
{DE, time.September, "Sep"},
{DE, time.October, "Okt"},
{DE, time.November, "Nov"},
{DE, time.December, "Dez"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.lang.MonthNameShort(tt.month)
if result != tt.expected {
t.Errorf("MonthNameShort(%v, %v) = %v, want %v", tt.lang, tt.month, result, tt.expected)
}
})
}
}
func TestWeekdayName(t *testing.T) {
tests := []struct {
lang Lang
weekday time.Weekday
expected string
}{
// English weekdays
{EN, time.Sunday, "Sunday"},
{EN, time.Monday, "Monday"},
{EN, time.Tuesday, "Tuesday"},
{EN, time.Wednesday, "Wednesday"},
{EN, time.Thursday, "Thursday"},
{EN, time.Friday, "Friday"},
{EN, time.Saturday, "Saturday"},
// German weekdays
{DE, time.Sunday, "Sonntag"},
{DE, time.Monday, "Montag"},
{DE, time.Tuesday, "Dienstag"},
{DE, time.Wednesday, "Mittwoch"},
{DE, time.Thursday, "Donnerstag"},
{DE, time.Friday, "Freitag"},
{DE, time.Saturday, "Samstag"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.lang.WeekdayName(tt.weekday)
if result != tt.expected {
t.Errorf("WeekdayName(%v, %v) = %v, want %v", tt.lang, tt.weekday, result, tt.expected)
}
})
}
}
func TestWeekdayNameShort(t *testing.T) {
tests := []struct {
lang Lang
weekday time.Weekday
expected string
}{
// English short weekdays
{EN, time.Sunday, "Sun"},
{EN, time.Monday, "Mon"},
{EN, time.Tuesday, "Tue"},
{EN, time.Wednesday, "Wed"},
{EN, time.Thursday, "Thu"},
{EN, time.Friday, "Fri"},
{EN, time.Saturday, "Sat"},
// German short weekdays
{DE, time.Sunday, "So"},
{DE, time.Monday, "Mo"},
{DE, time.Tuesday, "Di"},
{DE, time.Wednesday, "Mi"},
{DE, time.Thursday, "Do"},
{DE, time.Friday, "Fr"},
{DE, time.Saturday, "Sa"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.lang.WeekdayNameShort(tt.weekday)
if result != tt.expected {
t.Errorf("WeekdayNameShort(%v, %v) = %v, want %v", tt.lang, tt.weekday, result, tt.expected)
}
})
}
}
func TestDurationUnit(t *testing.T) {
tests := []struct {
lang Lang
unit string
plural bool
expected string
}{
// English singular
{EN, "year", false, "year"},
{EN, "month", false, "month"},
{EN, "week", false, "week"},
{EN, "day", false, "day"},
{EN, "hour", false, "hour"},
{EN, "minute", false, "minute"},
{EN, "second", false, "second"},
// English plural
{EN, "year", true, "years"},
{EN, "month", true, "months"},
{EN, "week", true, "weeks"},
{EN, "day", true, "days"},
{EN, "hour", true, "hours"},
{EN, "minute", true, "minutes"},
{EN, "second", true, "seconds"},
// German singular
{DE, "year", false, "Jahr"},
{DE, "month", false, "Monat"},
{DE, "week", false, "Woche"},
{DE, "day", false, "Tag"},
{DE, "hour", false, "Stunde"},
{DE, "minute", false, "Minute"},
{DE, "second", false, "Sekunde"},
// German plural
{DE, "year", true, "Jahre"},
{DE, "month", true, "Monate"},
{DE, "week", true, "Wochen"},
{DE, "day", true, "Tage"},
{DE, "hour", true, "Stunden"},
{DE, "minute", true, "Minuten"},
{DE, "second", true, "Sekunden"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.lang.DurationUnit(tt.unit, tt.plural)
if result != tt.expected {
t.Errorf("DurationUnit(%v, %v, %v) = %v, want %v", tt.lang, tt.unit, tt.plural, result, tt.expected)
}
})
}
}
func TestLanguageFallback(t *testing.T) {
// Unknown language should fallback to English
unknownLang := Lang("unknown")
t.Run("MonthName fallback", func(t *testing.T) {
result := unknownLang.MonthName(time.January)
if result != "January" {
t.Errorf("Unknown language should fallback to English, got %v", result)
}
})
t.Run("MonthNameShort fallback", func(t *testing.T) {
result := unknownLang.MonthNameShort(time.February)
if result != "Feb" {
t.Errorf("Unknown language should fallback to English, got %v", result)
}
})
t.Run("WeekdayName fallback", func(t *testing.T) {
result := unknownLang.WeekdayName(time.Monday)
if result != "Monday" {
t.Errorf("Unknown language should fallback to English, got %v", result)
}
})
t.Run("WeekdayNameShort fallback", func(t *testing.T) {
result := unknownLang.WeekdayNameShort(time.Friday)
if result != "Fri" {
t.Errorf("Unknown language should fallback to English, got %v", result)
}
})
t.Run("DurationUnit fallback", func(t *testing.T) {
result := unknownLang.DurationUnit("month", true)
if result != "months" {
t.Errorf("Unknown language should fallback to English, got %v", result)
}
})
}
func TestDurationUnitUnknownUnit(t *testing.T) {
// Unknown unit should return the unit string itself
result := EN.DurationUnit("unknown", false)
if result != "unknown" {
t.Errorf("Unknown unit should return unit string, got %v", result)
}
result = DE.DurationUnit("unknown", true)
if result != "unknown" {
t.Errorf("Unknown unit should return unit string, got %v", result)
}
}
func TestAllMonthsPresent(t *testing.T) {
// Ensure all languages have all 12 months
t.Run("monthNames", func(t *testing.T) {
for lang, months := range monthNames {
if len(months) != 12 {
t.Errorf("Language %v has %d months in monthNames, expected 12", lang, len(months))
}
// Check for empty strings
for i, name := range months {
if name == "" {
t.Errorf("Language %v has empty month name at index %d", lang, i)
}
}
}
})
t.Run("monthNamesShort", func(t *testing.T) {
for lang, months := range monthNamesShort {
if len(months) != 12 {
t.Errorf("Language %v has %d months in monthNamesShort, expected 12", lang, len(months))
}
// Check for empty strings
for i, name := range months {
if name == "" {
t.Errorf("Language %v has empty short month name at index %d", lang, i)
}
}
}
})
}
func TestAllWeekdaysPresent(t *testing.T) {
// Ensure all languages have all 7 weekdays
t.Run("weekdayNames", func(t *testing.T) {
for lang, weekdays := range weekdayNames {
if len(weekdays) != 7 {
t.Errorf("Language %v has %d weekdays in weekdayNames, expected 7", lang, len(weekdays))
}
// Check for empty strings
for i, name := range weekdays {
if name == "" {
t.Errorf("Language %v has empty weekday name at index %d", lang, i)
}
}
}
})
t.Run("weekdayNamesShort", func(t *testing.T) {
for lang, weekdays := range weekdayNamesShort {
if len(weekdays) != 7 {
t.Errorf("Language %v has %d weekdays in weekdayNamesShort, expected 7", lang, len(weekdays))
}
// Check for empty strings
for i, name := range weekdays {
if name == "" {
t.Errorf("Language %v has empty short weekday name at index %d", lang, i)
}
}
}
})
}
func TestAllDurationUnitsPresent(t *testing.T) {
// Ensure all languages have all expected duration units
expectedUnits := []string{"year", "month", "week", "day", "hour", "minute", "second"}
for lang, units := range durationUnits {
for _, unit := range expectedUnits {
forms, ok := units[unit]
if !ok {
t.Errorf("Language %v missing duration unit %v", lang, unit)
continue
}
// Check singular
if forms[0] == "" {
t.Errorf("Language %v has empty singular form for unit %v", lang, unit)
}
// Check plural
if forms[1] == "" {
t.Errorf("Language %v has empty plural form for unit %v", lang, unit)
}
}
}
}
func TestGermanSpecialCharacters(t *testing.T) {
// Verify German special characters are correctly stored
tests := []struct {
name string
value string
expected string
}{
{"March", DE.MonthName(time.March), "März"},
{"March short", DE.MonthNameShort(time.March), "Mär"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, tt.value)
}
})
}
}