From 9df28f74998a7eec28d555c6c5d084aedf280157 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:49:21 +0800 Subject: [PATCH 1/7] ci(release): isolate write permissions --- .github/workflows/release-cli.yml | 51 +++++++++++++++++------- Scripts/lint.sh | 6 +++ Scripts/test_release_cli_permissions.sh | 53 +++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 15 deletions(-) create mode 100755 Scripts/test_release_cli_permissions.sh diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 75a020a0c1..8b52818c0e 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -11,10 +11,12 @@ on: type: string permissions: - contents: write + contents: read jobs: build-cli: + permissions: + contents: read strategy: fail-fast: false matrix: @@ -396,20 +398,7 @@ jobs: echo "out_dir=$OUT_DIR" >> "$GITHUB_OUTPUT" echo "asset=$ASSET" >> "$GITHUB_OUTPUT" - - name: Upload release assets - if: github.event_name == 'release' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - set -euo pipefail - TAG="${RELEASE_TAG}" - OUT_DIR="${{ steps.pkg.outputs.out_dir }}" - ASSET="${{ steps.pkg.outputs.asset }}" - gh release upload "$TAG" "$OUT_DIR/$ASSET" "$OUT_DIR/$ASSET.sha256" --clobber - - - name: Upload workflow artifact (manual runs) - if: github.event_name == 'workflow_dispatch' + - name: Upload packaged artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: codexbar-cli-${{ matrix.name }} @@ -417,10 +406,42 @@ jobs: ${{ steps.pkg.outputs.out_dir }}/${{ steps.pkg.outputs.asset }} ${{ steps.pkg.outputs.out_dir }}/${{ steps.pkg.outputs.asset }}.sha256 + publish-release-assets: + runs-on: ubuntu-24.04 + needs: build-cli + if: github.event_name == 'release' + permissions: + actions: read + contents: write + steps: + - name: Download packaged artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: codexbar-cli-* + path: release-assets + merge-multiple: true + + - name: Upload release assets + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.ref_name }} + shell: bash + run: | + set -euo pipefail + mapfile -t assets < <(find release-assets -maxdepth 1 -type f \ + \( -name 'CodexBarCLI-*.tar.gz' -o -name 'CodexBarCLI-*.tar.gz.sha256' \) -print | sort) + if [[ "${#assets[@]}" -ne 12 ]]; then + printf 'Expected 12 CLI release files, found %s.\n' "${#assets[@]}" >&2 + printf '%s\n' "${assets[@]:-}" >&2 + exit 1 + fi + gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber + update-homebrew-tap: runs-on: ubuntu-24.04 needs: build-cli if: github.event_name == 'release' + permissions: {} steps: - name: Resolve release tag id: release diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 7db993e045..45d2a12ce4 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -79,6 +79,10 @@ check_documentation_links() { node "${ROOT_DIR}/Scripts/check-documentation-links.mjs" } +check_release_cli_permissions() { + "${ROOT_DIR}/Scripts/test_release_cli_permissions.sh" +} + check_llms_index() { node "${ROOT_DIR}/Scripts/generate-llms.mjs" --check } @@ -93,10 +97,12 @@ run_portable_checks() { check_sparkle_signing_paths check_swift_test_sharding check_ci_path_gate + check_release_cli_permissions check_repository_size check_shell_scripts check_documentation_links check_llms_index + check_release_cli_permissions check_site_locales } diff --git a/Scripts/test_release_cli_permissions.sh b/Scripts/test_release_cli_permissions.sh new file mode 100755 index 0000000000..b9dcd78485 --- /dev/null +++ b/Scripts/test_release_cli_permissions.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORKFLOW="${ROOT_DIR}/.github/workflows/release-cli.yml" + +python3 - "$WORKFLOW" <<'PY' +import pathlib +import re +import sys + +workflow = pathlib.Path(sys.argv[1]).read_text() + +if not re.search(r"(?ms)^permissions:\n contents: read\n", workflow): + raise SystemExit("release workflow must default to read-only repository contents") + + +def job(name: str) -> str: + match = re.search(rf"(?ms)^ {re.escape(name)}:\n(?P.*?)(?=^ [A-Za-z0-9_-]+:|\Z)", workflow) + if match is None: + raise SystemExit(f"missing {name} job") + return match.group("body") + + +build = job("build-cli") +if not re.search(r"(?ms)^ permissions:\n contents: read\n", build): + raise SystemExit("build-cli must receive only read access to repository contents") +if "contents: write" in build: + raise SystemExit("build-cli must not receive repository write access") +if "Upload packaged artifact" not in build or "if: github.event_name" in build.split("Upload packaged artifact", 1)[1].split("\n ", 1)[0]: + raise SystemExit("build-cli must upload its packaged artifact for every run") + +publisher = job("publish-release-assets") +for required in ( + "needs: build-cli", + "if: github.event_name == 'release'", + "actions: read", + "contents: write", + "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093", + "pattern: codexbar-cli-*", + "merge-multiple: true", + 'gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber', +): + if required not in publisher: + raise SystemExit(f"release publisher is missing: {required}") + +tap = job("update-homebrew-tap") +if "permissions: {}" not in tap: + raise SystemExit("tap updater must not receive the repository token") + +print("Release CLI permissions workflow tests passed.") +PY From 5f4357b0477894f05c615e264c84c68b90bb162f Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:52:56 +0800 Subject: [PATCH 2/7] Fix release asset publication ordering --- .github/workflows/release-cli.yml | 4 ++-- Scripts/test_release_cli_permissions.sh | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 8b52818c0e..8a0a71178f 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -435,11 +435,11 @@ jobs: printf '%s\n' "${assets[@]:-}" >&2 exit 1 fi - gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber + gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY" update-homebrew-tap: runs-on: ubuntu-24.04 - needs: build-cli + needs: publish-release-assets if: github.event_name == 'release' permissions: {} steps: diff --git a/Scripts/test_release_cli_permissions.sh b/Scripts/test_release_cli_permissions.sh index b9dcd78485..7ac9c93dee 100755 --- a/Scripts/test_release_cli_permissions.sh +++ b/Scripts/test_release_cli_permissions.sh @@ -40,12 +40,14 @@ for required in ( "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093", "pattern: codexbar-cli-*", "merge-multiple: true", - 'gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber', + 'gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY"', ): if required not in publisher: raise SystemExit(f"release publisher is missing: {required}") tap = job("update-homebrew-tap") +if "needs: publish-release-assets" not in tap: + raise SystemExit("tap updater must wait for release assets to upload") if "permissions: {}" not in tap: raise SystemExit("tap updater must not receive the repository token") From f38384489b3fa96f516f98f7a89cd5f39a119f3f Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:36:25 +0800 Subject: [PATCH 3/7] test(spend): serialize dashboard controller cases --- Tests/CodexBarTests/SpendDashboardControllerTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CodexBarTests/SpendDashboardControllerTests.swift b/Tests/CodexBarTests/SpendDashboardControllerTests.swift index 17e398ce42..03efb45ce5 100644 --- a/Tests/CodexBarTests/SpendDashboardControllerTests.swift +++ b/Tests/CodexBarTests/SpendDashboardControllerTests.swift @@ -4,6 +4,7 @@ import Testing @testable import CodexBar @MainActor +@Suite(.serialized) struct SpendDashboardControllerTests { @Test func `empty codex history loads as successful inactive source`() async { From 3eafbb465c877a87783990e252c7bef0abb2bce8 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:32:26 +0800 Subject: [PATCH 4/7] test(spend): isolate dashboard date range --- .../SpendDashboardControllerTests.swift | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardControllerTests.swift b/Tests/CodexBarTests/SpendDashboardControllerTests.swift index 03efb45ce5..de61b8736f 100644 --- a/Tests/CodexBarTests/SpendDashboardControllerTests.swift +++ b/Tests/CodexBarTests/SpendDashboardControllerTests.swift @@ -289,7 +289,7 @@ struct SpendDashboardControllerTests { let snapshot = Self.input(id: "claude", provider: .claude, cost: 3).snapshot store._setTokenSnapshotForTesting(snapshot, provider: .claude) store._test_tokenUsageRefreshOverride = { _, _ in } - let controller = SpendDashboardController(requestBuilder: { mode in + let controller = SpendDashboardController(userDefaults: settings.userDefaults, requestBuilder: { mode in await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) }) @@ -409,9 +409,11 @@ struct SpendDashboardControllerTests { environmentBase: [:]) store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 3).snapshot, provider: .claude) store._test_tokenUsageRefreshOverride = { _, _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) let firstConfiguration = SpendDashboardSource.configuration(settings: settings, store: store) controller.update(configuration: firstConfiguration) @@ -432,9 +434,11 @@ struct SpendDashboardControllerTests { #expect(controller.failedSourceCount == 1) #expect(store.tokenSnapshot(for: .claude)?.last30DaysCostUSD == 3) - let reopenedController = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let reopenedController = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) reopenedController.update(configuration: replacementConfiguration) await Self.waitUntil { !reopenedController.isRefreshing } #expect(reopenedController.model.groups.isEmpty) @@ -488,9 +492,11 @@ struct SpendDashboardControllerTests { store._setTokenSnapshotForTesting(Self.input(provider: .mistral, cost: 3).snapshot, provider: .mistral) store._test_providerRefreshOverride = { _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: selectedBackupConfiguration) await Self.waitUntil { !controller.isRefreshing } #expect(controller.model.groups.first?.totalCost == 3) @@ -528,9 +534,11 @@ struct SpendDashboardControllerTests { environmentBase: [:]) store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 4).snapshot, provider: .claude) store._test_tokenUsageRefreshOverride = { _, _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } #expect(controller.model.groups.first?.totalCost == 4) @@ -558,9 +566,11 @@ struct SpendDashboardControllerTests { environmentBase: [:]) store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 5).snapshot, provider: .claude) store._test_tokenUsageRefreshOverride = { _, _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) let firstConfiguration = SpendDashboardSource.configuration(settings: settings, store: store) controller.update(configuration: firstConfiguration) await Self.waitUntil { !controller.isRefreshing } @@ -627,9 +637,11 @@ struct SpendDashboardControllerTests { environmentBase: [:]) store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 5).snapshot, provider: .claude) store._test_tokenUsageRefreshOverride = { _, _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } #expect(controller.model.groups.first?.totalCost == 5) @@ -645,9 +657,11 @@ struct SpendDashboardControllerTests { #expect(controller.model.groups.isEmpty) #expect(controller.failedSourceCount == 1) - let reopenedController = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let reopenedController = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) reopenedController.update(configuration: reenabledConfiguration) await Self.waitUntil { !reopenedController.isRefreshing } #expect(reopenedController.model.groups.isEmpty) From 322f5296db213e2df179f185773de77b53c7e942 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:58:56 +0800 Subject: [PATCH 5/7] Stabilize dashboard token provenance tests --- .../SpendDashboardTokenProvenanceTests.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift b/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift index 88e594eca4..23902d4d79 100644 --- a/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift +++ b/Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift @@ -124,9 +124,11 @@ struct SpendDashboardTokenProvenanceTests { store.activateCachedTokenAccountSnapshot(provider: .mistral, accountID: account.id) #expect(store.tokenSnapshotPublicationRevision(for: .mistral) == baselineRevision) store._test_providerRefreshOverride = { _ in } - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } #expect(controller.model.groups.first?.totalCost == 3) @@ -148,9 +150,11 @@ struct SpendDashboardTokenProvenanceTests { return loadCount == 1 ? Self.tokenSnapshot(cost: 4) : Self.emptyTokenSnapshot() } await store.refreshTokenUsageNow(for: .bedrock, force: true) - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } #expect(controller.model.groups.first?.totalCost == 4) @@ -177,9 +181,11 @@ struct SpendDashboardTokenProvenanceTests { } await store.refreshTokenUsageNow(for: .bedrock, force: true) let publicationRevision = store.tokenSnapshotPublicationRevision(for: .bedrock) - let controller = SpendDashboardController(requestBuilder: { mode in - await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) - }) + let controller = SpendDashboardController( + userDefaults: settings.userDefaults, + requestBuilder: { mode in + await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode) + }) controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store)) await Self.waitUntil { !controller.isRefreshing } From 1bd85494c6d6f00ab590803d8273f082ddbba517 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:52:13 +0800 Subject: [PATCH 6/7] Fix localized session equivalents --- Sources/CodexBar/Localization.swift | 17 ++++++++++++++++- Sources/CodexBar/UsagePaceText.swift | 16 +++++++++------- .../LocalizationBundleCacheTests.swift | 13 +++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift index 38a6918e8b..d9fc9a3352 100644 --- a/Sources/CodexBar/Localization.swift +++ b/Sources/CodexBar/Localization.swift @@ -214,7 +214,22 @@ func L(_ key: String, language: String) -> String { } func codexBarLocalizedLocale() -> Locale { - let language = resolvedAppLanguage() + codexBarLocale(forLanguage: resolvedAppLanguage()) +} + +/// Returns the locale of the resource bundle currently selected by `L`. +/// +/// This can differ from `Locale.current` when the app falls back to a supported language. Plural +/// formatting must use this locale so it follows the same language as the resolved strings. +func codexBarLocalizedResourceLocale() -> Locale { + let bundleURL = localizedBundle().bundleURL + guard bundleURL.pathExtension == "lproj" else { + return codexBarLocalizedLocale() + } + return codexBarLocale(forLanguage: bundleURL.deletingPathExtension().lastPathComponent) +} + +private func codexBarLocale(forLanguage language: String) -> Locale { guard !language.isEmpty else { return .current } let normalized = language.lowercased() if normalized == "ar" || normalized.hasPrefix("ar-") { diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index 32823b6d22..bbe65498ec 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -39,19 +39,21 @@ enum UsagePaceText { static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail { let displayedEstimate = Self.boundedFullWindowCount(forecast.estimatedWindowsToExhaustWeekly) - let numberText = String.localizedStringWithFormat( - L("≈%d full 5h windows of weekly left · %d windows until reset"), - displayedEstimate, - forecast.windowsUntilReset) + let formattingLocale = codexBarLocalizedResourceLocale() + let numberText = String( + format: L("≈%d full 5h windows of weekly left · %d windows until reset"), + locale: formattingLocale, + arguments: [displayedEstimate, forecast.windowsUntilReset]) let verdictText: String if forecast.estimatedWindowsToExhaustWeekly >= forecast.availableWindowsUntilReset { verdictText = L("Weekly cannot run out before reset at this pace") } else { let windowsEarly = Self.boundedWindowCount( forecast.availableWindowsUntilReset - forecast.estimatedWindowsToExhaustWeekly) - verdictText = String.localizedStringWithFormat( - L("Weekly can run out ≈%d windows early"), - max(1, windowsEarly)) + verdictText = String( + format: L("Weekly can run out ≈%d windows early"), + locale: formattingLocale, + arguments: [max(1, windowsEarly)]) } return SessionEquivalentDetail( verdictText: verdictText, diff --git a/Tests/CodexBarTests/LocalizationBundleCacheTests.swift b/Tests/CodexBarTests/LocalizationBundleCacheTests.swift index 7a7d2572c2..3ab0ae15df 100644 --- a/Tests/CodexBarTests/LocalizationBundleCacheTests.swift +++ b/Tests/CodexBarTests/LocalizationBundleCacheTests.swift @@ -54,6 +54,19 @@ struct LocalizationBundleCacheTests { #expect(bundle.bundleURL.lastPathComponent == "en.lproj") } + @Test + func `format locale follows the resolved resource bundle`() { + let english = CodexBarLocalizationOverride.$appLanguage.withValue("en") { + codexBarLocalizedResourceLocale() + } + #expect(english.language.languageCode?.identifier == "en") + + let fallback = CodexBarLocalizationOverride.$appLanguage.withValue("zz-unknown") { + codexBarLocalizedResourceLocale() + } + #expect(fallback.language.languageCode?.identifier == "en") + } + @Test func `resolution survives an explicit cache reset`() { let first = CodexBarLocalizationOverride.$appLanguage.withValue("uk") { From 863ca1b2dca01dec6e2ce7018b6d4686e8191868 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:29:13 +0800 Subject: [PATCH 7/7] Cover localized stringsdict formatting --- .../CodexBarTests/LocalizationBundleCacheTests.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/CodexBarTests/LocalizationBundleCacheTests.swift b/Tests/CodexBarTests/LocalizationBundleCacheTests.swift index 3ab0ae15df..1d4e2f7b98 100644 --- a/Tests/CodexBarTests/LocalizationBundleCacheTests.swift +++ b/Tests/CodexBarTests/LocalizationBundleCacheTests.swift @@ -67,6 +67,18 @@ struct LocalizationBundleCacheTests { #expect(fallback.language.languageCode?.identifier == "en") } + @Test + func `resource locale expands English stringsdict singular forms`() { + let rendered = CodexBarLocalizationOverride.$appLanguage.withValue("en") { + String( + format: L("≈%d full 5h windows of weekly left · %d windows until reset"), + locale: codexBarLocalizedResourceLocale(), + arguments: [1, 1]) + } + + #expect(rendered == "≈1 full 5h window of weekly left · 1 window until reset") + } + @Test func `resolution survives an explicit cache reset`() { let first = CodexBarLocalizationOverride.$appLanguage.withValue("uk") {