# Gettext Internationalization Design

**Date:** 2026-02-02
**Status:** Approved
**Scope:** Comprehensive i18n for entire Towerops web application
**Initial Language:** English only (prepared for future multilingual support)

## Overview

Move all user-facing text in Towerops to Gettext for internationalization. This enables future language support while maintaining English as the primary language. The implementation uses domain-based organization for better maintainability as the application grows.

## Goals

1. Extract all user-facing text to Gettext translation files
2. Organize translations by feature domain for maintainability
3. Maintain English-only translations initially
4. Prepare infrastructure for future language additions
5. Ensure no functional regressions during migration

## Architecture

### Gettext Domain Structure

Organize translations into 6 domains:

| Domain | Purpose | Examples |
|--------|---------|----------|
| `default` | Common UI elements, navigation, generic actions | "Dashboard", "Save", "Cancel", "Loading..." |
| `errors` | Validation errors, system errors, API errors | "can't be blank", "Not found", "Unauthorized" |
| `auth` | Authentication flows, user management | "Log in", "Sign up", "Reset password", "TOTP" |
| `equipment` | Equipment/device management, SNMP, monitoring | "Add device", "SNMP settings", "Last polled", "Alerts" |
| `admin` | Admin-specific features | "Impersonate user", "GeoIP import", "Audit logs" |
| `emails` | Email templates and subjects | "Reset password instructions", email bodies |

**Rationale:**
- Smaller .po files are easier to review and maintain
- Clear ownership - developers know which domain to use
- Enables assigning different translators to different areas
- Standard pattern in mature i18n applications

### Helper Module Pattern

Create domain-specific helper functions to reduce verbosity:

```elixir
# lib/towerops_web/gettext_helpers.ex
defmodule ToweropsWeb.GettextHelpers do
  import ToweropsWeb.Gettext

  def t(msgid), do: dgettext("default", msgid)
  def t_auth(msgid), do: dgettext("auth", msgid)
  def t_equipment(msgid), do: dgettext("equipment", msgid)
  def t_admin(msgid), do: dgettext("admin", msgid)
  def t_email(msgid), do: dgettext("emails", msgid)
  # Errors use existing gettext/1
end
```

**Benefits:**
- Shorter syntax: `t("Save")` vs `dgettext("default", "Save")`
- Clear domain intent: `t_auth("Log in")`
- Easy refactoring if domain structure changes

### Import Strategy

**Web Layer (LiveViews, Components, Controllers):**
- Add `GettextHelpers` to `html_helpers/0` in `ToweropsWeb`
- All LiveViews and components get helpers automatically via `use ToweropsWeb, :live_view`

**Non-Web Layer (Emails, Background Jobs):**
- Explicit import: `import ToweropsWeb.GettextHelpers`
- Use in notifier modules, workers, contexts

## Implementation Patterns

### Template Patterns

**Basic string replacement:**

```heex
<!-- Before -->
<h1>Equipment List</h1>
<.button>Add Device</.button>

<!-- After -->
<h1>{t_equipment("Equipment List")}</h1>
<.button>{t_equipment("Add Device")}</.button>
```

**String interpolation:**

```heex
<p>{t_equipment("Last polled: %{time}", time: format_timestamp(@last_polled))}</p>
<p>{t_equipment("Found %{count} devices", count: @device_count)}</p>
```

**Pluralization:**

```elixir
ngettext("1 device", "%{count} devices", device_count)
dngettext("equipment", "1 alert", "%{count} alerts", alert_count)
```

### Flash Messages

```elixir
# Before
put_flash(socket, :info, "Device created successfully")

# After
put_flash(socket, :info, t_equipment("Device created successfully"))
```

### Email Templates

```elixir
# Before
subject = "Reset password instructions"
body = """
Hi #{user.email},

You can reset your password by visiting the URL below:
#{url}
"""

# After
subject = t_email("Reset password instructions")
body = t_email("""
Hi %{email},

You can reset your password by visiting the URL below:
%{url}

If you didn't request this change, please ignore this.
""", email: user.email, url: url)
```

### Validation Messages

Ecto changeset validations already use Gettext via the `errors` domain automatically. Ensure custom error messages use `dgettext("errors", ...)`.

### What NOT to Translate

- Log messages (internal, for developers)
- API response keys (machine-readable)
- Database column names
- Configuration keys
- Code comments
- Developer-facing error details (stack traces, debug info)

## Migration Workflow

### Phase 1: Setup (Infrastructure)

1. Create domain-specific .pot template files:
   - `priv/gettext/auth.pot`
   - `priv/gettext/equipment.pot`
   - `priv/gettext/admin.pot`
   - `priv/gettext/emails.pot`
   - Keep existing `default.pot` and `errors.pot`

2. Create corresponding English .po files:
   - `priv/gettext/en/LC_MESSAGES/auth.po`
   - `priv/gettext/en/LC_MESSAGES/equipment.po`
   - `priv/gettext/en/LC_MESSAGES/admin.po`
   - `priv/gettext/en/LC_MESSAGES/emails.po`

3. Create `lib/towerops_web/gettext_helpers.ex`

4. Update `lib/towerops_web.ex` to import helpers in `html_helpers/0`

5. Create Mix task to auto-populate English translations (msgstr = msgid)

### Phase 2: Module-by-Module Migration

**Order of migration (smallest to largest):**

1. **Emails** (`lib/towerops/accounts/user_notifier.ex`)
   - Self-contained, minimal dependencies
   - Domain: `emails`

2. **Auth Flows**
   - `lib/towerops_web/controllers/user_*_html.ex`
   - `lib/towerops_web/live/user_settings_live/`
   - Domain: `auth`

3. **Core Components** (`lib/towerops_web/components/`)
   - `core_components.ex` - Generic labels
   - `layouts.ex` - Navigation, user menu
   - Domain: `default`

4. **Equipment Features**
   - `lib/towerops_web/live/device_live/`
   - `lib/towerops_web/live/equipment_live/`
   - `lib/towerops_web/live/alert_live/`
   - Domain: `equipment`

5. **Admin Features**
   - `lib/towerops_web/live/admin/`
   - Domain: `admin`

6. **Error Messages**
   - Review all `put_flash(:error, ...)` calls
   - Ecto validation messages
   - Domain: `errors`

**Per-module process:**

1. Update module to use gettext calls
2. Run `mix gettext.extract --merge`
3. Run `mix populate_english` to fill English translations
4. Compile and verify no errors
5. Manual QA - check pages render correctly
6. Run `mix test` to verify no regressions
7. Commit changes for that module

### Phase 3: Verification

1. Run `mix gettext.extract --check-up-to-date` (fails if untranslated strings)
2. Add to CI pipeline to catch missing translations
3. Visual QA pass on all major pages
4. Test all flash messages, error states, email previews

## Gettext Commands Reference

```bash
# Extract new strings from code
mix gettext.extract

# Merge extracted strings into existing .po files (preserves translations)
mix gettext.merge priv/gettext

# Extract and merge in one step
mix gettext.extract --merge

# Check for unused translations (fail if out of date)
mix gettext.extract --check-up-to-date
```

## Testing Strategy

1. **Unit Tests:** Continue to work unchanged (test behavior, not strings)
2. **Integration Tests:** May need updates if asserting on specific text
3. **CI Check:** Add `mix gettext.extract --check-up-to-date` to CI pipeline
4. **Manual QA:** Visual inspection of key pages after migration
5. **Email Preview:** Use Swoosh mailbox (`/dev/mailbox`) to verify email translations

## Future Enhancements

### Adding New Languages

When ready to add a new language (e.g., Spanish):

1. Create `priv/gettext/es/LC_MESSAGES/*.po` files
2. Copy structure from English .po files
3. Replace msgstr values with Spanish translations
4. Recompile application
5. Set user locale via session/cookie

### Locale Selection

Add user preference for language:

1. Add `locale` field to users table (default: "en")
2. Set `Gettext.put_locale/1` in authentication pipeline
3. Add language selector in user settings
4. Persist choice in session/database

### Dynamic Content

For database-stored content that needs translation:

1. Consider using a translation table pattern
2. Or use JSONB columns with locale keys
3. Libraries like `trans` can help with database translations

## File Structure

```
priv/gettext/
├── default.pot         # Common UI template
├── errors.pot          # Error messages template
├── auth.pot            # Auth flows template
├── equipment.pot       # Equipment features template
├── admin.pot           # Admin features template
├── emails.pot          # Email templates template
└── en/
    └── LC_MESSAGES/
        ├── default.po      # English common UI
        ├── errors.po       # English errors
        ├── auth.po         # English auth
        ├── equipment.po    # English equipment
        ├── admin.po        # English admin
        └── emails.po       # English emails

lib/
├── towerops_web/
│   ├── gettext.ex              # Existing Gettext backend
│   ├── gettext_helpers.ex      # NEW: Domain helper functions
│   └── ...
└── mix/
    └── tasks/
        └── populate_english.exs  # NEW: Auto-fill English translations
```

## Risks & Mitigations

| Risk | Mitigation |
|------|------------|
| Forgetting to translate new strings | Add `mix gettext.extract --check-up-to-date` to CI |
| Breaking existing tests | Run test suite after each module migration |
| Inconsistent domain choices | Document examples in CLAUDE.md after implementation |
| Performance impact | Gettext compiles translations at build time (no runtime cost) |
| Large PR review burden | Migrate module-by-module with separate commits |

## Success Criteria

- ✅ All user-facing text extracted to Gettext
- ✅ All English .po files populated (msgstr = msgid)
- ✅ All tests passing
- ✅ CI check for untranslated strings
- ✅ No functional regressions
- ✅ Documentation updated in CLAUDE.md
- ✅ Infrastructure ready for adding new languages

## Estimated Scope

- **34 LiveView modules** + **22 HEEx templates**
- **~8 controller HTML modules**
- **1 email notifier module** (5 email templates)
- **Dozens of flash messages** across modules
- **Core components** with embedded strings

**Implementation Time:**
- Setup phase: ~2 hours
- Migration work: ~1-2 days (incremental, module-by-module)
- Testing & QA: ~4 hours

## References

- [Phoenix Gettext Guide](https://hexdocs.pm/phoenix/gettext.html)
- [Gettext Documentation](https://hexdocs.pm/gettext/Gettext.html)
- [GNU Gettext Manual](https://www.gnu.org/software/gettext/manual/gettext.html)
