secana/Forji
3
54
Fork
You've already forked Forji
7

Add German localization #76

Open
opened 2026年06月13日 13:50:33 +02:00 by secana · 0 comments

Add German localization (i18n) alongside English

Summary

Forji currently ships English-only with all user-facing strings hardcoded as literals. This issue tracks adding German as a second language using Apple's modern String Catalog (.xcstrings) workflow. The goal is a fully German-capable build that falls back to English for anything untranslated, while leaving Forgejo API data (repo names, issue titles, usernames) untouched.

Background / current state

  • No localization infrastructure exists: no .xcstrings, .strings, .stringsdict, or .lproj directories.
  • project.pbxproj: developmentRegion = en, knownRegions = (en, Base). German is not declared.
  • ~150+ user-facing strings across 54 view files, all string literals.
  • Target is iOS 18+ (uses Tab API + liquid glass), so String Catalogs are available and are the right tool.

Approach

Use a single Localizable.xcstrings String Catalog (not legacy .strings/.stringsdict). Most SwiftUI views take LocalizedStringKey, so existing literals like .navigationTitle("Repositories") are already valid keys and need no code change, only translation. Plain-String display logic in helpers/models needs wrapping in String(localized:).

Work items

1. Project setup

  • Add a Localizable.xcstrings String Catalog (File -> New -> String Catalog).
  • Add German via Project -> Info -> Localizations -> + -> German. Confirm knownRegions in project.pbxproj gains de.
  • Build once so Xcode's scan auto-populates the catalog from all LocalizedStringKey literals.

2. Wrap plain-String display logic in String(localized:)

These return raw String and are not auto-localized. Files confirmed to need edits:

  • Models/ReviewState.swift - ReviewEvent.title (Comment / Approve / Reject), ReviewState.label (approved / rejected / commented / pending).
  • Helpers/PRStatusStyle.swift - text values (Merged / Draft / Open / Closed).
  • Helpers/WorkflowStatus.swift - WorkflowRunFilter.label (All / Running / Success / Failed), WorkflowStatusStyle labels (Success / Failed / Cancelled / Skipped / Running / Waiting / Blocked / Unknown), and displayName ("Run #..." / "Run ...").
  • Sweep the rest of Models/ and Helpers/ for any other computed display strings missed above.

3. Fix interpolation edge cases (German word order / plurals)

  • Views/NotificationsOverviewView.swift - "There are no \(statusFilter) notifications": replace the interpolation with a switch over the filter yielding distinct full keys per case, since German word order differs.
  • Helpers/WorkflowStatus.swift - status.capitalized on API strings won't localize; map known statuses to localized keys via switch, or consciously leave unknown ones English.
  • Error-message prefixes like "PR created, but requesting reviewers failed: \(error.localizedDescription)" (Views/PullRequestCreateView.swift): localize the prefix; the localizedDescription is already system-localized.

4. Pluralization

Use the String Catalog's inline "Vary by Plural" for count-based strings (German has different plural rules):

  • "Comments (\(comments.count))" - Views/IssueDetailView.swift
  • "Reviews (\(reviews.count))" - Views/PullRequestDetailView.swift
  • Audit for other \(...count...) strings.

5. Translate

  • Fill the German column for every catalog entry, mark as reviewed.
  • Provide German for accessibility labels too (e.g. "Star repository" / "Unstar repository" in RepositoryListView.swift).

Explicitly out of scope / do NOT translate

  • Forgejo API data rendered via Text(variable): repository.name, issue.title, pullReq.title, user.login, server-provided notification statuses. The existing Text(variable) (non-literal) usage already prevents these from being localized; keep it that way.

Open question / needs investigation

  • ForgejoKit formatRelativeDate(_:) lives in the dependency package, not the app. If it uses RelativeDateTimeFormatter / Date.FormatStyle, relative dates localize automatically by locale. If it hand-builds strings like "2 days ago", that text will stay English and requires a change in ForgejoKit itself. Verify which, and file a follow-up against ForgejoKit if needed.

Acceptance criteria

  • App runs fully in German when device language is German, with no leftover English in app-authored UI (API data excepted).
  • Untranslated strings fall back to English gracefully.
  • Plurals render correctly in both languages (1 Kommentar vs 2 Kommentare).
  • Switching device language to English shows no regression.
  • just build, just test, just lint, just format all pass.

Testing notes

  • Use the Xcode scheme's App Language -> German run option (and the SwiftUI preview locale .environment(\.locale, .init(identifier: "de"))) to spot-check screens without changing the whole simulator.
## Add German localization (i18n) alongside English ### Summary Forji currently ships English-only with all user-facing strings hardcoded as literals. This issue tracks adding German as a second language using Apple's modern **String Catalog** (`.xcstrings`) workflow. The goal is a fully German-capable build that falls back to English for anything untranslated, while leaving Forgejo API data (repo names, issue titles, usernames) untouched. ### Background / current state - **No localization infrastructure exists**: no `.xcstrings`, `.strings`, `.stringsdict`, or `.lproj` directories. - `project.pbxproj`: `developmentRegion = en`, `knownRegions = (en, Base)`. German is not declared. - ~150+ user-facing strings across 54 view files, all string literals. - Target is iOS 18+ (uses `Tab` API + liquid glass), so String Catalogs are available and are the right tool. ### Approach Use a single `Localizable.xcstrings` String Catalog (not legacy `.strings`/`.stringsdict`). Most SwiftUI views take `LocalizedStringKey`, so existing literals like `.navigationTitle("Repositories")` are already valid keys and need no code change, only translation. Plain-`String` display logic in helpers/models needs wrapping in `String(localized:)`. ### Work items **1. Project setup** - [ ] Add a `Localizable.xcstrings` String Catalog (File -> New -> String Catalog). - [ ] Add German via Project -> Info -> Localizations -> `+` -> German. Confirm `knownRegions` in `project.pbxproj` gains `de`. - [ ] Build once so Xcode's scan auto-populates the catalog from all `LocalizedStringKey` literals. **2. Wrap plain-`String` display logic in `String(localized:)`** These return raw `String` and are not auto-localized. Files confirmed to need edits: - [ ] `Models/ReviewState.swift` - `ReviewEvent.title` (Comment / Approve / Reject), `ReviewState.label` (approved / rejected / commented / pending). - [ ] `Helpers/PRStatusStyle.swift` - `text` values (Merged / Draft / Open / Closed). - [ ] `Helpers/WorkflowStatus.swift` - `WorkflowRunFilter.label` (All / Running / Success / Failed), `WorkflowStatusStyle` labels (Success / Failed / Cancelled / Skipped / Running / Waiting / Blocked / Unknown), and `displayName` ("Run #..." / "Run ..."). - [ ] Sweep the rest of `Models/` and `Helpers/` for any other computed display strings missed above. **3. Fix interpolation edge cases (German word order / plurals)** - [ ] `Views/NotificationsOverviewView.swift` - `"There are no \(statusFilter) notifications"`: replace the interpolation with a `switch` over the filter yielding distinct full keys per case, since German word order differs. - [ ] `Helpers/WorkflowStatus.swift` - `status.capitalized` on API strings won't localize; map known statuses to localized keys via `switch`, or consciously leave unknown ones English. - [ ] Error-message prefixes like `"PR created, but requesting reviewers failed: \(error.localizedDescription)"` (`Views/PullRequestCreateView.swift`): localize the prefix; the `localizedDescription` is already system-localized. **4. Pluralization** Use the String Catalog's inline "Vary by Plural" for count-based strings (German has different plural rules): - [ ] `"Comments (\(comments.count))"` - `Views/IssueDetailView.swift` - [ ] `"Reviews (\(reviews.count))"` - `Views/PullRequestDetailView.swift` - [ ] Audit for other `\(...count...)` strings. **5. Translate** - [ ] Fill the German column for every catalog entry, mark as reviewed. - [ ] Provide German for accessibility labels too (e.g. "Star repository" / "Unstar repository" in `RepositoryListView.swift`). ### Explicitly out of scope / do NOT translate - Forgejo API data rendered via `Text(variable)`: `repository.name`, `issue.title`, `pullReq.title`, `user.login`, server-provided notification statuses. The existing `Text(variable)` (non-literal) usage already prevents these from being localized; keep it that way. ### Open question / needs investigation - [ ] **ForgejoKit `formatRelativeDate(_:)`** lives in the dependency package, not the app. If it uses `RelativeDateTimeFormatter` / `Date.FormatStyle`, relative dates localize automatically by locale. If it hand-builds strings like "2 days ago", that text will stay English and requires a change in ForgejoKit itself. Verify which, and file a follow-up against ForgejoKit if needed. ### Acceptance criteria - App runs fully in German when device language is German, with no leftover English in app-authored UI (API data excepted). - Untranslated strings fall back to English gracefully. - Plurals render correctly in both languages (1 Kommentar vs 2 Kommentare). - Switching device language to English shows no regression. - `just build`, `just test`, `just lint`, `just format` all pass. ### Testing notes - Use the Xcode scheme's **App Language -> German** run option (and the SwiftUI preview locale `.environment(\.locale, .init(identifier: "de"))`) to spot-check screens without changing the whole simulator.
Sign in to join this conversation.
No Branch/Tag specified
main
v1.6
v1.5
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
secana/Forji#76
Reference in a new issue
secana/Forji
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?